openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
2,729 lines • 97.2 kB
JavaScript
import { C as parseStrictNonNegativeInteger, P as resolvePositiveTimerTimeoutMs } from "./number-coercion-CLj0HTDM.js";
import { t as coerceErrorMessage } from "./error-coercion-D_-xJ90S.js";
import { a as asOptionalRecord } from "./record-coerce-DItp3I4t.js";
import { n as ok, t as err } from "./result-BQGgYouL.js";
import "./http-body-D3IMwTJJ.js";
import { i as readResponseWithLimit } from "./http-response-body-CwT_cCNz.js";
import "./llm-Cb7iG-ku.js";
import { A as consumeAgentCoreStream, C as formatFileOperations, E as BranchSummaryError, M as resolveAgentCoreStreamFn, S as extractSummaryText, T as serializeConversation, b as createFileOps, d as estimateTokens, i as SUMMARIZATION_SYSTEM_PROMPT, j as resolveAgentCoreCompleteFn, k as resolveAgentReasoningOption, w as mergeSummaryFileOperations, x as extractFileOpsFromMessage, y as computeFileLists } from "./compaction-Gq_Mr3Zu.js";
import { d as getInternalSteeringQueueObserver, f as getInternalSyncSteeringGetter, g as takeInternalToolBatchLifecycle, l as copyInternalToolResultState, n as appendToolLoopWarning, p as getInternalToolExecutionPreparer, r as attachInternalSyncSteeringGetter, u as getInternalBeforeToolBatch } from "./internal-hooks-CgPsiqhr.js";
import { n as TranscriptNotContinuableError } from "./errors-wmH7Ncz4.js";
import { n as runWithAgentToolExecutionContext } from "./tool-execution-context-C6v2UVPI.js";
import { i as streamSimple, n as completeSimple } from "./stream-Ci9pOvq7.js";
import { t as event_stream_exports } from "./event-stream-CPdae0II.js";
import { i as projectSessionEntryMessage, u as convertToLlm } from "./session-CcAchsL_.js";
import { replaceCompactionReplayOwnerContent } from "@openclaw/ai/transports";
import "@openclaw/ai/event-stream";
import { validateToolArguments } from "@openclaw/ai/validation";
import { createSseByteGuard, createToolArgumentPreviewSchedule, parseStreamingJson, parseTerminalToolCallArguments } from "@openclaw/ai/internal/runtime";
//#region packages/agent-core/src/harness/session/uuid.ts
let lastTimestamp = -Infinity;
let sequence = 0;
function fillRandomBytes(bytes) {
const crypto = globalThis.crypto;
if (crypto?.getRandomValues) {
crypto.getRandomValues(bytes);
return;
}
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
}
/** Generate a monotonic UUIDv7 string. */
function uuidv7() {
const random = /* @__PURE__ */ new Uint8Array(16);
fillRandomBytes(random);
const timestamp = Date.now();
if (timestamp > lastTimestamp) {
sequence = new DataView(random.buffer, random.byteOffset + 6, 4).getUint32(0);
lastTimestamp = timestamp;
} else {
sequence = sequence + 1 >>> 0;
if (sequence === 0) lastTimestamp++;
}
const bytes = /* @__PURE__ */ new Uint8Array(16);
bytes[0] = lastTimestamp / 1099511627776 & 255;
bytes[1] = lastTimestamp / 4294967296 & 255;
bytes[2] = lastTimestamp / 16777216 & 255;
bytes[3] = lastTimestamp / 65536 & 255;
bytes[4] = lastTimestamp / 256 & 255;
bytes[5] = lastTimestamp & 255;
bytes[6] = 112 | sequence >>> 28 & 15;
bytes[7] = sequence >>> 20 & 255;
bytes[8] = 128 | sequence >>> 14 & 63;
bytes[9] = sequence >>> 6 & 255;
const randomLowBits = random.at(10);
if (randomLowBits === void 0) throw new Error("UUID random buffer is shorter than 11 bytes");
bytes[10] = (sequence & 63) << 2 | randomLowBits & 3;
bytes.set(random.subarray(11), 11);
return formatUuid(bytes);
}
function formatUuid(bytes) {
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10, 16).join("")}`;
}
//#endregion
//#region packages/agent-core/src/stream-steering.ts
/** Forward queued input while its normal transcript owner retains commit ordering. */
function createStreamSteering(config, signal, convertSteering) {
const observer = getInternalSteeringQueueObserver(config.getSteeringMessages);
let open = true;
let cleanup;
let forwarding = Promise.resolve();
let needsContinuation;
let finished;
let failure;
let submitted = false;
const stop = () => {
open = false;
const release = cleanup;
cleanup = void 0;
release?.();
};
const onActiveResponse = (control) => {
if (!open || signal?.aborted) return;
needsContinuation = control.needsContinuation;
const outerCleanup = config.onActiveResponse?.(control);
const forward = () => {
if (!open || signal?.aborted || failure || !observer || submitted) return;
const messages = observer.peek();
if (messages.length === 0) return;
submitted = true;
const release = observer.reserve(messages);
let dispatched = false;
forwarding = forwarding.then(async () => {
if (!open || signal?.aborted) {
release();
return;
}
const converted = await convertSteering([...messages]);
const userMessages = converted.filter((message) => message.role === "user");
if (!open || signal?.aborted || userMessages.length !== converted.length || userMessages.length === 0) {
release();
stop();
return;
}
dispatched = true;
if (!await control.steer(userMessages)) {
release();
stop();
}
}).catch((error) => {
if (!dispatched) release();
failure ??= { error };
});
};
const unsubscribe = observer?.subscribe(forward);
cleanup = () => {
unsubscribe?.();
outerCleanup?.();
};
forward();
return stop;
};
return {
onActiveResponse,
finish() {
return finished ??= (async () => {
stop();
await forwarding;
if (failure) throw failure.error;
return needsContinuation?.() === true;
})();
}
};
}
//#endregion
//#region packages/agent-core/src/turn-interruption.ts
/** Canonical empty aborted/error assistant recorded when a run ends without output. */
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: error instanceof Error ? error.message : String(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
}
}
};
}
const INTERRUPTED_TURN_GUIDANCE = `<turn_aborted>
The previous turn was interrupted. Any running background processes may still be active. If any tools or commands were aborted, they may have partially executed.
</turn_aborted>`;
/**
* Aborts that end a turn as an intentional handoff (e.g. yield-style tools)
* mark it with an abort reason carrying `turnHandoff: true`. Interruption
* guidance is skipped for them: the next turn would otherwise be told tools
* may have partially executed after a clean, deliberate stop.
*/
function isTurnHandoffAbort(signal) {
if (!signal?.aborted) return false;
const reason = signal.reason;
return typeof reason === "object" && reason !== null && reason.turnHandoff === true;
}
function createInterruptedTurnMessage() {
return {
role: "custom",
customType: "openclaw:turn-aborted",
content: INTERRUPTED_TURN_GUIDANCE,
display: false,
timestamp: Date.now()
};
}
async function appendInterruptedTurnMessage(messages, emit) {
const interruption = createInterruptedTurnMessage();
messages.push(interruption);
await emit({
type: "message_start",
message: interruption
});
await emit({
type: "message_end",
message: interruption
});
}
function normalizeCoreContextMessages(messages) {
return messages.map((message) => {
if (message.role !== "custom" || message.customType !== "openclaw:turn-aborted") return message;
return {
role: "user",
content: typeof message.content === "string" ? [{
type: "text",
text: message.content
}] : message.content,
timestamp: message.timestamp
};
});
}
//#endregion
//#region packages/agent-core/src/agent-stream-response.ts
function appendTextDeltaToAssistantMessage(message, contentIndex, delta) {
const content = [...message.content];
const currentContent = content[contentIndex];
content[contentIndex] = currentContent?.type === "text" ? {
...currentContent,
text: currentContent.text + delta
} : {
type: "text",
text: delta
};
return {
...message,
content
};
}
function resolveAssistantMessageUpdate(event, currentMessage) {
if ("partial" in event && event.partial) return event.partial;
if (event.type === "text_delta") return appendTextDeltaToAssistantMessage(currentMessage, event.contentIndex, event.delta);
return currentMessage;
}
function removeNonExecutableToolCalls(message) {
if (message.stopReason === "toolUse") return message;
const content = message.content.filter((item) => item.type !== "toolCall" || item.async);
return content.length === message.content.length ? message : replaceCompactionReplayOwnerContent(message, content);
}
function ensureToolTurnIdentity(message) {
if (!(message.stopReason === "toolUse" || (message.stopReason === "stop" || message.stopReason === "length") && message.content.some((item) => item.type === "toolCall" && item.async)) || message.responseId?.trim() || message.turnId?.trim()) return message;
return {
...message,
turnId: uuidv7()
};
}
async function streamAgentResponse(context, config, signal, emit, newMessages, executeAsyncTools, prepareAssistantMessage, streamFn, runtime) {
const sourceMessages = [...context.messages];
const convertMessages = async (messages, projectionSignal = signal) => {
const transformed = config.transformContext ? await config.transformContext(messages, projectionSignal) : messages;
return config.convertToLlm(normalizeCoreContextMessages(transformed));
};
const llmMessages = await convertMessages(sourceMessages);
let requestPrefix;
const llmContext = {
systemPrompt: context.systemPrompt,
messages: llmMessages,
tools: context.tools
};
const streamFunction = resolveAgentCoreStreamFn(runtime, streamFn);
const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : void 0) || config.apiKey;
const executionAbort = new AbortController();
const executionSignal = signal ? AbortSignal.any([signal, executionAbort.signal]) : executionAbort.signal;
const abortFailedResponse = (message) => {
if (message?.stopReason === "error" || message?.stopReason === "aborted") executionAbort.abort(new Error(message.errorMessage ?? "Model response interrupted"));
};
const steering = createStreamSteering(config, executionSignal, async (pending) => {
requestPrefix ??= JSON.stringify(llmMessages);
const projected = await convertMessages([...sourceMessages, ...pending], executionSignal);
if (JSON.stringify(projected.slice(0, llmMessages.length)) !== requestPrefix) return [];
return projected.slice(llmMessages.length);
});
const executedIds = /* @__PURE__ */ new Set();
const batches = [];
let executions = Promise.resolve();
let admissions = Promise.resolve();
let executionFailure;
const emitToolEvent = async (event) => {
if (event.type === "message_end" && event.message.role === "toolResult") {
context.messages.push(event.message);
newMessages.push(event.message);
}
await emit(event);
};
const enqueueTools = (message) => {
if (message.stopReason === "error" || message.stopReason === "aborted") return;
const calls = message.content.filter((item) => item.type === "toolCall" && !executedIds.has(item.id) && (message.stopReason === "toolUse" || item.async === true));
if (calls.length === 0) return;
for (const call of calls) executedIds.add(call.id);
const previousExecutions = executions;
const previousAdmission = admissions;
let releaseAdmission;
admissions = new Promise((resolve) => {
releaseAdmission = resolve;
});
const execution = previousAdmission.then(async () => {
const batch = await executeAsyncTools(message, calls, executionSignal, emitToolEvent, {
waitForPrevious: () => previousExecutions,
onParallelStarted: releaseAdmission
});
batches.push(batch);
if (batch.fatal || batch.terminateRun) executionAbort.abort(batch.fatal?.error ?? /* @__PURE__ */ new Error("Tool batch terminated"));
}).catch((error) => {
executionFailure ??= { error };
executionAbort.abort(error);
}).finally(releaseAdmission);
executions = Promise.all([previousExecutions, execution]).then(() => {});
};
try {
const response = await streamFunction(config.model, llmContext, {
...config,
apiKey: resolvedApiKey,
signal: executionSignal,
onActiveResponse: steering.onActiveResponse,
asyncToolExecution: true
});
let partialMessage = null;
let partialIndex;
let committedContentCount = 0;
let streamedTurnId;
const remainingFragment = (message) => committedContentCount === 0 ? message : replaceCompactionReplayOwnerContent(message, message.content.slice(committedContentCount));
const updatePartial = async (message) => {
const fragment = remainingFragment(message);
if (partialIndex === void 0) {
partialIndex = context.messages.length;
context.messages.push(fragment);
await emit({
type: "message_start",
message: { ...fragment }
});
} else context.messages[partialIndex] = fragment;
return fragment;
};
const commitFragment = async (message) => {
if (partialIndex === void 0) {
context.messages.push(message);
await emit({
type: "message_start",
message: { ...message }
});
} else {
context.messages.splice(partialIndex, 1);
context.messages.push(message);
}
partialIndex = void 0;
newMessages.push(message);
await emit({
type: "message_end",
message
});
};
for await (const event of response) switch (event.type) {
case "start": {
const message = event.partial;
partialMessage = message;
await updatePartial(message);
break;
}
case "text_start":
case "text_delta":
case "text_end":
case "thinking_start":
case "thinking_delta":
case "thinking_end":
case "toolcall_start":
case "toolcall_delta":
case "toolcall_end":
if (partialMessage) {
const message = resolveAssistantMessageUpdate(event, partialMessage);
partialMessage = message;
if (event.contentIndex < committedContentCount) break;
const fragment = await updatePartial(message);
await emit({
type: "message_update",
assistantMessageEvent: {
...event,
contentIndex: event.contentIndex - committedContentCount,
..."partial" in event ? { partial: fragment } : {}
},
message: { ...fragment }
});
if (event.type === "toolcall_end" && event.toolCall.async && !executedIds.has(event.toolCall.id) && message.content.slice(committedContentCount, event.contentIndex).every((item) => item.type !== "toolCall" || item.async === true)) {
const prefix = prepareAssistantMessage(ensureToolTurnIdentity({
...replaceCompactionReplayOwnerContent(message, message.content.slice(committedContentCount, event.contentIndex + 1)),
...streamedTurnId ? { turnId: streamedTurnId } : {},
stopReason: "toolUse",
usage: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0
}
}
}));
streamedTurnId ??= prefix.turnId;
await commitFragment(prefix);
committedContentCount = event.contentIndex + 1;
enqueueTools(prefix);
}
}
break;
case "done":
case "error": return await finalizeAssistantMessage(event.type === "done" ? event.message : event.error);
}
return await finalizeAssistantMessage();
async function finalizeAssistantMessage(terminal) {
abortFailedResponse(terminal);
const result = await response.result();
abortFailedResponse(result);
const finalMessage = prepareAssistantMessage(ensureToolTurnIdentity(removeNonExecutableToolCalls({
...remainingFragment(result),
...streamedTurnId ? { turnId: streamedTurnId } : {}
})));
await commitFragment(finalMessage);
if (executedIds.size > 0) enqueueTools(finalMessage);
await executions;
if (executionFailure) throw executionFailure.error;
const continuationRequired = await steering.finish();
return {
message: finalMessage,
executedIds,
batches,
continuationRequired
};
}
} finally {
executionAbort.abort(/* @__PURE__ */ new Error("Model response closed"));
await executions;
await steering.finish();
}
}
//#endregion
//#region packages/agent-core/src/agent-loop.ts
const TOOL_LOOP_RECOVERY_TERMINATED_MESSAGE = "OpenClaw stopped this run because tool-loop recovery encountered another critical loop. No blocked tool action was executed.";
const STEERING_TOOL_SKIP_MESSAGE = "Skipped due to queued user message.";
const TOOL_ADMISSION_FAILURE_MESSAGE = "Tool execution was blocked before launch.";
const TOOL_ADMISSION_FAILURE_DETAILS = {
status: "blocked",
deniedReason: "tool-admission"
};
function getSteeringAtCheckpoint(config) {
const callback = config.getSteeringMessages;
if (!callback) return [];
return getInternalSyncSteeringGetter(callback)?.() ?? callback.call(config);
}
/** Run a prompt-started loop and emit events through a caller-owned sink. */
async function runAgentLoop(prompts, context, config, emit, signal, streamFn, runtime) {
return runAgentLoopCore(prompts, context, config, emit, signal, streamFn, runtime);
}
/** Continue an existing loop context and emit only newly produced messages. */
async function runAgentLoopContinue(context, config, emit, signal, streamFn, runtime) {
assertContinuableContext(context);
return runAgentLoopCore([], context, config, emit, signal, streamFn, runtime);
}
function assertContinuableContext(context) {
const lastMessage = context.messages.at(-1);
if (!lastMessage) throw new Error("Cannot continue: no messages in context");
if (lastMessage.role === "assistant") throw new TranscriptNotContinuableError(lastMessage.role);
}
async function runAgentLoopCore(prompts, context, config, emit, signal, streamFn, runtime) {
const newMessages = [];
const state = { context: {
...context,
messages: [...context.messages]
} };
await emit({ type: "agent_start" });
await emit({ type: "turn_start" });
for (const prompt of prompts) {
if (config.consumeQueuedMessageCancellation?.(prompt)) continue;
await emit({
type: "message_start",
message: prompt
});
if (config.consumeQueuedMessageCancellation?.(prompt)) continue;
await emit({
type: "message_end",
message: prompt
});
state.context.messages.push(prompt);
newMessages.push(prompt);
}
if (prompts.length > 0 && newMessages.length === 0) {
await emit({
type: "agent_end",
messages: []
});
return [];
}
return runLoop(state, newMessages, config, signal, emit, streamFn, runtime);
}
/**
* Own one replaceable context slot so this async frame does not retain earlier
* contexts after a next-turn hook replaces them.
*/
async function runLoop(state, newMessages, initialConfig, signal, emit, streamFn, runtime) {
let config = initialConfig;
let firstTurn = true;
let turnOpen = true;
let turnTainted = isActiveTurnTainted(state.context.messages);
const toolLoopRecoveryState = initialConfig.toolLoopRecoveryState ?? { criticalToolLoopSeen: false };
const initialSteering = getSteeringAtCheckpoint(config);
let pendingMessages = Array.isArray(initialSteering) ? initialSteering : await initialSteering;
const stopIfAborted = async () => {
if (!signal?.aborted) return false;
const abortedMessage = withAssistantTurnTaint(createFailureMessage(config.model, signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Agent run aborted"), true), turnTainted);
newMessages.push(abortedMessage);
if (!turnOpen) {
await emit({ type: "turn_start" });
turnOpen = true;
}
await emit({
type: "message_start",
message: abortedMessage
});
await emit({
type: "message_end",
message: abortedMessage
});
await emit({
type: "turn_end",
message: abortedMessage,
toolResults: []
});
turnOpen = false;
if (!isTurnHandoffAbort(signal)) await appendInterruptedTurnMessage(newMessages, emit);
await emit({
type: "agent_end",
messages: newMessages
});
return true;
};
while (true) {
let hasMoreToolCalls = true;
while (hasMoreToolCalls || pendingMessages.length > 0) {
if (await stopIfAborted()) return newMessages;
if (!firstTurn) {
await emit({ type: "turn_start" });
turnOpen = true;
} else firstTurn = false;
if (pendingMessages.length > 0) {
const messagesToInject = pendingMessages;
let injectedMessage = false;
pendingMessages = [];
for (const message of messagesToInject) {
if (config.consumeQueuedMessageCancellation?.(message)) continue;
await emit({
type: "message_start",
message
});
if (config.consumeQueuedMessageCancellation?.(message)) continue;
if (message.role === "user") turnTainted = false;
await emit({
type: "message_end",
message
});
state.context.messages.push(message);
newMessages.push(message);
injectedMessage = true;
}
if (!injectedMessage && !hasMoreToolCalls) continue;
}
if (await stopIfAborted()) return newMessages;
let streamedSteering = [];
const streamedConfig = {
...config,
getSteeringMessages: async () => {
if (streamedSteering.length === 0) streamedSteering = await getSteeringAtCheckpoint(config);
return streamedSteering;
}
};
const streamed = await streamAgentResponse(state.context, config, signal, emit, newMessages, async (assistantMessage, toolCalls, executionSignal, toolEmit, scheduling) => {
const batch = await executeToolCalls(state.context, assistantMessage, streamedConfig, executionSignal, toolEmit, toolLoopRecoveryState.criticalToolLoopSeen, toolCalls, scheduling);
if (batch.intervention) toolLoopRecoveryState.criticalToolLoopSeen = true;
return batch;
}, (message) => withAssistantTurnTaint(message, turnTainted), streamFn, runtime);
const { message } = streamed;
if (message.stopReason === "error" || message.stopReason === "aborted") {
await emit({
type: "turn_end",
message,
toolResults: streamed.batches.flatMap((batch) => batch.messages)
});
if (message.stopReason === "aborted" && signal?.aborted && !isTurnHandoffAbort(signal)) await appendInterruptedTurnMessage(newMessages, emit);
await emit({
type: "agent_end",
messages: newMessages
});
return newMessages;
}
const remainingToolCalls = message.content.filter((item) => item.type === "toolCall" && !streamed.executedIds.has(item.id) && (message.stopReason === "toolUse" || item.async === true));
const terminalToolBatch = remainingToolCalls.length > 0 ? await executeToolCalls(state.context, message, streamedSteering.length > 0 ? streamedConfig : config, signal, emit, toolLoopRecoveryState.criticalToolLoopSeen, remainingToolCalls) : void 0;
const batches = [...streamed.batches, ...terminalToolBatch ? [terminalToolBatch] : []];
const executedToolBatch = batches.length ? {
messages: batches.flatMap((batch) => batch.messages),
steeringMessages: [...new Set(batches.flatMap((batch) => batch.steeringMessages))],
terminate: batches.every((batch) => batch.terminate),
terminateRun: batches.some((batch) => batch.terminateRun),
intervention: batches.find((batch) => batch.intervention)?.intervention,
fatal: batches.find((batch) => batch.fatal)?.fatal
} : void 0;
const toolResults = executedToolBatch?.messages ?? [];
turnTainted ||= toolResults.some(toolResultTaintsTurn);
hasMoreToolCalls = streamed.continuationRequired || executedToolBatch !== void 0 && !executedToolBatch.terminate;
pendingMessages = executedToolBatch?.steeringMessages ?? [];
if (executedToolBatch?.intervention) toolLoopRecoveryState.criticalToolLoopSeen = true;
for (const result of terminalToolBatch?.messages ?? []) {
state.context.messages.push(result);
newMessages.push(result);
}
await emit({
type: "turn_end",
message,
toolResults
});
turnOpen = false;
if (executedToolBatch?.fatal) throw executedToolBatch.fatal.error;
if (await stopIfAborted()) return newMessages;
if (executedToolBatch?.terminateRun) {
const terminalMessage = {
...createFailureMessage(config.model, /* @__PURE__ */ new Error(TOOL_LOOP_RECOVERY_TERMINATED_MESSAGE), false),
content: [{
type: "text",
text: TOOL_LOOP_RECOVERY_TERMINATED_MESSAGE
}]
};
state.context.messages.push(terminalMessage);
newMessages.push(terminalMessage);
await emit({ type: "turn_start" });
turnOpen = true;
await emit({
type: "message_start",
message: terminalMessage
});
await emit({
type: "message_end",
message: terminalMessage
});
await emit({
type: "turn_end",
message: terminalMessage,
toolResults: []
});
turnOpen = false;
await emit({
type: "agent_end",
messages: newMessages
});
return newMessages;
}
const nextTurnSnapshot = await config.prepareNextTurn?.({
message,
toolResults,
context: state.context,
newMessages
});
if (nextTurnSnapshot) {
state.context = nextTurnSnapshot.context ?? state.context;
const nextModel = nextTurnSnapshot.model ?? config.model;
const nextThinkingLevel = nextTurnSnapshot.thinkingLevel ?? config.thinkingLevel;
const nextReasoning = (nextTurnSnapshot.thinkingLevel !== void 0 || nextTurnSnapshot.model !== void 0 && nextThinkingLevel !== void 0) && nextThinkingLevel !== void 0 ? resolveAgentReasoningOption(nextModel, nextThinkingLevel) : config.reasoning;
config = Object.assign({}, config, {
model: nextModel,
thinkingLevel: nextThinkingLevel,
reasoning: nextReasoning
});
}
if (await stopIfAborted()) return newMessages;
if (pendingMessages.length === 0) {
if (await config.shouldStopAfterTurn?.({
message,
toolResults,
context: state.context,
newMessages
})) {
await emit({
type: "agent_end",
messages: newMessages
});
return newMessages;
}
const steering = getSteeringAtCheckpoint(config);
pendingMessages = Array.isArray(steering) ? steering : await steering;
}
if (await stopIfAborted()) return newMessages;
}
pendingMessages = await config.getFollowUpMessages?.() || [];
if (pendingMessages.length === 0) {
const finalSteering = getSteeringAtCheckpoint(config);
pendingMessages = Array.isArray(finalSteering) ? finalSteering : await finalSteering;
}
if (pendingMessages.length === 0) break;
}
await emit({
type: "agent_end",
messages: newMessages
});
return newMessages;
}
/**
* Execute tool calls from an assistant message.
*/
async function executeToolCalls(currentContext, assistantMessage, config, signal, emit, criticalToolLoopSeen, toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall"), scheduling) {
const batch = {
currentContext,
assistantMessage,
config,
signal,
emit,
resolved: /* @__PURE__ */ new Map(),
validated: /* @__PURE__ */ new Map(),
onParallelStarted: scheduling?.onParallelStarted
};
if (config.beforeToolBatch) {
for (const toolCall of toolCalls) {
if (signal?.aborted) break;
batch.validated.set(toolCall, await validateToolCallForBatchAdmission(batch, toolCall));
}
const calls = toolCalls.flatMap((toolCall) => {
const validation = batch.validated.get(toolCall);
return validation?.kind === "prepared" ? [{
toolCall,
args: validation.args,
tool: validation.tool
}] : [];
});
if (calls.length > 0 && !signal?.aborted) {
const admission = await config.beforeToolBatch({
assistantMessage,
calls,
context: currentContext
}, signal);
if (admission?.intervention) return await completeToolLoopInterventionBatch(batch, {
toolCalls,
intervention: admission.intervention,
terminal: criticalToolLoopSeen
});
batch.lifecycle = admission ? takeInternalToolBatchLifecycle(admission) : void 0;
batch.warnings = admission?.warnings;
}
}
let hasSequentialToolCall = false;
if (config.toolExecution !== "sequential") for (const toolCall of toolCalls) {
if (signal?.aborted) break;
const resolution = await resolveToolCallTool(batch, toolCall);
if (resolution.kind === "resolved" && resolution.tool?.executionMode === "sequential") {
hasSequentialToolCall = true;
break;
}
}
const sequential = config.toolExecution === "sequential" || hasSequentialToolCall;
if (sequential && scheduling) await scheduling.waitForPrevious();
return executeToolCallGroups(batch, toolCalls, sequential);
}
function hidesToolCallFromChannelProgress(context, toolCall, resolvedToolCalls) {
const resolution = resolvedToolCalls.get(toolCall);
return (resolution?.kind === "resolved" ? resolution.tool : context.tools?.find((candidate) => candidate.name === toolCall.name))?.hideFromChannelProgress === true;
}
function validatedToolCallIds(batch, calls) {
return calls.filter((call) => batch.validated.get(call)?.kind === "prepared").map((call) => call.id);
}
async function executeToolCallGroups(batch, toolCalls, sequential) {
const finalizedCalls = [];
const messages = [];
let steeringMessages = [];
let cursor = 0;
let fatal;
while (cursor < toolCalls.length) {
if (sequential && !batch.signal?.aborted) {
const steering = getSteeringAtCheckpoint(batch.config);
steeringMessages = Array.isArray(steering) ? steering : await steering;
}
if (steeringMessages.length > 0) {
batch.lifecycle?.releaseSkippedCalls(validatedToolCallIds(batch, toolCalls.slice(cursor)));
break;
}
const entries = [];
try {
while (cursor < toolCalls.length) {
const toolCall = toolCalls[cursor++];
if (!toolCall) continue;
const entry = await prepareToolCallEntry(batch, toolCall);
entries.push(entry);
if (!("kind" in entry)) await emitToolExecutionEnd(entry, batch.emit);
if (sequential || batch.signal?.aborted) break;
}
const hasReady = entries.some((entry) => "kind" in entry);
if (!batch.signal?.aborted && (!sequential || hasReady)) {
const steering = getSteeringAtCheckpoint(batch.config);
steeringMessages = Array.isArray(steering) ? steering : await steering;
}
const ordered = entries.map((entry) => "kind" in entry ? void 0 : entry);
const settle = async (index, entry, outcome) => {
try {
const finalized = await finalizeExecutedToolCall(batch, entry, outcome, entry.execution.args);
if (sequential) entry.execution.dispose();
await emitToolExecutionEnd(finalized, batch.emit);
ordered[index] = finalized;
} finally {
entry.execution.dispose();
}
};
const launched = steeringMessages.length > 0 || sequential && !hasReady ? void 0 : await launchParallelToolCalls(entries, batch.lifecycle);
if (!sequential && launched?.started.length && !launched.rejected) batch.onParallelStarted?.();
for (const { index, entry, outcome } of launched?.completed ?? []) await settle(index, entry, outcome);
fatal = launched?.rejected ? { error: launched.rejected.error } : void 0;
const skippedIndex = steeringMessages.length > 0 ? 0 : launched?.rejected?.index ?? entries.length;
if (sequential && steeringMessages.length > 0) {
for (const entry of entries) if ("kind" in entry) entry.execution.dispose();
}
if (steeringMessages.length > 0 || fatal) {
const skippedIds = [...entries.slice(skippedIndex).flatMap((entry) => "kind" in entry ? [entry.toolCall.id] : []), ...validatedToolCallIds(batch, toolCalls.slice(cursor))];
if (sequential || fatal || skippedIds.length > 0) batch.lifecycle?.releaseSkippedCalls(skippedIds);
}
for (let index = skippedIndex; index < entries.length; index++) {
const entry = entries[index];
if (!entry || !("kind" in entry)) continue;
if (!sequential) entry.execution.dispose();
ordered[index] = await completeUnstartedToolCall(batch, entry.toolCall, {
reason: fatal ? "admission" : "steering",
args: entry.execution.args,
startEmitted: true
});
}
if (launched) await Promise.all(launched.started.map(async ({ index, entry, outcome }) => settle(index, entry, await outcome)));
for (const finalized of ordered) if (finalized) {
messages.push(await emitToolResultMessage(finalized, batch.emit));
finalizedCalls.push(finalized);
}
} finally {
for (const entry of entries) if ("kind" in entry) entry.execution.dispose();
}
if (steeringMessages.length > 0 || fatal || batch.signal?.aborted) break;
}
if (sequential && !fatal && !batch.signal?.aborted && steeringMessages.length === 0) {
const steering = getSteeringAtCheckpoint(batch.config);
steeringMessages = Array.isArray(steering) ? steering : await steering;
if (steeringMessages.length > 0) batch.lifecycle?.releaseSkippedCalls([]);
}
const skippedReason = fatal ? "admission" : steeringMessages.length > 0 ? "steering" : void 0;
for (; cursor < toolCalls.length; cursor++) {
const toolCall = toolCalls[cursor];
if (!toolCall) continue;
const finalized = await completeUnstartedToolCall(batch, toolCall, { reason: skippedReason });
messages.push(await emitToolResultMessage(finalized, batch.emit));
finalizedCalls.push(finalized);
}
return {
messages,
steeringMessages,
terminate: shouldTerminateToolBatch(finalizedCalls),
terminateRun: false,
...fatal ? { fatal } : {}
};
}
async function prepareToolCallEntry(batch, toolCall) {
const hideFromChannelProgress = hidesToolCallFromChannelProgress(batch.currentContext, toolCall, batch.resolved);
await batch.emit({
type: "tool_execution_start",
toolCallId: toolCall.id,
toolName: toolCall.name,
args: toolCall.arguments,
...hideFromChannelProgress ? { hideFromChannelProgress: true } : {}
});
const preparation = await prepareToolCall(batch, toolCall);
if (preparation.kind === "immediate") return await finalizeToolCallOutcome(batch, {
toolCall,
result: preparation.result,
isError: preparation.isError,
executionStarted: false,
...preparation.errorKind ? { errorKind: preparation.errorKind } : {},
...hideFromChannelProgress ? { hideFromChannelProgress: true } : {}
}, toolCall.arguments);
const execution = await prepareToolCallExecution(preparation, {
assistantMessage: batch.assistantMessage,
toolCall: preparation.toolCall
}, batch.signal, batch.emit);
return execution.kind === "immediate" ? await finalizeExecutedToolCall(batch, preparation, execution.outcome, preparation.args) : {
...preparation,
execution
};
}
async function launchParallelToolCalls(entries, batchLifecycle) {
const ready = entries.flatMap((entry, index) => "kind" in entry ? [{
entry,
index
}] : []);
const result = {
started: [],
completed: []
};
let cursor = 0;
let finish;
let finished = false;
const done = new Promise((resolve) => {
finish = () => {
if (!finished) {
finished = true;
resolve();
}
};
});
const launchNext = () => {
const current = ready[cursor++];
if (!current) {
finish();
return;
}
const launchState = {};
let started = false;
let rejected = false;
const onStart = () => {
try {
batchLifecycle?.commitReadyCalls([{
toolCallId: current.entry.toolCall.id,
args: current.entry.execution.args
}]);
} catch (error) {
rejected = true;
result.rejected = {
index: current.index,
error
};
finish();
throw error;
}
started = true;
if (launchState.outcome) result.started.push({
...current,
outcome: launchState.outcome
});
queueMicrotask(launchNext);
};
const outcome = current.entry.execution.execute(onStart);
launchState.outcome = outcome;
if (started) result.started.push({
...current,
outcome
});
outcome.then((completed) => {
if (!started && !rejected) {
result.completed.push({
...current,
outcome: completed
});
queueMicrotask(launchNext);
}
}, (error) => {
if (!rejected) {
result.rejected = {
index: current.index,
error
};
finish();
}
});
};
launchNext();
await done;
return result;
}
function shouldTerminateToolBatch(finalizedCalls) {
return finalizedCalls.length > 0 && finalizedCalls.every((finalized) => finalized.result.terminate === true);
}
function prepareToolCallArguments(tool, toolCall) {
if (!tool.prepareArguments) return toolCall;
const preparedArguments = tool.prepareArguments(toolCall.arguments);
if (preparedArguments === toolCall.arguments) return toolCall;
return {
...toolCall,
arguments: preparedArguments
};
}
async function resolveToolCallTool(batch, toolCall) {
const cached = batch.resolved.get(toolCall);
if (cached) return cached;
let resolution;
try {
let tool = batch.currentContext.tools?.find((t) => t.name === toolCall.name);
if (!tool) {
const resolvedTool = await batch.config.resolveDeferredTool?.({
assistantMessage: batch.assistantMessage,
toolCall,
context: batch.currentContext
}, batch.signal);
if (resolvedTool && resolvedTool.name !== toolCall.name) throw new Error(`Deferred tool resolver returned "${resolvedTool.name}" for requested "${toolCall.name}"`);
tool = resolvedTool;
if (tool) batch.currentContext.tools = [...batch.currentContext.tools ?? [], tool];
}
resolution = {
kind: "resolved",
...tool ? { tool } : {}
};
} catch (error) {
resolution = {
kind: "error",
error
};
}
batch.resolved.set(toolCall, resolution);
return resolution;
}
async function prepareToolCall(batch, toolCall) {
const cachedValidation = batch.validated.get(toolCall);
if (batch.signal?.aborted && !cachedValidation) return {
kind: "immediate",
result: createErrorToolResult("Operation aborted"),
isError: true
};
const validation = cachedValidation ?? await validateToolCallForBatchAdmission(batch, toolCall);
if (validation.kind === "immediate") return validation;
const { args: validatedArgs } = validation;
try {
if (batch.config.beforeToolCall) {
const beforeResult = await batch.config.beforeToolCall({
assistantMessage: batch.assistantMessage,
toolCall,
args: validatedArgs,
context: batch.currentContext
}, batch.signal);
if (batch.signal?.aborted) return {
kind: "immediate",
result: createErrorToolResult("Operation aborted"),
isError: true
};
if (beforeResult?.block) return {
kind: "immediate",
result: createErrorToolResult(beforeResult.reason || "Tool execution was blocked"),
isError: true
};
}
if (batch.signal?.aborted) return {
kind: "immediate",
result: createErrorToolResult("Operation aborted"),
isError: true
};
return validation;
} catch (error) {
return {
kind: "immediate",
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true
};
}
}
async function validateToolCallForBatchAdmission(batch, toolCall) {
const resolution = await resolveToolCallTool(batch, toolCall);
if (resolution.kind === "error") return {
kind: "immediate",
result: createErrorToolResult(batch.signal?.aborted ? "Operation aborted" : coerceErrorMessage(resolution.error)),
isError: true
};
const tool = resolution.tool;
if (!tool) return {
kind: "immediate",
result: createErrorToolResult(`Tool ${toolCall.name} not found`),
isError: true
};
let preparedToolCall;
try {
preparedToolCall = prepareToolCallArguments(tool, toolCall);
} catch (error) {
return {
kind: "immediate",
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true
};
}
let validatedArgs;
try {
validatedArgs = validateToolArguments(tool, preparedToolCall);
} catch (error) {
return {
kind: "immediate",
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true,
errorKind: "argument-validation"
};
}
return {
kind: "prepared",
toolCall,
tool,
args: validatedArgs
};
}
async function prepareToolCallExecution(prepared, executionContext, signal, emit) {
const updateEvents = [];
let acceptingUpdates = true;
const onUpdate = (partialResult) => {
if (!acceptingUpdates) return;
updateEvents.push(Promise.resolve(emit({
type: "tool_execution_update",
toolCallId: prepared.toolCall.id,
toolName: prepared.toolCall.name,
args: prepared.toolCall.arguments,
partialResult,
...prepared.tool.hideFromChannelProgress === true ? { hideFromChannelProgress: true } : {}
})));
};
const finishUpdates = async () => {
acceptingUpdates = false;
await Promise.all(updateEvents);
};
const immediateError = async (error) => {
await finishUpdates();
return {
kind: "immediate",
outcome: {
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true,
executionStarted: false
}
};
};
const readyExecution = (args, run, disposeSource = () => {}) => {
let disposed = false;
const dispose = () => {
if (!disposed) {
disposed = true;
acceptingUpdates = false;
disposeSource();
}
};
return {
kind: "ready",
args,
dispose,
async execute(onImplementationStart) {
let executionStarted = false;
let implementationStartError;
try {
if (signal?.aborted) return {
result: createErrorToolResult("Operation aborted"),
isError: true,
executionStarted: false
};
try {
const result = await run(() => {
try {
onImplementationStart?.();
} catch (error) {
implementationStartError = { error };
throw error;
}
executionStarted = true;
});
if (implementationStartError) throw implementationStartError.error;
return {
result,
isError: false,
executionStarted
};
} catch (error) {
if (implementationStartError) throw implementationStartError.error;
return {
result: createErrorToolResult(coerceErrorMessage(error)),
isError: true,
executionStarted,
...executionStarted && signal?.aborted && error === signal.reason ? { callerCancelled: true } : {}
};
}
} finally {
await finishUpdates();
dispose();
}
}
};
};
const preparer = getInternalToolExecutionPreparer(prepared.tool);
if (!preparer) return readyExecution(prepared.args, async (onImplementationStart) => {
if (signal?.aborted) throw signal.reason ?? /* @__PURE__ */ new Error("Operation aborted");
return await runWithAgentToolExecutionContext(executionContext, () => {
onImplementationStart();
return prepared.tool.execute(prepared.toolCall.id, prepared.args, signal, onUpdate);
});
});
let internalPreparation;
try {
internalPreparation = await runWithAgentToolExecutionContext(executionContext, () => preparer({
toolCallId: prepared.toolCall.id,
args: prepared.args,
...signal ? { signal } : {},
onUpdate
}));
} catch (error) {
return await immediateError(error);
}
if (internalPreparation.kind === "immediate") {
internalPreparation.dispose();
await finishUpdates();
return {
kind: "immediate",
outcome: internalPreparation.outcome.kind === "result" ? {
result: internalPreparation.outcome.result,
isError: internalPreparation.outcome.isError,
executionStarted: false
} : {
result: createErrorToolResult(coerceErrorMessage(internalPreparation.outcome.error)),
isError: true,
executionStarted: false
}
};
}
const readyPreparation = internalPreparation;
return readyExecution(readyPreparation.args, (onImplementationStart) => runWithAgentToolExecutionContext(executionContext, () => readyPreparation.execute(onImplementationStart)), readyPreparation.dispose);
}
async function finalizeExecutedToolCall(batch, prepared, executed, finalArgs) {
let result = executed.result;
let isError = executed.isError;
if (executed.executionStarted && batch.config.afterToolCall) try {
const afterResult = await batch.config.afterToolCall({
assistantMessage: batch.assistantMessage,
toolCall: prepared.toolCall,
args: finalArgs,
result,
isError,
context: batch.currentContext
}, batch.signal);
if (afterResult) {
result = copyInternalToolResultState(result, {
...result,
content: afterResult.content ?? result.content,
details: afterResult.details ?? result.details,
terminate: afterResult.terminate ?? result.terminate
});
isError = afterResult.isError ?? isError;
}
} catch (error) {
result = createErrorToolResult(coerceErrorMessage(error));
isError = true;
}
return await finalizeToolCallOutcome(batch, {
toolCall: prepared.toolCall,
result,
isError,
executionStarted: executed.executionStarted,
...prepared.tool.hideFromChannelProgress === true ? { hideFromChannelProgress: true } : {},
...executed.executionStarted && !executed.callerCancelled && prepared.tool.resultContentSource ? { resultContentSource: prepared.tool.resultContentSource } : {}
}, finalArgs);
}
async function finalizeToolCallOutcome(batch, finalized, args) {
const outcome = await applyToolOutcomeHook(batch, finalized, args);
const warning = batch.warnings?.find((entry) => entry.toolCallId === outcome.toolCall.id);
return warning ? {
...outcome,
result: appendToolLoopWarning(outcome.result, warning)
} : outcome;
}
async function applyToolOutcomeHook(batch, finalized, args) {
if (!batch.config.afterToolOutcome) return finalized;
try {
const afterResult = await batch.config.afterToolOutcome({
assistantMessage: batch.assistantMessage,
toolCall: finalized.toolCall,
args,
result: finalized.result,
isError: finalized.isError,
executionStarted: finalized.executionStarted,
...finalized.errorKind ? { errorKind: finalized.errorKind } : {},
context: batch.currentContext
}, batch.signal);
if (!afterResult) return finalized;
return {
...finalized,
result: copyInternalToolResultState(finalized.result, {
...finalized.result,
content: afterResult.content ?? finalized.result.content,
details: afterResult.details ?? finalized.result.details,
terminate: afterResult.terminate ?? finalized.result.terminate
}),
isError: afterResult.isError ?? finalized.isError
};
} catch (error) {
const errorResult = createErrorToolResult(coerceErrorMessage(error));
return {
...finalized,
result: {
...errorResult,
...finalized.result.terminate === void 0 ? {} : { terminate: finalized.result.terminate }
},
isError: true
};
}
}
async function completeToolLoopInterventionBatch(batch, params) {
const messages = [];
const finalizedCalls = [];
for (const toolCall of params.toolCalls) {
const hideFromChannelProgress = hidesToolCallFromChannelProgress(batch.currentContext, toolCall, batch.resolved);
await batch.emit({
type: "tool_execution_start",
toolCallId: toolCall.id,
toolName: toolCall.name,
args: toolCall.arguments,
...hideFromChannelProgress ? { hideFromChannelProgress: true } : {}
});
const isTrigger = toolCall.id === params.intervention.toolCallId;
const text = params.terminal ? isTrigger ? `${params.intervention.reason}\n\nCritical tool-loop recovery failed because another critical loop was detected. This run is stopping now.` : "This tool was not executed because another call in the batch repeated a critical tool loop. This run is stopping now." : isTrigger ? `${params.intervention.reason}\n\nDo not repeat this exact tool action. Reassess the task. You may answer the user, ask for clarification, or continue with a different tool or different arguments.` : "This tool was not executed because another call in the batch triggered critical tool-loop recovery. Reassess the task before choosing the next action.";
const validation = batch.validated.get(toolCall);
const finalized = await finalizeToolCallOutcome(batch, {
toolCall,
result: {
content: [{
type: "text",
text
}],
details: {
status: "blocked",
deniedReason: "tool-loop",
intervention: params.intervention
},
...params.terminal ? { terminate: true } : {}
},
isError: true,
executionStarted: false,
...hideFromChannelProgress ? { hideFromChannelProgress: true } : {}
}, validation?.kind === "prepared" ? validation.args : toolCall.arguments);
await emitToolExecutionEnd(finalized, batch.emit);
messages.push(await emitToolResultMessage(finalized, batch.emit));
finalizedCalls.push(finalized);
}
return {
messages,
steeringMessages: [],
terminate: params.terminal || shouldTerminateToolBatch(finalizedCalls),
terminateRun: params.terminal,
intervention: params.intervention
};
}
async function completeUnstartedToolCall(batch, toolCall, options = {}) {
const hideFromChannelProgress = hidesToolCallFromChannelProgress(batch.currentContext, toolCall, batch.resolved);
if (!options.startEmitted) await batch.emit({
type: "tool_execution_start",
toolCallId: toolCall.id,
toolName: toolCall.name,
args: toolCall.arguments,
...hideFromChannelProgress ? { hideFromChannelProgress: true } : {}
});
const finalized = await finalizeToolCallOutcome(batch, {
toolCall,
result: createErrorToolResult(options.reason === "admission" ? TOOL_ADMISSION_FAILURE_MESSAGE : options.reason === "steering" ? STEERING_TOOL_SKIP_MESSAGE : "Operation aborted", options.reason === "admission" ? TOOL_ADMISSION_FAILURE_DETAILS : options.reason === "steering" ? {
status: "skipped",
deniedReason: "steering"
} : void 0),
isError: true,
executionStarted: false,
...hideFromChannelProgress ? { hideFromChannelProgress: true } : {}
}, "args" in options ? options.args : toolCall.arguments);
await emitToolExecutionEnd(finalized, batch.emit);
return finalized;
}
function createErrorToolResult(message, details = {}) {
return {
content: [{
type: "text",
text: message
}],
details
};
}
async function emitToolExecutionEnd(finalized, emit) {
await emit({
type: "tool_execution_end",
toolCallId: finalized.toolCall.id,
toolName: finalized.toolCall.name,
result: finalized.result,
isError: finalized.isError,
executionStarted: finalized.executionStarted,
...finalized.errorKind ? { errorKind: finalized.errorKind } : {},
...finalized.hideFromChannelProgress === true ? { hideFromChannelProgress: true } : {}
});
}
async function emitToolResultMessage(finalized, emit) {
const message = copyInternalToolResultState(finalized.result, withToolResultContentSource({
role: "toolResult",
toolCallId: finalized.toolCall.id,
toolName: finalized.toolCall.name,
content: finalized.result.content ?? [],
details: finalized.result.details,
isError: finalized.isError,
timestamp: Date.now()
}, finalized.resultContentSource));
await emit({
type: "message_start",
message
});
await emit({
type: "message_end",
message
});
return message;
}
function readTurnTaintMetadata(message) {
const metadata = Reflect.get(message, "__openclaw");
const record = asOptionalRecord(metadata);
if (!record) return;
return {
...record.resultContentSource === "network" ? { resultContentSource: record.resultContentSource } : {},
...record.turnTainted === true ? { turnTainted: true } : {}
};
}
function toolResultTaintsTurn(message) {
return readTurnTaintMetadata(message)?.resultContentSource === "network";
}
function isActiveTurnTainted(messages) {
for (const message of messages.toReversed()) {
if (message.role === "user") return false;
const metadata = readTurnTaintMetadata(message);
if (metadata?.turnTainted === true || metadata?.resultContentSource === "network") return true;
}
return false;
}
function withAssistantTurnTaint(message, tainted) {
if (!tainted) return message;
return {
...message,
__openclaw: {
...readTurnTaintMetadata(message),
turnTainted: true
}
};
}
function withToolResultContentSource(message, source) {
if (!source) return message;
return {
...message,
__openclaw: {
...readTurnTaintMetadata(message),
resultContentSource: source
}
};
}
//#endregion
//#region packages/agent-core/src/agent.ts
function defaultConvertToLlm(messages) {
return messages.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
}
const DEFAULT_MODEL = {
id: "unknown",
name: "unknown",
api: "unknown",
provider: "unknown",
baseUrl: "",
reasoning: false,
input: [],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0
},
contextWindow: 0,
maxTokens: 0
};
function createMutableAgentState(initialState) {
let tools = initialState?.tools?.slice() ?? [];
let messages = initialState?.messages?.slice() ?? [];
return {
systemPrompt: initialState?.systemPrompt ?? "",
model: initialState?.model ?? DEFAULT_MODEL,
thinkingLevel: initialState?.thinkingLevel ?? "off",
get tools() {
return tools;
},
set tools(nextTools) {
tools = nextTools.slice();
},
get messages() {
return messages;
},
set messages(nextMessages) {
messages = nextMessages.slice();
},
isStreaming: false,
streamingMessage: void 0,
pendingToolCalls: /* @__PURE__ */ new Set(),
errorMessage: void 0
};
}
var PendingMessageQueue = class {
constructor(mode) {
this.messages = [];
this.listeners = /* @__PURE__ */ new Set();
this.submitted = /* @__PURE__ */ new Set();
this.inFlight = [];
this.cancelled = /* @__PURE__ */ new WeakSet();
this.mode = mode;
}
enqueue(message) {
this.messages.push(message);
for (const listener of this.listeners) listener();
}
peek() {
return (this.inFlight.length > 0 ? this.inFlight : this.messages).slice(0, this.mode === "all" ? void 0 : 1);
}
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
reserve(messages) {
for (const message of messages) this.submitted.add(message);
return () => {
for (const message of messages) this.submitted.delete(message);
};
}
hasItems() {
return this.messages.length > 0;
}
drain() {
let count = this.mode === "all" ? this.messages.length : 1;
const boundary = this.messages.findIndex((message) => !this.submitted.has(message));
if (boundary > 0) count = Math.min(count, boundary);
const drained = this.messages.splice(0, count);
this.inFlight.push(...drained);
return drained;
}
commit(message) {
this.submitted.delete(message);
this.cancelled.delete(message);
const index = this.inFlight.indexOf(message);
if (index >= 0) this.inFlight.splice(index, 1);
}
restore() {
this.messages = [...this.inFlight, ...this.messages];
this.inFlight = [];
this.submitted.clear();
}
cancelFirst(predicate) {
const pendingIndex = this.messages.findIndex(predicate);
const pendingMessage = this.messages[pendingIndex];
if (pendingMessage) {
if (this.submitted.has(pendingMessage)) return;
return this.messages.splice(pendingIndex, 1)[0];
}
const inFlightIndex = this.inFlight.findIndex(predicate);
const message = this.inFlight[inFlightIndex];
if (!message || this.submitted.has(message)) return;
this.inFlight.splice(inFlightIndex, 1);
this.cancelled.add(message);
return message;
}
consumeCancellation(message) {
return this.cancelled.delete(message);
}
clear() {
this.messages = this.messages.filter((message) => this.submitted.has(message));
this.inFlight = this.inFlight.filter((message) => this.submitted.has(message));
this.cancelled = /* @__PURE__ */ new WeakSet();
}
};
/**
* Stateful wrapper around the low-level agent loop.
*
* `Agent` owns the current transcript, emits lifecycle events, executes tools,
* and exposes queueing APIs for steering and follow-up messages.
*/
var Agent$1 = class {
constructor(options = {}) {
this.listeners = /* @__PURE__ */ new Set();
this.toolLoopRecoveryState = { criticalToolLoopSeen: false };
this.mutableState = createMutableAgentState(options.initialState);
this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
this.transformContext = options.transformContext;
this.runtime = options.runtime;
this.streamFn = resolveAgentCoreStreamFn(options.runtime, options.streamFn);
this.getApiKey = options.getApiKey;
this.onPayload = options.onPayload;
this.onResponse = options.onResponse;
this.beforeToolCall = options.beforeToolCall;
this.resolveDeferredTool = options.resolveDeferredTool;
this.afterToolCall = options.afterToolCall;
this.afterToolOutcome = options.afterToolOutcome;
this.prepareNextTurn = options.prepareNextTurn;
this.prepareNextTurnWithContext = options.prepareNextTurnWithContext;
this.steeringQueue = new PendingMessageQueue(options.steeringMode ?? "one-at-a-time");
this.followUpQueue = new PendingMessageQueue(options.followUpMode ?? "one-at-a-time");
this.sessionId = options.sessionId;
this.thinkingBudgets = options.thinkingBudgets;
this.transport = options.transport ?? "auto";
this.maxRetryDelayMs = options.maxRetryDelayMs;
this.toolExecution = options.toolExecution ?? "parallel";
}
/**
* Subscribe to agent lifecycle events.
*
* Listener promises are awaited in subscription order and are included in
* the current run's settlement. Listeners also receive the active abort
* signal for the current run.
*
* `agent_end` is the final emitted event for a run, but the agent does not
* become idle until all awaited listeners for that event have settled.
*/
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
/**
* Current agent state.
*
* Assigning `state.tools` or `state.messages` copies the provided top-level array.
*/
get state() {
return this.mutableState;
}
/** Controls how queued steering messages are drained. */
set steeringMode(mode) {
this.steeringQueue.mode = mode;
}
get steeringMode() {
return this.steeringQueue.mode;
}
/** Controls how queued follow-up messages are drained. */
set followUpMode(mode) {
this.followUpQueue.mode = mode;
}
get followUpMode() {
return this.followUpQueue.mode;
}
/**
* Queue a message for the active run. Running tools finish, while sequential
* tail calls or a parallel batch that has not launched yet are skipped.
*/
steer(message) {
this.steeringQueue.enqueue(message);
}
/** Cancel queued input unless a live provider response may already have admitted it. */
cancelSteeringMessage(predicate) {
return this.steeringQueue.cancelFirst(predicate);
}
/** Queue a message to run only after the agent would otherwise stop. */
followUp(message) {
this.followUpQueue.enqueue(message);
}
/** Remove queued steering messages that have not been submitted to a live response. */
clearSteeringQueue() {
this.steeringQueue.clear();
}
/** Remove all queued follow-up messages. */
clearFollowUpQueue() {
this.followUpQueue.clear();
}
/** Remove all queued steering and follow-up messages. */
clearAllQueues() {
this.clearSteeringQueue();
this.clearFollowUpQueue();
}
/** Returns true when either queue still contains pending messages. */
hasQueuedMessages() {
return this.steeringQueue.hasItems() || this.followUpQueue.hasItems();
}
/** Active abort signal for the current run, if any. */
get signal() {
return this.activeRun?.abortController.signal;
}
/** Abort the current run, if one is active. */
abort(reason) {
this.activeRun?.abortController.abort(reason);
}
/**
* Resolve when the current run and all awaited event listeners have finished.
*
* This resolves after `agent_end` listeners settle.
*/
waitForIdle() {
return this.activeRun?.promise ?? Promise.resolve();
}
/** Clear transcript state, runtime state, and queued messages. */
reset() {
this.mutableState.messages = [];
this.mutableState.isStreaming = false;
this.mutableState.streamingMessage = void 0;
this.mutableState.pendingToolCalls = /* @__PURE__ */ new Set();
this.mutableState.errorMessage = void 0;
this.toolLoopRecoveryState.criticalToolLoopSeen = false;
this.clearAllQueues();
}
async prompt(input, images) {
if (this.activeRun) throw new Error("Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.");
this.toolLoopRecoveryState.criticalToolLoopSeen = false;
const messages = this.normalizePromptInput(input, images);
await this.runPromptMessages(messages);
}
/** Continue from the current transcript. The last message must be a user or tool-result message. */
async continue() {
if (this.activeRun) throw new Error("Agent is already processing. Wait for completion before continuing.");
const lastMessage = this.mutableState.messages[this.mutableState.messages.length - 1];
if (!lastMessage) throw new Error("No messages to continue from");
if (lastMessage.role === "assistant" || lastMessage.role === "toolResult") {
const queuedSteering = this.steeringQueue.drain();
if (queuedSteering.length > 0) {
await this.runPromptMessages(queuedSteering, { skipInitialSteeringPoll: true });
return;
}
const queuedFollowUps = this.followUpQueue.drain();
if (queuedFollowUps.length > 0) {
await this.runPromptMessages(queuedFollowUps);
return;
}
}
if (lastMessage.role === "assistant") throw new TranscriptNotContinuableError(lastMessage.role);
await this.runContinuation();
}
normalizePromptInput(input, images) {
if (Array.isArray(input)) return input;
if (typeof input !== "string") return [input];
const content = [{
type: "text",
text: input
}];
if (images && images.length > 0) content.push(...images);
return [{
role: "user",
content,
timestamp: Date.now()
}];
}
async runPromptMessages(messages, options = {}) {
await this.runWithLifecycle(async (signal) => {
await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFn);
});
}
async runContinuation() {
await this.runWithLifecycle(async (signal) => {
await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFn);
});
}
createContextSnapshot() {
return {
systemPrompt: this.mutableState.systemPrompt,
messages: this.mutableState.messages.slice(),
tools: this.mutableState.tools.slice()
};
}
createLoopConfig(options = {}) {
let skipInitialSteeringPoll = options.skipInitialSteeringPoll === true;
const drainSteeringMessages = () => {
if (skipInitialSteeringPoll) {
skipInitialSteeringPoll = false;
return [];
}
return this.steeringQueue.drain();
};
const getSteeringMessages = attachInternalSyncSteeringGetter(async () => drainSteeringMessages(), drainSteeringMessages, {
peek: () => this.steeringQueue.peek(),
reserve: (messages) => this.steeringQueue.reserve(messages),
subscribe: (listener) => this.steeringQueue.subscribe(listener)
});
return {
model: this.mutableState.model,
thinkingLevel: this.mutableState.thinkingLevel,
reasoning: resolveAgentReasoningOption(this.mutableState.model, this.mutableState.thinkingLevel),
sessionId: this.sessionId,
onPayload: this.onPayload,
onResponse: this.onResponse,
transport: this.transport,
thinkingBudgets: this.thinkingBudgets,
maxRetryDelayMs: this.maxRetryDelayMs,
toolExecution: this.toolExecution,
beforeToolCall: this.beforeToolCall,
beforeToolBatch: getInternalBeforeToolBatch(this),
toolLoopRecoveryState: this.toolLoopRecoveryState,
resolveDeferredTool: this.resolveDeferredTool,
afterToolCall: this.afterToolCall,
afterToolOutcome: this.afterToolOutcome,
prepareNextTurn: this.prepareNextTurnWithContext || this.prepareNextTurn ? async (context) => {
if (this.prepareNextTurnWithContext) return await this.prepareNextTurnWithContext(context, this.signal);
return await this.prepareNextTurn?.(this.signal);
} : void 0,
convertToLlm: this.convertToLlm,
transformContext: this.transformContext,
getApiKey: this.getApiKey,
getSteeringMessages,
getFollowUpMessages: async () => this.followUpQueue.drain(),
consumeQueuedMessageCancellation: (message) => this.steeringQueue.consumeCancellation(message) || this.followUpQueue.consumeCancellation(message)
};
}
async runWithLifecycle(executor) {
if (this.activeRun) throw new Error("Agent is already processing.");
const abortController = new AbortController();
let resolvePromise = () => {};
const promise = new Promise((resolve) => {
resolvePromise = resolve;
});
this.activeRun = {
promise,
resolve: resolvePromise,
abortController
};
this.mutableState.isStreaming = true;
this.mutableState.streamingMessage = void 0;
this.mutableState.errorMessage = void 0;
try {
await executor(abortController.signal);
} catch (error) {
await this.handleRunFailure(error, abortController.signal.aborted);
} finally {
this.finishRun();
}
}
async handleRunFailure(error, aborted) {
const failureMessage = createFailureMessage(this.mutableState.model, error, aborted);
await this.processEvents({
type: "message_start",
message: failureMessage
});
await this.processEvents({
type: "message_end",
message: failureMessage
});
await this.processEvents({
type: "turn_end",
message: failureMessage,
toolResults: []
});
const messages = [failureMessage];
if (aborted && !isTurnHandoffAbort(this.signal)) await appendInterruptedTurnMessage(messages, (event) => this.processEvents(event));
await this.processEvents({
type: "agent_end",
messages
});
}
finishRun() {
this.steeringQueue.restore();
this.followUpQueue.restore();
this.mutableState.isStreaming = false;
this.mutableState.streamingMessage = void 0;
this.mutableState.pendingToolCalls = /* @__PURE__ */ new Set();
this.activeRun?.resolve();
this.activeRun = void 0;
}
/**
* Reduce internal state for a loop event, then await listeners.
*
* `agent_end` only means no further loop events will be emitted. The run is
* considered idle later, after all awaited listeners for `agent_end` finish
* and `finishRun()` clears runtime-owned state.
*/
async processEvents(event) {
switch (event.type) {
case "agent_start":
case "turn_start":
case "tool_execution_update": break;
case "message_start":
if (event.message.role !== "toolResult" || this.mutableState.streamingMessage?.role !== "assistant") this.mutableState.streamingMessage = event.message;
break;
case "message_update":
this.mutableState.streamingMessage = event.message;
break;
case "message_end":
if (event.message.role !== "toolResult" || this.mutableState.streamingMessage?.role !== "assistant") this.mutableState.streamingMessage = void 0;
this.mutableState.messages.push(event.message);
this.steeringQueue.commit(event.message);
this.followUpQueue.commit(event.message);
break;
case "tool_execution_start": {
const pendingToolCalls = new Set(this.mutableState.pendingToolCalls);
pendingToolCalls.add(event.toolCallId);
this.mutableState.pendingToolCalls = pendingToolCalls;
break;
}
case "tool_execution_end": {
const pendingToolCalls = new Set(this.mutableState.pendingToolCalls);
pendingToolCalls.delete(event.toolCallId);
this.mutableState.pendingToolCalls = pendingToolCalls;
break;
}
case "turn_end":
if (event.message.role === "assistant" && event.message.errorMessage) this.mutableState.errorMessage = event.message.errorMessage;
break;
case "agent_end": this.mutableState.streamingMessage = void 0;
}
const signal = this.activeRun?.abortController.signal;
if (!signal) throw new Error("Agent listener invoked outside active run");
for (const listener of this.listeners) await listener(event, signal);
}
};
//#endregion
//#region packages/agent-core/src/harness/prompt-template-arguments.ts
/** Parse an argument string using simple shell-style single and double quotes. */
function parseCommandArgs(argsString) {
const args = [];
let current = "";
let inQuote = null;
let hasToken = false;
for (const char of argsString) if (inQuote) {
if (char === inQuote) inQuote = null;
else {
hasToken = true;
current += char;
}
} else if (char === "\"" || char === "'") {
hasToken = true;
inQuote = char;
} else if (/\s/.test(char)) {
if (hasToken) {
args.push(current);
current = "";
hasToken = false;
}
} else {
hasToken = true;
current += char;
}
if (hasToken) args.push(current);
return args;
}
/**
* Substitute prompt template placeholders (`$1`, `$@`, `$ARGUMENTS`, `${@:N}`, `${@:N:L}`) with command arguments.
*
* Unsafe integer placeholders resolve to empty text instead of throwing, so malformed templates cannot abort prompt
* loading or invocation.
*/
function substituteArgs(content, args) {
const allArgs = args.join(" ");
return content.replace(/\$(\d+)|\$\{@:(\d+)(?::(\d+))?\}|\$ARGUMENTS|\$@/g, (_match, num, startStr, lengthStr) => {
if (num !== void 0) {
const parsed = parseStrictNonNegativeInteger(num);
if (parsed === void 0 || parsed <= 0) return "";
return args[parsed - 1] ?? "";
}
if (startStr !== void 0) {
const parsedStart = parseStrictNonNegativeInteger(startStr);
if (parsedStart === void 0) return "";
let start = parsedStart - 1;
if (start < 0) start = 0;
if (lengthStr) {
const length = parseStrictNonNegativeInteger(lengthStr);
if (length === void 0) return "";
return args.slice(start, start + length).join(" ");
}
return args.slice(start).join(" ");
}
return allArgs;
});
}
//#endregion
//#region packages/agent-core/src/harness/compaction/branch-summarization.ts
/** Collect entries that should be summarized before navigating to a different session tree entry. */
function collectEntriesForBranchSummaryFromBranches(oldBranch, targetBranch) {
const oldPath = new Set(oldBranch.map((entry) => entry.id));
let commonAncestorId = null;
for (const targetEntry of targetBranch.toReversed()) if (oldPath.has(targetEntry.id)) {
commonAncestorId = targetEntry.id;
break;
}
const firstSummarizedIndex = commonAncestorId === null ? 0 : oldBranch.findIndex((entry) => entry.id === commonAncestorId) + 1;
return {
entries: oldBranch.slice(firstSummarizedIndex),
commonAncestorId
};
}
/** Prepare branch entries for summarization within an optional token budget. */
function prepareBranchEntries(entries, tokenBudget = 0) {
const messages = [];
const fileOps = createFileOps();
let totalTokens = 0;
for (const entry of entries) if (entry.type === "branch_summary" && !entry.fromHook && entry.details) mergeSummaryFileOperations(fileOps, entry.details);
for (const entry of entries.toReversed()) {
const message = projectSessionEntryMessage(entry);
if (!message) continue;
extractFileOpsFromMessage(message, fileOps);
const tokens = estimateTokens(message);
if (tokenBudget > 0 && totalTokens + tokens > tokenBudget) {
if (entry.type === "compaction" || entry.type === "branch_summary") {
if (totalTokens < tokenBudget * .9) {
messages.unshift(message);
totalTokens += tokens;
}
}
break;
}
messages.unshift(message);
totalTokens += tokens;
}
return {
messages,
fileOps,
totalTokens
};
}
const BRANCH_SUMMARY_PREAMBLE = `The user explored a different conversation branch before returning here.
Summary of that exploration:
`;
const BRANCH_SUMMARY_PROMPT = `Create a structured summary of this conversation branch for context when returning later.
Use this EXACT format:
## Goal
[What was the user trying to accomplish in this branch?]
## Constraints & Preferences
- [Any constraints, preferences, or requirements mentioned]
- [Or "(none)" if none were mentioned]
## Progress
### Done
- [x] [Completed tasks/changes]
### In Progress
- [ ] [Work that was started but not finished]
### Blocked
- [Issues preventing progress, if any]
## Key Decisions
- **[Decision]**: [Brief rationale]
## Next Steps
1. [What should happen next to continue this work]
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
/** Generate a summary for abandoned branch entries. */
async function generateBranchSummary(entries, options) {
const { model, apiKey, headers, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;
const contextWindow = model.contextWindow || 128e3;
const maxSummaryOutputTokens = Math.min(2048, Math.max(1, Math.floor(contextWindow / 4)), model.maxTokens > 0 ? model.maxTokens : 2048);
const usableReserveTokens = reserveTokens < contextWindow ? reserveTokens : Math.floor(contextWindow / 2);
const { messages, fileOps } = prepareBranchEntries(entries, Math.max(1, contextWindow - Math.max(maxSummaryOutputTokens, usableReserveTokens)));
if (messages.length === 0) return ok({
summary: "No content to summarize",
readFiles: [],
modifiedFiles: []
});
const llmMessages = convertToLlm(messages);
const conversationText = serializeConversation(llmMessages);
let instructions;
if (replaceInstructions && customInstructions) instructions = customInstructions;
else if (customInstructions) instructions = `${BRANCH_SUMMARY_PROMPT}\n\nAdditional focus: ${customInstructions}`;
else instructions = BRANCH_SUMMARY_PROMPT;
const summarizationMessages = [{
role: "user",
content: [{
type: "text",
text: `<conversation>\n${conversationText}\n</conversation>\n\n${instructions}`
}],
timestamp: Date.now()
}];
const context = {
systemPrompt: SUMMARIZATION_SYSTEM_PROMPT,
messages: summarizationMessages
};
const streamOptions = {
apiKey,
headers,
signal,
maxTokens: maxSummaryOutputTokens
};
const response = options.streamFn ? await consumeAgentCoreStream(options.streamFn(model, context, streamOptions)) : await resolveAgentCoreCompleteFn(options.runtime)(model, context, streamOptions);
options.runtime?.internalUsageSink?.(response.usage);
if (response.stopReason === "aborted") return err(new BranchSummaryError("aborted", response.errorMessage || "Branch summary aborted"));
if (response.stopReason === "error") return err(new BranchSummaryError("summarization_failed", `Branch summary failed: ${response.errorMessage || "Unknown error"}`));
const summaryText = extractSummaryText(response);
if (summaryText === void 0) return err(new BranchSummaryError("summarization_failed", "Branch summary failed: model returned no summary text"));
let summary = BRANCH_SUMMARY_PREAMBLE + summaryText;
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
summary += formatFileOperations(readFiles, modifiedFiles);
return ok({
summary,
readFiles,
modifiedFiles
});
}
//#endregion
//#region packages/agent-core/src/harness/utils/truncate.ts
const DEFAULT_MAX_LINES = 2e3;
const DEFAULT_MAX_BYTES = 51200;
const runtimeBuffer = globalThis.Buffer;
function splitLinesForCounting(content) {
if (content.length === 0) return [];
const lines = content.split("\n");
if (content.endsWith("\n")) lines.pop();
return lines;
}
function findFirstNonAscii(content) {
for (let index = 0; index < content.length; index++) if (content.charCodeAt(index) > 127) return index;
return -1;
}
function utf8ByteLength(content) {
if (runtimeBuffer) return runtimeBuffer.byteLength(content, "utf8");
const firstNonAscii = findFirstNonAscii(content);
if (firstNonAscii === -1) return content.length;
let bytes = firstNonAscii;
for (let i = firstNonAscii; i < content.length; i++) {
const code = content.charCodeAt(i);
if (code <= 127) bytes += 1;
else if (code <= 2047) bytes += 2;
else if (code >= 55296 && code <= 56319 && i + 1 < content.length) {
const next = content.charCodeAt(i + 1);
if (next >= 56320 && next <= 57343) {
bytes += 4;
i++;
} else bytes += 3;
} else bytes += 3;
}
return bytes;
}
function replaceUnpairedSurrogates(content) {
let output = "";
for (let i = 0; i < content.length; i++) {
const code = content.charCodeAt(i);
if (code >= 55296 && code <= 56319) {
if (i + 1 < content.length) {
const next = content.charCodeAt(i + 1);
if (next >= 56320 && next <= 57343) {
output += content.charAt(i) + content.charAt(i + 1);
i++;
continue;
}
}
output += "�";
} else if (code >= 56320 && code <= 57343) output += "�";
else output += content.charAt(i);
}
return output;
}
/**
* Format byte counts for compact tool-output diagnostics.
*/
function formatSize(bytes) {
if (bytes < 1024) return `${bytes}B`;
else if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)}KB`;
return `${(bytes / 1048576).toFixed(1)}MB`;
}
function resolveTruncationInput(content, options) {
const maxLines = options.maxLines ?? 2e3;
const maxBytes = options.maxBytes ?? 51200;
const totalBytes = utf8ByteLength(content);
const lines = splitLinesForCounting(content);
return {
lines,
totalLines: lines.length,
totalBytes,
maxLines,
maxBytes
};
}
function buildTruncationResult(input, params) {
return {
content: params.content,
truncated: params.truncated,
truncatedBy: params.truncatedBy,
totalLines: input.totalLines,
totalBytes: input.totalBytes,
outputLines: params.outputLines,
outputBytes: params.outputBytes,
lastLinePartial: params.lastLinePartial ?? false,
firstLineExceedsLimit: params.firstLineExceedsLimit ?? false,
maxLines: input.maxLines,
maxBytes: input.maxBytes
};
}
/**
* Keep the beginning of content while respecting independent line and byte ceilings.
*
* Head truncation preserves complete lines; a first line that exceeds the byte
* ceiling produces empty output and sets firstLineExceedsLimit.
*/
function truncateHead(content, options = {}) {
const input = resolveTruncationInput(content, options);
if (input.totalLines <= input.maxLines && input.totalBytes <= input.maxBytes) return buildTruncationResult(input, {
content,
truncated: false,
truncatedBy: null,
outputLines: input.totalLines,
outputBytes: input.totalBytes
});
const firstLine = input.lines[0];
if (firstLine !== void 0 && utf8ByteLength(firstLine) > input.maxBytes) return buildTruncationResult(input, {
content: "",
truncated: true,
truncatedBy: "bytes",
outputLines: 0,
outputBytes: 0,
firstLineExceedsLimit: true
});
const outputLinesArr = [];
let outputBytesCount = 0;
let truncatedBy = input.totalLines > input.maxLines ? "lines" : "bytes";
for (const line of input.lines.slice(0, input.maxLines)) {
const lineBytes = utf8ByteLength(line) + (outputLinesArr.length > 0 ? 1 : 0);
if (outputBytesCount + lineBytes > input.maxBytes) {
truncatedBy = "bytes";
break;
}
outputLinesArr.push(line);
outputBytesCount += lineBytes;
}
if (input.totalLines > input.maxLines && outputLinesArr.length >= input.maxLines && outputBytesCount <= input.maxBytes) truncatedBy = "lines";
return buildTruncationResult(input, {
content: outputLinesArr.join("\n"),
truncated: true,
truncatedBy,
outputLines: outputLinesArr.length,
outputBytes: outputBytesCount
});
}
/**
* Keep the end of content while respecting independent line and byte ceilings.
*
* Tail truncation preserves recent output for command errors and may keep a
* partial first line when one final line alone exceeds the byte ceiling.
*/
function truncateTail(content, options = {}) {
const input = resolveTruncationInput(content, options);
if (input.totalLines <= input.maxLines && input.totalBytes <= input.maxBytes) return buildTruncationResult(input, {
content,
truncated: false,
truncatedBy: null,
outputLines: input.totalLines,
outputBytes: input.totalBytes
});
let outputLines = 0;
let outputBytesCount = 0;
let truncatedBy = input.totalLines > input.maxLines ? "lines" : "bytes";
let lastLinePartial = false;
for (let i = input.lines.length - 1; i >= 0 && outputLines < input.maxLines; i--) {
const line = input.lines.at(i);
if (line === void 0) continue;
const lineBytes = utf8ByteLength(line) + (outputLines > 0 ? 1 : 0);
if (outputBytesCount + lineBytes > input.maxBytes) {
truncatedBy = "bytes";
if (outputLines === 0) {
const truncatedLine = truncateStringToBytesFromEnd(line, input.maxBytes);
input.lines[i] = truncatedLine;
outputLines = 1;
outputBytesCount = utf8ByteLength(truncatedLine);
lastLinePartial = true;
}
break;
}
outputLines++;
outputBytesCount += lineBytes;
}
if (input.totalLines > input.maxLines && outputLines >= input.maxLines && outputBytesCount <= input.maxBytes) truncatedBy = "lines";
return buildTruncationResult(input, {
content: input.lines.slice(input.lines.length - outputLines).join("\n"),
truncated: true,
truncatedBy,
outputLines,
outputBytes: outputBytesCount,
lastLinePartial
});
}
/**
* Truncate a string to fit within a byte limit (from the end).
* Handles multi-byte UTF-8 characters correctly.
*/
function truncateStringToBytesFromEnd(str, maxBytes) {
if (maxBytes <= 0) return "";
let outputBytes = 0;
let start = str.length;
let needsReplacement = false;
for (let i = str.length; i > 0;) {
let characterStart = i - 1;
const code = str.charCodeAt(characterStart);
let characterBytes;
let unpairedSurrogate = false;
if (code >= 56320 && code <= 57343 && characterStart > 0) {
const previous = str.charCodeAt(characterStart - 1);
if (previous >= 55296 && previous <= 56319) {
characterStart--;
characterBytes = 4;
} else {
characterBytes = 3;
unpairedSurrogate = true;
}
} else if (code >= 55296 && code <= 57343) {
characterBytes = 3;
unpairedSurrogate = true;
} else characterBytes = code <= 127 ? 1 : code <= 2047 ? 2 : 3;
if (outputBytes + characterBytes > maxBytes) break;
outputBytes += characterBytes;
start = characterStart;
needsReplacement ||= unpairedSurrogate;
i = characterStart;
}
const output = str.slice(start);
return needsReplacement ? replaceUnpairedSurrogates(output) : output;
}
/**
* Trim a single display line and mark it with the grep-style truncation suffix.
*
* The cut point is backed off by one code unit when it would otherwise split a
* surrogate pair, so emoji / CJK Extension B characters crossing the boundary
* stay intact instead of rendering as replacement characters.
*/
function truncateLine(line, maxChars = 500) {
if (line.length <= maxChars) return {
text: line,
wasTruncated: false
};
let cut = maxChars;
if (cut < line.length) {
const lastCode = line.charCodeAt(cut - 1);
if (lastCode >= 55296 && lastCode <= 56319) {
const nextCode = line.charCodeAt(cut);
if (nextCode >= 56320 && nextCode <= 57343) cut -= 1;
}
}
return {
text: `${line.slice(0, cut)}... [truncated]`,
wasTruncated: true
};
}
//#endregion
//#region src/agents/runtime/proxy.ts
/**
* Proxy stream function for apps that route LLM calls through a server.
* The server manages auth and proxies requests to LLM providers.
*/
const PROXY_ERROR_BODY_MAX_BYTES = 16777216;
const PROXY_SSE_STREAM_MAX_BYTES = 16777216;
const PROXY_SSE_PENDING_BUFFER_MAX_BYTES = PROXY_SSE_STREAM_MAX_BYTES;
const PROXY_SSE_READ_IDLE_TIMEOUT_MS = 12e4;
var ProxyMessageEventStream = class extends event_stream_exports.EventStream {
constructor() {
super((event) => event.type === "done" || event.type === "error", (event) => {
if (event.type === "done") return event.message;
if (event.type === "error") return event.error;
throw new Error("Unexpected event type");
});
}
};
/**
* Stream function that proxies through a server instead of calling LLM providers directly.
* The server strips the partial field from delta events to reduce bandwidth.
* We reconstruct the partial message client-side.
*
* Use this as the `streamFn` option when creating an Agent that needs to go through a proxy.
*
* @example
* ```typescript
* const agent = new Agent({
* streamFn: (model, context, options) =>
* streamProxy(model, context, {
* ...options,
* authToken: await getAuthToken(),
* proxyUrl: "https://genai.example.com",
* }),
* });
* ```
*/
function buildProxyRequestOptions(options) {
return {
temperature: options.temperature,
maxTokens: options.maxTokens,
reasoning: options.reasoning,
cacheRetention: options.cacheRetention,
sessionId: options.sessionId,
promptCacheKey: options.promptCacheKey,
metadata: options.metadata,
transport: options.transport,
thinkingBudgets: options.thinkingBudgets,
maxRetryDelayMs: options.maxRetryDelayMs,
timeoutMs: options.timeoutMs
};
}
function sanitizeProxyModel(model) {
const { headers: _headers, ...safeModel } = model;
return safeModel;
}
function resolveProxyReadIdleTimeoutMs(timeoutMs) {
return resolvePositiveTimerTimeoutMs(timeoutMs, PROXY_SSE_READ_IDLE_TIMEOUT_MS);
}
function createProxyRequestTimeoutError(timeoutMs) {
const error = /* @__PURE__ */ new Error(`Proxy request timed out after ${timeoutMs}ms`);
error.name = "TimeoutError";
return error;
}
function buildProxyRequestAbort(callerSignal, timeoutMs) {
const timeoutController = new AbortController();
const timeoutId = setTimeout(() => {
timeoutController.abort(createProxyRequestTimeoutError(timeoutMs));
}, timeoutMs);
return {
signal: callerSignal ? AbortSignal.any([callerSignal, timeoutController.signal]) : timeoutController.signal,
clear: () => {
clearTimeout(timeoutId);
}
};
}
function isProxyRequestTimeoutError(params) {
if (params.callerSignal?.aborted || !params.requestSignal.aborted) return false;
if (!(params.error instanceof Error)) return false;
return params.error.name === "AbortError" || params.error.name === "TimeoutError" || params.error.message === "Request was aborted";
}
async function readProxyErrorData(response, readIdleTimeoutMs) {
const bytes = await readResponseWithLimit(response, PROXY_ERROR_BODY_MAX_BYTES, {
onOverflow: ({ maxBytes }) => /* @__PURE__ */ new Error(`Proxy error body exceeded ${maxBytes} bytes`),
chunkTimeoutMs: readIdleTimeoutMs,
onIdleTimeout: ({ chunkTimeoutMs }) => /* @__PURE__ */ new Error(`Proxy error body stalled: no data received for ${chunkTimeoutMs}ms`)
});
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
}
async function readProxySseChunk(reader, readIdleTimeoutMs, cancel) {
let timedOut = false;
return await new Promise((resolve, reject) => {
const timeoutError = /* @__PURE__ */ new Error(`Proxy SSE stream stalled: no data received for ${readIdleTimeoutMs}ms`);
const timeoutId = setTimeout(() => {
timedOut = true;
cancel(timeoutError);
reject(timeoutError);
}, readIdleTimeoutMs);
reader.read().then((result) => {
clearTimeout(timeoutId);
if (!timedOut) resolve(result);
}, (error) => {
clearTimeout(timeoutId);
if (!timedOut) reject(error instanceof Error ? error : new Error(String(error)));
});
});
}
function assertProxySsePendingBufferWithinLimit(buffer) {
if (new TextEncoder().encode(buffer).byteLength <= PROXY_SSE_PENDING_BUFFER_MAX_BYTES) return;
throw new Error(`Proxy SSE pending buffer exceeded ${PROXY_SSE_PENDING_BUFFER_MAX_BYTES} bytes`);
}
function streamProxy(model, context, options) {
const stream = new ProxyMessageEventStream();
(async () => {
const partial = {
role: "assistant",
stopReason: "stop",
content: [],
api: model.api,
provider: model.provider,
model: model.id,
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()
};
let reader;
let readerReachedEof = false;
let cancellation;
let cleanupReason;
const readIdleTimeoutMs = resolveProxyReadIdleTimeoutMs(options.timeoutMs);
const cancelReader = (reason) => reader ? cancellation ??= reader.cancel(reason).catch(() => void 0) : Promise.resolve();
const abortHandler = () => void cancelReader("Request aborted by user");
options.signal?.addEventListener("abort", abortHandler);
try {
const requestAbort = buildProxyRequestAbort(options.signal, readIdleTimeoutMs);
const response = await fetch(`${options.proxyUrl}/api/stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${options.authToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: sanitizeProxyModel(model),
context,
options: buildProxyRequestOptions(options)
}),
signal: requestAbort.signal
}).catch((error) => {
if (isProxyRequestTimeoutError({
error,
callerSignal: options.signal,
requestSignal: requestAbort.signal
})) throw new Error(`Proxy request timed out after ${readIdleTimeoutMs}ms`, { cause: error instanceof Error ? error : void 0 });
throw error;
}).finally(() => {
requestAbort.clear();
});
if (!response.ok) {
let errorMessage = `Proxy error: ${response.status} ${response.statusText}`;
try {
const errorData = await readProxyErrorData(response, readIdleTimeoutMs);
if (errorData?.error) errorMessage = `Proxy error: ${errorData.error}`;
} catch (error) {
if (error instanceof Error && error.message.startsWith("Proxy error body")) throw error;
}
throw new Error(errorMessage);
}
reader = response.body.getReader();
const sseReader = createSseByteGuard(reader, {
maxBytes: PROXY_SSE_STREAM_MAX_BYTES,
onOverflow: ({ maxBytes }) => /* @__PURE__ */ new Error(`Proxy SSE stream exceeded ${maxBytes} bytes`)
});
const decoder = new TextDecoder();
let buffer = "";
let terminalEventSeen = false;
const toolArgumentPreviewSchedules = /* @__PURE__ */ new Map();
const processSseLine = (line) => {
if (!line.startsWith("data:")) return false;
const data = line.slice(5).trim();
if (!data) return false;
const event = processProxyEvent(JSON.parse(data), partial, toolArgumentPreviewSchedules);
if (!event) return false;
stream.push(event);
return event.type === "done" || event.type === "error";
};
while (!terminalEventSeen) {
const { done, value } = await readProxySseChunk(sseReader, readIdleTimeoutMs, cancelReader);
if (done) {
readerReachedEof = cancellation === void 0;
break;
}
if (options.signal?.aborted) throw new Error("Request aborted by user");
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
assertProxySsePendingBufferWithinLimit(buffer);
for (const line of lines) {
terminalEventSeen = processSseLine(line);
if (terminalEventSeen) break;
}
}
if (options.signal?.aborted) throw new Error("Request aborted by user");
if (readerReachedEof) {
buffer += decoder.decode();
if (buffer.trim()) terminalEventSeen = processSseLine(buffer);
}
if (!terminalEventSeen) throw new Error("Proxy stream ended before terminal event");
stream.end();
} catch (error) {
cleanupReason = error;
const errorMessage = error instanceof Error ? error.message : String(error);
const reason = options.signal?.aborted ? "aborted" : "error";
partial.stopReason = reason;
partial.errorMessage = errorMessage;
stream.push({
type: "error",
reason,
error: partial
});
stream.end();
} finally {
try {
if (reader && !readerReachedEof) cancelReader(cleanupReason);
reader?.releaseLock();
} catch {}
options.signal?.removeEventListener("abort", abortHandler);
}
})();
return stream;
}
/**
* Process a proxy event and update the partial message.
*/
function processProxyEvent(proxyEvent, partial, toolArgumentPreviewSchedules) {
switch (proxyEvent.type) {
case "start": return {
type: "start",
partial
};
case "text_start":
partial.content[proxyEvent.contentIndex] = {
type: "text",
text: "",
...proxyEvent.contentSignature !== void 0 ? { textSignature: proxyEvent.contentSignature } : {}
};
return {
type: "text_start",
contentIndex: proxyEvent.contentIndex,
partial
};
case "text_delta": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "text") {
content.text += proxyEvent.delta;
return {
type: "text_delta",
contentIndex: proxyEvent.contentIndex,
delta: proxyEvent.delta,
partial
};
}
throw new Error("Received text_delta for non-text content");
}
case "text_end": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "text") {
if (proxyEvent.contentSignature !== void 0) content.textSignature = proxyEvent.contentSignature;
return {
type: "text_end",
contentIndex: proxyEvent.contentIndex,
content: content.text,
partial
};
}
throw new Error("Received text_end for non-text content");
}
case "thinking_start":
partial.content[proxyEvent.contentIndex] = {
type: "thinking",
thinking: ""
};
return {
type: "thinking_start",
contentIndex: proxyEvent.contentIndex,
partial
};
case "thinking_delta": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "thinking") {
content.thinking += proxyEvent.delta;
return {
type: "thinking_delta",
contentIndex: proxyEvent.contentIndex,
delta: proxyEvent.delta,
partial
};
}
throw new Error("Received thinking_delta for non-thinking content");
}
case "thinking_end": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "thinking") {
content.thinkingSignature = proxyEvent.contentSignature;
return {
type: "thinking_end",
contentIndex: proxyEvent.contentIndex,
content: content.thinking,
partial
};
}
throw new Error("Received thinking_end for non-thinking content");
}
case "toolcall_start": {
const content = {
type: "toolCall",
id: proxyEvent.id,
name: proxyEvent.toolName,
arguments: {},
partialJson: ""
};
partial.content[proxyEvent.contentIndex] = content;
toolArgumentPreviewSchedules.set(proxyEvent.contentIndex, createToolArgumentPreviewSchedule());
return {
type: "toolcall_start",
contentIndex: proxyEvent.contentIndex,
partial
};
}
case "toolcall_delta": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "toolCall") {
const streamingContent = content;
streamingContent.partialJson += proxyEvent.delta;
const previewSchedule = toolArgumentPreviewSchedules.get(proxyEvent.contentIndex);
if (!previewSchedule) throw new Error("Received toolcall_delta without a preview schedule");
if (previewSchedule(streamingContent.partialJson.length)) content.arguments = parseStreamingJson(streamingContent.partialJson);
partial.content[proxyEvent.contentIndex] = { ...content };
return {
type: "toolcall_delta",
contentIndex: proxyEvent.contentIndex,
delta: proxyEvent.delta,
partial
};
}
throw new Error("Received toolcall_delta for non-toolCall content");
}
case "toolcall_end": {
const content = partial.content[proxyEvent.contentIndex];
if (content?.type === "toolCall") {
const streamingContent = content;
content.arguments = streamingContent.partialJson ? parseTerminalToolCallArguments(streamingContent.partialJson) : {};
toolArgumentPreviewSchedules.delete(proxyEvent.contentIndex);
delete content.partialJson;
return {
type: "toolcall_end",
contentIndex: proxyEvent.contentIndex,
toolCall: content,
partial
};
}
return;
}
case "done":
partial.stopReason = proxyEvent.reason;
partial.usage = proxyEvent.usage;
return {
type: "done",
reason: proxyEvent.reason,
message: partial
};
case "error":
partial.stopReason = proxyEvent.reason;
partial.errorMessage = proxyEvent.errorMessage;
partial.usage = proxyEvent.usage;
return {
type: "error",
reason: proxyEvent.reason,
error: partial
};
default:
console.warn(`Unhandled proxy event type: ${proxyEvent.type}`);
return;
}
}
//#endregion
//#region src/plugin-sdk/agent-core.ts
/** Runtime adapter that lets the package agent-core use OpenClaw LLM helpers. */
const openClawAgentCoreRuntime = {
completeSimple: ((model, context, options) => completeSimple(model, context, options)),
streamSimple: ((model, context, options) => streamSimple(model, context, options))
};
/** Agent-core class preconfigured with OpenClaw runtime dependencies. */
var Agent = class extends Agent$1 {
constructor(options = {}) {
super({
runtime: openClawAgentCoreRuntime,
...options
});
}
};
//#endregion
export { DEFAULT_MAX_LINES as a, truncateLine as c, generateBranchSummary as d, prepareBranchEntries as f, uuidv7 as g, runAgentLoop as h, DEFAULT_MAX_BYTES as i, truncateTail as l, substituteArgs as m, openClawAgentCoreRuntime as n, formatSize as o, parseCommandArgs as p, streamProxy as r, truncateHead as s, Agent as t, collectEntriesForBranchSummaryFromBranches as u };