@sema-agent/core
Version:
Stateless, task-oriented AI agent core
102 lines (101 loc) • 4.4 kB
JavaScript
import { createAssistantMessageEventStream, } from "../internal/llm.js";
import { abortLikeName, extractErrorCode } from "./errors.js";
import { emitBrainTelemetry } from "./status-sink.js";
import { passthroughStream } from "./degrading.js";
import { thrownText } from "../core/thrown-value.js";
export function createFailoverBrain(brains) {
if (brains.length === 0) {
throw new Error("createFailoverBrain: need at least one brain");
}
const stream = (model, context, options) => {
if (options?.resilience?.allowFailover === false) {
return passthroughStream(brains[0], model, context, options);
}
const out = createAssistantMessageEventStream();
void (async () => {
let lastFailedCode;
try {
for (let i = 0; i < brains.length; i++) {
const isLast = i === brains.length - 1;
const inner = await brains[i].stream(model, context, options);
let sawContent = false;
let heldStart;
let terminal;
for await (const ev of inner) {
if (ev.type === "done" || ev.type === "error") {
terminal = ev;
break;
}
if (ev.type === "start" && !sawContent) {
heldStart = ev;
continue;
}
if (!sawContent) {
sawContent = true;
if (heldStart)
out.push(heldStart);
}
out.push(ev);
}
const message = await inner.result();
const cleanUpfrontFailure = message.stopReason === "error" &&
options?.signal?.aborted !== true &&
!sawContent &&
message.errorKind !== "length_empty";
if (cleanUpfrontFailure && !isLast) {
lastFailedCode = extractErrorCode(message.errorMessage);
continue;
}
if (i > 0) {
emitBrainTelemetry({
kind: "failover",
servedIndex: i,
total: brains.length,
...(lastFailedCode !== undefined ? { errorCode: lastFailedCode } : {}),
});
}
if (!sawContent && heldStart)
out.push(heldStart);
out.push(terminal ?? {
type: "done",
reason: message.stopReason,
message,
});
return;
}
}
catch (e) {
const aborted = options?.signal?.aborted === true || abortLikeName(e) !== undefined;
const errorMsg = {
role: "assistant",
content: [{ type: "text", text: "" }],
api: model.api,
provider: model.provider,
model: model.id,
stopReason: aborted ? "aborted" : "error",
errorMessage: thrownText(e),
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
timestamp: Date.now(),
};
out.push({ type: "error", reason: aborted ? "aborted" : "error", error: errorMsg });
}
})();
return out;
};
const adjudicateRoute = (model, perModelAuth) => {
let firstRefusal;
let sawUnjudged = false;
for (const b of brains) {
const verdict = b.adjudicateRoute?.(model, perModelAuth);
if (verdict === undefined) {
sawUnjudged = true;
continue;
}
if (verdict.ok)
return verdict;
firstRefusal ??= verdict;
}
return sawUnjudged ? undefined : firstRefusal;
};
return { stream, adjudicateRoute };
}