UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

102 lines (101 loc) • 4.4 kB
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 }; }