@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,483 lines • 60.1 kB
JavaScript
import { createAssistantMessageEventStream, ensureToolTurnInvariants, snapshotActorAssertion, stripEngineMetadata } from "../llm/index.js";
import { finalizeToolOutcome, runAgentLoop } from "../loop/agent-loop.js";
import { resolveAgentCoreStreamFn } from "../loop/runtime-deps.js";
import { normalizeEngineSegments } from "../../core/untrusted-text.js";
import { AUTH_CARRIER_NAMES } from "../../brain/request-params.js";
import { isProviderEchoShape } from "../../brain/errors.js";
import { convertToLlm } from "./messages.js";
import { readMember, thrownText } from "../../core/thrown-value.js";
import { AgentHarnessError, CompactionError, SessionError, SYNTHETIC_MESSAGE_MODEL, toError, } from "./types.js";
const API_FAILURE_RECORD_MAX_CHARS = 2000;
function createUserMessage(text, images, provenance) {
const content = text.length > 0 ? [{ type: "text", text }] : [];
if (images) {
content.push(...images);
}
const engineNote = provenance?.provenance === "engine-note";
const segments = normalizeEngineSegments(text, provenance?.engineSegments);
return {
role: "user",
content,
timestamp: Date.now(),
...(segments.state === "valid" ? { engineSegments: segments.segments } : {}),
...(segments.state === "absent" &&
provenance?.enginePrefixChars !== undefined &&
Number.isInteger(provenance.enginePrefixChars) &&
provenance.enginePrefixChars > 0 &&
provenance.enginePrefixChars <= text.length
? { enginePrefixChars: provenance.enginePrefixChars }
: {}),
...(provenance?.engineMinted === true && !engineNote ? { engineMinted: true } : {}),
...(engineNote ? { provenance: "engine-note" } : {}),
...(engineNote && typeof provenance?.engineNoteKey === "string" && provenance.engineNoteKey.length > 0 ? { engineNoteKey: provenance.engineNoteKey } : {}),
...(provenance?.actor !== undefined &&
typeof provenance.actor.id === "string" &&
provenance.actor.id.length > 0 &&
typeof provenance.actor.hostAsserted === "boolean"
? { actor: snapshotActorAssertion(provenance.actor) }
: {}),
};
}
const engineNotePayloads = new WeakMap();
const engineAuthoredInjections = new WeakSet();
const userInputParkRecords = new WeakMap();
const drainedFromLane = new WeakMap();
const immediateClassInjections = new WeakSet();
const capPreferredInjections = new WeakSet();
function isEngineAuthoredInjection(options) {
if (options === undefined)
return false;
if (options.engineMinted === true || options.enginePayload !== undefined)
return true;
return options.provenance === "engine-note" && options.callerAuthored !== true;
}
const ENGINE_NOTE_STEER_BACKLOG_CAP = 50;
function createFailureMessage(model, error, aborted) {
return {
role: "assistant",
content: [{ type: "text", text: "" }],
api: model.api,
provider: model.provider,
model: model.id,
stopReason: aborted ? "aborted" : "error",
errorMessage: thrownText(error),
timestamp: Date.now(),
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
},
};
}
function cloneStreamOptions(streamOptions) {
return {
...streamOptions,
headers: streamOptions?.headers ? { ...streamOptions.headers } : undefined,
metadata: streamOptions?.metadata ? { ...streamOptions.metadata } : undefined,
};
}
function mergeHeaders(...headers) {
const merged = {};
let hasHeaders = false;
for (const entry of headers) {
if (!entry) {
continue;
}
Object.assign(merged, entry);
hasHeaders = true;
}
return hasHeaders ? merged : undefined;
}
function dropAuthCarriers(bag) {
if (!bag)
return bag;
const out = {};
for (const [k, v] of Object.entries(bag)) {
if (!AUTH_CARRIER_NAMES.has(k.toLowerCase()))
out[k] = v;
}
return out;
}
function refuseAuthCarriersInStreamOptions(bag) {
if (!bag)
return;
for (const k of Object.keys(bag)) {
if (AUTH_CARRIER_NAMES.has(k.toLowerCase())) {
throw new Error(`streamOptions.headers must not carry an auth header ("${k}") — the run-static bag is not an auth channel ` +
`(the route pairing law reads options-borne carriers as per-model credentials, which a run-wide bag is not). ` +
`Put a deployment credential on the brain config (apiKey / headers, where its URL half is judged), or serve ` +
`per-model credentials through getApiKeyAndHeaders.`);
}
}
}
function applyStreamOptionsPatch(base, patch) {
const result = cloneStreamOptions(base);
if (!patch) {
return result;
}
if (Object.hasOwn(patch, "transport")) {
result.transport = patch.transport;
}
if (Object.hasOwn(patch, "timeoutMs")) {
result.timeoutMs = patch.timeoutMs;
}
if (Object.hasOwn(patch, "maxRetries")) {
result.maxRetries = patch.maxRetries;
}
if (Object.hasOwn(patch, "maxRetryDelayMs")) {
result.maxRetryDelayMs = patch.maxRetryDelayMs;
}
if (Object.hasOwn(patch, "cacheRetention")) {
result.cacheRetention = patch.cacheRetention;
}
if (Object.hasOwn(patch, "headers")) {
if (patch.headers === undefined) {
result.headers = undefined;
}
else {
const headers = { ...result.headers };
for (const [key, value] of Object.entries(patch.headers)) {
if (value === undefined) {
delete headers[key];
}
else {
headers[key] = value;
}
}
result.headers = Object.keys(headers).length > 0 ? headers : undefined;
}
}
if (Object.hasOwn(patch, "metadata")) {
if (patch.metadata === undefined) {
result.metadata = undefined;
}
else {
const metadata = { ...result.metadata };
for (const [key, value] of Object.entries(patch.metadata)) {
if (value === undefined) {
delete metadata[key];
}
else {
metadata[key] = value;
}
}
result.metadata = Object.keys(metadata).length > 0 ? metadata : undefined;
}
}
return result;
}
const SUBSCRIBER_EVENT_TYPE = "*";
function normalizeHarnessError(error, fallbackCode) {
if (error instanceof AgentHarnessError) {
return error;
}
const cause = toError(error);
if (cause instanceof SessionError) {
return new AgentHarnessError("session", cause.message, cause);
}
if (cause instanceof CompactionError) {
return new AgentHarnessError("compaction", cause.message, cause);
}
return new AgentHarnessError(fallbackCode, cause.message, cause);
}
function normalizeHookError(error) {
return normalizeHarnessError(error, "hook");
}
export class AgentHarness {
env;
session;
phase = "idle";
get acceptsInjections() {
return this.phase !== "idle" && !this.aborting && !this.stopRequested;
}
stopRequestSources = new Set();
get stopRequested() {
return this.stopRequestSources.size > 0;
}
boundaryStopConsumed;
lastPublishedSeat;
seatCutSources = new WeakMap();
trailingBatchEndedOn;
turnCutConsumed;
aborting = false;
drainedPendingInjection = [];
runAbortController;
runPromise;
pendingSessionWrites = [];
model;
modelRef;
thinkingLevel;
onUndrainedEngineNotes;
onToolTurnRepaired;
runHistory;
onUndrainedUserInputs;
onEngineNoteConsumed;
onUserInputConsumed;
onFrameCommitted;
engineInjectionsHeld;
holdToolTurnLaw(messages) {
const view = ensureToolTurnInvariants(messages, this.runHistory);
if (view.repairs.length > 0) {
try {
this.onToolTurnRepaired?.(view.repairs);
}
catch {
}
}
return view.messages;
}
recoverUndrainedEngineNotes() {
this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
}
sweepUndrainedEngineNotes(queues) {
const undrained = [];
for (const q of queues) {
for (let i = 0; i < q.length; i++) {
const payload = engineNotePayloads.get(q[i]);
if (payload !== undefined) {
undrained.push(payload);
q.splice(i, 1);
i--;
}
}
}
if (undrained.length > 0 && this.onUndrainedEngineNotes) {
try {
this.onUndrainedEngineNotes(undrained);
}
catch {
}
}
}
pendingUserInputsByLane() {
const inFlight = (lane) => this.drainedPendingInjection.filter((m) => !engineAuthoredInjections.has(m) && drainedFromLane.get(m) === lane).length;
return {
steer: this.steerQueue.filter((m) => !engineAuthoredInjections.has(m)).length + inFlight("steer"),
followUp: this.followUpQueue.filter((m) => !engineAuthoredInjections.has(m)).length + inFlight("followUp"),
};
}
hasPendingUserInput() {
if (this.aborting)
return false;
return this.pendingUserInputsByLane().steer > 0;
}
announceUndrainedUserInputs() {
const counts = this.pendingUserInputsByLane();
if ((counts.steer > 0 || counts.followUp > 0) && this.onUndrainedUserInputs) {
try {
this.onUndrainedUserInputs(counts);
}
catch {
}
}
return counts;
}
systemPrompt;
systemBlocks;
streamOptions;
getApiKeyAndHeaders;
runtime;
resources;
tools = new Map();
activeToolNames;
steerQueue = [];
steeringQueueMode;
maxOutputTokens;
maxOutputTokensPerCall;
stallTimeoutsPerCall;
abortResultDetails;
loopTrace;
onToolInputValidationFault;
resilience;
maxToolConcurrency;
streamingToolExecution;
followUpQueue = [];
followUpQueueMode;
nextTurnQueue = [];
loopRecovery;
stopGate;
turnInterruptSeat;
handlers = new Map();
constructor(options) {
this.env = options.env;
this.session = options.session;
this.resources = options.resources ?? {};
refuseAuthCarriersInStreamOptions(options.streamOptions?.headers);
this.streamOptions = cloneStreamOptions(options.streamOptions);
this.systemPrompt = options.systemPrompt;
this.systemBlocks = options.systemBlocks;
this.getApiKeyAndHeaders = options.getApiKeyAndHeaders;
this.runtime = options.runtime;
for (const tool of options.tools ?? []) {
this.tools.set(tool.name, tool);
}
this.model = options.model;
this.modelRef = options.modelRef ?? options.model;
this.thinkingLevel = options.thinkingLevel;
this.activeToolNames =
options.activeToolNames ?? (options.tools ?? []).map((tool) => tool.name);
this.steeringQueueMode = options.steeringMode ?? "one-at-a-time";
this.maxOutputTokens = options.maxOutputTokens;
this.maxOutputTokensPerCall = options.maxOutputTokensPerCall;
this.stallTimeoutsPerCall = options.stallTimeoutsPerCall;
this.abortResultDetails = options.abortResultDetails;
this.loopTrace = options.loopTrace;
this.onToolInputValidationFault = options.onToolInputValidationFault;
this.resilience = options.resilience;
this.maxToolConcurrency = options.maxToolConcurrency;
this.streamingToolExecution = options.streamingToolExecution;
this.followUpQueueMode = options.followUpMode ?? "one-at-a-time";
}
getHandlers(type) {
return this.handlers.get(type);
}
async emitOwn(event, signal) {
for (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) {
try {
await listener(event, signal);
}
catch (error) {
throw normalizeHookError(error);
}
}
}
async emitAny(event, signal) {
for (const listener of this.getHandlers(SUBSCRIBER_EVENT_TYPE) ?? []) {
try {
await listener(event, signal);
}
catch (error) {
throw normalizeHookError(error);
}
}
}
async emitHook(event) {
const handlers = this.getHandlers(event.type);
if (!handlers || handlers.size === 0) {
return undefined;
}
let lastResult;
for (const handler of handlers) {
try {
const result = (await handler(event));
if (result !== undefined) {
lastResult = result;
}
}
catch (error) {
throw normalizeHookError(error);
}
}
return lastResult;
}
async emitBeforeProviderRequest(model, sessionId, streamOptions) {
const handlers = this.getHandlers("before_provider_request");
let current = cloneStreamOptions(streamOptions);
if (!handlers || handlers.size === 0) {
return current;
}
for (const handler of handlers) {
try {
const result = (await handler({
type: "before_provider_request",
model,
sessionId,
streamOptions: cloneStreamOptions(current),
}));
if (result?.streamOptions) {
current = applyStreamOptionsPatch(current, result.streamOptions);
}
}
catch (error) {
throw normalizeHookError(error);
}
}
return current;
}
async emitBeforeProviderPayload(model, payload) {
const handlers = this.getHandlers("before_provider_payload");
let current = payload;
if (!handlers || handlers.size === 0) {
return current;
}
for (const handler of handlers) {
try {
const result = (await handler({
type: "before_provider_payload",
model,
payload: current,
}));
if (result !== undefined) {
current = result.payload;
}
}
catch (error) {
throw normalizeHookError(error);
}
}
return current;
}
async emitQueueUpdate() {
await this.emitOwn({
type: "queue_update",
steer: [...this.steerQueue],
followUp: [...this.followUpQueue],
nextTurn: [...this.nextTurnQueue],
});
}
startRunPromise() {
let finish = () => { };
this.runPromise = new Promise((resolve) => {
finish = resolve;
});
return () => {
this.runPromise = undefined;
finish();
};
}
async createTurnState() {
const windowTokens = this.model.contextTokens ?? this.model.contextWindow;
const context = await this.session.buildContext({
...(typeof windowTokens === "number" && windowTokens > 0 ? { windowTokens } : {}),
...(this.model.charsPerToken !== undefined ? { charsPerToken: this.model.charsPerToken } : {}),
});
const resources = this.getResources();
const sessionMetadata = await this.session.getMetadata();
const tools = [...this.tools.values()];
const activeTools = this.activeToolNames
.map((name) => this.tools.get(name))
.filter((tool) => tool !== undefined);
let systemPrompt = "You are a helpful assistant.";
if (typeof this.systemPrompt === "string") {
systemPrompt = this.systemPrompt;
}
else if (this.systemPrompt) {
systemPrompt = await this.systemPrompt({
env: this.env,
session: this.session,
model: this.model,
thinkingLevel: this.thinkingLevel,
activeTools,
resources,
});
}
return {
messages: context.messages,
resources,
streamOptions: cloneStreamOptions(this.streamOptions),
sessionId: sessionMetadata.id,
systemPrompt,
model: this.model,
thinkingLevel: this.thinkingLevel,
tools,
activeTools,
};
}
createContext(turnState, systemPrompt) {
const effectivePrompt = systemPrompt ?? turnState.systemPrompt;
const blocksSafe = this.systemBlocks !== undefined &&
systemPrompt === undefined &&
typeof this.systemPrompt === "string" &&
this.systemBlocks.map((b) => b.text).join("\n\n") === effectivePrompt;
return {
systemPrompt: effectivePrompt,
...(blocksSafe ? { systemBlocks: this.systemBlocks } : {}),
messages: turnState.messages.slice(),
tools: turnState.activeTools.slice(),
};
}
createStreamFn(getTurnState) {
return async (model, context, streamOptions) => {
const turnState = getTurnState();
const auth = await this.getApiKeyAndHeaders?.(model);
const snapshotOptions = {
...turnState.streamOptions,
headers: mergeHeaders(dropAuthCarriers(turnState.streamOptions.headers), auth?.headers),
};
const requestOptions = await this.emitBeforeProviderRequest(model, turnState.sessionId, snapshotOptions);
if (streamOptions?.signal?.aborted) {
const out = createAssistantMessageEventStream();
out.push({
type: "error",
reason: "aborted",
error: {
role: "assistant",
content: [{ type: "text", text: "" }],
api: model.api,
provider: model.provider,
model: model.id,
stopReason: "aborted",
timestamp: Date.now(),
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
usageMissing: true,
},
});
return out;
}
return resolveAgentCoreStreamFn(this.runtime)(model, context, {
...(auth?.apiKey !== undefined ? { apiKey: auth.apiKey } : {}),
cacheRetention: requestOptions.cacheRetention,
headers: requestOptions.headers,
maxRetries: requestOptions.maxRetries,
maxRetryDelayMs: requestOptions.maxRetryDelayMs,
metadata: requestOptions.metadata,
onPayload: async (payload) => await this.emitBeforeProviderPayload(model, payload),
onResponse: async (response) => {
const headers = { ...response.headers };
await this.emitOwn({ type: "after_provider_response", status: response.status, headers }, streamOptions?.signal);
},
reasoning: streamOptions?.reasoning,
maxTokens: streamOptions?.maxTokens,
stallTimeouts: streamOptions?.stallTimeouts,
maxTokensDynamic: streamOptions?.maxTokensDynamic,
staticReasoningCutDowngrade: streamOptions?.staticReasoningCutDowngrade,
resilience: streamOptions?.resilience,
signal: streamOptions?.signal,
sessionId: turnState.sessionId,
timeoutMs: requestOptions.timeoutMs,
transport: requestOptions.transport,
apiKey: auth?.apiKey,
});
};
}
async drainQueuedMessages(queue, mode) {
let messages;
if (this.aborting)
return [];
if (this.engineInjectionsHeld?.() === true) {
messages = [];
const limit = mode === "all" ? Number.POSITIVE_INFINITY : 1;
for (let i = 0; i < queue.length && messages.length < limit; i++) {
if (engineAuthoredInjections.has(queue[i]))
continue;
messages.push(...queue.splice(i, 1));
i--;
}
}
else {
let count = mode === "all" ? queue.length : 1;
if (mode !== "all" && queue.length > 1 && engineNotePayloads.has(queue[0])) {
count = 1;
while (count < queue.length && engineNotePayloads.has(queue[count]))
count++;
}
messages = queue.splice(0, count);
}
if (messages.length === 0) {
return messages;
}
const lane = queue === this.steerQueue ? "steer" : queue === this.followUpQueue ? "followUp" : undefined;
if (lane !== undefined) {
for (const m of messages)
drainedFromLane.set(m, lane);
this.drainedPendingInjection.push(...messages);
}
try {
await this.emitQueueUpdate();
for (const m of messages) {
const payload = engineNotePayloads.get(m);
if (payload !== undefined) {
try {
this.onEngineNoteConsumed?.(payload);
}
catch {
}
}
}
return messages;
}
catch (error) {
queue.unshift(...messages);
this.unbookInFlight(messages);
throw normalizeHookError(error);
}
}
unbookInFlight(messages) {
if (messages.length === 0 || this.drainedPendingInjection.length === 0)
return;
const gone = new Set(messages);
this.drainedPendingInjection = this.drainedPendingInjection.filter((m) => !gone.has(m));
}
settleInjectedFrame(message, entryId) {
if (!this.drainedPendingInjection.includes(message))
return;
this.unbookInFlight([message]);
const parkRecord = userInputParkRecords.get(message);
userInputParkRecords.delete(message);
if (parkRecord !== undefined) {
try {
this.onUserInputConsumed?.(parkRecord);
}
catch {
}
}
if (entryId !== undefined) {
const payload = engineNotePayloads.get(message);
if (payload !== undefined) {
try {
this.onFrameCommitted?.({ kind: "engine_note", payload }, entryId);
}
catch {
}
}
}
engineNotePayloads.delete(message);
}
createLoopConfig(getTurnState, setTurnState, runPromptOverride) {
const turnState = getTurnState();
const self = this;
return {
model: turnState.model,
reasoning: turnState.thinkingLevel,
...(this.maxOutputTokens !== undefined ? { maxTokens: this.maxOutputTokens } : {}),
...(this.maxOutputTokensPerCall !== undefined ? { maxTokensPerCall: this.maxOutputTokensPerCall } : {}),
...(this.stallTimeoutsPerCall !== undefined ? { stallTimeoutsPerCall: this.stallTimeoutsPerCall } : {}),
...(this.abortResultDetails !== undefined ? { abortResultDetails: this.abortResultDetails } : {}),
...(this.onToolInputValidationFault !== undefined ? { onToolInputValidationFault: this.onToolInputValidationFault } : {}),
...(this.resilience !== undefined ? { resilience: this.resilience } : {}),
...(this.maxToolConcurrency !== undefined ? { maxToolConcurrency: this.maxToolConcurrency } : {}),
...(this.streamingToolExecution === true && (this.getHandlers("tool_call")?.size ?? 0) === 0
? { streamingToolExecution: true }
: {}),
convertToLlm: (messages) => this.holdToolTurnLaw(stripEngineMetadata(convertToLlm(messages))),
shouldStopAfterTurn: () => this.consultStopLatch("post_turn"),
shouldHaltBeforeTurn: () => this.consultStopLatch("pre_turn"),
transformContext: async (messages, signal) => {
const result = await this.emitHook({ type: "context", messages: [...messages], ...(signal !== undefined ? { signal } : {}) });
if (result?.adoptSessionRebuild !== true) {
return result?.messages ?? messages;
}
await this.flushPendingSessionWrites();
const nextTurnState = await this.createTurnState();
setTurnState(nextTurnState);
const rebuiltContext = this.createContext(nextTurnState, runPromptOverride);
const recheck = await this.emitHook({ type: "context", messages: [...rebuiltContext.messages], ...(signal !== undefined ? { signal } : {}), recheck: true });
return {
messages: recheck?.messages ?? rebuiltContext.messages,
adoptedContext: {
messages: rebuiltContext.messages,
systemPrompt: rebuiltContext.systemPrompt,
...(rebuiltContext.systemBlocks !== undefined ? { systemBlocks: rebuiltContext.systemBlocks } : {}),
},
};
},
beforeToolCall: async ({ toolCall, args }, signal) => {
const result = await this.emitHook({
type: "tool_call",
toolCallId: toolCall.id,
toolName: toolCall.name,
input: args,
...(signal !== undefined ? { signal } : {}),
});
return result;
},
afterToolCall: ({ toolCall, args, result, isError }) => this.afterToolCallHook(toolCall, args, result, isError),
prepareNextTurn: async () => {
await this.flushPendingSessionWrites();
await this.emitHook({ type: "turn_boundary", model: this.model });
const nextTurnState = await this.createTurnState();
setTurnState(nextTurnState);
return {
context: this.createContext(nextTurnState, runPromptOverride),
model: nextTurnState.model,
thinkingLevel: nextTurnState.thinkingLevel,
};
},
getSteeringMessages: async () => this.drainQueuedMessages(this.steerQueue, this.steeringQueueMode),
publishTurnInterruptSeat: (seat) => {
if (seat !== undefined) {
this.lastPublishedSeat = seat;
this.turnCutConsumed = undefined;
}
this.turnInterruptSeat = seat;
},
noteTurnCutConsumed: (seat) => this.noteTurnCutConsumed(seat),
recheckImmediateInjections: async (pending) => this.absorbImmediateSteering(pending),
pendingInjectionCount: () => this.pendingInjectionCount(),
getFollowUpMessages: async () => {
const queued = await this.drainQueuedMessages(this.followUpQueue, this.followUpQueueMode);
if (queued.length > 0)
return queued;
return this.stopGate ? await this.stopGate() : queued;
},
recovery: {
get truncatedOutput() {
return self.loopRecovery?.truncatedOutput;
},
get malformedToolUse() {
return self.loopRecovery?.malformedToolUse;
},
get thinkingOnly() {
return self.loopRecovery?.thinkingOnly;
},
get degenerateOutput() {
return self.loopRecovery?.degenerateOutput;
},
get promptTooLong() {
const ptl = self.loopRecovery?.promptTooLong;
if (!ptl)
return undefined;
return {
detect: ptl.detect,
maxRetries: ptl.maxRetries,
withholdErrorEvents: ptl.withholdErrorEvents,
recover: async (_messages, attempt, turnSignal) => {
let shrank = false;
try {
shrank = await ptl.recover(attempt, turnSignal);
}
catch {
shrank = false;
}
if (!shrank)
return undefined;
const nextTurnState = await self.createTurnState();
setTurnState(nextTurnState);
const rebuilt = self.createContext(nextTurnState, runPromptOverride);
return {
messages: rebuilt.messages,
systemPrompt: rebuilt.systemPrompt,
...(rebuilt.systemBlocks !== undefined ? { systemBlocks: rebuilt.systemBlocks } : {}),
};
},
};
},
},
};
}
setLoopRecovery(recovery) {
this.loopRecovery = recovery;
}
setStopGate(gate) {
this.stopGate = gate;
}
validateToolNames(toolNames, tools = this.tools) {
const missing = toolNames.filter((name) => !tools.has(name));
if (missing.length > 0) {
throw new AgentHarnessError("invalid_argument", `Unknown tool(s): ${missing.join(", ")}`);
}
}
async flushPendingSessionWrites() {
if (this.runAbortController?.signal.aborted) {
return;
}
while (this.pendingSessionWrites.length > 0) {
const write = this.pendingSessionWrites[0];
if (write.type === "message") {
await this.session.appendMessage(write.message);
}
else if (write.type === "model_change") {
await this.session.appendModelChange(write.provider, write.modelId);
}
else if (write.type === "thinking_level_change") {
await this.session.appendThinkingLevelChange(write.thinkingLevel);
}
else {
const unreachable = write;
throw new AgentHarnessError("invalid_argument", `flushPendingSessionWrites: unhandled queued write type ${unreachable.type}`);
}
this.pendingSessionWrites.shift();
}
}
async recordApiFailure(failure) {
const errorMessage = readMember(failure, "errorMessage");
const apiErrorStatus = readMember(failure, "apiErrorStatus");
const requestId = readMember(failure, "requestId");
const errorKind = readMember(failure, "errorKind");
const raw = typeof errorMessage === "string" ? errorMessage.trim() : undefined;
const detail = raw !== undefined && raw.length > API_FAILURE_RECORD_MAX_CHARS
? `${raw.slice(0, API_FAILURE_RECORD_MAX_CHARS)}… (truncated)`
: raw;
const record = {
role: "assistant",
content: [{ type: "text", text: detail ? `API Error: ${detail}` : "API Error" }],
api: failure.api,
provider: failure.provider,
model: SYNTHETIC_MESSAGE_MODEL,
isApiErrorMessage: true,
...(typeof apiErrorStatus === "number" ? { apiErrorStatus } : {}),
...(typeof requestId === "string" && isProviderEchoShape(requestId) ? { requestId } : {}),
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 } },
usageMissing: true,
stopReason: "error",
...(typeof errorMessage === "string" ? { errorMessage } : {}),
...(errorKind === "length_empty" || errorKind === "degenerate" || errorKind === "input_too_long" ? { errorKind } : {}),
timestamp: failure.timestamp,
};
try {
await this.session.appendMessage(record);
}
catch {
}
}
async handleAgentEvent(event, signal) {
if (event.type === "message_end") {
let entryId;
if (!isEmptyFailureAssistant(event.message)) {
entryId = await this.session.appendMessage(event.message);
this.settleInjectedFrame(event.message, entryId);
if (entryId !== undefined && event.message.role === "toolResult" && typeof event.message.toolCallId === "string") {
try {
this.onFrameCommitted?.({ kind: "tool_result", toolCallId: event.message.toolCallId, content: event.message.content }, entryId);
}
catch {
}
}
}
else if (event.message.isApiErrorMessage === true) {
await this.recordApiFailure(event.message);
}
await this.emitAny(entryId !== undefined ? { ...event, entryId } : event, signal);
return;
}
if (event.type === "turn_end") {
let eventError;
try {
await this.emitAny(event, signal);
}
catch (error) {
eventError = error;
}
const queued = this.pendingSessionWrites.length > 0;
await this.flushPendingSessionWrites();
const hadPendingMutations = queued && this.pendingSessionWrites.length === 0;
if (eventError) {
throw toLintErrorObject(eventError, "Non-Error thrown");
}
await this.emitOwn({ type: "save_point", hadPendingMutations });
return;
}
if (event.type === "agent_end") {
await this.flushPendingSessionWrites();
this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
const undrainedUser = this.announceUndrainedUserInputs();
this.phase = "idle";
await this.emitAny(event, signal);
await this.emitOwn({
type: "settled",
nextTurnCount: this.nextTurnQueue.length,
...(undrainedUser.steer > 0 ? { undrainedSteerCount: undrainedUser.steer } : {}),
...(undrainedUser.followUp > 0 ? { undrainedFollowUpCount: undrainedUser.followUp } : {}),
}, signal);
return;
}
await this.emitAny(event, signal);
}
async emitRunFailure(model, error, aborted, signal) {
const failureMessage = createFailureMessage(model, error, aborted);
await this.handleAgentEvent({ type: "message_start", message: failureMessage }, signal);
await this.handleAgentEvent({ type: "message_end", message: failureMessage }, signal);
await this.handleAgentEvent({ type: "turn_end", message: failureMessage, toolResults: [] }, signal);
await this.handleAgentEvent({ type: "agent_end", messages: [failureMessage] }, signal);
return [failureMessage];
}
async executeTurn(turnState, text, options, abortController) {
let activeTurnState = turnState;
const userMessage = createUserMessage(text, options?.images, options);
this.runHistory = new WeakSet(turnState.messages.filter((m) => m.role === "assistant"));
const beforeResult = await this.emitHook({
type: "before_agent_start",
prompt: text,
images: options?.images,
systemPrompt: turnState.systemPrompt,
resources: turnState.resources,
});
const takePrompts = async () => {
let messages = [userMessage];
if (this.nextTurnQueue.length > 0) {
const queuedMessages = this.nextTurnQueue.splice(0);
try {
await this.emitQueueUpdate();
}
catch (error) {
this.nextTurnQueue.unshift(...queuedMessages);
throw normalizeHookError(error);
}
for (const m of queuedMessages) {
const payload = engineNotePayloads.get(m);
if (payload !== undefined) {
try {
this.onEngineNoteConsumed?.(payload);
}
catch {
}
}
engineNotePayloads.delete(m);
}
messages = [...queuedMessages, messages[0]];
}
if (beforeResult?.messages) {
messages = [...messages, ...beforeResult.messages];
}
return messages;
};
const getTurnState = () => activeTurnState;
const setTurnState = (nextTurnState) => {
activeTurnState = nextTurnState;
};
const runResultPromise = (async () => {
try {
return await runAgentLoop(takePrompts, this.createContext(turnState, beforeResult?.systemPrompt), this.createLoopConfig(getTurnState, setTurnState, beforeResult?.systemPrompt), (event) => this.handleAgentEvent(event, abortController.signal), abortController.signal, this.createStreamFn(getTurnState), undefined, this.loopTrace, {
...(options?.settleUnansweredToolCalls !== undefined
? {
settle: {
...(options.settleUnansweredToolCalls.halt !== undefined ? { halt: options.settleUnansweredToolCalls.halt } : {}),
report: (outcome) => {
if (outcome.aborted)
this.trailingBatchEndedOn = outcome.assistantMessage;
},
},
}
: {}),
...(options?.onDelivered !== undefined ? { onPromptsDelivered: options.onDelivered } : {}),
});
}
catch (error) {
try {
return await this.emitRunFailure(activeTurnState.model, error, abortController.signal.aborted, abortController.signal);
}
catch (failureError) {
const cause = new AggregateError([toError(error), toError(failureError)], "Agent run failed and failure reporting failed");
throw new AgentHarnessError("unknown", cause.message, cause);
}
}
})();
try {
const newMessages = await runResultPromise;
for (let i = newMessages.length - 1; i >= 0; i--) {
const message = newMessages[i];
if (message.role === "assistant") {
return mergeTruncatedOutputChain(newMessages, i);
}
}
if (this.stopRequested || abortController.signal.aborted)
return undefined;
if (this.trailingBatchEndedOn !== undefined)
return this.trailingBatchEndedOn;
throw new AgentHarnessError("invalid_state", "AgentHarness prompt completed without an assistant message");
}
finally {
try {
await this.flushPendingSessionWrites();
}
finally {
this.runAbortController = undefined;
}
}
}
async prompt(text, options) {
if (this.phase !== "idle") {
throw new AgentHarnessError("busy", "AgentHarness is busy");
}
this.trailingBatchEndedOn = undefined;
this.stopRequestSources.clear();
this.boundaryStopConsumed = undefined;
this.turnCutConsumed = undefined;
this.lastPublishedSeat = undefined;
this.aborting = false;
if (this.drainedPendingInjection.length > 0) {
const stranded = this.drainedPendingInjection;
this.drainedPendingInjection = [];
for (let i = stranded.length - 1; i >= 0; i--) {
const frame = stranded[i];
const lane = drainedFromLane.get(frame);
if (lane === "steer")
this.steerQueue.unshift(frame);
else if (lane === "followUp")
this.followUpQueue.unshift(frame);
}
}
this.phase = "turn";
const abortController = new AbortController();
this.runAbortController = abortController;
const finishRunPromise = this.startRunPromise();
try {
await this.flushPendingSessionWrites();
const turnState = await this.createTurnState();
return await this.executeTurn(turnState, text, options, abortController);
}
catch (error) {
this.phase = "idle";
throw normalizeHarnessError(error, "unknown");
}
finally {
if (this.runAbortController === abortController)
this.runAbortController = undefined;
finishRunPromise();
}
}
static emptyInjection(text, options) {
return text.trim().length === 0 && (options?.images === undefined || options.images.length === 0);
}
async enqueueInjection(queue, text, options) {
if (AgentHarness.emptyInjection(text, options))
return undefined;
let displaced;
if (options?.enginePayload !== undefined && queue.filter((q) => engineNotePayloads.has(q)).length >= ENGINE_NOTE_STEER_BACKLOG_CAP) {
const victimIdx = options.capPreferred === true
? queue.findIndex((q) => engineNotePayloads.has(q) && !capPreferredInjections.has(q))
: -1;
if (victimIdx === -1) {
throw new AgentHarnessError("invalid_state", `engine-note backlog at cap (${ENGINE_NOTE_STEER_BACKLOG_CAP}) — park the payload for the session's next run`);
}
displaced = { frame: queue.splice(victimIdx, 1)[0], at: victimIdx };
}
const m = createUserMessage(text, options?.images, options);
if (options?.enginePayload !== undefined)
engineNotePayloads.set(m, options.enginePayload);
if (isEngineAuthoredInjection(options))
engineAuthoredInjections.add(m);
if (options?.parkRecord !== undefined)
userInputParkRecords.set(m, options.parkRecord);
if (options?.immediate === true)
immediateClassInjections.add(m);
if (options?.capPreferred === true && options?.enginePayload !== undefined)
capPreferredInjections.add(m);
if (options?.immediate === true && queue === this.steerQueue) {
let at = 0;
while (at < queue.length && immediateClassInjections.has(queue[at]))
at++;
queue.splice(at, 0, m);
}
else {
queue.push(m);
}
try {
await this.emitQueueUpdate();
}
catch (error) {
const at = queue.indexOf(m);
if (at !== -1) {
queue.splice(at, 1);
if (displaced !== undefined)
queue.splice(Math.min(displaced.at, queue.length), 0, displaced.frame);
throw error;
}
}
if (displaced !== undefined) {
const victimPayload = engineNotePayloads.get(displaced.frame);
engineNotePayloads.delete(displaced.frame);
if (victimPayload !== undefined && this.onUndrainedEngineNotes) {
try {
this.onUndrainedEngineNotes([victimPayload]);
}
catch {
}
}
}
return m;
}
refuseHeldEngineInjection(options) {
if (isEngineAuthoredInjection(options) && this.engineInjectionsHeld?.() === true) {
throw new AgentHarnessError("invalid_state", "engine-authored injection refused: the run is halted awaiting the user's own direction — park the payload for the session's next run");
}
}
async steer(text, options) {
if (this.phase === "idle") {
throw new AgentHarnessError("invalid_state", "Cannot steer while idle");
}
this.refuseHeldEngineInjection(options);
return await this.enqueueInjection(this.steerQueue, text, options);
}
async followUp(text, options) {
if (this.phase === "idle") {
throw new AgentHarnessError("invalid_state", "Cannot follow up while idle");
}
this.refuseHeldEngineInjection(options);
await this.enqueueInjection(this.followUpQueue, text, options);
}
interruptTurn(frame) {
if (this.aborting)
return false;
if (!this.steerQueue.includes(frame))
return false;
const seat = this.turnInterruptSeat;
if (seat === undefined || seat.signal.aborted)
return false;
this.cutSeat(seat, "steer");
return true;
}
halt() {
if (this.phase === "idle") {
throw new AgentHarnessError("invalid_state", "Cannot halt while idle");
}
if (this.aborting)
return { turnCut: false, accepted: false };
this.stopRequestSources.add("halt");
const seat = this.turnInterruptSeat;
if (seat === undefined || seat.signal.aborted)
return { turnCut: false, accepted: true };
this.cutSeat(seat, "halt");
return { turnCut: true, accepted: true };
}
cutSeat(seat, source) {
this.seatCutSources.set(seat, source);
const runQuietBefore = this.runAbortController?.signal.aborted !== true;
seat.abort();
if (runQuietBefore && this.runAbortController?.signal.aborted === true)
this.turnCutConsumed = { source };
}
haltConsumption() {
const turnCut = this.turnCutConsumed?.source;
return {
...(turnCut !== undefined ? { turnCut } : {}),
...(this.boundaryStopConsumed !== undefined ? { boundaryStop: this.boundaryStopConsumed } : {}),
};
}
consultStopLatch(face) {
if (!this.stopRequested)
return false;
if (this.runAbortController?.signal.aborted !== true) {
this.boundaryStopConsumed ??= { face, sources: [...this.stopRequestSources] };
}
return true;
}
noteTurnCutConsumed(seat) {
if (seat !== this.lastPublishedSeat) {
throw new AgentHarnessError("invalid_state", "turn-cut consumption reported for a seat the harness did not publish last");
}
this.turnCutConsumed = { source: this.seatCutSources.get(seat) };
}
pendingInjectionCount() {
if (this.aborting)
return 0;
const held = this.engineInjectionsHeld?.() === true;
const drainable = (q) => held ? q.filter((m) => !engineAuthoredInjections.has(m)).length : q.length;
return drainable(this.steerQueue) + drainable(this.followUpQueue);
}
async absorbImmediateSteering(pending) {
if (this.aborting)
return pending;
const held = this.engineInjectionsHeld?.() === true;
const taken = [];
for (let i = 0; i < this.steerQueue.length; i++) {
const m = this.steerQueue[i];
if (!immediateClassInjections.has(m))
break;
if (held && engineAuthoredInjections.has(m))
continue;
taken.push(...this.steerQueue.splice(i, 1));
i--;
}
if (taken.length === 0)
return pending;
for (const m of taken)
drainedFromLane.set(m, "steer");
this.drainedPendingInjection.push(...taken);
try {
await this.emitQueueUpdate();
for (const m of taken) {
const payload = engineNotePayloads.get(m);
if (payload !== undefined) {
try {
this.onEngineNoteConsumed?.(payload);
}
catch {
}
}
}
}
catch (error) {
this.steerQueue.unshift(...taken);
this.unbookInFlight(taken);
throw normalizeHookError(error);
}
let at = 0;
while (at < pending.length && immediateClassInjections.has(pending[at]))
at++;
return [...pending.slice(0, at), ...taken, ...pending.slice(at)];
}
async nextTurn(text, options) {
await this.enqueueInjection(this.nextTurnQueue, text, options);
}
async appendMessage(message) {
try {
if (this.phase === "idle") {
await this.session.appendMessage(message);
}
else {
this.pendingSessionWrites.push({ type: "message", message });
}
}
catch (error) {
throw normalizeHarnessError(error, "session");
}
}
async afterToolCallHook(toolCall, args, result, isError) {
const patch = await this.emitHook({
type: "tool_result",
toolCallId: toolCall.id,
toolName: toolCall.name,
input: args,
content: result.content,
details: result.details,
isError,
});
return patch;
}
finalizeToolCall(toolCall, args, executed) {
return finalizeToolOutcome(toolCall, executed, (result, isError) => this.afterToolCallHook(toolCall, args, result, isError));
}
getModel() {
return this.model;
}
getModelRef() {
return this.modelRef;
}
getThinkingLevel() {
return this.thinkingLevel;
}
async setModel(model, ref) {
try {
const previousModel = this.model;
if (this.phase === "idle") {
await this.session.appendModelChange(model.provider, model.id);
}
else {
this.pendingSessionWrites.push({
type: "model_change",
provider: model.provider,
modelId: model.id,
});
}
this.model = model;
this.modelRef = ref ?? model;
await this.emitOwn({ type: "model_select", model, previousModel, source: "set" });
}
catch (error) {
throw normalizeHarnessError(error, "session");
}
}
async setThinkingLevel(level) {
try {
const previousLevel = this.thinkingLevel;
if (this.phase === "idle") {
await this.session.appendThinkingLevelChange(level);
}
else {
this.pendingSessionWrites.push({ type: "thinking_level_change", thinkingLevel: level });
}
this.thinkingLevel = level;
await this.emitOwn({ type: "thinking_level_select", level, previousLevel });
}
catch (error) {
throw normalizeHarnessError(error, "session");
}
}
async setActiveTools(toolNames) {
try {
this.validateToolNames(toolNames);
this.activeToolNames = [...toolNames];
}
catch (error) {
throw normalizeHarnessError(error, "invalid_argument");
}
}
getSteeringMode() {
return this.steeringQueueMode;
}
async setSteeringMode(mode) {
this.steeringQueueMode = mode;
}
getFollowUpMode() {
return this.followUpQueueMode;
}
async setFollowUpMode(mode) {
this.followUpQueueMode = mode;
}
getResources() {
return {
skills: this.resources.skills?.slice(),
promptTemplates: this.resources.promptTemplates?.slice(),
};
}
async setResources(resources) {
const previousResources = this.getResources();
this.resources = {
skills: resources.skills?.slice(),
promptTemplates: resources.promptTemplates?.slice(),
};
await this.emitOwn({
type: "resources_update",
resources: this.getResources(),
previousResources,
});
}
getStreamOptions() {
return cloneStreamOptions(this.streamOptions);
}
async setStreamOptions(streamOptions) {
refuseAuthCarriersInStreamOptions(streamOptions?.headers);
this.streamOptions = cloneStreamOptions(streamOptions);
}
setSystemPrompt(prompt, blocks) {
this.systemPrompt = prompt;
this.systemBlocks = blocks;
}
async setTools(tools, activeToolNames) {
try {
const nextTools = new Map(tools.map((tool) => [tool.name, tool]));
const nextActiveToolNames = activeToolNames ? [...activeToolNames] : this.activeToolNames;
this.validateToolNames(nextActiveToolNames, nextTools);
this.tools = nextTools;
this.activeToolNames = [...nextActiveToolNames];
}
catch (error) {
throw normalizeHarnessError(error, "invalid_argument");
}
}
requestStopAfterTurn() {
this.stopRequestSources.add("soft");
}
async flushQueuedSessionWrites() {
try {
await this.flushPendingSessionWrites();
}
catch (error) {
throw normalizeHarnessError(error, "session");
}
}
readParkableUserInputs() {
const out = [];
for (const q of [this.drainedPendingInjection, this.steerQueue, this.followUpQueue]) {
for (const m of q) {
const record = userInputParkRecords.get(m);
if (record !== undefined)
out.push(record);
}
}
return out;
}
dropParkedUserInputs(records) {
const dropped = { steer: 0, followUp: 0 };
if (records.length === 0)
return dropped;
const migrated = new Set(records);
for (const q of [this.drainedPendingInjection, this.steerQueue, this.followUpQueue]) {
const fixedLane = q === this.steerQueue ? "steer" : q === this.followUpQueue ? "followUp" : undefined;
for (let i = 0; i < q.length; i++) {
const frame = q[i];
const record = userInputParkRecords.get(frame);
if (record === undefined || !migrated.has(record))
continue;
const lane = fixedLane ?? drainedFromLane.get(frame);
userInputParkRecords.delete(frame);
q.splice(i, 1);
i--;
if (lane !== undefined)
dropped[lane] += 1;
}
}
return dropped;
}
async abort() {
this.aborting = true;
this.runAbortController?.abort();
const errors = [];
try {
await this.emitQueueUpdate();
}
catch (error) {
errors.push(toError(error));
}
try {
await this.waitForIdle();
}
catch (error) {
errors.push(toError(error));
}
this.sweepUndrainedEngineNotes([this.drainedPendingInjection, this.nextTurnQueue, this.steerQueue, this.followUpQueue]);
const userAuthored = (m) => !engineAuthoredInjections.has(m);
const inFlightOf = (lane) => this.drainedPendingInjection.filter((m) => userAuthored(m) && drainedFromLane.get(m) === lane);
const undrainedSteer = [...inFlightOf("steer"), ...this.steerQueue.filter(userAuthored)];
const undrainedFollowUp = [...inFlightOf("followUp"), ...this.followUpQueue.filter(userAuthored)];
try {
await this.emitOwn({ type: "abort", undrainedSteer, undrainedFollowUp });
}
catch (error) {
errors.push(toError(error));
}
if (errors.length > 0) {
const cause = errors.length === 1 ? errors[0] : new AggregateError(errors, "Abort completed with errors");
throw normalizeHarnessError(cause, "hook");
}
return { undrainedSteer, undrainedFollowUp };
}
async waitForIdle() {
await this.runPromise;
}
subscribe(listener) {
let handlers = this.handlers.get(SUBSCRIBER_EVENT_TYPE);
if (!handlers) {
handlers = new Set();
this.handlers.set(SUBSCRIBER_EVENT_TYPE, handlers);
}
handlers.add(listener);
return () => handlers.delete(listener);
}
on(type, handler) {
let handlers = this.handlers.get(type);
if (!handlers) {
handlers = new Set();
this.handlers.set(type, handlers);
}
handlers.add(handler);
return () => handlers.delete(handler);
}
}
function isEmptyFailureAssistant(message) {
if (message.role !== "assistant")
return false;
const m = message;
if (m.stopReason !== "aborted" && m.stopReason !== "error")
return false;
return m.content.every((c) => c.type === "text" ? c.text.trim() === "" : c.type === "thinking" ? c.thinking.trim() === "" : false);
}
function mergeTruncatedOutputChain(messages, lastAssistantIndex) {
const last = messages[lastAssistantIndex];
const lastText = last.content.map((c) => (c.type === "text" ? c.text : "")).join("");
const parts = [];
let i = lastAssistantIndex;
while (i - 2 >= 0) {
const mid = messages[i - 1];
if (mid.role !== "custom")
break;
const h = messages[i - 2];
if (h.role !== "assistant")
break;
const head = h;
const step = (mid.customType === "loop:truncated-output-continue" && head.stopReason === "length") ||
(mid.customType === "loop:midstream-partial-continue" && head.stopReason === "stop");
if (!step)
break;
parts.unshift(head.content.map((c) => (c.type === "text" ? c.text : "")).join(""));
i = i - 2;
}
if (parts.length === 0) {
return last;
}
return {
...last,
content: [
{ type: "text", text: parts.join("") + lastText },
...last.content.filter((c) => c.type !== "text"),
],
};
}
function toLintErrorObject(value, fallbackMessage) {
if (value instanceof Error) {
return value;
}
if (typeof value === "string") {
return new Error(value);
}
const error = new Error(fallbackMessage, { cause: value });
if ((typeof value === "object" && value !== null) || typeof value === "function") {
Object.assign(error, value);
}
return error;
}