@tangle-network/agent-app 0.43.69 → 0.43.71
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/dist/chat-routes/index.d.ts +213 -7
- package/dist/chat-routes/index.js +222 -15
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chunk-IVUN7FL7.js +72 -0
- package/dist/chunk-IVUN7FL7.js.map +1 -0
- package/dist/chunk-KFTRTOT2.js +111 -0
- package/dist/chunk-KFTRTOT2.js.map +1 -0
- package/dist/{chunk-3ALFBTIW.js → chunk-NWYIACBB.js} +9 -1
- package/dist/chunk-NWYIACBB.js.map +1 -0
- package/dist/failover-H0x12kY3.d.ts +100 -0
- package/dist/fingerprint-DbmOgy0n.d.ts +69 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/model-resolution/index.d.ts +2 -99
- package/dist/model-resolution/index.js +8 -101
- package/dist/model-resolution/index.js.map +1 -1
- package/dist/profile/index.d.ts +2 -0
- package/dist/profile/index.js +8 -0
- package/dist/profile/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +2 -0
- package/dist/sandbox/index.js +2 -1
- package/package.json +1 -1
- package/dist/chunk-3ALFBTIW.js.map +0 -1
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// src/profile/fingerprint.ts
|
|
2
|
+
async function sha256Hex(text) {
|
|
3
|
+
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text));
|
|
4
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5
|
+
}
|
|
6
|
+
async function fingerprintAgentProfile(profile, context) {
|
|
7
|
+
const systemPrompt = profile.prompt?.systemPrompt ?? "";
|
|
8
|
+
const promptSha = await sha256Hex(systemPrompt);
|
|
9
|
+
const mcpKeys = Object.keys(profile.mcp ?? {}).sort();
|
|
10
|
+
const subagentNames = Object.keys(profile.subagents ?? {}).sort();
|
|
11
|
+
const fileMountPaths = (profile.resources?.files ?? []).map((mount) => mount.path).sort();
|
|
12
|
+
const connectionIds = (profile.connections ?? []).map((connection) => connection.alias ? `${connection.connectionId}:${connection.alias}` : connection.connectionId).sort();
|
|
13
|
+
const hash = await sha256Hex(
|
|
14
|
+
JSON.stringify([
|
|
15
|
+
promptSha,
|
|
16
|
+
mcpKeys,
|
|
17
|
+
subagentNames,
|
|
18
|
+
fileMountPaths,
|
|
19
|
+
connectionIds,
|
|
20
|
+
context?.model ?? null,
|
|
21
|
+
context?.harness ?? null
|
|
22
|
+
])
|
|
23
|
+
);
|
|
24
|
+
return {
|
|
25
|
+
hash,
|
|
26
|
+
promptSha,
|
|
27
|
+
promptBytes: new TextEncoder().encode(systemPrompt).length,
|
|
28
|
+
mcpKeys,
|
|
29
|
+
subagentNames,
|
|
30
|
+
fileMountPaths,
|
|
31
|
+
connectionIds,
|
|
32
|
+
model: context?.model,
|
|
33
|
+
harness: context?.harness
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function channelValue(fingerprint, channel) {
|
|
37
|
+
const value = fingerprint[channel];
|
|
38
|
+
if (Array.isArray(value)) return value.length === 0 ? "(none)" : value.join(",");
|
|
39
|
+
return String(value ?? "(unset)");
|
|
40
|
+
}
|
|
41
|
+
var DRIFT_CHANNELS = [
|
|
42
|
+
"promptSha",
|
|
43
|
+
"promptBytes",
|
|
44
|
+
"mcpKeys",
|
|
45
|
+
"subagentNames",
|
|
46
|
+
"fileMountPaths",
|
|
47
|
+
"connectionIds",
|
|
48
|
+
"model",
|
|
49
|
+
"harness"
|
|
50
|
+
];
|
|
51
|
+
function diffProfileFingerprints(a, b) {
|
|
52
|
+
const drift = [];
|
|
53
|
+
for (const channel of DRIFT_CHANNELS) {
|
|
54
|
+
const left = channelValue(a, channel);
|
|
55
|
+
const right = channelValue(b, channel);
|
|
56
|
+
if (left !== right) drift.push({ channel, a: left, b: right });
|
|
57
|
+
}
|
|
58
|
+
return { equal: drift.length === 0, drift };
|
|
59
|
+
}
|
|
60
|
+
function formatProfileDrift(drift) {
|
|
61
|
+
if (drift.equal) return "profiles identical";
|
|
62
|
+
const lines = drift.drift.map((entry) => ` ${entry.channel}: ${entry.a} != ${entry.b}`);
|
|
63
|
+
return `profile drift on ${drift.drift.length} channel(s):
|
|
64
|
+
${lines.join("\n")}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export {
|
|
68
|
+
fingerprintAgentProfile,
|
|
69
|
+
diffProfileFingerprints,
|
|
70
|
+
formatProfileDrift
|
|
71
|
+
};
|
|
72
|
+
//# sourceMappingURL=chunk-IVUN7FL7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/profile/fingerprint.ts"],"sourcesContent":["/**\n * Profile fingerprinting — prove WHICH profile a turn actually executed.\n *\n * The backtest invariant this exists to enforce: an eval's score is only worth\n * publishing if the benchmarked profile IS the shipped profile. The failure\n * mode is structural, not hypothetical — a product's eval composed the\n * production profile in one module, executed a hand-rolled stub in another,\n * and stamped the composed profile's identity onto the stub's scorecard.\n * Nothing compared the two, so nothing could notice.\n *\n * A `ProfileFingerprint` is a channelled identity of the profile handed to the\n * sandbox SDK: the system-prompt digest plus the names of every capability\n * surface (MCP servers, subagents, file mounts, hub connections) and the\n * model/harness the turn dispatched at. It is deliberately NOT a byte-exhaustive\n * serialization — channels are what drift in practice, and a channelled diff\n * names the surface that moved instead of reporting \"bytes differ\".\n *\n * The write half of the seam is `StreamSandboxPromptOptions.onProfileResolved`\n * (`/sandbox`): the one place the final profile exists is inside\n * `streamSandboxPrompt` after the system-prompt override, the MCP merge, and\n * reasoning-effort attachment, so that is where the fingerprint is taken. A\n * caller re-deriving a profile to fingerprint it would reintroduce the exact\n * gap this closes.\n */\n\nimport type { AgentProfile } from '@tangle-network/agent-interface'\n\n/** The dispatch context a profile cannot see but a turn's identity includes. */\nexport interface ProfileFingerprintContext {\n model?: string\n harness?: string\n}\n\n/** Channelled identity of one executed (or composed) profile. */\nexport interface ProfileFingerprint {\n /** sha256 over every channel below — one value to log/compare. */\n hash: string\n /** sha256 of `prompt.systemPrompt` ('' when absent). */\n promptSha: string\n /** UTF-8 byte length of `prompt.systemPrompt`. */\n promptBytes: number\n /** Sorted MCP server keys. */\n mcpKeys: string[]\n /** Sorted subagent names. */\n subagentNames: string[]\n /** Sorted `resources.files[].path` mounts. */\n fileMountPaths: string[]\n /** Sorted hub connection ids (alias-qualified when present). */\n connectionIds: string[]\n model?: string\n harness?: string\n}\n\nasync function sha256Hex(text: string): Promise<string> {\n const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text))\n return Array.from(new Uint8Array(digest))\n .map((byte) => byte.toString(16).padStart(2, '0'))\n .join('')\n}\n\n/** Fingerprint a profile as the SDK would receive it. */\nexport async function fingerprintAgentProfile(\n profile: AgentProfile,\n context?: ProfileFingerprintContext,\n): Promise<ProfileFingerprint> {\n const systemPrompt = profile.prompt?.systemPrompt ?? ''\n const promptSha = await sha256Hex(systemPrompt)\n const mcpKeys = Object.keys(profile.mcp ?? {}).sort()\n const subagentNames = Object.keys(profile.subagents ?? {}).sort()\n const fileMountPaths = (profile.resources?.files ?? []).map((mount) => mount.path).sort()\n const connectionIds = (profile.connections ?? [])\n .map((connection) => (connection.alias ? `${connection.connectionId}:${connection.alias}` : connection.connectionId))\n .sort()\n const hash = await sha256Hex(\n JSON.stringify([\n promptSha,\n mcpKeys,\n subagentNames,\n fileMountPaths,\n connectionIds,\n context?.model ?? null,\n context?.harness ?? null,\n ]),\n )\n return {\n hash,\n promptSha,\n promptBytes: new TextEncoder().encode(systemPrompt).length,\n mcpKeys,\n subagentNames,\n fileMountPaths,\n connectionIds,\n model: context?.model,\n harness: context?.harness,\n }\n}\n\n/** One drifted channel between two fingerprints, rendered as comparable strings. */\nexport interface ProfileDriftEntry {\n channel: 'promptSha' | 'promptBytes' | 'mcpKeys' | 'subagentNames' | 'fileMountPaths' | 'connectionIds' | 'model' | 'harness'\n a: string\n b: string\n}\n\nexport interface ProfileDrift {\n equal: boolean\n drift: ProfileDriftEntry[]\n}\n\nfunction channelValue(fingerprint: ProfileFingerprint, channel: ProfileDriftEntry['channel']): string {\n const value = fingerprint[channel]\n if (Array.isArray(value)) return value.length === 0 ? '(none)' : value.join(',')\n return String(value ?? '(unset)')\n}\n\nconst DRIFT_CHANNELS: ProfileDriftEntry['channel'][] = [\n 'promptSha',\n 'promptBytes',\n 'mcpKeys',\n 'subagentNames',\n 'fileMountPaths',\n 'connectionIds',\n 'model',\n 'harness',\n]\n\n/** Channel-by-channel comparison of two fingerprints. */\nexport function diffProfileFingerprints(a: ProfileFingerprint, b: ProfileFingerprint): ProfileDrift {\n const drift: ProfileDriftEntry[] = []\n for (const channel of DRIFT_CHANNELS) {\n const left = channelValue(a, channel)\n const right = channelValue(b, channel)\n if (left !== right) drift.push({ channel, a: left, b: right })\n }\n return { equal: drift.length === 0, drift }\n}\n\n/** Human-readable drift report; exactly 'profiles identical' when equal. */\nexport function formatProfileDrift(drift: ProfileDrift): string {\n if (drift.equal) return 'profiles identical'\n const lines = drift.drift.map((entry) => ` ${entry.channel}: ${entry.a} != ${entry.b}`)\n return `profile drift on ${drift.drift.length} channel(s):\\n${lines.join('\\n')}`\n}\n"],"mappings":";AAqDA,eAAe,UAAU,MAA+B;AACtD,QAAM,SAAS,MAAM,OAAO,OAAO,OAAO,WAAW,IAAI,YAAY,EAAE,OAAO,IAAI,CAAC;AACnF,SAAO,MAAM,KAAK,IAAI,WAAW,MAAM,CAAC,EACrC,IAAI,CAAC,SAAS,KAAK,SAAS,EAAE,EAAE,SAAS,GAAG,GAAG,CAAC,EAChD,KAAK,EAAE;AACZ;AAGA,eAAsB,wBACpB,SACA,SAC6B;AAC7B,QAAM,eAAe,QAAQ,QAAQ,gBAAgB;AACrD,QAAM,YAAY,MAAM,UAAU,YAAY;AAC9C,QAAM,UAAU,OAAO,KAAK,QAAQ,OAAO,CAAC,CAAC,EAAE,KAAK;AACpD,QAAM,gBAAgB,OAAO,KAAK,QAAQ,aAAa,CAAC,CAAC,EAAE,KAAK;AAChE,QAAM,kBAAkB,QAAQ,WAAW,SAAS,CAAC,GAAG,IAAI,CAAC,UAAU,MAAM,IAAI,EAAE,KAAK;AACxF,QAAM,iBAAiB,QAAQ,eAAe,CAAC,GAC5C,IAAI,CAAC,eAAgB,WAAW,QAAQ,GAAG,WAAW,YAAY,IAAI,WAAW,KAAK,KAAK,WAAW,YAAa,EACnH,KAAK;AACR,QAAM,OAAO,MAAM;AAAA,IACjB,KAAK,UAAU;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,SAAS,SAAS;AAAA,MAClB,SAAS,WAAW;AAAA,IACtB,CAAC;AAAA,EACH;AACA,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,aAAa,IAAI,YAAY,EAAE,OAAO,YAAY,EAAE;AAAA,IACpD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,SAAS;AAAA,IAChB,SAAS,SAAS;AAAA,EACpB;AACF;AAcA,SAAS,aAAa,aAAiC,SAA+C;AACpG,QAAM,QAAQ,YAAY,OAAO;AACjC,MAAI,MAAM,QAAQ,KAAK,EAAG,QAAO,MAAM,WAAW,IAAI,WAAW,MAAM,KAAK,GAAG;AAC/E,SAAO,OAAO,SAAS,SAAS;AAClC;AAEA,IAAM,iBAAiD;AAAA,EACrD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,SAAS,wBAAwB,GAAuB,GAAqC;AAClG,QAAM,QAA6B,CAAC;AACpC,aAAW,WAAW,gBAAgB;AACpC,UAAM,OAAO,aAAa,GAAG,OAAO;AACpC,UAAM,QAAQ,aAAa,GAAG,OAAO;AACrC,QAAI,SAAS,MAAO,OAAM,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAAA,EAC/D;AACA,SAAO,EAAE,OAAO,MAAM,WAAW,GAAG,MAAM;AAC5C;AAGO,SAAS,mBAAmB,OAA6B;AAC9D,MAAI,MAAM,MAAO,QAAO;AACxB,QAAM,QAAQ,MAAM,MAAM,IAAI,CAAC,UAAU,KAAK,MAAM,OAAO,KAAK,MAAM,CAAC,OAAO,MAAM,CAAC,EAAE;AACvF,SAAO,oBAAoB,MAAM,MAAM,MAAM;AAAA,EAAiB,MAAM,KAAK,IAAI,CAAC;AAChF;","names":[]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/model-resolution/failover.ts
|
|
2
|
+
var UPSTREAM_UNAVAILABLE_CODES = [
|
|
3
|
+
"provider_inference_unavailable",
|
|
4
|
+
"upstream_unavailable",
|
|
5
|
+
"insufficient_quota",
|
|
6
|
+
"model_not_available",
|
|
7
|
+
"server_error",
|
|
8
|
+
"bad_gateway",
|
|
9
|
+
"service_unavailable"
|
|
10
|
+
];
|
|
11
|
+
var UPSTREAM_UNAVAILABLE_STATUSES = [429, 500, 502, 503, 504];
|
|
12
|
+
var UPSTREAM_UNAVAILABLE_MESSAGES = [
|
|
13
|
+
"bad gateway",
|
|
14
|
+
"service unavailable",
|
|
15
|
+
"inference temporarily unavailable",
|
|
16
|
+
"provider inference is unavailable",
|
|
17
|
+
"insufficient balance",
|
|
18
|
+
"usage limits",
|
|
19
|
+
"quota exceeded",
|
|
20
|
+
"rate limit",
|
|
21
|
+
"overloaded",
|
|
22
|
+
"temporarily unavailable"
|
|
23
|
+
];
|
|
24
|
+
function readString(source, key) {
|
|
25
|
+
const value = source[key];
|
|
26
|
+
return typeof value === "string" && value.trim().length > 0 ? value : void 0;
|
|
27
|
+
}
|
|
28
|
+
function isUpstreamUnavailable(signal) {
|
|
29
|
+
if (signal === null || typeof signal !== "object") return false;
|
|
30
|
+
const record = signal;
|
|
31
|
+
if (record.success === true) return false;
|
|
32
|
+
const nested = record.error;
|
|
33
|
+
const nestedRecord = nested !== null && typeof nested === "object" ? nested : void 0;
|
|
34
|
+
const code = readString(record, "errorCode") ?? readString(record, "code") ?? (nestedRecord ? readString(nestedRecord, "code") ?? readString(nestedRecord, "type") : void 0);
|
|
35
|
+
if (code && UPSTREAM_UNAVAILABLE_CODES.includes(code)) return true;
|
|
36
|
+
for (const key of ["status", "statusCode", "httpStatus"]) {
|
|
37
|
+
const value = record[key];
|
|
38
|
+
if (typeof value === "number" && UPSTREAM_UNAVAILABLE_STATUSES.includes(value)) return true;
|
|
39
|
+
}
|
|
40
|
+
const message = readString(record, "message") ?? readString(record, "error") ?? (nestedRecord ? readString(nestedRecord, "message") : void 0);
|
|
41
|
+
if (!message) return false;
|
|
42
|
+
const lowered = message.toLowerCase();
|
|
43
|
+
return UPSTREAM_UNAVAILABLE_MESSAGES.some((fragment) => lowered.includes(fragment));
|
|
44
|
+
}
|
|
45
|
+
var ModelFailoverExhaustedError = class extends Error {
|
|
46
|
+
attempts;
|
|
47
|
+
constructor(attempts) {
|
|
48
|
+
const trail = attempts.map((a) => `${a.model}: ${a.reason ?? "failed"}`).join(" | ");
|
|
49
|
+
super(`All ${attempts.length} model(s) failed. ${trail}`);
|
|
50
|
+
this.name = "ModelFailoverExhaustedError";
|
|
51
|
+
this.attempts = attempts;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function describe(signal) {
|
|
55
|
+
if (signal instanceof Error) return signal.message;
|
|
56
|
+
if (signal !== null && typeof signal === "object") {
|
|
57
|
+
const record = signal;
|
|
58
|
+
const message = readString(record, "error") ?? readString(record, "message") ?? readString(record, "errorCode");
|
|
59
|
+
if (message) return message;
|
|
60
|
+
}
|
|
61
|
+
return String(signal);
|
|
62
|
+
}
|
|
63
|
+
async function runWithModelFailover(input) {
|
|
64
|
+
const models = input.models.map((m) => m.trim()).filter((m) => m.length > 0);
|
|
65
|
+
if (models.length === 0) throw new Error("runWithModelFailover requires at least one model");
|
|
66
|
+
const isUnavailableResult = input.isUnavailableResult ?? ((r) => isUpstreamUnavailable(r));
|
|
67
|
+
const isUnavailableError = input.isUnavailableError ?? isUpstreamUnavailable;
|
|
68
|
+
const attempts = [];
|
|
69
|
+
for (let index = 0; index < models.length; index += 1) {
|
|
70
|
+
const model = models[index];
|
|
71
|
+
let result;
|
|
72
|
+
try {
|
|
73
|
+
result = await input.run(model);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!isUnavailableError(error)) throw error;
|
|
76
|
+
const attempt = { model, ok: false, reason: describe(error) };
|
|
77
|
+
attempts.push(attempt);
|
|
78
|
+
const next = models[index + 1];
|
|
79
|
+
if (next) input.onFallback?.(attempt, next);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (isUnavailableResult(result)) {
|
|
83
|
+
const attempt = { model, ok: false, reason: describe(result) };
|
|
84
|
+
attempts.push(attempt);
|
|
85
|
+
const next = models[index + 1];
|
|
86
|
+
if (next) input.onFallback?.(attempt, next);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
attempts.push({ model, ok: true });
|
|
90
|
+
return { value: result, model, attempts, usedFallback: index > 0 };
|
|
91
|
+
}
|
|
92
|
+
throw new ModelFailoverExhaustedError(attempts);
|
|
93
|
+
}
|
|
94
|
+
function buildModelChain(preferred, fallbacks) {
|
|
95
|
+
const chain = [];
|
|
96
|
+
for (const model of [preferred, ...fallbacks]) {
|
|
97
|
+
const cleaned = typeof model === "string" ? model.trim() : "";
|
|
98
|
+
if (cleaned.length > 0 && !chain.includes(cleaned)) chain.push(cleaned);
|
|
99
|
+
}
|
|
100
|
+
return chain;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
UPSTREAM_UNAVAILABLE_CODES,
|
|
105
|
+
UPSTREAM_UNAVAILABLE_STATUSES,
|
|
106
|
+
isUpstreamUnavailable,
|
|
107
|
+
ModelFailoverExhaustedError,
|
|
108
|
+
runWithModelFailover,
|
|
109
|
+
buildModelChain
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=chunk-KFTRTOT2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/model-resolution/failover.ts"],"sourcesContent":["/**\n * Model failover — the answer to \"one upstream hit a quota wall and the customer\n * got a Bad Gateway even though the router had abundant healthy capacity\".\n *\n * This is deliberately NOT a health probe. Probing before every turn buys a\n * round-trip of latency on the happy path and still races the outage (a model\n * healthy at probe time can 502 a second later). Failover is REACTIVE: run the\n * preferred model, and on an upstream-outage signal move to the next model in\n * the chain. Zero added latency when the preferred model works.\n *\n * Two facts drive the design, both measured against a live box during the\n * 2026-07-25 Anthropic/DeepSeek outage:\n *\n * 1. An outage is NOT always a thrown error. The sandbox resolves with a\n * payload — `{ success: false, errorCode: 'provider_inference_unavailable' }`\n * — so a classifier that only inspects `catch` misses the customer-visible\n * case entirely. `isUpstreamUnavailable` inspects resolved values too.\n * 2. Catalog membership is NOT liveness. The router's `/v1/models` still listed\n * every dead model during the outage, so `validateChatModelId` admitted\n * `claude-sonnet-4-6` while every call to it returned 502. Nothing upstream\n * of the actual call can be trusted to tell you a model is serving.\n *\n * Substrate-free: the caller injects `run`, so this composes with a sandbox\n * turn, a router chat completion, or a test fake without importing any of them.\n */\n\n/**\n * Error codes that mean \"this model's upstream is unavailable — a different\n * model may still work\". Deliberately excludes codes that would fail identically\n * on every model (bad request, auth, content filter): retrying those down a\n * chain burns latency and money to reach the same failure.\n */\nexport const UPSTREAM_UNAVAILABLE_CODES: readonly string[] = [\n 'provider_inference_unavailable',\n 'upstream_unavailable',\n 'insufficient_quota',\n 'model_not_available',\n 'server_error',\n 'bad_gateway',\n 'service_unavailable',\n]\n\n/** HTTP statuses that indicate an upstream capacity/availability problem. */\nexport const UPSTREAM_UNAVAILABLE_STATUSES: readonly number[] = [429, 500, 502, 503, 504]\n\n/**\n * Message fragments emitted by real upstreams during this class of outage.\n * Matched case-insensitively as a last resort, after code and status.\n */\nconst UPSTREAM_UNAVAILABLE_MESSAGES: readonly string[] = [\n 'bad gateway',\n 'service unavailable',\n 'inference temporarily unavailable',\n 'provider inference is unavailable',\n 'insufficient balance',\n 'usage limits',\n 'quota exceeded',\n 'rate limit',\n 'overloaded',\n 'temporarily unavailable',\n]\n\nfunction readString(source: Record<string, unknown>, key: string): string | undefined {\n const value = source[key]\n return typeof value === 'string' && value.trim().length > 0 ? value : undefined\n}\n\n/**\n * True when `signal` — a thrown error OR a resolved result payload — indicates\n * the model's upstream is unavailable and another model is worth trying.\n *\n * Checked in order of decreasing confidence: explicit code, HTTP status, then\n * message text. A resolved payload only counts as a failure when it carries an\n * explicit failure marker (`success: false`, or an `error`/`errorCode` field) —\n * a successful result is never misread as an outage.\n */\nexport function isUpstreamUnavailable(signal: unknown): boolean {\n if (signal === null || typeof signal !== 'object') return false\n const record = signal as Record<string, unknown>\n\n // A resolved payload that explicitly reports success is never an outage.\n if (record.success === true) return false\n\n const nested = record.error\n const nestedRecord = nested !== null && typeof nested === 'object' ? (nested as Record<string, unknown>) : undefined\n\n const code =\n readString(record, 'errorCode') ??\n readString(record, 'code') ??\n (nestedRecord ? (readString(nestedRecord, 'code') ?? readString(nestedRecord, 'type')) : undefined)\n if (code && UPSTREAM_UNAVAILABLE_CODES.includes(code)) return true\n\n for (const key of ['status', 'statusCode', 'httpStatus']) {\n const value = record[key]\n if (typeof value === 'number' && UPSTREAM_UNAVAILABLE_STATUSES.includes(value)) return true\n }\n\n const message =\n readString(record, 'message') ??\n readString(record, 'error') ??\n (nestedRecord ? readString(nestedRecord, 'message') : undefined)\n if (!message) return false\n const lowered = message.toLowerCase()\n return UPSTREAM_UNAVAILABLE_MESSAGES.some((fragment) => lowered.includes(fragment))\n}\n\n/** One model tried, and how it went. */\nexport interface ModelFailoverAttempt {\n model: string\n ok: boolean\n /** Why this model was abandoned. Absent when `ok`. */\n reason?: string\n}\n\n/** The outcome of a failover run: the value plus the full attempt trail. */\nexport interface ModelFailoverResult<T> {\n value: T\n /** The model that actually produced `value`. */\n model: string\n attempts: ModelFailoverAttempt[]\n /** True when the preferred (first) model did not serve the request. */\n usedFallback: boolean\n}\n\n/** Every model in the chain failed; carries the trail for logging. */\nexport class ModelFailoverExhaustedError extends Error {\n readonly attempts: ModelFailoverAttempt[]\n constructor(attempts: ModelFailoverAttempt[]) {\n const trail = attempts.map((a) => `${a.model}: ${a.reason ?? 'failed'}`).join(' | ')\n super(`All ${attempts.length} model(s) failed. ${trail}`)\n this.name = 'ModelFailoverExhaustedError'\n this.attempts = attempts\n }\n}\n\n/** Inputs to {@link runWithModelFailover}. */\nexport interface RunWithModelFailoverInput<T> {\n /** Preferred model first, then fallbacks in descending preference. */\n models: readonly string[]\n /** Executes one turn with the given model. */\n run: (model: string) => Promise<T>\n /**\n * Classifies a resolved result as an upstream outage. Defaults to\n * {@link isUpstreamUnavailable}, which understands the sandbox's\n * `{ success: false, errorCode }` payload.\n */\n isUnavailableResult?: (result: T) => boolean\n /** Classifies a thrown error. Defaults to {@link isUpstreamUnavailable}. */\n isUnavailableError?: (error: unknown) => boolean\n /** Observability hook fired each time a model is abandoned. */\n onFallback?: (attempt: ModelFailoverAttempt, nextModel: string) => void\n}\n\nfunction describe(signal: unknown): string {\n if (signal instanceof Error) return signal.message\n if (signal !== null && typeof signal === 'object') {\n const record = signal as Record<string, unknown>\n const message = readString(record, 'error') ?? readString(record, 'message') ?? readString(record, 'errorCode')\n if (message) return message\n }\n return String(signal)\n}\n\n/**\n * Run `run` against the first model in `models` that does not report an upstream\n * outage, falling through the chain in order.\n *\n * A non-outage failure (bad request, auth, content filter) is re-thrown\n * immediately rather than retried down the chain — those fail identically on\n * every model, so walking the chain would only multiply latency and spend.\n *\n * @throws ModelFailoverExhaustedError when every model reports an outage.\n */\nexport async function runWithModelFailover<T>(\n input: RunWithModelFailoverInput<T>,\n): Promise<ModelFailoverResult<T>> {\n const models = input.models.map((m) => m.trim()).filter((m) => m.length > 0)\n if (models.length === 0) throw new Error('runWithModelFailover requires at least one model')\n\n const isUnavailableResult = input.isUnavailableResult ?? ((r: T) => isUpstreamUnavailable(r))\n const isUnavailableError = input.isUnavailableError ?? isUpstreamUnavailable\n const attempts: ModelFailoverAttempt[] = []\n\n for (let index = 0; index < models.length; index += 1) {\n const model = models[index]!\n let result: T\n try {\n result = await input.run(model)\n } catch (error) {\n if (!isUnavailableError(error)) throw error\n const attempt: ModelFailoverAttempt = { model, ok: false, reason: describe(error) }\n attempts.push(attempt)\n const next = models[index + 1]\n if (next) input.onFallback?.(attempt, next)\n continue\n }\n\n if (isUnavailableResult(result)) {\n const attempt: ModelFailoverAttempt = { model, ok: false, reason: describe(result) }\n attempts.push(attempt)\n const next = models[index + 1]\n if (next) input.onFallback?.(attempt, next)\n continue\n }\n\n attempts.push({ model, ok: true })\n return { value: result, model, attempts, usedFallback: index > 0 }\n }\n\n throw new ModelFailoverExhaustedError(attempts)\n}\n\n/**\n * Build a failover chain: the preferred model first, then `fallbacks`, with\n * duplicates removed so a model is never retried twice in one turn.\n */\nexport function buildModelChain(preferred: string, fallbacks: readonly string[]): string[] {\n const chain: string[] = []\n for (const model of [preferred, ...fallbacks]) {\n const cleaned = typeof model === 'string' ? model.trim() : ''\n if (cleaned.length > 0 && !chain.includes(cleaned)) chain.push(cleaned)\n }\n return chain\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fingerprintAgentProfile
|
|
3
|
+
} from "./chunk-IVUN7FL7.js";
|
|
1
4
|
import {
|
|
2
5
|
assertHarnessModelCompatible
|
|
3
6
|
} from "./chunk-CQZSAR77.js";
|
|
@@ -1358,6 +1361,11 @@ async function* streamSandboxPrompt(shell, box, message, options) {
|
|
|
1358
1361
|
const extraMcp = mergeExtraMcp(appToolMcp, options?.baseProfileMcp ?? {}, options?.extraMcp);
|
|
1359
1362
|
const profile = shell.profile({ systemPrompt: options?.systemPrompt, extraMcp, harness });
|
|
1360
1363
|
const profileWithEffort = attachReasoningEffort(profile, harness, options?.effort);
|
|
1364
|
+
if (options?.onProfileResolved) {
|
|
1365
|
+
options.onProfileResolved(
|
|
1366
|
+
await fingerprintAgentProfile(profileWithEffort, { model: model?.model, harness })
|
|
1367
|
+
);
|
|
1368
|
+
}
|
|
1361
1369
|
const stream = box.streamPrompt(prompt, {
|
|
1362
1370
|
sessionId: options?.sessionId,
|
|
1363
1371
|
executionId: options?.executionId,
|
|
@@ -1630,4 +1638,4 @@ export {
|
|
|
1630
1638
|
isTerminalPromptEvent,
|
|
1631
1639
|
detectInteractiveQuestion
|
|
1632
1640
|
};
|
|
1633
|
-
//# sourceMappingURL=chunk-
|
|
1641
|
+
//# sourceMappingURL=chunk-NWYIACBB.js.map
|