@sellable/install 0.1.801 → 0.1.803
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/container/Dockerfile +4 -4
- package/container/README.md +1 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-customer-image/claude-code-runs-api.mjs +19 -10
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +267 -129
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.d.mts +49 -2
- package/lib/sellable-agent/fly-customer-image/endpoint-model-selection.mjs +150 -14
- package/lib/sellable-agent/fly-customer-image/runs-api-health-check.mjs +12 -4
- package/lib/sellable-agent/host-bootstrap.mjs +1 -1
- package/lib/sellable-agent/host-worker.mjs +1 -1
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +1 -1
- package/package.json +1 -1
package/container/Dockerfile
CHANGED
|
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
6
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.803
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.957
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
9
|
ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.803" \
|
|
12
12
|
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.957" \
|
|
13
13
|
&& test -n "${INSTALLER_INTEGRITY}" \
|
|
14
14
|
&& test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
|
|
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.801" \
|
|
|
23
23
|
&& mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
|
|
24
24
|
&& npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
|
|
25
25
|
&& npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
|
|
26
|
-
&& node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.
|
|
26
|
+
&& node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.803' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.957') throw new Error('package identity rejected');" \
|
|
27
27
|
&& node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
|
|
28
|
-
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.
|
|
28
|
+
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.803', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.957', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
|
|
29
29
|
&& npm cache clean --force
|
|
30
30
|
|
|
31
31
|
COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
|
package/container/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
|
|
4
4
|
|
|
5
5
|
Build from this directory after
|
|
6
|
-
`@sellable/install@0.1.
|
|
6
|
+
`@sellable/install@0.1.803` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
|
|
|
8
8
|
|
|
9
9
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.957
|
|
10
10
|
ARG MCP_INTEGRITY=sha512-tIZuO5IEanT/afamDIWdorUNMdqZA6xnjL9jGAmGqYIlIqkXdHB9slWiwfLfmM44iEp7OT98IBHJlvbpMThk6A==
|
|
11
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
11
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.803
|
|
12
12
|
ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.202
|
|
13
13
|
ARG ADMIN_MCP_INTEGRITY=sha512-ym3aye/0vQ8tfULJlKz9JFOxvIQsl1G+n3np+TTgK/tecwNblBo/j3AkTnRqH9iomKzPbZzP+n4tEQsvsU8ByA==
|
|
14
14
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
@@ -332,6 +332,9 @@ export function claudeUsageLimit(stdout, now = Date.now()) {
|
|
|
332
332
|
return { retryAt };
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
+
// A failure reason the run may report: a fixed code, never raw CLI text.
|
|
336
|
+
const RUN_FAILURE_CODE = /^claude_runs_[a-z_]{1,48}$/;
|
|
337
|
+
|
|
335
338
|
export function parseClaudeResult(stdout, request) {
|
|
336
339
|
const result = JSON.parse(stdout);
|
|
337
340
|
const models = Object.keys(result.modelUsage ?? {});
|
|
@@ -341,15 +344,19 @@ export function parseClaudeResult(stdout, request) {
|
|
|
341
344
|
result.subtype !== "success" ||
|
|
342
345
|
(request.schema
|
|
343
346
|
? !record(result.structured_output)
|
|
344
|
-
: typeof result.result !== "string")
|
|
345
|
-
|
|
347
|
+
: typeof result.result !== "string")
|
|
348
|
+
)
|
|
349
|
+
throw new Error("claude_runs_output_rejected");
|
|
350
|
+
if ((result.permission_denials ?? []).length)
|
|
351
|
+
throw new Error("claude_runs_permission_denied");
|
|
352
|
+
if (
|
|
346
353
|
!models.length ||
|
|
347
354
|
models.some(
|
|
348
355
|
(model) =>
|
|
349
356
|
model !== request.model && !model.startsWith(`${request.model}-`)
|
|
350
357
|
)
|
|
351
358
|
)
|
|
352
|
-
throw new Error("
|
|
359
|
+
throw new Error("claude_runs_model_mismatch");
|
|
353
360
|
const output = request.schema
|
|
354
361
|
? JSON.stringify(result.structured_output)
|
|
355
362
|
: result.result;
|
|
@@ -447,8 +454,12 @@ export function startClaudeRun(
|
|
|
447
454
|
return reject(new Error(reason ?? "claude_runs_cli_failed"));
|
|
448
455
|
try {
|
|
449
456
|
resolve(parseClaudeResult(stdout, request));
|
|
450
|
-
} catch {
|
|
451
|
-
reject(
|
|
457
|
+
} catch (error) {
|
|
458
|
+
reject(
|
|
459
|
+
error instanceof Error && RUN_FAILURE_CODE.test(error.message)
|
|
460
|
+
? error
|
|
461
|
+
: new Error("claude_runs_output_rejected")
|
|
462
|
+
);
|
|
452
463
|
}
|
|
453
464
|
});
|
|
454
465
|
child.stdin.on("error", () => {});
|
|
@@ -728,11 +739,9 @@ export function createClaudeRunsApi({
|
|
|
728
739
|
? { status: "cancelled" }
|
|
729
740
|
: {
|
|
730
741
|
status: "failed",
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
"claude_runs_usage_limited",
|
|
735
|
-
].includes(error.message)
|
|
742
|
+
// The reason, so a failure can be told apart later;
|
|
743
|
+
// anything else the CLI threw stays generic.
|
|
744
|
+
error: RUN_FAILURE_CODE.test(error?.message ?? "")
|
|
736
745
|
? error.message
|
|
737
746
|
: "claude_runs_generation_failed",
|
|
738
747
|
}
|
|
@@ -88,6 +88,11 @@ import {
|
|
|
88
88
|
} from "../profile-materializer.mjs";
|
|
89
89
|
import { reconcilePublicHermesSkills } from "../public-skills.mjs";
|
|
90
90
|
import { createCustomerRuntimePhaseTelemetry } from "./customer-runtime-phase-telemetry.mjs";
|
|
91
|
+
import {
|
|
92
|
+
endpointChainStepAdmissionDeadline,
|
|
93
|
+
endpointModelSelectionChain,
|
|
94
|
+
nextEndpointChainStep,
|
|
95
|
+
} from "./endpoint-model-selection.mjs";
|
|
91
96
|
import {
|
|
92
97
|
holdEndpointUatNativeCompletion,
|
|
93
98
|
validateEndpointUatCompletionControl,
|
|
@@ -98,11 +103,11 @@ import {
|
|
|
98
103
|
validateEndpointModelSelection,
|
|
99
104
|
validateOpenerOutputContract,
|
|
100
105
|
} from "./hermes-runs-client.mjs";
|
|
101
|
-
import { createConfiguredRunsClient } from "./runs-api-connections.mjs";
|
|
102
106
|
import {
|
|
103
107
|
createOpenerStageExecution,
|
|
104
108
|
usesOpenerDraftStrategy,
|
|
105
109
|
} from "./opener-stage-execution.mjs";
|
|
110
|
+
import { createConfiguredRunsClient } from "./runs-api-connections.mjs";
|
|
106
111
|
|
|
107
112
|
const DATA_ROOT = "/opt/data";
|
|
108
113
|
const PROFILES_ROOT = join(DATA_ROOT, "profiles");
|
|
@@ -126,7 +131,7 @@ const RUNTIME_GENERATION = Number(
|
|
|
126
131
|
const BOOT_SESSION_ID = randomUUID();
|
|
127
132
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.21.5";
|
|
128
133
|
const INSTALLER_PACKAGE =
|
|
129
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
134
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.803";
|
|
130
135
|
const MCP_PACKAGE =
|
|
131
136
|
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.957";
|
|
132
137
|
export const ENDPOINT_MAX_CONCURRENT_RUNS = 10;
|
|
@@ -1074,6 +1079,10 @@ function endpointEventEnvelope(event, ordinal) {
|
|
|
1074
1079
|
outputDigest: event.outputDigest ?? null,
|
|
1075
1080
|
errorDigest: event.errorDigest ?? null,
|
|
1076
1081
|
evidenceDigest: event.evidence?.digest ?? null,
|
|
1082
|
+
// Which model of an endpoint's fallback chain the event belongs to.
|
|
1083
|
+
...(Number.isSafeInteger(event.chainStep)
|
|
1084
|
+
? { chainStep: event.chainStep }
|
|
1085
|
+
: {}),
|
|
1077
1086
|
...(event.servedRuntime || event.usage
|
|
1078
1087
|
? {
|
|
1079
1088
|
servedProvider: event.servedRuntime?.provider ?? null,
|
|
@@ -1402,10 +1411,19 @@ export function endpointOpenerOutputContract(claim) {
|
|
|
1402
1411
|
// A single-run opener on a caller-named model (Opus on OpenRouter) is held to
|
|
1403
1412
|
// the same strict schema, which the bridge sends as the provider's json_schema
|
|
1404
1413
|
// so the model cannot return malformed JSON. The machine's default model has
|
|
1405
|
-
// no strict transport, so it gets no contract.
|
|
1406
|
-
|
|
1414
|
+
// no strict transport, so it gets no contract. Neither does a Codex step: the
|
|
1415
|
+
// bridge enforces the strict format only on OpenRouter. In a fallback chain
|
|
1416
|
+
// the server attaches the schema for its custom Runs steps, so a Hermes step
|
|
1417
|
+
// (Codex or OpenRouter) runs without it, as it would on its own.
|
|
1418
|
+
export function endpointStrictOutputContract(
|
|
1419
|
+
claim,
|
|
1420
|
+
modelSelection,
|
|
1421
|
+
{ chained = false } = {}
|
|
1422
|
+
) {
|
|
1407
1423
|
if (
|
|
1408
1424
|
!modelSelection ||
|
|
1425
|
+
(!modelSelection.runsApiBaseUrl &&
|
|
1426
|
+
(chained || modelSelection.provider === "openai-codex")) ||
|
|
1409
1427
|
usesOpenerDraftStrategy(claim) ||
|
|
1410
1428
|
claim?.run?.productExecution?.workKind !== "OPENER" ||
|
|
1411
1429
|
!claim.run.productExecution.providerOutputSchema
|
|
@@ -1954,7 +1972,22 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
1954
1972
|
throw new Error("customer_endpoint_mcp_descriptor_rejected");
|
|
1955
1973
|
}
|
|
1956
1974
|
const apiKeyFile = getApiKeyFile({ claim, activeRuntime });
|
|
1957
|
-
|
|
1975
|
+
// The models this run tries, in order. Each step opens its own client from
|
|
1976
|
+
// a claim carrying exactly that step's selection, so the trusted
|
|
1977
|
+
// connection checks are the same as for a single-model run.
|
|
1978
|
+
const chain = endpointModelSelectionChain(modelSelection);
|
|
1979
|
+
const chained = chain.length > 1;
|
|
1980
|
+
// A one-model run is a chain of one; no selection is the Agent default.
|
|
1981
|
+
const steps = chain.length ? chain : [null];
|
|
1982
|
+
const stepClaim = (index) =>
|
|
1983
|
+
chain.length === 0
|
|
1984
|
+
? claim
|
|
1985
|
+
: { ...claim, run: { ...claim.run, modelSelection: chain[index] } };
|
|
1986
|
+
const runs = createRunsClient({
|
|
1987
|
+
apiKeyFile,
|
|
1988
|
+
claim: stepClaim(0),
|
|
1989
|
+
activeRuntime,
|
|
1990
|
+
});
|
|
1958
1991
|
const controller = new AbortController();
|
|
1959
1992
|
const abortFromOuter = () => controller.abort(outerSignal.reason);
|
|
1960
1993
|
outerSignal.addEventListener("abort", abortFromOuter, { once: true });
|
|
@@ -2056,7 +2089,6 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2056
2089
|
await abortablePause(20_000, controller.signal, sleepImpl);
|
|
2057
2090
|
}
|
|
2058
2091
|
})();
|
|
2059
|
-
let terminalEvent = null;
|
|
2060
2092
|
let ordinal = 0;
|
|
2061
2093
|
const pendingStreamEvents = [];
|
|
2062
2094
|
const flushStreamEvents = async () => {
|
|
@@ -2066,104 +2098,31 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2066
2098
|
pendingStreamEvents.splice(0, batch.length);
|
|
2067
2099
|
held.committedSequence = sequences.at(-1);
|
|
2068
2100
|
};
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
append(claim, identity, event, ordinal++)
|
|
2086
|
-
);
|
|
2087
|
-
return stageEventChain;
|
|
2088
|
-
};
|
|
2089
|
-
held.openerStages = createOpenerStageExecution({
|
|
2090
|
-
runs,
|
|
2091
|
-
input: claim.run.input,
|
|
2092
|
-
instructions: endpointRunInstructions(claim),
|
|
2093
|
-
outputContract: endpointOpenerOutputContract(claim),
|
|
2094
|
-
correlation: `${claim.attempt.conversationId}:${claim.attempt.runId}:${claim.attempt.id}:${randomUUID()}`,
|
|
2095
|
-
deadlineAt: new Date(deadlineMs),
|
|
2096
|
-
signal: controller.signal,
|
|
2097
|
-
now,
|
|
2098
|
-
validateOutput: (output) =>
|
|
2099
|
-
endpointProductProjection(claim.run.productExecution, output).ok,
|
|
2100
|
-
onSubmitted: async ({ stage, runId, requestDigest }) => {
|
|
2101
|
-
const first = held.nativeRunId === null;
|
|
2102
|
-
held.nativeRunId ??= runId;
|
|
2103
|
-
await appendStageEvent({
|
|
2104
|
-
kind: first
|
|
2105
|
-
? "NATIVE_SUBMIT_ACCEPTED"
|
|
2106
|
-
: "OPENER_STAGE_SUBMIT_ACCEPTED",
|
|
2107
|
-
stage,
|
|
2108
|
-
requestDigest,
|
|
2109
|
-
nativeRunIdDigest: sha256(runId),
|
|
2110
|
-
});
|
|
2111
|
-
if (held.pendingControl) await queueControl(held.pendingControl);
|
|
2112
|
-
},
|
|
2113
|
-
onReceipt: async (receipt) => {
|
|
2114
|
-
await appendStageEvent({
|
|
2115
|
-
kind: "OPENER_STAGE_COMPLETED",
|
|
2116
|
-
...receipt,
|
|
2101
|
+
const done = (result) => ({ done: true, result });
|
|
2102
|
+
// Runs one model of the chain. A final outcome has already completed the
|
|
2103
|
+
// attempt (`done`); a clean pre-output failure returns its class and a
|
|
2104
|
+
// `finish` that completes the attempt as a single model would have.
|
|
2105
|
+
const runNativeStep = async (stepIndex, stepRuns) => {
|
|
2106
|
+
const selection = chain[stepIndex] ?? null;
|
|
2107
|
+
// Events of a chained run name the model they belong to.
|
|
2108
|
+
const tag = (event) =>
|
|
2109
|
+
chained ? { ...event, chainStep: stepIndex } : event;
|
|
2110
|
+
if (!stepRuns) {
|
|
2111
|
+
return {
|
|
2112
|
+
failure: "STEP_UNAVAILABLE",
|
|
2113
|
+
finish: async () => {
|
|
2114
|
+
await complete(claim, identity, {
|
|
2115
|
+
attemptStatus: "FAILED",
|
|
2116
|
+
status: "FAILED",
|
|
2117
2117
|
});
|
|
2118
|
+
return { ok: false, code: "FAILED" };
|
|
2118
2119
|
},
|
|
2119
|
-
}
|
|
2120
|
-
const selected = await held.openerStages.run();
|
|
2121
|
-
const observed = await holdEndpointUatNativeCompletion({
|
|
2122
|
-
control: uatCompletionControl,
|
|
2123
|
-
claim,
|
|
2124
|
-
event: {
|
|
2125
|
-
kind: "RUN_COMPLETED",
|
|
2126
|
-
terminal: true,
|
|
2127
|
-
output: selected.output,
|
|
2128
|
-
outputDigest: selected.outputDigest,
|
|
2129
|
-
timestamp: selected.timestamp,
|
|
2130
|
-
nativeEventIdentity: selected.nativeEventIdentity,
|
|
2131
|
-
},
|
|
2132
|
-
now,
|
|
2133
|
-
sleep: sleepImpl,
|
|
2134
|
-
signal: controller.signal,
|
|
2135
|
-
});
|
|
2136
|
-
const late = deadlineExpired || now().getTime() >= deadlineMs;
|
|
2137
|
-
await append(
|
|
2138
|
-
claim,
|
|
2139
|
-
identity,
|
|
2140
|
-
late
|
|
2141
|
-
? {
|
|
2142
|
-
kind: "LATE_COMPLETION_NOTICE",
|
|
2143
|
-
outputDigest: observed.outputDigest,
|
|
2144
|
-
timestamp: observed.timestamp,
|
|
2145
|
-
nativeEventIdentity: observed.nativeEventIdentity,
|
|
2146
|
-
}
|
|
2147
|
-
: observed,
|
|
2148
|
-
ordinal++
|
|
2149
|
-
);
|
|
2150
|
-
if (late) return { ok: true, code: "LATE_NOTICE_ONLY" };
|
|
2151
|
-
const projection = endpointProductProjection(
|
|
2152
|
-
claim.run.productExecution,
|
|
2153
|
-
selected.output
|
|
2154
|
-
);
|
|
2155
|
-
if (!projection.ok) throw new Error("OPENER_STAGE_OUTPUT_INVALID");
|
|
2156
|
-
const completion = await complete(claim, identity, {
|
|
2157
|
-
attemptStatus: "SUCCEEDED",
|
|
2158
|
-
status: "SUCCEEDED",
|
|
2159
|
-
output: selected.output,
|
|
2160
|
-
...projection.completion,
|
|
2161
|
-
});
|
|
2162
|
-
return completion?.accepted === true
|
|
2163
|
-
? { ok: true, code: "SUCCEEDED" }
|
|
2164
|
-
: { ok: false, code: "OPENER_STAGE_COMPLETION_REJECTED" };
|
|
2120
|
+
};
|
|
2165
2121
|
}
|
|
2122
|
+
held.runs = stepRuns;
|
|
2123
|
+
held.nativeRunId = null;
|
|
2166
2124
|
let submitted;
|
|
2125
|
+
let terminalEvent = null;
|
|
2167
2126
|
const submitRequest = {
|
|
2168
2127
|
input: claim.run.input,
|
|
2169
2128
|
instructions: endpointRunInstructions(claim),
|
|
@@ -2171,17 +2130,26 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2171
2130
|
claim.run.conversationHistory
|
|
2172
2131
|
),
|
|
2173
2132
|
correlation: `${claim.attempt.conversationId}:${claim.attempt.runId}:${claim.attempt.id}`,
|
|
2174
|
-
...(
|
|
2133
|
+
...(selection ? { modelSelection: selection } : {}),
|
|
2175
2134
|
};
|
|
2176
2135
|
const strictOutputContract = endpointStrictOutputContract(
|
|
2177
2136
|
claim,
|
|
2178
|
-
|
|
2137
|
+
selection,
|
|
2138
|
+
{ chained }
|
|
2179
2139
|
);
|
|
2180
2140
|
if (strictOutputContract)
|
|
2181
2141
|
submitRequest.outputContract = strictOutputContract;
|
|
2142
|
+
// A busy step stops waiting at its own budget so the next model still
|
|
2143
|
+
// has time; the last step (or a single model) waits until the deadline.
|
|
2144
|
+
const admissionDeadlineMs = endpointChainStepAdmissionDeadline({
|
|
2145
|
+
startedAtMs: now().getTime(),
|
|
2146
|
+
deadlineMs,
|
|
2147
|
+
stepIndex,
|
|
2148
|
+
stepCount: steps.length,
|
|
2149
|
+
});
|
|
2182
2150
|
while (!submitted) {
|
|
2183
2151
|
try {
|
|
2184
|
-
submitted = await
|
|
2152
|
+
submitted = await stepRuns.submit(submitRequest, {
|
|
2185
2153
|
signal: controller.signal,
|
|
2186
2154
|
});
|
|
2187
2155
|
} catch (error) {
|
|
@@ -2190,22 +2158,27 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2190
2158
|
attemptStatus: "UNCERTAIN",
|
|
2191
2159
|
status: "UNCERTAIN",
|
|
2192
2160
|
});
|
|
2193
|
-
return { ok: false, code: "SUBMIT_AMBIGUOUS" };
|
|
2161
|
+
return done({ ok: false, code: "SUBMIT_AMBIGUOUS" });
|
|
2194
2162
|
}
|
|
2195
2163
|
if (error?.classification !== "SUBMIT_NOT_ADMITTED") throw error;
|
|
2196
|
-
const
|
|
2197
|
-
if (deadlineExpired || remainingMs <= 0) {
|
|
2164
|
+
const notAdmittedTimeout = async () => {
|
|
2198
2165
|
await complete(claim, identity, {
|
|
2199
2166
|
attemptStatus: "TIMED_OUT",
|
|
2200
2167
|
status: "TIMED_OUT",
|
|
2201
2168
|
});
|
|
2202
2169
|
return { ok: false, code: "SUBMIT_NOT_ADMITTED_TIMEOUT" };
|
|
2203
|
-
}
|
|
2170
|
+
};
|
|
2171
|
+
const remainingMs = deadlineMs - now().getTime();
|
|
2172
|
+
if (deadlineExpired || remainingMs <= 0)
|
|
2173
|
+
return done(await notAdmittedTimeout());
|
|
2174
|
+
const admissionRemainingMs = admissionDeadlineMs - now().getTime();
|
|
2175
|
+
if (admissionRemainingMs <= 0)
|
|
2176
|
+
return { failure: "NOT_ADMITTED", finish: notAdmittedTimeout };
|
|
2204
2177
|
const retryAfterMs = Number.isFinite(error.retryAfterMs)
|
|
2205
2178
|
? Math.max(100, Math.min(5_000, error.retryAfterMs))
|
|
2206
2179
|
: 1_000;
|
|
2207
2180
|
await abortablePause(
|
|
2208
|
-
Math.min(retryAfterMs,
|
|
2181
|
+
Math.min(retryAfterMs, admissionRemainingMs),
|
|
2209
2182
|
controller.signal,
|
|
2210
2183
|
sleepImpl
|
|
2211
2184
|
);
|
|
@@ -2219,17 +2192,17 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2219
2192
|
await append(
|
|
2220
2193
|
claim,
|
|
2221
2194
|
identity,
|
|
2222
|
-
{
|
|
2195
|
+
tag({
|
|
2223
2196
|
kind: "NATIVE_SUBMIT_ACCEPTED",
|
|
2224
2197
|
requestDigest: submitted.requestDigest,
|
|
2225
2198
|
nativeRunIdDigest: sha256(submitted.runId),
|
|
2226
|
-
},
|
|
2199
|
+
}),
|
|
2227
2200
|
ordinal++
|
|
2228
2201
|
);
|
|
2229
2202
|
if (held.pendingControl) await queueControl(held.pendingControl);
|
|
2230
2203
|
let streamed;
|
|
2231
2204
|
try {
|
|
2232
|
-
streamed = await
|
|
2205
|
+
streamed = await stepRuns.streamEvents(submitted.runId, {
|
|
2233
2206
|
signal: controller.signal,
|
|
2234
2207
|
onEvent: async (event) => {
|
|
2235
2208
|
const observedEvent = await holdEndpointUatNativeCompletion({
|
|
@@ -2259,7 +2232,7 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2259
2232
|
}
|
|
2260
2233
|
: observedEvent;
|
|
2261
2234
|
pendingStreamEvents.push({
|
|
2262
|
-
event: durableEvent,
|
|
2235
|
+
event: tag(durableEvent),
|
|
2263
2236
|
ordinal: ordinal++,
|
|
2264
2237
|
});
|
|
2265
2238
|
const approvalBoundary =
|
|
@@ -2283,7 +2256,7 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2283
2256
|
}
|
|
2284
2257
|
}
|
|
2285
2258
|
if (!terminalEvent && !controller.signal.aborted) {
|
|
2286
|
-
const status = await
|
|
2259
|
+
const status = await stepRuns.status(submitted.runId, {
|
|
2287
2260
|
signal: controller.signal,
|
|
2288
2261
|
});
|
|
2289
2262
|
if (status.terminal) {
|
|
@@ -2300,7 +2273,7 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2300
2273
|
late: deadlineExpired || now().getTime() >= deadlineMs,
|
|
2301
2274
|
};
|
|
2302
2275
|
} else if (status.ok === false) {
|
|
2303
|
-
await append(claim, identity, status, ordinal++);
|
|
2276
|
+
await append(claim, identity, tag(status), ordinal++);
|
|
2304
2277
|
}
|
|
2305
2278
|
}
|
|
2306
2279
|
if (
|
|
@@ -2315,18 +2288,24 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2315
2288
|
// Server timeout is already canonical. The committed digest-only
|
|
2316
2289
|
// late notice above is the sole mutation; never replace timeout with
|
|
2317
2290
|
// a second completion or expose the late output as canonical.
|
|
2318
|
-
return { ok: true, code: "LATE_NOTICE_ONLY" };
|
|
2291
|
+
return done({ ok: true, code: "LATE_NOTICE_ONLY" });
|
|
2319
2292
|
}
|
|
2320
2293
|
const productProjection = endpointProductProjection(
|
|
2321
2294
|
claim.run.productExecution,
|
|
2322
2295
|
terminalEvent.output
|
|
2323
2296
|
);
|
|
2324
2297
|
if (!productProjection.ok) {
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2298
|
+
// The output was never used, so another model may still write.
|
|
2299
|
+
return {
|
|
2300
|
+
failure: "OUTPUT_CONTRACT_FAILED",
|
|
2301
|
+
finish: async () => {
|
|
2302
|
+
await complete(claim, identity, {
|
|
2303
|
+
attemptStatus: "SUCCEEDED",
|
|
2304
|
+
status: "OUTPUT_CONTRACT_FAILED",
|
|
2305
|
+
});
|
|
2306
|
+
return { ok: false, code: "OUTPUT_CONTRACT_FAILED" };
|
|
2307
|
+
},
|
|
2308
|
+
};
|
|
2330
2309
|
}
|
|
2331
2310
|
await complete(claim, identity, {
|
|
2332
2311
|
attemptStatus: "SUCCEEDED",
|
|
@@ -2334,7 +2313,7 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2334
2313
|
output: terminalEvent.output,
|
|
2335
2314
|
...productProjection.completion,
|
|
2336
2315
|
});
|
|
2337
|
-
return { ok: true, code: "SUCCEEDED" };
|
|
2316
|
+
return done({ ok: true, code: "SUCCEEDED" });
|
|
2338
2317
|
}
|
|
2339
2318
|
if (
|
|
2340
2319
|
terminalEvent?.classification === "CANCELLED_POSITIVE" ||
|
|
@@ -2345,26 +2324,185 @@ export function createCustomerEndpointRelay(options = {}) {
|
|
|
2345
2324
|
status: "CANCELLED_PRE_EFFECT",
|
|
2346
2325
|
positiveCancellation: true,
|
|
2347
2326
|
});
|
|
2348
|
-
return { ok: true, code: "CANCELLED" };
|
|
2327
|
+
return done({ ok: true, code: "CANCELLED" });
|
|
2349
2328
|
}
|
|
2350
2329
|
if (terminalEvent) {
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2330
|
+
// A native failure carries no output (Claude Code usage limit and
|
|
2331
|
+
// sign-in refusals arrive this way too).
|
|
2332
|
+
return {
|
|
2333
|
+
failure: "NATIVE_FAILED",
|
|
2334
|
+
finish: async () => {
|
|
2335
|
+
await complete(claim, identity, {
|
|
2336
|
+
attemptStatus: "FAILED",
|
|
2337
|
+
status: "FAILED",
|
|
2338
|
+
});
|
|
2339
|
+
return { ok: false, code: "FAILED" };
|
|
2340
|
+
},
|
|
2341
|
+
};
|
|
2356
2342
|
}
|
|
2357
2343
|
const deadline = deadlineExpired || now().getTime() >= deadlineMs;
|
|
2358
2344
|
await complete(claim, identity, {
|
|
2359
2345
|
attemptStatus: deadline ? "TIMED_OUT_UNCERTAIN" : "UNCERTAIN",
|
|
2360
2346
|
status: deadline ? "TIMED_OUT_UNCERTAIN" : "UNCERTAIN",
|
|
2361
2347
|
});
|
|
2362
|
-
return {
|
|
2348
|
+
return done({
|
|
2363
2349
|
ok: false,
|
|
2364
2350
|
code:
|
|
2365
2351
|
streamed?.classification ??
|
|
2366
2352
|
(deadline ? "TIMED_OUT_UNCERTAIN" : "NATIVE_LOST_UNCERTAIN"),
|
|
2367
|
-
};
|
|
2353
|
+
});
|
|
2354
|
+
};
|
|
2355
|
+
try {
|
|
2356
|
+
if (usesOpenerDraftStrategy(claim) && modelSelection) {
|
|
2357
|
+
// The three-draft stages are pinned to the Codex output-format path in
|
|
2358
|
+
// the Hermes bridge; refuse rather than silently serve the default.
|
|
2359
|
+
await complete(claim, identity, {
|
|
2360
|
+
attemptStatus: "FAILED",
|
|
2361
|
+
status: "FAILED",
|
|
2362
|
+
});
|
|
2363
|
+
return { ok: false, code: "OPENER_STAGE_MODEL_SELECTION_UNSUPPORTED" };
|
|
2364
|
+
}
|
|
2365
|
+
if (usesOpenerDraftStrategy(claim)) {
|
|
2366
|
+
// Phase 206: writers are private native sessions within this one fenced
|
|
2367
|
+
// claim. Only the editor's validated result reaches durable completion.
|
|
2368
|
+
let stageEventChain = Promise.resolve();
|
|
2369
|
+
const appendStageEvent = (event) => {
|
|
2370
|
+
stageEventChain = stageEventChain.then(() =>
|
|
2371
|
+
append(claim, identity, event, ordinal++)
|
|
2372
|
+
);
|
|
2373
|
+
return stageEventChain;
|
|
2374
|
+
};
|
|
2375
|
+
held.openerStages = createOpenerStageExecution({
|
|
2376
|
+
runs,
|
|
2377
|
+
input: claim.run.input,
|
|
2378
|
+
instructions: endpointRunInstructions(claim),
|
|
2379
|
+
outputContract: endpointOpenerOutputContract(claim),
|
|
2380
|
+
correlation: `${claim.attempt.conversationId}:${claim.attempt.runId}:${claim.attempt.id}:${randomUUID()}`,
|
|
2381
|
+
deadlineAt: new Date(deadlineMs),
|
|
2382
|
+
signal: controller.signal,
|
|
2383
|
+
now,
|
|
2384
|
+
validateOutput: (output) =>
|
|
2385
|
+
endpointProductProjection(claim.run.productExecution, output).ok,
|
|
2386
|
+
onSubmitted: async ({ stage, runId, requestDigest }) => {
|
|
2387
|
+
const first = held.nativeRunId === null;
|
|
2388
|
+
held.nativeRunId ??= runId;
|
|
2389
|
+
await appendStageEvent({
|
|
2390
|
+
kind: first
|
|
2391
|
+
? "NATIVE_SUBMIT_ACCEPTED"
|
|
2392
|
+
: "OPENER_STAGE_SUBMIT_ACCEPTED",
|
|
2393
|
+
stage,
|
|
2394
|
+
requestDigest,
|
|
2395
|
+
nativeRunIdDigest: sha256(runId),
|
|
2396
|
+
});
|
|
2397
|
+
if (held.pendingControl) await queueControl(held.pendingControl);
|
|
2398
|
+
},
|
|
2399
|
+
onReceipt: async (receipt) => {
|
|
2400
|
+
await appendStageEvent({
|
|
2401
|
+
kind: "OPENER_STAGE_COMPLETED",
|
|
2402
|
+
...receipt,
|
|
2403
|
+
});
|
|
2404
|
+
},
|
|
2405
|
+
});
|
|
2406
|
+
const selected = await held.openerStages.run();
|
|
2407
|
+
const observed = await holdEndpointUatNativeCompletion({
|
|
2408
|
+
control: uatCompletionControl,
|
|
2409
|
+
claim,
|
|
2410
|
+
event: {
|
|
2411
|
+
kind: "RUN_COMPLETED",
|
|
2412
|
+
terminal: true,
|
|
2413
|
+
output: selected.output,
|
|
2414
|
+
outputDigest: selected.outputDigest,
|
|
2415
|
+
timestamp: selected.timestamp,
|
|
2416
|
+
nativeEventIdentity: selected.nativeEventIdentity,
|
|
2417
|
+
},
|
|
2418
|
+
now,
|
|
2419
|
+
sleep: sleepImpl,
|
|
2420
|
+
signal: controller.signal,
|
|
2421
|
+
});
|
|
2422
|
+
const late = deadlineExpired || now().getTime() >= deadlineMs;
|
|
2423
|
+
await append(
|
|
2424
|
+
claim,
|
|
2425
|
+
identity,
|
|
2426
|
+
late
|
|
2427
|
+
? {
|
|
2428
|
+
kind: "LATE_COMPLETION_NOTICE",
|
|
2429
|
+
outputDigest: observed.outputDigest,
|
|
2430
|
+
timestamp: observed.timestamp,
|
|
2431
|
+
nativeEventIdentity: observed.nativeEventIdentity,
|
|
2432
|
+
}
|
|
2433
|
+
: observed,
|
|
2434
|
+
ordinal++
|
|
2435
|
+
);
|
|
2436
|
+
if (late) return { ok: true, code: "LATE_NOTICE_ONLY" };
|
|
2437
|
+
const projection = endpointProductProjection(
|
|
2438
|
+
claim.run.productExecution,
|
|
2439
|
+
selected.output
|
|
2440
|
+
);
|
|
2441
|
+
if (!projection.ok) throw new Error("OPENER_STAGE_OUTPUT_INVALID");
|
|
2442
|
+
const completion = await complete(claim, identity, {
|
|
2443
|
+
attemptStatus: "SUCCEEDED",
|
|
2444
|
+
status: "SUCCEEDED",
|
|
2445
|
+
output: selected.output,
|
|
2446
|
+
...projection.completion,
|
|
2447
|
+
});
|
|
2448
|
+
return completion?.accepted === true
|
|
2449
|
+
? { ok: true, code: "SUCCEEDED" }
|
|
2450
|
+
: { ok: false, code: "OPENER_STAGE_COMPLETION_REJECTED" };
|
|
2451
|
+
}
|
|
2452
|
+
// A step that failed cleanly before producing any output hands the run
|
|
2453
|
+
// to the next model; every other outcome completes this attempt once.
|
|
2454
|
+
// Only when the failed step cannot have changed anything: a product draft
|
|
2455
|
+
// run has no effects by construction, and an endpoint without tools has
|
|
2456
|
+
// nothing to call. Any other run may already have used a tool, so another
|
|
2457
|
+
// model must not repeat the turn.
|
|
2458
|
+
const chainMayAdvance =
|
|
2459
|
+
claim.run.productExecution != null ||
|
|
2460
|
+
(descriptorBinding.expected.allowedToolNames ?? []).length === 0;
|
|
2461
|
+
const history = [];
|
|
2462
|
+
let stepIndex = 0;
|
|
2463
|
+
let stepRuns = runs;
|
|
2464
|
+
for (;;) {
|
|
2465
|
+
const outcome = await runNativeStep(stepIndex, stepRuns);
|
|
2466
|
+
if (outcome.done) return outcome.result;
|
|
2467
|
+
// A stop, a gateway fence or a lost lease (which aborts the
|
|
2468
|
+
// controller) ends the run as a single model would have ended it.
|
|
2469
|
+
const stopRequested =
|
|
2470
|
+
held.stopRequested ||
|
|
2471
|
+
held.pendingControl?.kind === "STOP_PENDING" ||
|
|
2472
|
+
controller.signal.aborted;
|
|
2473
|
+
const decision =
|
|
2474
|
+
stopRequested || !chainMayAdvance
|
|
2475
|
+
? { advance: false }
|
|
2476
|
+
: nextEndpointChainStep(
|
|
2477
|
+
steps,
|
|
2478
|
+
history,
|
|
2479
|
+
outcome.failure,
|
|
2480
|
+
deadlineMs - now().getTime()
|
|
2481
|
+
);
|
|
2482
|
+
if (!decision.advance) return await outcome.finish();
|
|
2483
|
+
await append(
|
|
2484
|
+
claim,
|
|
2485
|
+
identity,
|
|
2486
|
+
{
|
|
2487
|
+
kind: "CHAIN_STEP_ADVANCED",
|
|
2488
|
+
classification: outcome.failure,
|
|
2489
|
+
chainStep: stepIndex,
|
|
2490
|
+
},
|
|
2491
|
+
ordinal++
|
|
2492
|
+
);
|
|
2493
|
+
history.push(outcome.failure);
|
|
2494
|
+
stepIndex = decision.stepIndex;
|
|
2495
|
+
try {
|
|
2496
|
+
stepRuns = createRunsClient({
|
|
2497
|
+
apiKeyFile,
|
|
2498
|
+
claim: stepClaim(stepIndex),
|
|
2499
|
+
activeRuntime,
|
|
2500
|
+
});
|
|
2501
|
+
} catch {
|
|
2502
|
+
// Nothing was sent for this step; it counts as a clean failure.
|
|
2503
|
+
stepRuns = null;
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2368
2506
|
} catch (error) {
|
|
2369
2507
|
if (heartbeatFailure) throw heartbeatFailure;
|
|
2370
2508
|
if (
|
|
@@ -5,6 +5,7 @@ export const ENDPOINT_MODEL_SELECTION_ALLOWLIST: Readonly<{
|
|
|
5
5
|
"openai/gpt-6-luna",
|
|
6
6
|
"anthropic/claude-sonnet-5.5",
|
|
7
7
|
];
|
|
8
|
+
"openai-codex": readonly ["gpt-6.1-sol"];
|
|
8
9
|
}>;
|
|
9
10
|
export const ENDPOINT_MODEL_REASONING_EFFORTS: readonly [
|
|
10
11
|
"none",
|
|
@@ -13,14 +14,60 @@ export const ENDPOINT_MODEL_REASONING_EFFORTS: readonly [
|
|
|
13
14
|
"high",
|
|
14
15
|
"xhigh",
|
|
15
16
|
];
|
|
16
|
-
export
|
|
17
|
+
export const ENDPOINT_MODEL_MAX_FALLBACKS: 2;
|
|
18
|
+
export type EndpointModelStep = Readonly<{
|
|
17
19
|
provider: string;
|
|
18
20
|
model: string;
|
|
19
21
|
reasoningEffort: (typeof ENDPOINT_MODEL_REASONING_EFFORTS)[number];
|
|
20
22
|
runsApiBaseUrl?: string;
|
|
21
|
-
fallbackPolicy?: "STOP" | "PRODUCT_DEFAULT";
|
|
22
23
|
}>;
|
|
24
|
+
export type EndpointModelSelection = Readonly<
|
|
25
|
+
EndpointModelStep & {
|
|
26
|
+
fallbackPolicy?: "STOP" | "PRODUCT_DEFAULT";
|
|
27
|
+
fallbacks?: readonly EndpointModelStep[];
|
|
28
|
+
}
|
|
29
|
+
>;
|
|
30
|
+
export type EndpointChainFailure =
|
|
31
|
+
| "NATIVE_FAILED"
|
|
32
|
+
| "OUTPUT_CONTRACT_FAILED"
|
|
33
|
+
| "NOT_ADMITTED"
|
|
34
|
+
| "STEP_UNAVAILABLE";
|
|
35
|
+
export type EndpointChainDecision =
|
|
36
|
+
| Readonly<{
|
|
37
|
+
advance: true;
|
|
38
|
+
stepIndex: number;
|
|
39
|
+
selection: EndpointModelStep;
|
|
40
|
+
}>
|
|
41
|
+
| Readonly<{
|
|
42
|
+
advance: false;
|
|
43
|
+
reason:
|
|
44
|
+
| "INVALID"
|
|
45
|
+
| "NOT_ADVANCEABLE"
|
|
46
|
+
| "CHAIN_EXHAUSTED"
|
|
47
|
+
| "NO_TIME_LEFT";
|
|
48
|
+
}>;
|
|
23
49
|
export function normalizeRunsApiBaseUrl(value: unknown): string | null;
|
|
24
50
|
export function compileEndpointModelSelection(
|
|
25
51
|
value: unknown
|
|
26
52
|
): EndpointModelSelection | null | "invalid";
|
|
53
|
+
export function endpointModelSelectionChain(
|
|
54
|
+
selection: unknown
|
|
55
|
+
): EndpointModelStep[];
|
|
56
|
+
export function endpointModelSelectionSource(
|
|
57
|
+
selection: { provider: string; runsApiBaseUrl?: string } | null | undefined
|
|
58
|
+
): "default" | "openrouter" | "codex" | "custom";
|
|
59
|
+
export const ENDPOINT_CHAIN_STEP_ADMISSION_MS: number;
|
|
60
|
+
export const ENDPOINT_CHAIN_MIN_STEP_MS: number;
|
|
61
|
+
export const ENDPOINT_CHAIN_ADVANCE_FAILURES: readonly EndpointChainFailure[];
|
|
62
|
+
export function nextEndpointChainStep(
|
|
63
|
+
chain: readonly EndpointModelStep[],
|
|
64
|
+
history: readonly string[],
|
|
65
|
+
failure: string,
|
|
66
|
+
remainingMs?: number
|
|
67
|
+
): EndpointChainDecision;
|
|
68
|
+
export function endpointChainStepAdmissionDeadline(input: {
|
|
69
|
+
startedAtMs: number;
|
|
70
|
+
deadlineMs: number;
|
|
71
|
+
stepIndex: number;
|
|
72
|
+
stepCount: number;
|
|
73
|
+
}): number;
|
|
@@ -8,6 +8,9 @@ export const ENDPOINT_MODEL_SELECTION_ALLOWLIST = Object.freeze({
|
|
|
8
8
|
"openai/gpt-6-luna",
|
|
9
9
|
"anthropic/claude-sonnet-5.5",
|
|
10
10
|
]),
|
|
11
|
+
// The Agent's own Codex model (model-auth-reconciler.mjs), served by Hermes
|
|
12
|
+
// per run from the machine's Codex subscription pool.
|
|
13
|
+
"openai-codex": Object.freeze(["gpt-6.1-sol"]),
|
|
11
14
|
});
|
|
12
15
|
export const ENDPOINT_MODEL_REASONING_EFFORTS = Object.freeze([
|
|
13
16
|
"none",
|
|
@@ -16,6 +19,7 @@ export const ENDPOINT_MODEL_REASONING_EFFORTS = Object.freeze([
|
|
|
16
19
|
"high",
|
|
17
20
|
"xhigh",
|
|
18
21
|
]);
|
|
22
|
+
export const ENDPOINT_MODEL_MAX_FALLBACKS = 2;
|
|
19
23
|
|
|
20
24
|
export function normalizeRunsApiBaseUrl(value) {
|
|
21
25
|
if (
|
|
@@ -45,39 +49,33 @@ export function normalizeRunsApiBaseUrl(value) {
|
|
|
45
49
|
}
|
|
46
50
|
}
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if (typeof value !== "object" || Array.isArray(value)) return
|
|
52
|
+
// One model: provider, model, effort and, for a custom Runs API, its URL.
|
|
53
|
+
function compileEndpointModelStep(value, extraKeys = []) {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
51
55
|
const custom = Object.hasOwn(value, "runsApiBaseUrl");
|
|
52
|
-
const hasFallbackPolicy = Object.hasOwn(value, "fallbackPolicy");
|
|
53
56
|
if (
|
|
54
57
|
Object.keys(value)
|
|
55
|
-
.filter((key) => key
|
|
58
|
+
.filter((key) => !extraKeys.includes(key))
|
|
56
59
|
.sort()
|
|
57
60
|
.join(",") !==
|
|
58
61
|
(custom
|
|
59
62
|
? "model,provider,reasoningEffort,runsApiBaseUrl"
|
|
60
63
|
: "model,provider,reasoningEffort")
|
|
61
64
|
)
|
|
62
|
-
return
|
|
63
|
-
if (
|
|
64
|
-
hasFallbackPolicy &&
|
|
65
|
-
!["STOP", "PRODUCT_DEFAULT"].includes(value.fallbackPolicy)
|
|
66
|
-
)
|
|
67
|
-
return "invalid";
|
|
65
|
+
return null;
|
|
68
66
|
const { provider, model, reasoningEffort } = value;
|
|
69
67
|
if (
|
|
70
68
|
typeof provider !== "string" ||
|
|
71
69
|
typeof model !== "string" ||
|
|
72
70
|
!ENDPOINT_MODEL_REASONING_EFFORTS.includes(reasoningEffort)
|
|
73
71
|
)
|
|
74
|
-
return
|
|
72
|
+
return null;
|
|
75
73
|
if (
|
|
76
74
|
!custom &&
|
|
77
75
|
(!Object.hasOwn(ENDPOINT_MODEL_SELECTION_ALLOWLIST, provider) ||
|
|
78
76
|
!ENDPOINT_MODEL_SELECTION_ALLOWLIST[provider].includes(model))
|
|
79
77
|
)
|
|
80
|
-
return
|
|
78
|
+
return null;
|
|
81
79
|
const runsApiBaseUrl = custom
|
|
82
80
|
? normalizeRunsApiBaseUrl(value.runsApiBaseUrl)
|
|
83
81
|
: null;
|
|
@@ -87,12 +85,150 @@ export function compileEndpointModelSelection(value) {
|
|
|
87
85
|
!/^[a-z][a-z0-9_-]{0,63}$/.test(provider) ||
|
|
88
86
|
!/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,199}$/.test(model))
|
|
89
87
|
)
|
|
90
|
-
return
|
|
88
|
+
return null;
|
|
91
89
|
return Object.freeze({
|
|
92
90
|
provider,
|
|
93
91
|
model,
|
|
94
92
|
reasoningEffort,
|
|
95
93
|
...(custom ? { runsApiBaseUrl } : {}),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sameStep = (left, right) =>
|
|
98
|
+
left.provider === right.provider &&
|
|
99
|
+
left.model === right.model &&
|
|
100
|
+
left.reasoningEffort === right.reasoningEffort &&
|
|
101
|
+
left.runsApiBaseUrl === right.runsApiBaseUrl;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The top-level fields are the endpoint's first model. An optional ordered
|
|
105
|
+
* `fallbacks` list holds up to two more, each a full model choice of its own;
|
|
106
|
+
* `fallbackPolicy` applies only after every model in the chain has failed.
|
|
107
|
+
*/
|
|
108
|
+
export function compileEndpointModelSelection(value) {
|
|
109
|
+
if (value === undefined || value === null) return null;
|
|
110
|
+
if (typeof value !== "object" || Array.isArray(value)) return "invalid";
|
|
111
|
+
const primary = compileEndpointModelStep(value, [
|
|
112
|
+
"fallbackPolicy",
|
|
113
|
+
"fallbacks",
|
|
114
|
+
]);
|
|
115
|
+
if (!primary) return "invalid";
|
|
116
|
+
const hasFallbackPolicy = Object.hasOwn(value, "fallbackPolicy");
|
|
117
|
+
if (
|
|
118
|
+
hasFallbackPolicy &&
|
|
119
|
+
!["STOP", "PRODUCT_DEFAULT"].includes(value.fallbackPolicy)
|
|
120
|
+
)
|
|
121
|
+
return "invalid";
|
|
122
|
+
let fallbacks = null;
|
|
123
|
+
if (Object.hasOwn(value, "fallbacks")) {
|
|
124
|
+
if (
|
|
125
|
+
!Array.isArray(value.fallbacks) ||
|
|
126
|
+
value.fallbacks.length < 1 ||
|
|
127
|
+
value.fallbacks.length > ENDPOINT_MODEL_MAX_FALLBACKS
|
|
128
|
+
)
|
|
129
|
+
return "invalid";
|
|
130
|
+
fallbacks = value.fallbacks.map((step) => compileEndpointModelStep(step));
|
|
131
|
+
if (
|
|
132
|
+
fallbacks.some(
|
|
133
|
+
(step, index) =>
|
|
134
|
+
!step ||
|
|
135
|
+
[primary, ...fallbacks.slice(0, index)].some((earlier) =>
|
|
136
|
+
sameStep(earlier, step)
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return "invalid";
|
|
141
|
+
}
|
|
142
|
+
return Object.freeze({
|
|
143
|
+
...primary,
|
|
96
144
|
...(hasFallbackPolicy ? { fallbackPolicy: value.fallbackPolicy } : {}),
|
|
145
|
+
...(fallbacks ? { fallbacks: Object.freeze(fallbacks) } : {}),
|
|
97
146
|
});
|
|
98
147
|
}
|
|
148
|
+
|
|
149
|
+
/** The models a run tries, in order; empty means the Agent's own default. */
|
|
150
|
+
export function endpointModelSelectionChain(selection) {
|
|
151
|
+
const compiled = compileEndpointModelSelection(selection);
|
|
152
|
+
if (!compiled || compiled === "invalid") return [];
|
|
153
|
+
const { fallbacks = [], fallbackPolicy: _policy, ...primary } = compiled;
|
|
154
|
+
return [Object.freeze(primary), ...fallbacks];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The settings source a model belongs to: Agent default, OpenRouter, Codex or a custom Runs API. */
|
|
158
|
+
export function endpointModelSelectionSource(selection) {
|
|
159
|
+
if (!selection) return "default";
|
|
160
|
+
if (selection.runsApiBaseUrl) return "custom";
|
|
161
|
+
return selection.provider === "openai-codex" ? "codex" : "openrouter";
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A step that cannot be admitted waits this long at most before the next
|
|
165
|
+
// model takes the run. Claude Code runs take 9-15 s, so a busy slot normally
|
|
166
|
+
// frees well inside it.
|
|
167
|
+
export const ENDPOINT_CHAIN_STEP_ADMISSION_MS = 30_000;
|
|
168
|
+
// Starting another model with less time than this left cannot finish it.
|
|
169
|
+
export const ENDPOINT_CHAIN_MIN_STEP_MS = 5_000;
|
|
170
|
+
// A step that failed in one of these ways produced no output, so another
|
|
171
|
+
// model can write without risking a second draft or send.
|
|
172
|
+
export const ENDPOINT_CHAIN_ADVANCE_FAILURES = Object.freeze([
|
|
173
|
+
"NATIVE_FAILED", // the run failed with no output (incl. Claude Code usage limit or sign-in refusals)
|
|
174
|
+
"OUTPUT_CONTRACT_FAILED", // its output failed the product contract and was not used
|
|
175
|
+
"NOT_ADMITTED", // the service stayed busy (429/503) for the step's admission budget
|
|
176
|
+
"STEP_UNAVAILABLE", // this machine could not open the step's connection; nothing was sent
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Decide whether a run moves to the next model of its chain. `history` holds
|
|
181
|
+
* the failure classes of the steps already left behind; `failure` is how the
|
|
182
|
+
* current step ended. Anything not listed in ENDPOINT_CHAIN_ADVANCE_FAILURES
|
|
183
|
+
* (an ambiguous submit, a lost stream, a stop or cancel, a late completion,
|
|
184
|
+
* success including a SUPPRESS decision, a timeout) ends the run.
|
|
185
|
+
*/
|
|
186
|
+
export function nextEndpointChainStep(
|
|
187
|
+
chain,
|
|
188
|
+
history,
|
|
189
|
+
failure,
|
|
190
|
+
remainingMs = Number.POSITIVE_INFINITY
|
|
191
|
+
) {
|
|
192
|
+
if (
|
|
193
|
+
!Array.isArray(chain) ||
|
|
194
|
+
chain.length < 1 ||
|
|
195
|
+
chain.length > ENDPOINT_MODEL_MAX_FALLBACKS + 1 ||
|
|
196
|
+
!Array.isArray(history) ||
|
|
197
|
+
history.length >= chain.length ||
|
|
198
|
+
history.some((item) => !ENDPOINT_CHAIN_ADVANCE_FAILURES.includes(item))
|
|
199
|
+
)
|
|
200
|
+
return Object.freeze({ advance: false, reason: "INVALID" });
|
|
201
|
+
if (!ENDPOINT_CHAIN_ADVANCE_FAILURES.includes(failure))
|
|
202
|
+
return Object.freeze({ advance: false, reason: "NOT_ADVANCEABLE" });
|
|
203
|
+
const stepIndex = history.length + 1;
|
|
204
|
+
if (stepIndex >= chain.length)
|
|
205
|
+
return Object.freeze({ advance: false, reason: "CHAIN_EXHAUSTED" });
|
|
206
|
+
if (!(remainingMs >= ENDPOINT_CHAIN_MIN_STEP_MS))
|
|
207
|
+
return Object.freeze({ advance: false, reason: "NO_TIME_LEFT" });
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
advance: true,
|
|
210
|
+
stepIndex,
|
|
211
|
+
selection: chain[stepIndex],
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* When a busy step stops waiting for admission. Every step but the last gets
|
|
217
|
+
* an equal share of the time left, at most ENDPOINT_CHAIN_STEP_ADMISSION_MS;
|
|
218
|
+
* the last step, like a single model, waits until the attempt deadline.
|
|
219
|
+
*/
|
|
220
|
+
export function endpointChainStepAdmissionDeadline({
|
|
221
|
+
startedAtMs,
|
|
222
|
+
deadlineMs,
|
|
223
|
+
stepIndex,
|
|
224
|
+
stepCount,
|
|
225
|
+
}) {
|
|
226
|
+
if (stepIndex >= stepCount - 1) return deadlineMs;
|
|
227
|
+
const share = Math.floor(
|
|
228
|
+
(deadlineMs - startedAtMs) / (stepCount - stepIndex)
|
|
229
|
+
);
|
|
230
|
+
return Math.min(
|
|
231
|
+
deadlineMs,
|
|
232
|
+
startedAtMs + Math.max(0, Math.min(ENDPOINT_CHAIN_STEP_ADMISSION_MS, share))
|
|
233
|
+
);
|
|
234
|
+
}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
compileEndpointModelSelection,
|
|
6
|
+
endpointModelSelectionChain,
|
|
7
|
+
endpointModelSelectionSource,
|
|
8
|
+
} from "./endpoint-model-selection.mjs";
|
|
5
9
|
import { createHermesRunsClient } from "./hermes-runs-client.mjs";
|
|
6
10
|
import {
|
|
7
11
|
createConfiguredRunsClient,
|
|
@@ -132,8 +136,12 @@ export async function verifyRunsApiSelection(
|
|
|
132
136
|
!/^[A-Za-z0-9_-]{1,128}$/.test(request.profileId ?? "")
|
|
133
137
|
)
|
|
134
138
|
return result("NOT_CONFIGURED");
|
|
135
|
-
const
|
|
136
|
-
if (
|
|
139
|
+
const compiled = compileEndpointModelSelection(request.modelSelection);
|
|
140
|
+
if (compiled === "invalid") return result("NOT_CONFIGURED");
|
|
141
|
+
// A fallback chain is verified one step per call. The control plane sends
|
|
142
|
+
// the primary step's check the whole chain, so a machine whose compiler
|
|
143
|
+
// cannot run a chain refuses it here and no chain is saved for it.
|
|
144
|
+
const selection = endpointModelSelectionChain(compiled)[0] ?? null;
|
|
137
145
|
let verificationWorkKind = "STANDARD";
|
|
138
146
|
if (selection?.runsApiBaseUrl) {
|
|
139
147
|
const connection = runsApiConnectionsForRuntime(
|
|
@@ -156,7 +164,7 @@ export async function verifyRunsApiSelection(
|
|
|
156
164
|
const readiness = await health(request, { env });
|
|
157
165
|
if (readiness.state !== "READY") return readiness;
|
|
158
166
|
}
|
|
159
|
-
} else if (request.connectionId !== (selection
|
|
167
|
+
} else if (request.connectionId !== endpointModelSelectionSource(selection))
|
|
160
168
|
return result("NOT_CONFIGURED");
|
|
161
169
|
const apiKeyFile =
|
|
162
170
|
env.SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND === "ADMIN"
|
|
@@ -38,7 +38,7 @@ import { installAgentHostServices } from "./service-installer.mjs";
|
|
|
38
38
|
const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
39
39
|
const VERSION = "sellable-agent-host-bootstrap/v1";
|
|
40
40
|
const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
|
|
41
|
-
const INSTALLER_PACKAGE = "@sellable/install@0.1.
|
|
41
|
+
const INSTALLER_PACKAGE = "@sellable/install@0.1.803";
|
|
42
42
|
const MCP_PACKAGE = "@sellable/mcp@0.1.957";
|
|
43
43
|
const HERMES_VERSION = "0.18.0";
|
|
44
44
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
@@ -1546,7 +1546,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
|
|
|
1546
1546
|
]) ||
|
|
1547
1547
|
pinned.hermesCli !== "hermes" ||
|
|
1548
1548
|
pinned.hermesVersion !== "0.18.0" ||
|
|
1549
|
-
pinned.installerPackage !== "@sellable/install@0.1.
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.803" ||
|
|
1550
1550
|
pinned.mcpPackage !== "@sellable/mcp@0.1.957" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
@@ -312,7 +312,7 @@ function validateDesired(desired) {
|
|
|
312
312
|
desired.serviceCredentialGeneration < 1 ||
|
|
313
313
|
desired.hermesCli !== "hermes" ||
|
|
314
314
|
desired.hermesVersion !== "0.18.0" ||
|
|
315
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
315
|
+
desired.installerPackage !== "@sellable/install@0.1.803" ||
|
|
316
316
|
desired.mcpPackage !== "@sellable/mcp@0.1.957" ||
|
|
317
317
|
!Array.isArray(desired.toolInclude) ||
|
|
318
318
|
desired.toolInclude.length === 0 ||
|
|
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
import { deriveContainedProfileId } from "./profile-materializer.mjs";
|
|
26
26
|
|
|
27
27
|
export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
|
|
28
|
-
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.803";
|
|
29
29
|
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.957";
|
|
30
30
|
const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
|
|
31
31
|
PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
|