@sema-agent/core
Version:
Stateless, task-oriented AI agent core
385 lines (384 loc) • 17 kB
JavaScript
import { AUTH_CARRIER_NAMES } from "./request-params.js";
import { BrainError } from "./errors.js";
const REDACTED_VALUE = "«redacted»";
const REDACTED_CREDENTIALS = "[redacted-credentials]";
function servableParse(raw) {
let u;
try {
u = new URL(raw);
}
catch {
return undefined;
}
return u.protocol === "http:" || u.protocol === "https:" ? u : undefined;
}
function composableParse(raw) {
let u;
try {
u = new URL(raw);
}
catch {
return undefined;
}
return u.host !== "" || u.pathname.startsWith("/") ? u : undefined;
}
export function routeKeyOf(url) {
if (url === undefined)
return "";
if (typeof url !== "string") {
throw new TypeError(`route pairing: baseUrl must be a string (got ${typeof url})`);
}
const raw = url.trim();
if (raw === "")
return "";
const u = servableParse(raw);
if (u === undefined)
return raw.replace(/\/+$/, "");
return canonicalRootSpelling(u);
}
function canonicalRootSpelling(u) {
return `${u.protocol}//${u.host}${u.pathname.replace(/\/+$/, "")}${u.search}`;
}
const QUERY_PURE_TEXT = /^[A-Za-z0-9._~[\]\-+,*!'()$ ]*$/;
const QUERY_DECODE_ROUNDS = 3;
function decodedToFixedPoint(text) {
let cur = text;
for (let round = 0; round < QUERY_DECODE_ROUNDS; round++) {
if (!cur.includes("%"))
return cur;
let next;
try {
next = decodeURIComponent(cur);
}
catch {
return cur;
}
if (next === cur)
return cur;
cur = next;
}
return cur;
}
function readQuery(search) {
if (search === "" || search === "?")
return { pure: true, segments: [] };
const segments = search.replace(/^\?/, "").split("&").map((raw) => {
const eq = raw.indexOf("=");
return eq === -1 ? { raw } : { raw, name: raw.slice(0, eq), value: raw.slice(eq + 1) };
});
const pure = segments.every((sg) => {
if (sg.name === undefined)
return QUERY_PURE_TEXT.test(decodedToFixedPoint(sg.raw));
const name = decodedToFixedPoint(sg.name);
return name !== "" && QUERY_PURE_TEXT.test(name) && QUERY_PURE_TEXT.test(decodedToFixedPoint(sg.value ?? ""));
});
return { pure, segments };
}
export function displayUrlOf(url) {
const key = routeKeyOf(url);
if (key === "")
return "";
const u = composableParse(key);
if (u === undefined) {
const cut = key.search(/[?#]/);
const head = cut === -1 ? key : key.slice(0, cut);
const at = head.lastIndexOf("@");
const shown = at === -1 ? head : `${REDACTED_VALUE}@${head.slice(at + 1)}`;
return cut === -1 ? shown : `${shown}${key[cut]}${REDACTED_VALUE}`;
}
const read = readQuery(u.search);
const query = read.segments.length === 0
? ""
: !read.pure
? `?${REDACTED_VALUE}`
: `?${read.segments.map((sg) => (sg.raw === "" ? "" : sg.name === undefined ? REDACTED_VALUE : `${sg.name}=${REDACTED_VALUE}`)).join("&")}`;
return `${u.protocol}//${u.host}${u.pathname}${query}`.replace(/\/+$/, "");
}
function queryDifferenceNote(requestKey, declaredKey) {
if (displayUrlOf(requestKey) !== displayUrlOf(declaredKey))
return "";
const names = new Set();
const ua = composableParse(requestKey);
const ub = composableParse(declaredKey);
if (ua !== undefined && ub !== undefined) {
const ra = readQuery(ua.search);
const rb = readQuery(ub.search);
if (!ra.pure || !rb.pure) {
if (ra.segments.length !== rb.segments.length) {
return ` (same rendering — the query differs in segment count (${ra.segments.length} vs ${rb.segments.length}); a value here is not plain text, so this line withholds every name)`;
}
const at = ra.segments.flatMap((sg, i) => (sg.raw === rb.segments[i]?.raw ? [] : [i + 1]));
return at.length === 0
? ` (same rendering — the difference is in a part this line withholds or folds)`
: ` (same rendering — the query differs at segment${at.length > 1 ? "s" : ""} ${at.join(", ")} of ${ra.segments.length}; a value here is not plain text, so this line withholds every name)`;
}
const a = ra.segments;
const b = rb.segments;
const valuesOf = (segs, n) => segs.filter((sg) => sg.name === n).map((sg) => sg.value).join("\u0000");
for (const n of new Set([...a, ...b].flatMap((sg) => (sg.name === undefined ? [] : [sg.name])))) {
if (valuesOf(a, n) !== valuesOf(b, n))
names.add(n);
}
}
return names.size > 0
? ` (same rendering — the query values differ under: ${[...names].sort().join(", ")})`
: ` (same rendering — the difference is in a part this line withholds or folds)`;
}
export function assertServableRequestRoot(url, seat) {
return judgeRequestRoot(url, seat)?.trimmed;
}
function judgeRequestRoot(url, seat) {
if (typeof url !== "string" || url.trim() === "")
return undefined;
const trimmed = url.trim();
const parsed = URL.canParse(trimmed) ? new URL(trimmed) : undefined;
if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
const named = parsed === undefined ? "it did not parse as a URL" : displayUrlOf(trimmed);
throw new BrainError("invalid_request", `${seat} is not a servable request root (${named}) — it must be an ABSOLUTE http(s) URL. Another scheme, ` +
`or a spelling that is not a URL at all, fails at the transport in a shape indistinguishable from an ` +
`outage, so it would be retried for the whole backoff ladder instead of refused here.`);
}
if (parsed.username !== "" || parsed.password !== "") {
throw new BrainError("invalid_request", `${seat} carries credentials in its authority (${parsed.protocol}//${REDACTED_CREDENTIALS}@${parsed.host}${parsed.pathname}) — ` +
`a request cannot be built from a URL with userinfo, so every call through this brain would fail: put the ` +
`credential on the apiKey knob or an auth header and leave the URL bare`);
}
if (parsed.hash !== "") {
throw new BrainError("invalid_request", `${seat} carries a URL fragment (${displayUrlOf(trimmed)}#…) — a fragment is never sent on the wire, so the ` +
`request would silently target the root's own path and the endpoint suffix would land inside the discarded ` +
`part: drop everything from the "#" on. A per-deployment QUERY is kept and rides every request ` +
`(a gateway's \`?api-version=…\` is supported); a fragment cannot be.`);
}
return { trimmed, parsed };
}
export function joinRequestUrl(root, suffix) {
if (typeof suffix !== "string" || /[?#]/.test(suffix)) {
throw new TypeError(`request URL: the endpoint suffix must be a path (got ${typeof suffix === "string" ? JSON.stringify(suffix) : typeof suffix}) — ` +
`a request's query belongs to its ROOT, which declares it once for every request`);
}
const judged = judgeRequestRoot(root, "the request root");
if (judged === undefined) {
throw new BrainError("invalid_request", `no request root is declared, so no request URL can be built for "${suffix}" — set config.baseUrl or Model.baseUrl`);
}
const { parsed } = judged;
const tail = suffix.replace(/^\/+/, "");
if (tail === "")
return canonicalRootSpelling(parsed);
const base = parsed.pathname.replace(/\/+$/, "");
const composed = `${parsed.protocol}//${parsed.host}${base}/${tail}${parsed.search}`;
const reread = servableParse(composed);
if (reread === undefined || !reread.pathname.startsWith(base === "" ? "/" : `${base}/`)) {
throw new TypeError(`request URL: the endpoint suffix ${JSON.stringify(suffix)} does not stay under the request root's path ` +
`(${base === "" ? "/" : base}) — a suffix names an endpoint of that root, and one that walks out of it would ` +
`send the request, and the credential paired with that root, somewhere the pairing law never judged`);
}
return composed;
}
export function stripUrlCredentials(text) {
return text.replace(/([A-Za-z][A-Za-z0-9+.\-]*:\/\/)[^/\s"'@]*@/g, `$1${REDACTED_CREDENTIALS}@`);
}
export function resolveRequestRoot(modelBaseUrl, configBaseUrl, laneDefault = "") {
const entry = judgeRequestRoot(modelBaseUrl, "Model.baseUrl");
const declared = judgeRequestRoot(configBaseUrl, "config.baseUrl");
const chosen = (entry ?? declared)?.parsed ?? servableParse(laneDefault.trim());
return chosen === undefined ? laneDefault : canonicalRootSpelling(chosen);
}
export function hasAuthCarrier(headers) {
if (!headers)
return false;
for (const k of Object.keys(headers)) {
if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
return true;
}
return false;
}
export function resolveRouteCredential(input) {
const { optionsApiKey, optionsHeaders, modelHeaders, configApiKey, configHeaders } = input;
if (optionsApiKey !== undefined) {
if (typeof optionsApiKey !== "string")
throw new TypeError(`route pairing: apiKey must be a string (got ${typeof optionsApiKey})`);
return { source: "per-model", apiKey: optionsApiKey };
}
if (hasAuthCarrier(optionsHeaders))
return { source: "per-model", headersBorne: true, carrierBag: "options" };
if (hasAuthCarrier(modelHeaders))
return { source: "per-model", headersBorne: true, carrierBag: "model" };
const configHeadersBorne = hasAuthCarrier(configHeaders);
if (configApiKey !== undefined || configHeadersBorne) {
if (configApiKey !== undefined && typeof configApiKey !== "string") {
throw new TypeError(`route pairing: apiKey must be a string (got ${typeof configApiKey})`);
}
return {
source: "deployment-config",
...(configApiKey !== undefined ? { apiKey: configApiKey } : {}),
...(configHeadersBorne ? { headersBorne: true, carrierBag: "config" } : {}),
};
}
return { source: "absent" };
}
export function adjudicateModelRoute(model, credential, config) {
const entry = routeKeyOf(model.baseUrl);
const declared = routeKeyOf(config.baseUrl);
const request = entry || declared;
const entryShown = displayUrlOf(entry);
const declaredShown = displayUrlOf(declared);
const requestShown = displayUrlOf(request);
if (credential.source === "per-model")
return { ok: true, posture: "per-model" };
if (credential.source === "deployment-config") {
if (declared === "")
return { ok: true, posture: "unpinned" };
if (request === declared)
return { ok: true, posture: "paired" };
return {
ok: false,
code: "route.credential_mismatch",
detail: {
modelId: model.id,
entryBaseUrl: entryShown,
requestBaseUrl: requestShown,
configBaseUrl: declaredShown,
fixHint: `model "${model.id}" targets ${requestShown} but the deployment credential is paired with ${declaredShown}` +
`${queryDifferenceNote(request, declared)}: ` +
`give the model its own credential (a per-model key via getApiKeyAndHeaders, or an auth header on Model.headers), ` +
`or serve it through a brain whose config.baseUrl is the model's own root`,
},
};
}
if (declared === "" || entry === "" || request === declared)
return { ok: true, posture: "keyless" };
return {
ok: false,
code: "route.credential_missing",
detail: {
modelId: model.id,
entryBaseUrl: entryShown,
requestBaseUrl: requestShown,
configBaseUrl: declaredShown,
fixHint: `model "${model.id}" targets ${requestShown} but no credential is configured for that endpoint ` +
`(the deployment credential root is ${declaredShown})${queryDifferenceNote(request, declared)}: supply a per-model credential ` +
`(getApiKeyAndHeaders, or an auth header on Model.headers), or serve it through a brain constructed for that root`,
},
};
}
export function routeRefusalText(verdict) {
return `${verdict.code}: ${verdict.detail.fixHint}`;
}
export function applyRouteCredentialHeaders(merged, credential, bags) {
if (credential.source !== "per-model")
return;
for (const k of Object.keys(merged)) {
if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
delete merged[k];
}
if (credential.headersBorne !== true)
return;
const winner = credential.carrierBag === "options" ? bags.options : bags.model;
if (!winner)
return;
for (const [k, v] of Object.entries(winner)) {
if (AUTH_CARRIER_NAMES.has(k.toLowerCase()))
merged[k] = v;
}
}
export function createBrainRouteJudge(config) {
return (model, perModelAuth) => {
const credential = resolveRouteCredential({
optionsApiKey: perModelAuth?.apiKey,
optionsHeaders: perModelAuth?.headers,
modelHeaders: model.headers,
configApiKey: config.apiKey,
configHeaders: config.headers,
});
return adjudicateModelRoute(model, credential, { baseUrl: config.baseUrl });
};
}
export function routePairingStatus(brain, model, perModelAuth) {
const verdict = brain.adjudicateRoute?.(model, perModelAuth);
if (verdict === undefined)
return "unknown";
if (verdict.ok)
return `ok:${verdict.posture}`;
return verdict.code === "route.credential_mismatch" ? "broken:credential_mismatch" : "broken:credential_missing";
}
export const DERIVED_ROUTE_PREFLIGHT_HOOK_BOUND_MS = 5_000;
export async function adjudicateDerivedRoute(args) {
const judge = args.brain.adjudicateRoute;
if (judge === undefined)
return undefined;
let auth;
if (args.getApiKeyAndHeaders !== undefined) {
let timer;
const ABSTAIN = Symbol("preflight-abstain");
try {
const bound = new Promise((resolve) => {
timer = setTimeout(() => resolve(ABSTAIN), DERIVED_ROUTE_PREFLIGHT_HOOK_BOUND_MS);
});
const hookResult = Promise.resolve(args.getApiKeyAndHeaders(args.model));
hookResult.catch(() => { });
const raced = await Promise.race([hookResult, bound]);
if (raced === ABSTAIN)
return undefined;
auth = raced;
}
catch {
return undefined;
}
finally {
if (timer !== undefined)
clearTimeout(timer);
}
}
try {
return judge(args.model, auth);
}
catch {
return undefined;
}
}
export function sameRouteIdentity(a, b) {
return a.id === b.id && routeKeyOf(a.baseUrl) === routeKeyOf(b.baseUrl);
}
export function fallbackToPrimaryNotice(args) {
return {
code: "route.fallback_to_primary",
message: `derived model "${args.from}" (${args.seat}) is not routable (${args.verdict.code}) — ` +
`falling back to the primary model "${args.to}". ${args.verdict.detail.fixHint}`,
detail: {
seat: args.seat,
from: args.from,
to: args.to,
cause: args.verdict.code,
fixHint: args.verdict.detail.fixHint,
...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
...(args.runId !== undefined ? { runId: args.runId } : {}),
},
};
}
export function unroutableSeatNotice(args) {
return {
code: "route.classifier_unroutable",
message: `derived model "${args.from}" (${args.seat}) is not routable (${args.verdict.code}) and this deployment declared no fallback — ` +
`the seat is kept and its calls are refused rather than served by "${args.primary}". ${args.verdict.detail.fixHint}`,
detail: {
seat: args.seat,
from: args.from,
cause: args.verdict.code,
fixHint: args.verdict.detail.fixHint,
action: "refused",
...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
...(args.runId !== undefined ? { runId: args.runId } : {}),
},
};
}
export function authCarrierFingerprint(headers) {
if (!headers)
return "";
const rows = [];
for (const [k, v] of Object.entries(headers)) {
const fold = k.toLowerCase();
if (AUTH_CARRIER_NAMES.has(fold))
rows.push(`${fold}=${v}`);
}
return rows.sort().join("\n");
}