@sellable/install 0.1.800 → 0.1.802
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/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 +19 -8
- package/lib/sellable-agent/hermes-memory-snapshot.mjs +37 -323
- 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
|
@@ -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"
|
|
@@ -183,10 +191,13 @@ export async function verifyRunsApiSelection(
|
|
|
183
191
|
},
|
|
184
192
|
})
|
|
185
193
|
: makeDefaultClient({ apiKeyFile });
|
|
194
|
+
// A fresh token per check. Claude Opus 5.5 answers one fixed string
|
|
195
|
+
// ("SELLABLE_CONNECTION_OK") with nothing, so Claude Code fell back to
|
|
196
|
+
// another model and every Claude Code verification failed.
|
|
197
|
+
const token = `ready-${randomUUID().replaceAll("-", "").slice(0, 8)}`;
|
|
186
198
|
const compiled = {
|
|
187
|
-
input:
|
|
188
|
-
instructions:
|
|
189
|
-
"This is a connection check. Do not use tools or take actions. Return exactly SELLABLE_CONNECTION_OK and nothing else.",
|
|
199
|
+
input: `Connection verification. Reply with ${token}.`,
|
|
200
|
+
instructions: `This is a connection check. Do not use tools or take actions. Return exactly ${token} and nothing else.`,
|
|
190
201
|
conversationHistory: [],
|
|
191
202
|
correlation: `connection-check-${randomUUID()}`,
|
|
192
203
|
...(selection ? { modelSelection: selection } : {}),
|
|
@@ -203,7 +214,7 @@ export async function verifyRunsApiSelection(
|
|
|
203
214
|
if (
|
|
204
215
|
status.status !== "completed" ||
|
|
205
216
|
typeof status.output !== "string" ||
|
|
206
|
-
status.output.trim() !==
|
|
217
|
+
status.output.trim() !== token ||
|
|
207
218
|
!served
|
|
208
219
|
)
|
|
209
220
|
return result("UNAVAILABLE");
|