@bitkyc08/opencodex 2.84.0 → 2.86.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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single reader of `advisor` config.
|
|
3
|
+
*
|
|
4
|
+
* Nothing else reads the `advisor` key directly: the sidecar planner, the management API and the
|
|
5
|
+
* CLI all resolve through here, so they cannot disagree about defaults or validity. Type-only
|
|
6
|
+
* config import keeps this module free of runtime edges.
|
|
7
|
+
*/
|
|
8
|
+
import type { OcxConfig } from "../types";
|
|
9
|
+
import { ADVISOR_CONTEXT_SHARING_CONSENT_VERSION } from "./disclosure";
|
|
10
|
+
|
|
11
|
+
export { ADVISOR_CONTEXT_SHARING_CONSENT_VERSION };
|
|
12
|
+
|
|
13
|
+
export type AdvisorPolicy = "manual" | "preflight";
|
|
14
|
+
|
|
15
|
+
export const ADVISOR_EFFORTS = ["low", "medium", "high", "xhigh", "max", "ultra"] as const;
|
|
16
|
+
export type AdvisorEffort = (typeof ADVISOR_EFFORTS)[number];
|
|
17
|
+
|
|
18
|
+
export const ADVISOR_POLICIES = ["manual", "preflight"] as const;
|
|
19
|
+
|
|
20
|
+
export interface AdvisorSettings {
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
model: string;
|
|
23
|
+
effort: AdvisorEffort;
|
|
24
|
+
policy: AdvisorPolicy;
|
|
25
|
+
timeoutMs: number;
|
|
26
|
+
/**
|
|
27
|
+
* Current context-sharing consent, or null when absent, stale, or malformed.
|
|
28
|
+
* `enabled` does not imply this. Only the current version allows data transfer.
|
|
29
|
+
*/
|
|
30
|
+
contextSharingConsent: typeof ADVISOR_CONTEXT_SHARING_CONSENT_VERSION | null;
|
|
31
|
+
/** Where each resolved value came from, so the GUI/CLI can show real runtime state. */
|
|
32
|
+
sources: {
|
|
33
|
+
enabled: "default" | "configured";
|
|
34
|
+
model: "default" | "configured";
|
|
35
|
+
effort: "default" | "configured";
|
|
36
|
+
policy: "default" | "configured";
|
|
37
|
+
contextSharingConsent: "default" | "configured";
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DEFAULT_ADVISOR_SETTINGS: Readonly<AdvisorSettings> = Object.freeze({
|
|
42
|
+
enabled: false,
|
|
43
|
+
model: "",
|
|
44
|
+
effort: "max",
|
|
45
|
+
policy: "manual",
|
|
46
|
+
timeoutMs: 120_000,
|
|
47
|
+
contextSharingConsent: null,
|
|
48
|
+
sources: Object.freeze({
|
|
49
|
+
enabled: "default",
|
|
50
|
+
model: "default",
|
|
51
|
+
effort: "default",
|
|
52
|
+
policy: "default",
|
|
53
|
+
contextSharingConsent: "default",
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
type Rec = Record<string, unknown>;
|
|
58
|
+
function isRec(value: unknown): value is Rec {
|
|
59
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function isValidAdvisorEffort(value: unknown): value is AdvisorEffort {
|
|
63
|
+
return typeof value === "string" && (ADVISOR_EFFORTS as readonly string[]).includes(value);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function isValidAdvisorPolicy(value: unknown): value is AdvisorPolicy {
|
|
67
|
+
return typeof value === "string" && (ADVISOR_POLICIES as readonly string[]).includes(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** True only for the current context-sharing consent version. Anything else is not a grant. */
|
|
71
|
+
export function isCurrentAdvisorContextSharingConsent(
|
|
72
|
+
value: unknown,
|
|
73
|
+
): value is typeof ADVISOR_CONTEXT_SHARING_CONSENT_VERSION {
|
|
74
|
+
return value === ADVISOR_CONTEXT_SHARING_CONSENT_VERSION;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Resolve advisor settings with conservative defaults for every absent or malformed field.
|
|
79
|
+
* A malformed block resolves to fully disabled defaults rather than throwing: the advisor is
|
|
80
|
+
* optional and must never take the request path down with it.
|
|
81
|
+
*/
|
|
82
|
+
export function resolveAdvisorSettings(config: Pick<OcxConfig, "advisor">): AdvisorSettings {
|
|
83
|
+
const raw: unknown = config.advisor;
|
|
84
|
+
if (!isRec(raw)) return { ...DEFAULT_ADVISOR_SETTINGS, sources: { ...DEFAULT_ADVISOR_SETTINGS.sources } };
|
|
85
|
+
const enabled = typeof raw.enabled === "boolean" ? raw.enabled : DEFAULT_ADVISOR_SETTINGS.enabled;
|
|
86
|
+
const model = typeof raw.model === "string" ? raw.model.trim() : DEFAULT_ADVISOR_SETTINGS.model;
|
|
87
|
+
const effort = isValidAdvisorEffort(raw.effort) ? raw.effort : DEFAULT_ADVISOR_SETTINGS.effort;
|
|
88
|
+
const policy = isValidAdvisorPolicy(raw.policy) ? raw.policy : DEFAULT_ADVISOR_SETTINGS.policy;
|
|
89
|
+
const timeoutRaw = raw.timeoutMs;
|
|
90
|
+
const timeoutMs = typeof timeoutRaw === "number" && Number.isFinite(timeoutRaw) && timeoutRaw >= 1_000
|
|
91
|
+
? Math.min(Math.floor(timeoutRaw), 600_000)
|
|
92
|
+
: DEFAULT_ADVISOR_SETTINGS.timeoutMs;
|
|
93
|
+
// A present but non-current value (stale version, wrong type) resolves as no consent.
|
|
94
|
+
// The stored bytes are left untouched; this reader never writes an upgrade.
|
|
95
|
+
const consentConfigured = Object.prototype.hasOwnProperty.call(raw, "contextSharingConsent");
|
|
96
|
+
const contextSharingConsent = isCurrentAdvisorContextSharingConsent(raw.contextSharingConsent)
|
|
97
|
+
? raw.contextSharingConsent
|
|
98
|
+
: null;
|
|
99
|
+
return {
|
|
100
|
+
enabled,
|
|
101
|
+
model,
|
|
102
|
+
effort,
|
|
103
|
+
policy,
|
|
104
|
+
timeoutMs,
|
|
105
|
+
contextSharingConsent,
|
|
106
|
+
sources: {
|
|
107
|
+
enabled: typeof raw.enabled === "boolean" ? "configured" : "default",
|
|
108
|
+
model: typeof raw.model === "string" && raw.model.trim() !== "" ? "configured" : "default",
|
|
109
|
+
effort: isValidAdvisorEffort(raw.effort) ? "configured" : "default",
|
|
110
|
+
policy: isValidAdvisorPolicy(raw.policy) ? "configured" : "default",
|
|
111
|
+
contextSharingConsent: consentConfigured ? "configured" : "default",
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether a consultation may send task context. Requires the switch, a model, and the
|
|
118
|
+
* current context-sharing consent. `enabled` is not consent. A miss fails closed for the
|
|
119
|
+
* data transfer and leaves the worker request itself running.
|
|
120
|
+
*/
|
|
121
|
+
export function advisorRunnable(settings: AdvisorSettings): boolean {
|
|
122
|
+
return settings.enabled
|
|
123
|
+
&& settings.model.trim() !== ""
|
|
124
|
+
&& settings.contextSharingConsent === ADVISOR_CONTEXT_SHARING_CONSENT_VERSION;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Enabled, with a model, but without current consent. The management surface reports
|
|
129
|
+
* `advisor_context_sharing_consent_required` and the runtime sends no task context.
|
|
130
|
+
*/
|
|
131
|
+
export function advisorContextSharingBlocked(settings: AdvisorSettings): boolean {
|
|
132
|
+
return settings.enabled
|
|
133
|
+
&& settings.model.trim() !== ""
|
|
134
|
+
&& settings.contextSharingConsent !== ADVISOR_CONTEXT_SHARING_CONSENT_VERSION;
|
|
135
|
+
}
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { advisorResultIsAdvice } from "./context";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Conversation- and task-scoped advisor state.
|
|
6
|
+
*
|
|
7
|
+
* Three scopes, deliberately separate:
|
|
8
|
+
*
|
|
9
|
+
* 1. REQUEST-scoped state lives in the per-request plan closure (see runtime.ts) — consultation
|
|
10
|
+
* count, dedup fingerprints, the preflight flag. Born and dies with one request.
|
|
11
|
+
*
|
|
12
|
+
* 2. TASK-scoped preflight ledger (this file): a bounded, process-local claim table that makes
|
|
13
|
+
* "at most one automatic consultation per task" atomic across concurrent requests. A claim
|
|
14
|
+
* requires a STABLE conversation identity plus the current task boundary; a client that sends
|
|
15
|
+
* no identity never enters this ledger (see `advisorLedgerKey`) and therefore fails open —
|
|
16
|
+
* it may be consulted once per request rather than risk two independent tasks suppressing
|
|
17
|
+
* each other through a shared guess.
|
|
18
|
+
*
|
|
19
|
+
* 3. PROVENANCE, split by authority:
|
|
20
|
+
* - MANUAL advice is verifiable history: a `toolResult` whose `toolName` is the synthetic
|
|
21
|
+
* advisor tool and whose content parses as a runtime-written advice object. Ordinary tool
|
|
22
|
+
* output, developer text, user text, and failure notices can never match it. Bytes inside
|
|
23
|
+
* the quoted advice field cannot change the runtime-owned status.
|
|
24
|
+
* - AUTOMATIC preflight dedup is NOT decided from history at all. The developer transport
|
|
25
|
+
* envelope labels the payload for the worker; any client could echo or forge such a message,
|
|
26
|
+
* so the ledger below is the authoritative source for "this task was already consulted" —
|
|
27
|
+
* `success`, `inflight` and `cooldown` states. A conversation with no stable identity gets
|
|
28
|
+
* no ledger and therefore fails open (at most one extra attempt). A forged marker or a
|
|
29
|
+
* forged developer message cannot suppress the policy.
|
|
30
|
+
*
|
|
31
|
+
* Ledger entries are plain state records — no message bodies, no credentials. Every state is
|
|
32
|
+
* bounded by entry count and its own TTL.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/** Long-lived success suppression: the task lifetime approximation (also the ledger TTL cap). */
|
|
36
|
+
export const ADVISOR_SUCCESS_TTL_MS = 24 * 60 * 60 * 1000;
|
|
37
|
+
/**
|
|
38
|
+
* Failure cooldown. One minute is the repository's standing minute-scale unit (tray polling,
|
|
39
|
+
* subagent availability polling); it turns a transient 503 into a short pause instead of
|
|
40
|
+
* silencing the policy for the rest of the coding session.
|
|
41
|
+
*/
|
|
42
|
+
export const ADVISOR_FAILURE_COOLDOWN_MS = 60 * 1000;
|
|
43
|
+
/**
|
|
44
|
+
* In-flight claim expiry. Longer than the largest configurable consultation timeout (600s
|
|
45
|
+
* upper bound on `advisor.timeoutMs`, default 120s) so a slow-but-alive consultation is never
|
|
46
|
+
* mistaken for a wedged one, while a crashed claim cannot block a task forever.
|
|
47
|
+
*/
|
|
48
|
+
export const ADVISOR_INFLIGHT_TTL_MS = 10 * 60 * 1000;
|
|
49
|
+
|
|
50
|
+
const MAX_ENTRIES = 512;
|
|
51
|
+
|
|
52
|
+
export type AdvisorClaimState =
|
|
53
|
+
/** The caller now owns the consultation for this task. */
|
|
54
|
+
| "claimed"
|
|
55
|
+
/** Another request for the same task is consulting right now. */
|
|
56
|
+
| "inflight"
|
|
57
|
+
/** This task already received advice; suppression holds until the success TTL expires. */
|
|
58
|
+
| "complete"
|
|
59
|
+
/** A recent consultation failed; suppression holds for the short failure cooldown. */
|
|
60
|
+
| "cooldown"
|
|
61
|
+
/**
|
|
62
|
+
* The ledger is full of live claims and granted nothing. Fail-open: the worker continues
|
|
63
|
+
* without a new automatic consultation rather than evicting a claim that is still running.
|
|
64
|
+
*/
|
|
65
|
+
| "saturated";
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The result of a claim attempt. `token` identifies THIS claim and is present only on
|
|
69
|
+
* `"claimed"`; settlement must present it, so a slow consultation whose in-flight entry expired
|
|
70
|
+
* cannot settle the successor claim that took its place.
|
|
71
|
+
*/
|
|
72
|
+
export interface AdvisorClaim {
|
|
73
|
+
state: AdvisorClaimState;
|
|
74
|
+
token?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface LedgerEntry {
|
|
78
|
+
state: "inflight" | "success" | "failed";
|
|
79
|
+
at: number;
|
|
80
|
+
/** Present on in-flight entries: which claim owns this entry. */
|
|
81
|
+
token?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface AdvisorPreflightLedger {
|
|
85
|
+
/**
|
|
86
|
+
* Atomically try to own the consultation for a task key. Returns `claimed` exactly once per
|
|
87
|
+
* task (with the ownership token) until a matching settlement releases it, so two concurrent
|
|
88
|
+
* requests cannot both consult.
|
|
89
|
+
*/
|
|
90
|
+
claim(key: string, now?: number): AdvisorClaim;
|
|
91
|
+
/**
|
|
92
|
+
* Settle a claim the caller owns. A settlement whose token does not match the current entry
|
|
93
|
+
* is a no-op: the entry now belongs to a successor claim (the caller's in-flight window
|
|
94
|
+
* expired), and it must not be able to erase or overwrite that successor's state.
|
|
95
|
+
*/
|
|
96
|
+
complete(key: string, token: string, now?: number): void;
|
|
97
|
+
fail(key: string, token: string, now?: number): void;
|
|
98
|
+
release(key: string, token: string, now?: number): void;
|
|
99
|
+
/**
|
|
100
|
+
* Fact, not settlement: this task received advice (used by a successful MANUAL consultation,
|
|
101
|
+
* which owns no preflight claim). Records success regardless of the current entry, because
|
|
102
|
+
* "the task was advised" is true whichever consultation produced it.
|
|
103
|
+
*/
|
|
104
|
+
markAdvised(key: string, now?: number): void;
|
|
105
|
+
/** Test/observability seam: current entry count. */
|
|
106
|
+
size(): number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createAdvisorPreflightLedger(): AdvisorPreflightLedger {
|
|
110
|
+
const entries = new Map<string, LedgerEntry>();
|
|
111
|
+
let claimSequence = 0;
|
|
112
|
+
|
|
113
|
+
const isExpired = (entry: LedgerEntry, now: number): boolean => {
|
|
114
|
+
const ttl = entry.state === "success"
|
|
115
|
+
? ADVISOR_SUCCESS_TTL_MS
|
|
116
|
+
: entry.state === "failed"
|
|
117
|
+
? ADVISOR_FAILURE_COOLDOWN_MS
|
|
118
|
+
: ADVISOR_INFLIGHT_TTL_MS;
|
|
119
|
+
return now - entry.at > ttl;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Make room for ONE new claim without ever evicting a live in-flight entry, because that entry
|
|
124
|
+
* is the only thing preventing a second automatic consultation for its task. Order: expired
|
|
125
|
+
* entries first, then settled ones (success before failure), oldest first. Returns false when
|
|
126
|
+
* every entry is a live claim — the caller then reports `saturated` instead of breaking the
|
|
127
|
+
* "at most one in-flight consultation per task" guarantee.
|
|
128
|
+
*/
|
|
129
|
+
const makeRoom = (now: number): boolean => {
|
|
130
|
+
if (entries.size < MAX_ENTRIES) return true;
|
|
131
|
+
for (const [key, entry] of entries) {
|
|
132
|
+
if (isExpired(entry, now)) entries.delete(key);
|
|
133
|
+
}
|
|
134
|
+
if (entries.size < MAX_ENTRIES) return true;
|
|
135
|
+
for (const state of ["success", "failed"] as const) {
|
|
136
|
+
for (const [key, entry] of entries) {
|
|
137
|
+
if (entry.state === state) {
|
|
138
|
+
entries.delete(key);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const liveEntry = (key: string, now: number): LedgerEntry | undefined => {
|
|
147
|
+
const entry = entries.get(key);
|
|
148
|
+
if (!entry) return undefined;
|
|
149
|
+
if (isExpired(entry, now)) {
|
|
150
|
+
entries.delete(key);
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return entry;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
// Replacing an existing key never grows the map, and every NEW key is admitted only through
|
|
157
|
+
// claim()'s makeRoom gate, so the table cannot exceed MAX_ENTRIES.
|
|
158
|
+
const set = (key: string, state: LedgerEntry["state"], now: number, token?: string): void => {
|
|
159
|
+
if (entries.has(key)) entries.delete(key);
|
|
160
|
+
entries.set(key, { state, at: now, ...(token !== undefined ? { token } : {}) });
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/** True when the caller's token still owns the current in-flight entry for this key. */
|
|
164
|
+
const owns = (key: string, token: string, now: number): boolean => {
|
|
165
|
+
const entry = liveEntry(key, now);
|
|
166
|
+
return entry?.state === "inflight" && entry.token === token;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
claim(key, now = Date.now()) {
|
|
171
|
+
const entry = liveEntry(key, now);
|
|
172
|
+
if (entry?.state === "success") return { state: "complete" };
|
|
173
|
+
if (entry?.state === "failed") return { state: "cooldown" };
|
|
174
|
+
if (entry?.state === "inflight") return { state: "inflight" };
|
|
175
|
+
if (!makeRoom(now)) return { state: "saturated" };
|
|
176
|
+
claimSequence += 1;
|
|
177
|
+
const token = `claim-${claimSequence.toString(36)}`;
|
|
178
|
+
set(key, "inflight", now, token);
|
|
179
|
+
return { state: "claimed", token };
|
|
180
|
+
},
|
|
181
|
+
complete(key, token, now = Date.now()) {
|
|
182
|
+
// A settlement that no longer owns the entry is a no-op: the claim it belonged to expired
|
|
183
|
+
// and a successor now owns the state.
|
|
184
|
+
if (owns(key, token, now)) set(key, "success", now);
|
|
185
|
+
},
|
|
186
|
+
fail(key, token, now = Date.now()) {
|
|
187
|
+
if (owns(key, token, now)) set(key, "failed", now);
|
|
188
|
+
},
|
|
189
|
+
release(key, token, now = Date.now()) {
|
|
190
|
+
if (owns(key, token, now)) entries.delete(key);
|
|
191
|
+
},
|
|
192
|
+
markAdvised(key, now = Date.now()) {
|
|
193
|
+
// Recording the FACT must respect the same cap as claiming: a key that is not present and a
|
|
194
|
+
// table holding nothing but live claims means no room, so the record is skipped rather than
|
|
195
|
+
// evicting a claim that is still running. Fail-open: at worst one extra automatic attempt.
|
|
196
|
+
const present = liveEntry(key, now) !== undefined;
|
|
197
|
+
if (!present && !makeRoom(now)) return;
|
|
198
|
+
set(key, "success", now);
|
|
199
|
+
},
|
|
200
|
+
size() {
|
|
201
|
+
return entries.size;
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Stable conversation identity for the ledger, reusing the repository's existing request
|
|
208
|
+
* identities in specificity order: the client's own thread, the shared parent thread, the
|
|
209
|
+
* Cursor conversation, the Cursor client thread, then the reasoning-replay scope's thread.
|
|
210
|
+
* Returns undefined for a client that sends no identity at all — such a caller stays out of the
|
|
211
|
+
* process-global ledger on purpose (fail-open, see the module header).
|
|
212
|
+
*/
|
|
213
|
+
export function advisorConversationIdentity(parsed: {
|
|
214
|
+
_codexOwnThreadId?: string;
|
|
215
|
+
_clientThreadId?: string;
|
|
216
|
+
_cursorConversationId?: string;
|
|
217
|
+
_cursorClientThreadId?: string;
|
|
218
|
+
_reasoningReplayScope?: { clientThreadId?: string };
|
|
219
|
+
}): string | undefined {
|
|
220
|
+
return parsed._codexOwnThreadId
|
|
221
|
+
?? parsed._clientThreadId
|
|
222
|
+
?? parsed._cursorConversationId
|
|
223
|
+
?? parsed._cursorClientThreadId
|
|
224
|
+
?? parsed._reasoningReplayScope?.clientThreadId
|
|
225
|
+
?? undefined;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Domain-separated SHA-256 digest. Task identity and suppression are CORRECTNESS boundaries, so a
|
|
230
|
+
* 32-bit non-cryptographic hash is not an acceptable primary digest: distinct tasks must not
|
|
231
|
+
* collide because of a short fold. The ledger stores only this digest, never the raw text, so a
|
|
232
|
+
* captured key reveals nothing about the conversation. The domain prefix keeps digests from
|
|
233
|
+
* different purposes apart even if their inputs coincide.
|
|
234
|
+
*/
|
|
235
|
+
function sha256Hex(domain: string, value: string, hexChars: number): string {
|
|
236
|
+
return createHash("sha256").update(`${domain}\0${value}`, "utf8").digest("hex").slice(0, hexChars);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* The current task boundary inside a conversation: how many user turns the history carries and a
|
|
241
|
+
* digest of the FULL latest user text. A new user message moves the boundary (a new task gets its
|
|
242
|
+
* own claim); the same turn re-sent by a stateless full-history client keeps the same boundary,
|
|
243
|
+
* and a `previous_response_id` expansion replays the same user turns, so continuations dedup.
|
|
244
|
+
* The whole text participates — no truncation — so two tasks that share an opening prefix still
|
|
245
|
+
* get different boundaries.
|
|
246
|
+
*/
|
|
247
|
+
export function advisorTaskBoundary(parsed: {
|
|
248
|
+
context: { messages: readonly { role: string; content: unknown }[] };
|
|
249
|
+
}): string {
|
|
250
|
+
let userTurns = 0;
|
|
251
|
+
let lastUserText = "";
|
|
252
|
+
for (const message of parsed.context.messages) {
|
|
253
|
+
if (message.role !== "user") continue;
|
|
254
|
+
userTurns += 1;
|
|
255
|
+
lastUserText = contentText(message.content);
|
|
256
|
+
}
|
|
257
|
+
return `t${userTurns}:${sha256Hex("advisor-task-boundary", lastUserText, 32)}`;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* The ledger key: one domain-separated SHA-256 digest over conversation identity + task boundary
|
|
262
|
+
* + worker model. Returns undefined when the caller has no stable conversation identity — the
|
|
263
|
+
* caller then relies on request-scoped dedup and genuine in-history provenance instead of a
|
|
264
|
+
* shared guess (documented fail-open).
|
|
265
|
+
*/
|
|
266
|
+
export function advisorLedgerKey(
|
|
267
|
+
parsed: {
|
|
268
|
+
context: { messages: readonly { role: string; content: unknown }[] };
|
|
269
|
+
_codexOwnThreadId?: string;
|
|
270
|
+
_clientThreadId?: string;
|
|
271
|
+
_cursorConversationId?: string;
|
|
272
|
+
_cursorClientThreadId?: string;
|
|
273
|
+
_reasoningReplayScope?: { clientThreadId?: string };
|
|
274
|
+
},
|
|
275
|
+
workerModelId: string,
|
|
276
|
+
): string | undefined {
|
|
277
|
+
const identity = advisorConversationIdentity(parsed);
|
|
278
|
+
if (!identity) return undefined;
|
|
279
|
+
const material = `${identity}\0${advisorTaskBoundary(parsed)}\0${workerModelId}`;
|
|
280
|
+
return `ak-${sha256Hex("advisor-task-key", material, 40)}`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Text projection for string-or-parts content; used only for hashing, never transmitted. */
|
|
284
|
+
export function contentText(content: unknown): string {
|
|
285
|
+
if (typeof content === "string") return content;
|
|
286
|
+
if (!Array.isArray(content)) return "";
|
|
287
|
+
return content
|
|
288
|
+
.filter((part): part is { type: "text"; text: string } =>
|
|
289
|
+
!!part && typeof part === "object" && (part as { type?: unknown }).type === "text"
|
|
290
|
+
&& typeof (part as { text?: unknown }).text === "string")
|
|
291
|
+
.map(part => part.text)
|
|
292
|
+
.join("");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Legacy marker spellings. Failure text still neutralizes them so an upstream error cannot
|
|
297
|
+
* look like an old wrapper. They are not suppression authority and are not emitted.
|
|
298
|
+
*/
|
|
299
|
+
export const ADVISOR_PREFLIGHT_MARKER = "<opencodex_advisor_preflight>";
|
|
300
|
+
|
|
301
|
+
/** The synthetic advisor tool's wire name; kept in sync with the tool definition by test. */
|
|
302
|
+
export const ADVISOR_RESULT_TOOL_NAME = "advisor";
|
|
303
|
+
|
|
304
|
+
/** Legacy manual wrapper spelling. Detection does not search for this substring. */
|
|
305
|
+
export const ADVISOR_ADVICE_MARKER = "<opencodex_advisor>";
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Detect an ALREADY-PRESENT MANUAL advisor result — by provenance, never by a bare string.
|
|
309
|
+
* The message must be a `toolResult` whose `toolName` is the synthetic advisor tool, and its
|
|
310
|
+
* content must parse as a runtime-written advice object (`status === "advice"` on the sibling
|
|
311
|
+
* field the runtime sets). Text inside `advice` cannot flip that field.
|
|
312
|
+
*
|
|
313
|
+
* Only messages after the latest user turn count. Manual advice from an earlier task in the
|
|
314
|
+
* same thread does not suppress preflight for a later task; the ledger keys those separately.
|
|
315
|
+
* Developer messages are deliberately NOT inspected. Automatic preflight dedup lives in the
|
|
316
|
+
* ledger. A client-echoed developer envelope, a shell result, or a failure notice matches nothing.
|
|
317
|
+
*/
|
|
318
|
+
export function historyHasManualAdvisorResult(parsed: {
|
|
319
|
+
context: { messages: readonly { role: string; content?: unknown; toolName?: string }[] };
|
|
320
|
+
}): boolean {
|
|
321
|
+
for (let i = parsed.context.messages.length - 1; i >= 0; i -= 1) {
|
|
322
|
+
const message = parsed.context.messages[i]!;
|
|
323
|
+
if (message.role === "user") break;
|
|
324
|
+
if (message.role !== "toolResult") continue;
|
|
325
|
+
if (message.toolName !== ADVISOR_RESULT_TOOL_NAME) continue;
|
|
326
|
+
if (advisorResultIsAdvice(contentText(message.content))) return true;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Kept for callers that only need the first user text (payload building, tests). */
|
|
332
|
+
export function firstUserText(parsed: { context: { messages: readonly { role: string; content: unknown }[] } }): string {
|
|
333
|
+
for (const message of parsed.context.messages) {
|
|
334
|
+
if (message.role !== "user") continue;
|
|
335
|
+
const text = contentText(message.content);
|
|
336
|
+
if (text.trim() !== "") return text;
|
|
337
|
+
}
|
|
338
|
+
return "";
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Deterministic preflight trigger: has this conversation already produced orientation evidence
|
|
343
|
+
* since the latest user message? The documented approximation for "before the first substantive
|
|
344
|
+
* implementation" — the protocol layer offers no safe pre-mutation checkpoint, so OpenCodex fires
|
|
345
|
+
* the automatic attempt on the first worker reasoning turn that arrives with that evidence.
|
|
346
|
+
* Evidence is an assistant tool call OR a tool result after the latest user message (both forms
|
|
347
|
+
* are genuinely accepted; the docs say so). Only text/toolResult content is inspected — never
|
|
348
|
+
* reasoning, never encrypted items.
|
|
349
|
+
*/
|
|
350
|
+
export function hasOrientationEvidence(parsed: { context: { messages: readonly { role: string; content: unknown }[] } }): boolean {
|
|
351
|
+
let latestUserIndex = -1;
|
|
352
|
+
for (let i = parsed.context.messages.length - 1; i >= 0; i -= 1) {
|
|
353
|
+
if (parsed.context.messages[i]?.role === "user") {
|
|
354
|
+
latestUserIndex = i;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (latestUserIndex < 0) return false;
|
|
359
|
+
for (let i = latestUserIndex + 1; i < parsed.context.messages.length; i += 1) {
|
|
360
|
+
const message = parsed.context.messages[i];
|
|
361
|
+
if (message.role === "toolResult") return true;
|
|
362
|
+
if (message.role === "assistant" && Array.isArray(message.content)
|
|
363
|
+
&& message.content.some(part =>
|
|
364
|
+
!!part && typeof part === "object" && (part as { type?: unknown }).type === "toolCall")) {
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The synthetic `advisor` tool injected into routed worker turns.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors src/web-search/synthetic-tool.ts: the proxy OWNS this tool — the worker only expresses
|
|
5
|
+
* "I want to consult the expert" (optionally with a focus), and OpenCodex builds everything else
|
|
6
|
+
* (task context, conversation, tool catalog, identities) itself. The worker never passes a
|
|
7
|
+
* transcript, provider, model, or files.
|
|
8
|
+
*/
|
|
9
|
+
import type { OcxTool } from "../types";
|
|
10
|
+
|
|
11
|
+
export const ADVISOR_TOOL_NAME = "advisor";
|
|
12
|
+
|
|
13
|
+
export function buildAdvisorTool(): OcxTool {
|
|
14
|
+
return {
|
|
15
|
+
name: ADVISOR_TOOL_NAME,
|
|
16
|
+
description:
|
|
17
|
+
"Consult an independent expert advisor model about the current task. The advisor receives a "
|
|
18
|
+
+ "summary of what you have done so far (task, conversation, tool results) and returns "
|
|
19
|
+
+ "strategic advice, critique, root-cause reasoning, or alternative approaches. Use it when "
|
|
20
|
+
+ "you are stuck, before starting substantive implementation on a hard task, or when you want "
|
|
21
|
+
+ "a second opinion on a plan or diagnosis. The advisor cannot execute tools or edit files; "
|
|
22
|
+
+ "you remain responsible for all execution. Optionally pass a short `question` to focus the "
|
|
23
|
+
+ "consultation.",
|
|
24
|
+
parameters: {
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
question: { type: "string", description: "Optional short focus question for the advisor." },
|
|
28
|
+
},
|
|
29
|
+
required: [],
|
|
30
|
+
},
|
|
31
|
+
advisor: true,
|
|
32
|
+
};
|
|
33
|
+
}
|
package/src/bridge/errors.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
CYBER_POLICY_ERROR_CODE,
|
|
14
14
|
isCyberPolicyCode,
|
|
15
15
|
SPEND_LEDGER_STORAGE_UNAVAILABLE_CODE,
|
|
16
|
+
UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
16
17
|
type OcxErrorPayload,
|
|
17
18
|
} from "../lib/errors";
|
|
18
19
|
|
|
@@ -31,6 +32,10 @@ export function formatErrorResponse(
|
|
|
31
32
|
error.code = CYBER_POLICY_ERROR_CODE;
|
|
32
33
|
error.type = cyberPolicyErrorType(type);
|
|
33
34
|
}
|
|
35
|
+
// The undeclared-tool refusal is deterministic, not a transient upstream failure: its code
|
|
36
|
+
// survives so the Anthropic translator can classify it instead of folding it into a
|
|
37
|
+
// retryable overload (and so clients see the SSE path's rejection identity).
|
|
38
|
+
if (options?.code === UNDECLARED_TOOL_CALL_ERROR_CODE) error.code = options.code;
|
|
34
39
|
// Only the allowlisted transport verdicts survive this formatter. Do not forward
|
|
35
40
|
// arbitrary provider codes, and preserve the existing cyber-policy precedence.
|
|
36
41
|
const replayBlocked = error.code !== CYBER_POLICY_ERROR_CODE
|
package/src/chat/inbound.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function assertChatCompletionsRoutingBody(raw: unknown): asserts raw is C
|
|
|
36
36
|
const OUTPUT_CONFIG_EFFORTS = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"]);
|
|
37
37
|
const OUTPUT_CONFIG_SUMMARIES = new Set(["auto", "concise", "detailed", "none"]);
|
|
38
38
|
|
|
39
|
-
function contentToText(content: unknown): string {
|
|
39
|
+
export function contentToText(content: unknown): string {
|
|
40
40
|
if (typeof content === "string") return content;
|
|
41
41
|
if (!Array.isArray(content)) return "";
|
|
42
42
|
const parts: string[] = [];
|
|
@@ -62,7 +62,7 @@ const imageUrlFromPart = chatImageUrlFromPart;
|
|
|
62
62
|
* understanding is requested by (#3271). The object form is the only one that
|
|
63
63
|
* can carry it — `video_url` as a bare string has nowhere to put it.
|
|
64
64
|
*/
|
|
65
|
-
function videoFromPart(part: Rec): { url: string; processing?: string } | null {
|
|
65
|
+
export function videoFromPart(part: Rec): { url: string; processing?: string } | null {
|
|
66
66
|
if (part.type !== "video_url") return null;
|
|
67
67
|
const videoUrl = part.video_url;
|
|
68
68
|
if (typeof videoUrl === "string" && videoUrl.length > 0) return { url: videoUrl };
|
|
@@ -123,7 +123,7 @@ function userContentToBlocks(content: unknown): Rec[] {
|
|
|
123
123
|
* was dropped in silence (#5212). A part with no inline bytes is still not translatable and is
|
|
124
124
|
* left to the untranslated-media refusal, which runs before this loop.
|
|
125
125
|
*/
|
|
126
|
-
function fileFromPart(part: Rec): Rec | null {
|
|
126
|
+
export function fileFromPart(part: Rec): Rec | null {
|
|
127
127
|
if (part.type !== "file" && part.type !== "input_file") return null;
|
|
128
128
|
const file = isRec(part.file) ? part.file : part;
|
|
129
129
|
const fileData = file.file_data;
|
|
@@ -143,7 +143,7 @@ function fileFromPart(part: Rec): Rec | null {
|
|
|
143
143
|
* Only representable plaintext is read. No signature, encrypted payload or
|
|
144
144
|
* provider-issued item id is reconstructed — see the reasoning item built below.
|
|
145
145
|
*/
|
|
146
|
-
function assistantReasoningText(msg: Rec): string | undefined {
|
|
146
|
+
export function assistantReasoningText(msg: Rec): string | undefined {
|
|
147
147
|
if (typeof msg.reasoning_content === "string" && msg.reasoning_content.length > 0) {
|
|
148
148
|
return msg.reasoning_content;
|
|
149
149
|
}
|
|
@@ -601,6 +601,32 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
601
601
|
...(legacyFunctionsToResponses(raw.functions) ?? []),
|
|
602
602
|
];
|
|
603
603
|
if (tools.length > 0) body.tools = tools;
|
|
604
|
+
applyChatSettings(raw, body);
|
|
605
|
+
return body;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* The request settings of a Chat body as a Responses body with no conversation: `input` is
|
|
610
|
+
* empty and there are no `instructions` or `tools`. The Antigravity direct lane hands this to
|
|
611
|
+
* the Responses core so routing, effort policy, tier and logging keep reading the fields they
|
|
612
|
+
* read today, while the conversation is decoded straight into the IR
|
|
613
|
+
* (`src/protocols/codecs/chat-ir.ts`). Field order and validation are the bridge body's own:
|
|
614
|
+
* both are built by `applyChatSettings`.
|
|
615
|
+
*/
|
|
616
|
+
export function chatSettingsToResponsesBody(raw: unknown): Rec {
|
|
617
|
+
assertChatCompletionsRoutingBody(raw);
|
|
618
|
+
const body: Rec = {
|
|
619
|
+
model: raw.model,
|
|
620
|
+
input: [],
|
|
621
|
+
stream: raw.stream === true,
|
|
622
|
+
store: false,
|
|
623
|
+
};
|
|
624
|
+
applyChatSettings(raw, body);
|
|
625
|
+
return body;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/** Everything after the conversation and tool catalog: choice, limits, sampling, reasoning, format. */
|
|
629
|
+
function applyChatSettings(raw: ChatCompletionsRoutingBody, body: Rec): void {
|
|
604
630
|
if (raw.tool_choice !== undefined) toolChoiceToResponses(raw.tool_choice, body);
|
|
605
631
|
else legacyFunctionChoiceToResponses(raw.function_call, body);
|
|
606
632
|
|
|
@@ -637,6 +663,4 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
637
663
|
|
|
638
664
|
const text = responseFormatToText(raw.response_format);
|
|
639
665
|
if (text) body.text = text;
|
|
640
|
-
|
|
641
|
-
return body;
|
|
642
666
|
}
|
package/src/chat/outbound.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
CYBER_POLICY_ERROR_CODE,
|
|
20
20
|
isCyberPolicyCode,
|
|
21
21
|
isCyberPolicyMessage,
|
|
22
|
+
UNDECLARED_TOOL_CALL_ERROR_CODE,
|
|
22
23
|
} from "../lib/errors";
|
|
23
24
|
import { redactSecretString } from "../lib/redact";
|
|
24
25
|
import { createSseBlockBuffer, sseDataPayload } from "../server/sse-payload-rewrite";
|
|
@@ -196,6 +197,10 @@ export function chatCompletionsStreamErrorPayload(
|
|
|
196
197
|
} else if (isCyberPolicyCode(details?.code) || classified.code === CYBER_POLICY_ERROR_CODE) {
|
|
197
198
|
classified.code = CYBER_POLICY_ERROR_CODE;
|
|
198
199
|
classified.type = cyberPolicyErrorType(details?.type);
|
|
200
|
+
} else if (details?.code === UNDECLARED_TOOL_CALL_ERROR_CODE) {
|
|
201
|
+
// The guard's deterministic refusal keeps its wire identity past classifyError's
|
|
202
|
+
// generic 5xx remap — same code the buffered Chat error and the SSE frame carry.
|
|
203
|
+
classified.code = UNDECLARED_TOOL_CALL_ERROR_CODE;
|
|
199
204
|
} else if (details?.code !== undefined && details.code !== null && !classified.code) {
|
|
200
205
|
classified.code = details.code;
|
|
201
206
|
}
|