openclaw / src /agents /command /attempt-execution.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
e249c6d verified
Raw History Blame Contribute Delete
78.5 kB
/**
* Orchestrates one agent attempt across embedded, CLI, and ACP runtimes.
*/
import type { AcpRuntimeEvent } from "@openclaw/acp-core/runtime/types";
import { asOptionalRecord } from "@openclaw/normalization-core/record-coerce";
import {
normalizeOptionalLowercaseString,
type FastMode,
} from "@openclaw/normalization-core/string-coerce";
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { sanitizeForLog } from "../../../packages/terminal-core/src/ansi.js";
import { ACP_TURN_TIMEOUT_DETAIL_CODE } from "../../acp/control-plane/manager.turn-timeout.js";
import { formatAcpErrorChain } from "../../acp/runtime/errors.js";
import { resolveAcpToolTerminalOutcome } from "../../acp/tool-status.js";
import { normalizeReplyPayload } from "../../auto-reply/reply/normalize-reply.js";
import {
readChannelSourceTurnId,
readChannelSourceTurnSameThreadRequired,
setChannelSourceTurnId,
setChannelSourceTurnSameThreadRequired,
} from "../../auto-reply/reply/source-turn-id.js";
import { messageToolOwnsVisibleReply } from "../../auto-reply/source-reply-delivery-mode.js";
import type { ThinkLevel, VerboseLevel } from "../../auto-reply/thinking.js";
import { resolveCollapsedSessionAuthPinSource } from "../../config/sessions/auth-profile-override-provenance.js";
import {
loadSessionEntry,
persistSessionTranscriptTurn,
type SessionTranscriptRuntimeTarget,
type TranscriptMessageAppendResult,
} from "../../config/sessions/session-accessor.js";
import type { PrepareAssistantTranscriptMessage } from "../../config/sessions/transcript-assistant-delivery.js";
import type { SessionEntry } from "../../config/sessions/types.js";
import type { OpenClawConfig } from "../../config/types.openclaw.js";
import {
injectTimestamp,
timestampOptsFromConfig,
} from "../../gateway/server-methods/agent-timestamp.js";
import { emitAgentAuditEvent, emitAgentEvent } from "../../infra/agent-events.js";
import { emitTrustedDiagnosticEvent } from "../../infra/diagnostic-events.js";
import type { StopReason } from "../../llm/types.js";
import { redactSensitiveText } from "../../logging/redact.js";
import { createSubsystemLogger } from "../../logging/subsystem.js";
import type { PluginMetadataSnapshot } from "../../plugins/plugin-metadata-snapshot.types.js";
import { isSubagentSessionKey } from "../../routing/session-key.js";
import { resolveSessionPinnedHarnessId } from "../../sessions/agent-harness-session-key.js";
import { annotateInterSessionPromptText } from "../../sessions/input-provenance.js";
import {
buildPersistedUserTurnMessage,
preparePersistedUserTurnMessageForTranscriptWrite,
type PersistedUserTurnMessage,
type UserTurnInput,
type UserTurnTranscriptRecorder,
} from "../../sessions/user-turn-transcript.js";
import type { SkillSnapshot } from "../../skills/types.js";
import {
getGeneratedMediaTaskIdsForSessionKey,
hasNewGeneratedMediaTaskForSessionKey,
} from "../../tasks/task-status-access.js";
import { resolveUserPath } from "../../utils.js";
import { resolveMessageChannel } from "../../utils/message-channel.js";
import type { PreparedAgentRunAdmission } from "../admitted-run-context.js";
import {
buildAgentRunTerminalOutcomeFromLifecycleEvent,
classifyAgentRunTerminalOutcome,
type AgentRunTerminalOutcome,
} from "../agent-run-terminal-outcome.js";
import type { AgentRunTerminalReplySnapshot } from "../agent-run-terminal-reply.types.js";
import { resolveAuthProfileOrder } from "../auth-profiles/order.js";
import { ensureAuthProfileStore } from "../auth-profiles/store-runtime.js";
import {
resizeExecApprovalContinuationPrompt,
type ExecApprovalContinuationPromptRange,
} from "../bash-tools.exec-approval-output.js";
import { resolveBootstrapWarningSignaturesSeen } from "../bootstrap-budget.js";
import { resolveCliBackendConfig } from "../cli-backends.js";
import {
cliBackendAcceptsAuthProfileForwarding,
resolveCliExecutionAuthProfileId,
} from "../cli-execution-auth.js";
import { runCliAgent } from "../cli-runner.js";
import { hasCliLiveSession } from "../cli-runner/cli-live-session-registry.js";
import { buildCliMcpDelegationCapabilityBinding } from "../cli-runner/mcp-grant-context.js";
import { resolveCliRuntimeToolsAllow } from "../cli-runner/tool-policy.js";
import { clearCliSessionInStore, persistCliSessionBindingResult } from "../cli-session-store.js";
import {
getCliSessionBinding,
resolveCliSessionClearReason,
shouldClearFailedCliSessionBinding,
} from "../cli-session.js";
import { resolveConversationCapabilityProfile } from "../conversation-capability-profile.js";
import { resolveConversationToolPolicies } from "../conversation-tool-policy-pipeline.js";
import { resolveDelegationCapability } from "../delegation-capability.js";
import { mergeForcedEmbeddedAttemptToolsAllow } from "../embedded-agent-runner/run/attempt-tool-construction-plan.js";
import type { DeferredEmbeddedRunLifecycleManager } from "../embedded-agent-runner/run/deferred-lifecycle-owner.js";
import type { RunEmbeddedAgentInternalParams } from "../embedded-agent-runner/run/internal-params.js";
import { runEmbeddedAgent, type EmbeddedAgentRunResult } from "../embedded-agent.js";
import type { ContextEngineLogicalTurnLease } from "../harness/context-engine-logical-turn.js";
import type { ContextEngineTurnAttemptFacts } from "../harness/context-engine-turn-attempt.js";
import { runAgentHarnessBeforeMessageWriteHook } from "../harness/hook-helpers.js";
import { resolveAvailableAgentHarnessPolicy } from "../harness/selection.js";
import { AGENT_LANE_SUBAGENT } from "../lanes.js";
import type { ModelFallbackResultClassification } from "../model-fallback-attempt.js";
import type { ModelFallbackAttemptProvenance } from "../model-fallback.types.js";
import { resolveCliRuntimeExecutionProvider } from "../model-runtime-aliases.js";
import { isCliProvider } from "../model-selection.js";
import { resolveOpenAIRuntimeProvider } from "../openai-routing.js";
import type { PreparedModelRuntimePluginGeneration } from "../prepared-model-runtime.types.js";
import { hasVerifiedRequesterCompletionHandoff } from "../requester-tool-policy.js";
import {
createAgentRunSupersededAbortError,
resolveAgentRunAbortLifecycleFields,
} from "../run-termination.js";
import { buildAgentRuntimeAuthPlan } from "../runtime-plan/auth.js";
import type { AgentMessage } from "../runtime/index.js";
import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js";
import { withLocalSessionPlacementTurnSettlement } from "../session-placement-admission.js";
import { buildUsageWithNoCost } from "../stream-message-shared.js";
import {
isSubagentAnnounceCompletionHandoff,
isTrustedSubagentCompletionHandoffForRun,
} from "../subagents/announce/subagent-announce-handoff.js";
import { isRuntimeToolAllowed, isToolAllowedByPolicies } from "../tool-policy-match.js";
import { DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS } from "../tool-result-limits.js";
import type { ContextUsage } from "../usage.js";
import {
buildClaudeCliFallbackContextPrelude,
claudeCliSessionTranscriptHasContent,
resolveFallbackRetryPrompt,
} from "./attempt-execution.helpers.js";
import { resolveAgentRunContext } from "./run-context.js";
import {
consumeCliSessionForkInStore,
persistCliSessionForkSuccessorInStore,
restoreCliSessionForkInStore,
} from "./session-store.js";
import type { AgentCommandOpts } from "./types.js";
export {
createAcpVisibleTextAccumulator,
sessionTranscriptHasContent,
} from "./attempt-execution.helpers.js";
const log = createSubsystemLogger("agents/agent-command");
function rebaseExecApprovalContinuationPromptRange(params: {
body: string;
prompt: string;
range?: ExecApprovalContinuationPromptRange;
}): ExecApprovalContinuationPromptRange | undefined {
if (!params.range) {
return undefined;
}
if (!params.prompt.endsWith(params.body)) {
throw new Error("exec approval continuation prompt range could not be rebased");
}
const offset = params.prompt.length - params.body.length;
return {
start: offset + params.range.start,
end: offset + params.range.end,
};
}
const ACP_TRANSCRIPT_USAGE = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
totalTokens: 0,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
} as const;
const CLI_TRANSCRIPT_UNAVAILABLE_USAGE = {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
contextUsage: { state: "unavailable" },
} as const;
function resolveCliTranscriptUsage(usage: TranscriptUsage | undefined): TranscriptUsage {
if (!usage) {
return CLI_TRANSCRIPT_UNAVAILABLE_USAGE;
}
if (usage.contextUsage) {
return usage;
}
const promptTokens = (usage.input ?? 0) + (usage.cacheRead ?? 0) + (usage.cacheWrite ?? 0);
return {
...usage,
contextUsage:
promptTokens > 0
? {
state: "available",
promptTokens,
totalTokens: promptTokens + (usage.output ?? 0),
}
: { state: "unavailable" },
};
}
function shouldSuppressEmbeddedLiveStreamOutput(params: { opts: AgentCommandOpts }): boolean {
return params.opts.sessionEffects === "internal" && params.opts.deliver !== true;
}
type TranscriptUsage = {
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
total?: number;
contextUsage?: ContextUsage;
};
type PersistTextTurnTranscriptParams = {
prepareAssistantTranscriptMessage?: PrepareAssistantTranscriptMessage;
body: string;
transcriptBody?: string;
userMessage?: PersistedUserTurnMessage;
userTurnTranscriptRecorder?: UserTurnTranscriptRecorder;
assistantIdempotencyKey?: string;
expectedSessionId?: string;
finalText: string;
sessionId: string;
sessionKey: string;
sessionFile?: string;
sessionEntry: SessionEntry | undefined;
sessionStore?: Record<string, SessionEntry>;
storePath?: string;
sessionAgentId: string;
threadId?: string | number;
sessionCwd: string;
config: OpenClawConfig;
skipAssistantTurn?: boolean;
assistant: {
api: string;
provider: string;
model: string;
stopReason: StopReason;
usage?: TranscriptUsage;
};
};
type PersistTextTurnTranscriptResult =
| {
kind: "persisted";
sessionEntry: SessionEntry | undefined;
assistantTranscript?: TranscriptMessageAppendResult<unknown>;
}
| { kind: "session-rebound"; sessionEntry: undefined };
type HarnessAuthProfileSelection = {
authProfileId?: string;
authProfileIdSource?: "auto" | "user";
authProfileProvider: string;
authProfileMode?: string;
};
function resolveProfileAuthFromStore(params: { agentDir: string; profileId: string | undefined }): {
provider?: string;
mode?: string;
} {
const profileId = params.profileId?.trim();
if (!profileId) {
return {};
}
const credential = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
externalCliProfileIds: [profileId],
}).profiles[profileId];
return { provider: credential?.provider, mode: credential?.type };
}
function resolveHarnessAuthProfileSelection(params: {
config: OpenClawConfig;
agentDir: string;
workspaceDir: string;
provider: string;
authProfileProvider: string;
sessionAuthProfileId?: string;
sessionAuthProfileSource?: "auto" | "user";
harnessId?: string;
harnessRuntime?: string;
metadataSnapshot?: PluginMetadataSnapshot;
providerAuthAliasesEnabled?: boolean;
allowHarnessAuthProfileForwarding: boolean;
}): HarnessAuthProfileSelection {
const sessionAuthProfileId = params.sessionAuthProfileId?.trim();
if (sessionAuthProfileId) {
const profileAuth = resolveProfileAuthFromStore({
agentDir: params.agentDir,
profileId: sessionAuthProfileId,
});
return {
authProfileId: sessionAuthProfileId,
authProfileIdSource: params.sessionAuthProfileSource,
authProfileProvider: profileAuth.provider ?? params.authProfileProvider,
authProfileMode: profileAuth.mode,
};
}
if (!params.allowHarnessAuthProfileForwarding) {
return { authProfileProvider: params.authProfileProvider };
}
const runtimeAuthPlan = buildAgentRuntimeAuthPlan({
provider: params.provider,
authProfileProvider: params.authProfileProvider,
config: params.config,
workspaceDir: params.workspaceDir,
...(params.metadataSnapshot ? { metadataSnapshot: params.metadataSnapshot } : {}),
providerAuthAliasesEnabled: params.providerAuthAliasesEnabled,
harnessId: params.harnessId,
harnessRuntime: params.harnessRuntime,
allowHarnessAuthProfileForwarding: params.allowHarnessAuthProfileForwarding,
});
const harnessAuthProvider = runtimeAuthPlan.harnessAuthProvider;
if (!harnessAuthProvider) {
return { authProfileProvider: params.authProfileProvider };
}
const store = ensureAuthProfileStore(params.agentDir, {
allowKeychainPrompt: false,
externalCliProviderIds: [harnessAuthProvider],
});
const authProfileId = resolveAuthProfileOrder({
cfg: params.config,
store,
provider: harnessAuthProvider,
})[0];
return authProfileId
? {
authProfileId,
authProfileIdSource: "auto",
authProfileProvider: harnessAuthProvider,
}
: { authProfileProvider: params.authProfileProvider };
}
function resolveTranscriptUsage(usage: PersistTextTurnTranscriptParams["assistant"]["usage"]) {
if (!usage) {
return ACP_TRANSCRIPT_USAGE;
}
const resolved = buildUsageWithNoCost({
input: usage.input,
output: usage.output,
cacheRead: usage.cacheRead,
cacheWrite: usage.cacheWrite,
totalTokens: usage.total,
});
return usage.contextUsage ? { ...resolved, contextUsage: usage.contextUsage } : resolved;
}
async function persistTextTurnTranscript(
params: PersistTextTurnTranscriptParams,
): Promise<PersistTextTurnTranscriptResult> {
const promptText = params.transcriptBody ?? params.body;
const replyText = params.skipAssistantTurn === true ? "" : params.finalText;
const userMessage =
params.userMessage ??
(await params.userTurnTranscriptRecorder?.resolveMessage()) ??
(promptText
? ({
role: "user",
content: promptText,
timestamp: Date.now(),
} as PersistedUserTurnMessage)
: undefined);
if (!userMessage && !replyText) {
return { kind: "persisted", sessionEntry: params.sessionEntry };
}
const messages = [];
if (userMessage) {
messages.push({
message: userMessage,
// Early persistence already owns this row, even when the input has no message key.
eventId: params.userTurnTranscriptRecorder?.getAdmissionReceipt()?.entryId,
idempotencyLookup: "scan" as const,
prepareMessageAfterIdempotencyCheck: (message: unknown) =>
preparePersistedUserTurnMessageForTranscriptWrite(message as PersistedUserTurnMessage, {
agentId: params.sessionAgentId,
sessionKey: params.sessionKey,
beforeMessageWrite: runAgentHarnessBeforeMessageWriteHook,
}),
});
}
if (replyText) {
const prepareAssistantTranscriptMessage = params.prepareAssistantTranscriptMessage;
messages.push({
idempotencyLookup: "scan-assistant" as const,
message: {
role: "assistant",
...(params.assistantIdempotencyKey
? { idempotencyKey: params.assistantIdempotencyKey }
: {}),
content: [{ type: "text", text: replyText }],
api: params.assistant.api,
provider: params.assistant.provider,
model: params.assistant.model,
usage: resolveTranscriptUsage(params.assistant.usage),
stopReason: params.assistant.stopReason,
timestamp: Date.now(),
},
...(prepareAssistantTranscriptMessage
? {
prepareMessageAfterIdempotencyCheck: (message: unknown) =>
prepareAssistantTranscriptMessage(
// SAFETY: This append creates the assistant row above; the preparer cannot receive another row.
message as Parameters<PrepareAssistantTranscriptMessage>[0],
replyText,
),
}
: {}),
});
}
const turn = await persistSessionTranscriptTurn(
{
sessionId: params.sessionId,
sessionKey: params.sessionKey,
sessionFile: params.sessionFile,
sessionEntry: params.sessionEntry,
sessionStore: params.sessionStore,
storePath: params.storePath,
agentId: params.sessionAgentId,
threadId: params.threadId,
},
{
config: params.config,
cwd: params.sessionCwd,
messages,
publishWhen: "always",
touchSessionEntry: true,
updateMode: "file-only",
expectedSessionId:
params.expectedSessionId ??
(params.sessionStore && params.storePath ? params.sessionId : undefined),
},
);
if (turn.rejectedReason === "session-rebound") {
return { kind: "session-rebound", sessionEntry: undefined };
}
const persistedUser = turn.messages.find(
(entry) => asOptionalRecord(entry.message)?.role === "user",
);
if (persistedUser) {
params.userTurnTranscriptRecorder?.markRuntimePersisted(
// SAFETY: The typed user-write hook above is the only producer of this batch's user row.
persistedUser.message as PersistedUserTurnMessage,
persistedUser.anchor,
{ appended: persistedUser.appended },
);
}
const assistantTranscript = turn.messages.find(
(entry) => asOptionalRecord(entry.message)?.role === "assistant",
);
return {
kind: "persisted",
sessionEntry: turn.sessionEntry,
...(assistantTranscript ? { assistantTranscript } : {}),
};
}
export function resolveCliTranscriptReplyText(result: EmbeddedAgentRunResult): string {
const visibleText = result.meta.finalAssistantVisibleText?.trim();
if (visibleText) {
return visibleText;
}
return (result.payloads ?? [])
.filter((payload) => !payload.isError && !payload.isReasoning)
.map((payload) => payload.text?.trim() ?? "")
.filter(Boolean)
.join("\n\n");
}
function isClaudeCliProvider(provider: string): boolean {
return provider.trim().toLowerCase() === "claude-cli";
}
export async function persistAcpTurnTranscript(params: {
prepareAssistantTranscriptMessage?: PrepareAssistantTranscriptMessage;
body: string;
transcriptBody?: string;
userInput?: UserTurnInput;
userTurnTranscriptRecorder?: UserTurnTranscriptRecorder;
assistantIdempotencyKey?: string;
expectedSessionId?: string;
finalText: string;
terminalOutcome: AgentRunTerminalOutcome;
sessionId: string;
sessionKey: string;
sessionFile?: string;
sessionEntry: SessionEntry | undefined;
sessionStore?: Record<string, SessionEntry>;
storePath?: string;
sessionAgentId: string;
threadId?: string | number;
sessionCwd: string;
config: OpenClawConfig;
}): Promise<PersistTextTurnTranscriptResult> {
const outcome = classifyAgentRunTerminalOutcome(params.terminalOutcome);
return await persistTextTurnTranscript({
...params,
...(params.userInput ? { userMessage: buildPersistedUserTurnMessage(params.userInput) } : {}),
assistant: {
api: "openai-responses",
provider: "openclaw",
model: "acp-runtime",
stopReason: outcome === "success" ? "stop" : outcome === "failure" ? "error" : "aborted",
},
});
}
export async function persistCliTurnTranscript(params: {
body: string;
transcriptBody?: string;
userMessage?: PersistedUserTurnMessage;
result: EmbeddedAgentRunResult;
sessionId: string;
sessionKey: string;
sessionFile?: string;
sessionEntry: SessionEntry | undefined;
sessionStore?: Record<string, SessionEntry>;
storePath?: string;
sessionAgentId: string;
threadId?: string | number;
sessionCwd: string;
config: OpenClawConfig;
skipUserTurn?: boolean;
skipAssistantTurn?: boolean;
}): Promise<PersistTextTurnTranscriptResult> {
const { result, skipUserTurn: requestedSkipUserTurn, ...transcript } = params;
const replyText = resolveCliTranscriptReplyText(result);
const provider = result.meta.agentMeta?.provider?.trim() ?? "cli";
const model = result.meta.agentMeta?.model?.trim() ?? "default";
const skipUserTurn = requestedSkipUserTurn === true;
return await persistTextTurnTranscript({
...transcript,
body: skipUserTurn ? "" : transcript.body,
transcriptBody: skipUserTurn ? undefined : transcript.transcriptBody,
userMessage: skipUserTurn ? undefined : transcript.userMessage,
finalText: replyText,
assistant: {
api: "cli",
provider,
model,
stopReason: "stop",
// The marker is terminal for fallback scans: without it, readers could
// skip this turn and revive an older cumulative usage record as fresh.
usage: resolveCliTranscriptUsage(result.meta.agentMeta?.lastCallUsage),
},
});
}
export function runAgentAttempt(params: {
preparedRunAdmission: PreparedAgentRunAdmission;
providerOverride: string;
modelOverride: string;
modelHasVision?: boolean;
modelThinkingCapability?: RunEmbeddedAgentInternalParams["modelThinkingCapability"];
configuredAuthProfileId?: string;
originalProvider: string;
cfg: OpenClawConfig;
sessionEntry: SessionEntry | undefined;
agentHarnessRuntimeOverride?: string;
sessionId: string;
sessionKey: string | undefined;
sessionTarget?: SessionTranscriptRuntimeTarget;
sessionAgentId: string;
sessionFile: string;
workspaceDir: string;
cwd?: string;
body: string;
transcriptBody?: string;
isFallbackRetry: boolean;
preserveCliSessionBinding?: boolean;
classifyResult?: (result: EmbeddedAgentRunResult) => ModelFallbackResultClassification;
modelRoutingProvenance: ModelFallbackAttemptProvenance;
resolvedThinkLevel: ThinkLevel;
fastMode?: FastMode;
fastModeStartedAtMs?: number;
fastModeAutoOnSeconds?: number;
isFinalFallbackAttempt?: boolean;
timeoutMs: number;
runTimeoutOverrideMs?: number;
runId: string;
lifecycleGeneration: string;
opts: AgentCommandOpts;
runContext: ReturnType<typeof resolveAgentRunContext>;
spawnedBy: string | undefined;
messageChannel: ReturnType<typeof resolveMessageChannel>;
skillsSnapshot: SkillSnapshot | undefined;
resolvedVerboseLevel: VerboseLevel | undefined;
agentDir: string;
onAgentEvent: (evt: {
stream: string;
data?: Record<string, unknown>;
sessionKey?: string;
}) => void | Promise<void>;
deferTerminalLifecycle?: boolean;
deferredLifecycle?: DeferredEmbeddedRunLifecycleManager;
authProfileProvider: string;
sessionStore?: Record<string, SessionEntry>;
storePath?: string;
pluginsEnabled?: boolean;
metadataSnapshot?: PluginMetadataSnapshot;
pluginGeneration: PreparedModelRuntimePluginGeneration | undefined;
allowTransientCooldownProbe?: boolean;
modelFallbacksOverride?: string[];
sessionHasHistory?: boolean;
fallbackRuntimeState?: { originRuntime?: "cli" | "embedded" };
suppressPromptPersistenceOnRetry?: boolean;
userTurnTranscriptRecorder?: UserTurnTranscriptRecorder;
assistantErrorTranscript?: RunEmbeddedAgentInternalParams["assistantErrorTranscript"];
authProfileFailurePolicy?: RunEmbeddedAgentInternalParams["authProfileFailurePolicy"];
contextEngineLogicalTurnLease?: ContextEngineLogicalTurnLease;
onUserMessagePersisted?: (message: Extract<AgentMessage, { role: "user" }>) => void;
onContextEngineTurnCandidate?: (facts: ContextEngineTurnAttemptFacts) => void;
onLifecycleGenerationChanged?: (lifecycleGeneration: string) => void;
onCompactionAccounting?: RunEmbeddedAgentInternalParams["onCompactionAccounting"];
onCompactionRequestBudget?: RunEmbeddedAgentInternalParams["onCompactionRequestBudget"];
onSuccessfulAuthProfile?: (selection: {
authProfileId?: string;
authProfileIdSource?: "auto" | "user";
}) => void;
}) {
const onRuntimeActivity = (info: { phase: string }) => {
// CLI preparation and child launch do not prove a native turn. Parsed
// assistant/tool activity does, even when the backend omits lifecycle events.
if (info.phase === "assistant_output_started" || info.phase === "tool_execution_started") {
void params.onAgentEvent({ stream: "lifecycle", data: { phase: "start" } });
}
};
const sessionAuthProfileId = params.sessionEntry?.authProfileOverride?.trim();
const sessionAuthProfileSource = resolveCollapsedSessionAuthPinSource(params.sessionEntry);
// An explicit session choice owns the conversation. Otherwise the profile
// bound to the configured model replaces a stale automatic session choice.
const selectedAuthProfile =
sessionAuthProfileId && sessionAuthProfileSource !== "auto"
? { id: sessionAuthProfileId, source: sessionAuthProfileSource }
: params.configuredAuthProfileId?.trim()
? { id: params.configuredAuthProfileId.trim(), source: "user" as const }
: sessionAuthProfileId
? { id: sessionAuthProfileId, source: sessionAuthProfileSource }
: undefined;
const isRawModelRun = params.opts.modelRun === true || params.opts.promptMode === "none";
const isSubagentLane = params.opts.lane === AGENT_LANE_SUBAGENT;
// A completion handoff relays frozen child output, so only a verified private
// capability plus persisted requester lineage may restore its tool surface.
const isSubagentAnnounceHandoff = isSubagentAnnounceCompletionHandoff({
inputProvenance: params.opts.inputProvenance,
internalEvents: params.opts.internalEvents,
});
const exactSubagentAnnounceHandoff =
isSubagentAnnounceHandoff &&
isTrustedSubagentCompletionHandoffForRun({
handoff: params.opts.trustedInternalHandoff,
inputProvenance: params.opts.inputProvenance,
internalEvents: params.opts.internalEvents,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
provider: params.providerOverride,
model: params.modelOverride,
});
const trustedSubagentAnnounceHandoff =
exactSubagentAnnounceHandoff &&
hasVerifiedRequesterCompletionHandoff({
config: params.cfg,
sessionKey: params.sessionKey,
inputProvenance: params.opts.inputProvenance,
trustedInternalHandoff: params.opts.trustedInternalHandoff,
sessionId: params.sessionId,
modelProvider: params.providerOverride,
modelId: params.modelOverride,
});
const completionRequestsMessageDelivery =
trustedSubagentAnnounceHandoff &&
!isRawModelRun &&
params.opts.disableMessageTool !== true &&
messageToolOwnsVisibleReply(params.opts);
const completionSandboxStatus = completionRequestsMessageDelivery
? resolveSandboxRuntimeStatus({
cfg: params.cfg,
sessionKey: params.sessionKey,
agentId: params.sessionAgentId,
})
: undefined;
const completionCapabilityProfile = completionRequestsMessageDelivery
? resolveConversationCapabilityProfile({
config: params.cfg,
sessionKey: params.sessionKey,
sessionId: params.sessionId,
agentId: params.sessionAgentId,
senderId: params.runContext.senderId,
modelProvider: params.providerOverride,
modelId: params.modelOverride,
sandboxToolPolicy: completionSandboxStatus?.sandboxed
? completionSandboxStatus.toolPolicy
: undefined,
inputProvenance: params.opts.inputProvenance,
trustedInternalHandoff: params.opts.trustedInternalHandoff,
})
: undefined;
const completionToolPolicies = completionCapabilityProfile
? resolveConversationToolPolicies({
capabilityProfile: completionCapabilityProfile,
additionalProfileAllow: ["message"],
// The source-bound delivery grant extends restrictive allowlists only;
// explicit denies still win at every policy layer.
additionalPolicyAllow: ["message"],
additionalInheritedAllow: ["message"],
})
: undefined;
// Forced private delivery is not authority: retain every parent/operator cap
// and mint only the source-bound message capability from a verified envelope.
const completionNeedsMessageDelivery =
completionCapabilityProfile?.policy.requesterPolicySource === "completion-handoff" &&
completionToolPolicies !== undefined &&
isToolAllowedByPolicies("message", Object.values(completionToolPolicies)) &&
isRuntimeToolAllowed("message", params.opts.toolsAllow);
const claudeCliFallbackPrelude =
!isRawModelRun &&
params.isFallbackRetry &&
isClaudeCliProvider(params.originalProvider) &&
!isClaudeCliProvider(params.providerOverride)
? buildClaudeCliFallbackContextPrelude({
cliSessionId: getCliSessionBinding(params.sessionEntry, "claude-cli")?.sessionId,
})
: "";
const resolvedPrompt = resolveFallbackRetryPrompt({
body: params.body,
isFallbackRetry: params.isFallbackRetry,
sessionHasHistory: params.sessionHasHistory,
priorContextPrelude: claudeCliFallbackPrelude,
});
const effectivePrompt = isRawModelRun
? resolvedPrompt
: annotateInterSessionPromptText(resolvedPrompt, params.opts.inputProvenance);
const embeddedExecApprovalContinuationPromptRange = rebaseExecApprovalContinuationPromptRange({
body: params.body,
prompt: effectivePrompt,
range: params.opts.execApprovalContinuationPromptRange,
});
const continuationTranscriptBody = params.opts.execApprovalContinuationPromptRange
? (params.transcriptBody ?? params.body)
: params.transcriptBody;
const continuationTranscriptPromptRange =
params.opts.execApprovalContinuationTranscriptPromptRange ??
params.opts.execApprovalContinuationPromptRange;
const bootstrapPromptWarningSignaturesSeen = resolveBootstrapWarningSignaturesSeen(
params.sessionEntry?.systemPromptReport,
);
const bootstrapPromptWarningSignature =
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1];
const requestedAgentHarnessId = isRawModelRun ? "openclaw" : undefined;
const sessionRuntimeOverride = isRawModelRun ? undefined : params.agentHarnessRuntimeOverride;
const pinnedHarnessId = isRawModelRun
? undefined
: resolveSessionPinnedHarnessId(params.sessionEntry);
const locksSessionRuntimeOverride =
pinnedHarnessId !== undefined && sessionRuntimeOverride === pinnedHarnessId;
const sessionCliRuntime =
sessionRuntimeOverride &&
!locksSessionRuntimeOverride &&
isCliProvider(sessionRuntimeOverride, params.cfg)
? sessionRuntimeOverride
: undefined;
const configuredCliRuntime =
!isRawModelRun && !sessionRuntimeOverride
? resolveCliRuntimeExecutionProvider({
provider: params.providerOverride,
cfg: params.cfg,
agentId: params.sessionAgentId,
modelId: params.modelOverride,
authProfileId: selectedAuthProfile?.id,
})
: undefined;
const cliExecutionProvider = isRawModelRun
? params.providerOverride
: (sessionCliRuntime ?? configuredCliRuntime ?? params.providerOverride);
const isCliExecutionProvider = sessionRuntimeOverride
? sessionCliRuntime !== undefined
: isCliProvider(cliExecutionProvider, params.cfg);
const completionRetainsRequesterTools =
trustedSubagentAnnounceHandoff &&
!isRawModelRun &&
!isCliExecutionProvider &&
(!messageToolOwnsVisibleReply(params.opts) || completionNeedsMessageDelivery);
// Message-tool-only delivery constrains the visible reply, not the parent
// continuation's verified authority. Keep the inherited cap while requiring
// message to survive every applicable policy before enabling any tools.
// An explicit cap is enforced even when tools are disabled; clear it so a
// denied completion can finish tool-free and its owner can relay frozen text.
const runtimeToolsAllow = isSubagentAnnounceHandoff
? completionRetainsRequesterTools
? params.opts.toolsAllow
: completionNeedsMessageDelivery
? ["message"]
: undefined
: params.opts.toolsAllow;
// Collector output is mandatory result transport, even on a narrowed tool
// surface. The CLI grant is minted from this list and enforced exactly on the
// loopback server, so a plugin-launched or cron-continued collector needs the
// same forced merge the embedded runner applies before its own construction.
const cliRuntimeToolsAllow = mergeForcedEmbeddedAttemptToolsAllow(runtimeToolsAllow, {
forceToolNames:
params.opts.swarmCollector && params.opts.swarmOutputSchema
? ["structured_output"]
: undefined,
});
const disableTools =
params.opts.modelRun === true ||
(isSubagentAnnounceHandoff &&
!completionRetainsRequesterTools &&
!completionNeedsMessageDelivery);
const toolContext = {
messageChannel: params.messageChannel,
messageProvider: params.opts.messageProvider ?? params.messageChannel,
agentAccountId: params.runContext.accountId,
groupId: params.runContext.groupId,
groupChannel: params.runContext.groupChannel,
groupSpace: params.runContext.groupSpace,
spawnedBy: params.spawnedBy,
currentChannelId: params.runContext.currentChannelId,
chatId: params.runContext.chatId,
channelContext: params.runContext.channelContext,
currentThreadTs: params.runContext.currentThreadTs,
currentInboundAudio: params.runContext.currentInboundAudio,
replyToMode: params.runContext.replyToMode,
senderId: params.runContext.senderId,
senderIsOwner: params.opts.senderIsOwner,
scheduledToolPolicy: params.opts.scheduledToolPolicy,
pinnedWidgetAuthoring: params.opts.pinnedWidgetAuthoring,
};
if (params.fallbackRuntimeState && params.fallbackRuntimeState.originRuntime === undefined) {
params.fallbackRuntimeState.originRuntime =
!isRawModelRun && isCliExecutionProvider ? "cli" : "embedded";
}
const shouldForwardImagesToEmbedded =
!params.isFallbackRetry || params.fallbackRuntimeState?.originRuntime === "cli";
const allowCliAuthProfileForwarding =
isCliExecutionProvider &&
cliBackendAcceptsAuthProfileForwarding({
provider: cliExecutionProvider,
config: params.cfg,
agentId: params.sessionAgentId,
});
const agentHarnessPolicy = isRawModelRun
? ({ runtime: "openclaw", runtimeSource: "model" } as const)
: sessionRuntimeOverride
? ({ runtime: sessionRuntimeOverride, runtimeSource: "model" } as const)
: resolveAvailableAgentHarnessPolicy({
provider: params.providerOverride,
modelId: params.modelOverride,
config: params.cfg,
agentId: params.sessionAgentId,
sessionKey: params.sessionKey ?? params.sessionId,
});
const harnessAuthSelection = resolveHarnessAuthProfileSelection({
config: params.cfg,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
provider: params.providerOverride,
authProfileProvider: params.authProfileProvider,
sessionAuthProfileId: selectedAuthProfile?.id,
sessionAuthProfileSource: selectedAuthProfile?.source,
harnessId: requestedAgentHarnessId,
harnessRuntime: agentHarnessPolicy.runtime,
...(params.metadataSnapshot ? { metadataSnapshot: params.metadataSnapshot } : {}),
providerAuthAliasesEnabled: params.pluginsEnabled,
allowHarnessAuthProfileForwarding: !isCliExecutionProvider,
});
const runtimeAuthPlan = buildAgentRuntimeAuthPlan({
provider: params.providerOverride,
authProfileProvider: harnessAuthSelection.authProfileProvider,
authProfileMode: harnessAuthSelection.authProfileMode,
sessionAuthProfileId: harnessAuthSelection.authProfileId,
config: params.cfg,
workspaceDir: params.workspaceDir,
...(params.metadataSnapshot ? { metadataSnapshot: params.metadataSnapshot } : {}),
providerAuthAliasesEnabled: params.pluginsEnabled,
harnessId: requestedAgentHarnessId,
harnessRuntime: agentHarnessPolicy.runtime,
allowHarnessAuthProfileForwarding: !isCliExecutionProvider,
});
// Explicit pins keep synchronous validation; automatic selection needs the admitted binding.
const cliAuthNeedsSessionBinding =
allowCliAuthProfileForwarding &&
!isRawModelRun &&
(!harnessAuthSelection.authProfileId || harnessAuthSelection.authProfileIdSource === "auto");
const authProfileId =
allowCliAuthProfileForwarding && !cliAuthNeedsSessionBinding
? resolveCliExecutionAuthProfileId({
cliExecutionProvider,
authProfileProvider: params.authProfileProvider,
config: params.cfg,
agentDir: params.agentDir,
selected: harnessAuthSelection,
})
: runtimeAuthPlan.forwardedAuthProfileId;
const embeddedAgentProvider = resolveOpenAIRuntimeProvider({
provider: params.providerOverride,
harnessRuntime: agentHarnessPolicy.runtime,
agentHarnessId: requestedAgentHarnessId,
authProfileProvider: runtimeAuthPlan.authProfileProviderForAuth,
authProfileId,
config: params.cfg,
workspaceDir: params.workspaceDir,
});
const embeddedAgentHarnessOverride =
requestedAgentHarnessId ??
sessionRuntimeOverride ??
(agentHarnessPolicy.runtime === "openclaw" && agentHarnessPolicy.runtimeSource !== "implicit"
? "openclaw"
: undefined);
if (!isRawModelRun && isCliExecutionProvider) {
const expectedLifecycleRevision = params.sessionEntry?.lifecycleRevision;
return withLocalSessionPlacementTurnSettlement(
{
sessionId: params.sessionId,
sessionKey: params.sessionKey ?? params.sessionId,
agentId: params.sessionAgentId,
runId: params.runId,
},
async (assertSettlementCurrent) => {
if (params.sessionKey && params.storePath) {
params.sessionEntry = loadSessionEntry({
sessionKey: params.sessionKey,
storePath: params.storePath,
readConsistency: "latest",
});
if (
params.sessionEntry?.sessionId !== params.sessionId ||
params.sessionEntry.lifecycleRevision !== expectedLifecycleRevision
) {
throw createAgentRunSupersededAbortError();
}
}
const cliSessionBinding = getCliSessionBinding(params.sessionEntry, cliExecutionProvider);
const cliAuthProfileId = cliAuthNeedsSessionBinding
? resolveCliExecutionAuthProfileId({
cliExecutionProvider,
authProfileProvider: params.authProfileProvider,
config: params.cfg,
agentDir: params.agentDir,
selected: harnessAuthSelection,
sessionBinding: cliSessionBinding,
})
: authProfileId;
const diagnosticOwner = params.deferredLifecycle?.handoffToCli();
const cliProcessCwd = params.cwd ? resolveUserPath(params.cwd) : params.workspaceDir;
const cliContinuationBody = params.opts.execApprovalContinuationPromptRange
? resizeExecApprovalContinuationPrompt({
prompt: params.body,
range: params.opts.execApprovalContinuationPromptRange,
maxOutputUtf16Units: DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS,
})
: params.body;
const cliResolvedPrompt = params.opts.execApprovalContinuationPromptRange
? resolveFallbackRetryPrompt({
body: cliContinuationBody,
isFallbackRetry: params.isFallbackRetry,
sessionHasHistory: params.sessionHasHistory,
priorContextPrelude: claudeCliFallbackPrelude,
})
: resolvedPrompt;
const cliEffectivePrompt = params.opts.execApprovalContinuationPromptRange
? annotateInterSessionPromptText(cliResolvedPrompt, params.opts.inputProvenance)
: effectivePrompt;
const cliTranscriptPrompt =
continuationTranscriptBody === undefined || !continuationTranscriptPromptRange
? continuationTranscriptBody
: resizeExecApprovalContinuationPrompt({
prompt: continuationTranscriptBody,
range: continuationTranscriptPromptRange,
maxOutputUtf16Units: DEFAULT_MAX_LIVE_TOOL_RESULT_CHARS,
});
params.userTurnTranscriptRecorder?.replaceTextBeforePersistence?.(
cliTranscriptPrompt ?? cliContinuationBody,
);
const cliPrompt =
params.opts.inputProvenance?.kind === "inter_session"
? cliEffectivePrompt
: injectTimestamp(cliEffectivePrompt, timestampOptsFromConfig(params.cfg));
const mutableCliSessionStore =
params.sessionKey && params.sessionStore && params.storePath
? {
sessionKey: params.sessionKey,
sessionStore: params.sessionStore,
storePath: params.storePath,
expectedSessionId: params.sessionId,
assertCommitAllowed: assertSettlementCurrent,
}
: undefined;
const resolveReusableCliSessionBinding = async () => {
const hasManagedClaudeLiveSession = Boolean(
isClaudeCliProvider(cliExecutionProvider) &&
cliSessionBinding?.sessionId &&
hasCliLiveSession({
backendId: cliExecutionProvider,
agentAccountId: params.runContext.accountId,
agentId: params.sessionAgentId,
authProfileId: cliSessionBinding.authProfileId,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
}),
);
if (
!isClaudeCliProvider(cliExecutionProvider) ||
!cliSessionBinding?.sessionId ||
hasManagedClaudeLiveSession ||
(await claudeCliSessionTranscriptHasContent({
sessionId: cliSessionBinding.sessionId,
workspaceDir: cliProcessCwd,
}))
) {
return cliSessionBinding;
}
log.warn(
`cli session reset: provider=${sanitizeForLog(cliExecutionProvider)} reason=transcript-missing sessionKey=${params.sessionKey ?? params.sessionId}`,
);
if (mutableCliSessionStore) {
params.sessionEntry =
(await clearCliSessionInStore({
provider: cliExecutionProvider,
...mutableCliSessionStore,
})) ?? params.sessionEntry;
}
// The store is already cleared above, so no stale --resume can leak to a
// later turn. Still return the bound id as the reuse candidate: prepare
// re-detects the missing transcript, keeps useResume=false, and arms
// raw-transcript reseed from prior OpenClaw history. Returning undefined
// strips the candidate and starves reseed, losing warm-stdin continuity.
return cliSessionBinding;
};
const mediaTaskIdsBefore = getGeneratedMediaTaskIdsForSessionKey(params.sessionKey);
const runCliWithSession = async (
nextCliSessionId: string | undefined,
activeCliSessionBinding = cliSessionBinding,
) => {
const forkCliSessionOnResume = activeCliSessionBinding?.forkNextResume === true;
const resolvedCliBackend = resolveCliBackendConfig(cliExecutionProvider, params.cfg, {
agentId: params.sessionAgentId,
});
const supportsCliSessionFork = Boolean(resolvedCliBackend?.config.forkArg);
if (forkCliSessionOnResume && !supportsCliSessionFork) {
throw new Error(`CLI backend "${cliExecutionProvider}" does not support session forks`);
}
const forkStoreParams =
supportsCliSessionFork && nextCliSessionId && mutableCliSessionStore
? {
provider: cliExecutionProvider,
expectedCliSessionId: nextCliSessionId,
...mutableCliSessionStore,
assertCommitAllowed: () => {
assertSettlementCurrent();
(params.deferredLifecycle?.signal ?? params.opts.abortSignal)?.throwIfAborted();
},
}
: undefined;
return await runCliAgent({
preparedRunAdmission: params.preparedRunAdmission,
diagnosticOwner,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
sessionTarget: params.sessionTarget,
sessionEntry: params.sessionEntry,
chatType: params.sessionEntry?.chatType,
contextWindow: params.sessionEntry?.contextWindow,
agentId: params.sessionAgentId,
trigger: "user",
sessionFile: params.sessionFile,
storePath: params.storePath,
persistAssistantTranscript:
params.storePath !== undefined && params.sessionStore !== undefined,
workspaceDir: params.workspaceDir,
cwd: params.cwd,
config: params.cfg,
prompt: cliPrompt,
transcriptPrompt: cliTranscriptPrompt,
modelProvider: params.providerOverride,
requesterModel: { provider: params.providerOverride, model: params.modelOverride },
modelHasVision: params.modelHasVision,
provider: cliExecutionProvider,
model: params.modelOverride,
modelRoutingProvenance: params.modelRoutingProvenance,
thinkLevel: params.resolvedThinkLevel,
fastMode: params.fastMode,
fastModeStartedAtMs: params.fastModeStartedAtMs,
fastModeAutoOnSeconds: params.fastModeAutoOnSeconds,
timeoutMs: params.timeoutMs,
runTimeoutOverrideMs: params.runTimeoutOverrideMs,
runId: params.runId,
lifecycleGeneration: params.lifecycleGeneration,
abortSignal: params.deferredLifecycle?.signal ?? params.opts.abortSignal,
onExecutionStarted: params.opts.onExecutionStarted,
onExecutionPhase: onRuntimeActivity,
lane: params.opts.lane,
extraSystemPrompt: params.opts.extraSystemPrompt,
inputProvenance: params.opts.inputProvenance,
skillLibraryAuthoring: params.opts.skillLibraryAuthoring,
cronCreatorCallerOrigin: params.opts.cronCreatorAuthorityCapability?.callerOrigin,
sourceReplyDeliveryMode: params.opts.sourceReplyDeliveryMode,
requireExplicitMessageTarget:
params.opts.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
cliSessionBindingFacts: params.opts.cliSessionBindingFacts,
cliSessionId: nextCliSessionId,
cliSessionBinding:
nextCliSessionId === activeCliSessionBinding?.sessionId
? activeCliSessionBinding
: undefined,
forkCliSessionOnResume,
...(forkStoreParams
? {
claimCliSessionFork: async () => {
const claimed = await consumeCliSessionForkInStore(forkStoreParams);
if (claimed) {
params.sessionEntry = claimed;
}
return Boolean(claimed);
},
restoreCliSessionFork: async () => {
// Restoring the fork is current-owner cleanup, including after cancellation.
const restored = await restoreCliSessionForkInStore({
...forkStoreParams,
assertCommitAllowed: assertSettlementCurrent,
});
if (restored) {
params.sessionEntry = restored;
}
},
persistCliSessionForkSuccessor: async (successorCliSessionId: string) => {
const persisted = await persistCliSessionForkSuccessorInStore({
...forkStoreParams,
successorCliSessionId,
});
if (!persisted) {
throw new Error("CLI session fork successor could not be persisted");
}
params.sessionEntry = persisted;
},
}
: {}),
authProfileId: cliAuthProfileId,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature,
// Image discovery must use the original turn, before retry/history decoration.
imagePrompt: params.body,
// Fallback prompts repeat the current task, so prompt-local images must
// accompany every CLI process. Native dedupe requires a runtime receipt.
images: params.opts.images,
imageOrder: params.opts.imageOrder,
media: params.opts.media,
skillsSnapshot: params.skillsSnapshot,
...toolContext,
streamParams: params.opts.streamParams,
// Completion relays can carry the trusted source only in their
// delivery target; the restricted CLI grant must retain that owner.
currentChannelId:
params.runContext.currentChannelId ??
(completionNeedsMessageDelivery
? (params.opts.replyTo ?? params.opts.to)
: undefined),
approvalReviewerDeviceId: params.opts.approvalReviewerDeviceId,
bashElevated: params.opts.bashElevated,
toolsAllow: resolveCliRuntimeToolsAllow(
cliRuntimeToolsAllow,
params.opts.toolsAllowIsDefault,
),
// This loop is the command-origin sibling of the auto-reply fallback
// candidate, so its CLI grant needs the same delegation gate; the
// inputs match the tool state this invocation actually runs with.
...buildCliMcpDelegationCapabilityBinding(
resolveDelegationCapability({
fallbackActive: params.isFallbackRetry,
inputProvenance: params.opts.inputProvenance,
disableTools,
toolsAllow: runtimeToolsAllow,
}),
),
cleanupBundleMcpOnRunEnd: params.opts.cleanupBundleMcpOnRunEnd,
cleanupCliLiveSessionOnRunEnd: params.opts.cleanupCliLiveSessionOnRunEnd,
oneShotCliRun: params.opts.oneShotCliRun,
userTurnTranscriptRecorder: params.userTurnTranscriptRecorder,
contextEngineLogicalTurnLease: params.contextEngineLogicalTurnLease,
onContextEngineTurnCandidate: params.onContextEngineTurnCandidate,
suppressNextUserMessagePersistence: params.suppressPromptPersistenceOnRetry === true,
disableTools,
allowEmptyAssistantReplyAsSilent: isSubagentLane || isSubagentAnnounceHandoff,
...(forkStoreParams && !forkCliSessionOnResume
? {
onBeforeForkedCliSessionRetry: async (retry) => {
if (
hasNewGeneratedMediaTaskForSessionKey(
params.sessionKey,
mediaTaskIdsBefore,
) ||
retry.sessionId !== activeCliSessionBinding?.sessionId
) {
return false;
}
log.warn(
`CLI session stalled, arming forked recovery: provider=${sanitizeForLog(cliExecutionProvider)} sessionKey=${forkStoreParams.sessionKey}`,
);
const armed = await restoreCliSessionForkInStore(forkStoreParams);
if (armed) {
params.sessionEntry = armed;
}
return Boolean(armed);
},
}
: {}),
...(mutableCliSessionStore
? {
onBeforeFreshCliSessionRetry: async (retry) => {
if (
hasNewGeneratedMediaTaskForSessionKey(
params.sessionKey,
mediaTaskIdsBefore,
) ||
getCliSessionBinding(
loadSessionEntry({
sessionKey: mutableCliSessionStore.sessionKey,
storePath: mutableCliSessionStore.storePath,
readConsistency: "latest",
}),
cliExecutionProvider,
)?.sessionId !== retry.sessionId
) {
return false;
}
log.warn(
`CLI session failed, clearing before fresh retry: provider=${sanitizeForLog(cliExecutionProvider)} sessionKey=${mutableCliSessionStore.sessionKey} reason=${sanitizeForLog(retry.reason)}`,
);
const cleared = await clearCliSessionInStore({
provider: cliExecutionProvider,
expectedCliSessionId: retry.sessionId,
...mutableCliSessionStore,
});
if (!cleared) {
return false;
}
params.sessionEntry = cleared;
return true;
},
}
: {}),
});
};
const activeCliSessionBinding = await resolveReusableCliSessionBinding();
let result: EmbeddedAgentRunResult;
try {
result = await runCliWithSession(
activeCliSessionBinding?.sessionId,
activeCliSessionBinding,
);
} catch (err) {
const failedCliSessionBinding = getCliSessionBinding(
params.sessionEntry,
cliExecutionProvider,
);
const failedCliSessionId = failedCliSessionBinding?.sessionId;
if (
isClaudeCliProvider(cliExecutionProvider) &&
shouldClearFailedCliSessionBinding({
error: err,
binding: failedCliSessionBinding,
bindingReplacedDuringRun: failedCliSessionId !== activeCliSessionBinding?.sessionId,
hasNewGeneratedMediaTask: hasNewGeneratedMediaTaskForSessionKey(
params.sessionKey,
mediaTaskIdsBefore,
),
}) &&
failedCliSessionId &&
mutableCliSessionStore
) {
log.warn(
`CLI session cleared after failed reused turn: provider=${sanitizeForLog(cliExecutionProvider)} sessionKey=${mutableCliSessionStore.sessionKey} reason=${sanitizeForLog(resolveCliSessionClearReason(err))}`,
);
params.sessionEntry =
(await clearCliSessionInStore({
provider: cliExecutionProvider,
expectedCliSessionId: failedCliSessionId,
...mutableCliSessionStore,
})) ?? params.sessionEntry;
}
throw err;
}
const classification = params.classifyResult?.(result);
if (
!params.preserveCliSessionBinding &&
(!classification || result.meta.agentMeta?.clearCliSessionBinding === true)
) {
return await persistCliSessionBindingResult({
provider: cliExecutionProvider,
result,
sessionKey: params.sessionKey,
storePath: params.storePath,
sessionStore: params.sessionStore,
expectedSession: params.sessionEntry,
assertSettlementCurrent,
abortSignal: params.deferredLifecycle?.signal ?? params.opts.abortSignal,
});
}
return result;
},
{
preparedRunAdmission: params.preparedRunAdmission,
lifecycleGeneration: params.lifecycleGeneration,
abortSignal: params.deferredLifecycle?.signal ?? params.opts.abortSignal,
trigger: "user",
inputProvenance: params.opts.inputProvenance,
},
);
}
const embeddedRunParams: RunEmbeddedAgentInternalParams = {
preparedRunAdmission: params.preparedRunAdmission,
sessionId: params.sessionId,
sessionKey: params.sessionKey,
chatType: params.sessionEntry?.chatType,
contextWindow: params.sessionEntry?.contextWindow,
sessionTarget: params.sessionTarget,
sandboxSessionKey: params.sessionKey,
agentId: params.sessionAgentId,
trigger: "user",
// Subagent lifecycle owns the stricter explicit visible/silent/empty evidence check.
terminalReplyExpectation: isSubagentLane ? "optional" : undefined,
...toolContext,
messageTo: params.opts.replyTo ?? params.opts.to,
messageThreadId: params.opts.threadId,
hasRepliedRef: params.runContext.hasRepliedRef,
sessionFile: params.sessionFile,
workspaceDir: params.workspaceDir,
cwd: params.cwd,
permissionMode: params.sessionEntry?.permissionMode,
toolOverrides: params.sessionEntry?.toolOverrides,
sessionRoot: params.sessionEntry?.sessionRoot,
config: params.cfg,
...(params.pluginGeneration ? { pluginGeneration: params.pluginGeneration } : {}),
agentHarnessId: pinnedHarnessId,
modelSelectionLocked: !isRawModelRun && params.sessionEntry?.modelSelectionLocked === true,
agentHarnessRuntimeOverride: embeddedAgentHarnessOverride,
agentHarnessRuntimePreparationHint:
agentHarnessPolicy.runtimeSource !== "implicit" ? agentHarnessPolicy.runtime : undefined,
skillsSnapshot: params.skillsSnapshot,
prompt: effectivePrompt,
transcriptPrompt: continuationTranscriptBody,
// CLI-origin retries cannot rely on transcript replay: orphan-user repair
// removes the persisted CLI turn before the embedded prompt is submitted.
images: shouldForwardImagesToEmbedded ? params.opts.images : undefined,
imageOrder: shouldForwardImagesToEmbedded ? params.opts.imageOrder : undefined,
media: params.opts.media,
clientTools: params.opts.clientTools,
provider: embeddedAgentProvider,
model: params.modelOverride,
modelRoutingProvenance: params.modelRoutingProvenance,
requestedRouteResolution: "resolved",
modelHasVision: params.modelHasVision,
modelThinkingCapability: params.modelThinkingCapability,
modelFallbacksOverride: params.modelFallbacksOverride,
authProfileId,
authProfileIdSource: authProfileId ? harnessAuthSelection.authProfileIdSource : undefined,
thinkLevel: params.resolvedThinkLevel,
fastMode: params.fastMode,
fastModeStartedAtMs: params.fastModeStartedAtMs,
fastModeAutoOnSeconds: params.fastModeAutoOnSeconds,
isFinalFallbackAttempt: params.isFinalFallbackAttempt,
verboseLevel: params.resolvedVerboseLevel,
bashElevated: params.opts.bashElevated,
execApprovalContinuationPromptRange: embeddedExecApprovalContinuationPromptRange,
execApprovalContinuationTranscriptPromptRange: continuationTranscriptPromptRange,
approvalReviewerDeviceId: params.opts.approvalReviewerDeviceId,
timeoutMs: params.timeoutMs,
runTimeoutOverrideMs: params.runTimeoutOverrideMs,
runId: params.runId,
lifecycleGeneration: params.lifecycleGeneration,
lane: params.opts.lane,
// Hidden internal runs lack an event consumer; visible lanes still feed UI and parent relays.
suppressLiveStreamOutput: shouldSuppressEmbeddedLiveStreamOutput(params),
abortSignal: params.opts.abortSignal,
extraSystemPrompt: params.opts.extraSystemPrompt,
bootstrapContextMode: params.opts.bootstrapContextMode,
bootstrapContextRunKind: params.opts.bootstrapContextRunKind,
toolsAllow: runtimeToolsAllow,
runtimePluginToolGrant: params.opts.runtimePluginToolGrant,
trustedInternalHandoff: trustedSubagentAnnounceHandoff
? params.opts.trustedInternalHandoff
: undefined,
cronCreatorAuthorityCapability: params.opts.cronCreatorAuthorityCapability,
skillLibraryAuthoring: params.opts.skillLibraryAuthoring,
internalEvents: params.opts.internalEvents,
runtimeContextFragments: params.opts.runtimeContextFragments,
inputProvenance: params.opts.inputProvenance,
sourceReplyDeliveryMode: params.opts.sourceReplyDeliveryMode,
requireExplicitMessageTarget: params.opts.requireExplicitMessageTarget,
disableMessageTool: params.opts.disableMessageTool,
swarmCollector: params.opts.swarmCollector,
swarmOutputSchema: params.opts.swarmOutputSchema,
forceRestartSafeTools: params.opts.forceRestartSafeTools,
forceCodeModeTools: params.opts.forceCodeModeTools,
codeModeOverride: params.opts.codeModeOverride,
streamParams: params.opts.streamParams,
agentDir: params.agentDir,
allowGatewaySubagentBinding: params.opts.allowGatewaySubagentBinding,
allowTransientCooldownProbe: params.allowTransientCooldownProbe,
cleanupBundleMcpOnRunEnd: params.opts.cleanupBundleMcpOnRunEnd,
oneShotCliRun: params.opts.oneShotCliRun,
modelRun: params.opts.modelRun,
promptMode: params.opts.promptMode,
disableTools,
allowEmptyAssistantReplyAsSilent: isSubagentLane || isSubagentAnnounceHandoff,
onAgentEvent: params.onAgentEvent,
onExecutionPhase: onRuntimeActivity,
deferTerminalLifecycle: params.deferTerminalLifecycle,
onDeferredLifecycleOwner: params.deferredLifecycle?.adopt,
onDeferredLifecycleAbort: params.deferredLifecycle?.abort,
onRetryWait: params.deferredLifecycle?.beginRetryWait,
suppressNextUserMessagePersistence: params.suppressPromptPersistenceOnRetry === true,
userTurnTranscriptRecorder: params.userTurnTranscriptRecorder,
assistantErrorTranscript: params.assistantErrorTranscript,
authProfileFailurePolicy: params.authProfileFailurePolicy,
contextEngineLogicalTurnLease: params.contextEngineLogicalTurnLease,
onContextEngineTurnCandidate: params.onContextEngineTurnCandidate,
onUserMessagePersisted: params.onUserMessagePersisted,
onCompactionAccounting: params.onCompactionAccounting,
onCompactionRequestBudget: params.onCompactionRequestBudget,
onSuccessfulAuthProfile: params.onSuccessfulAuthProfile
? (successfulProfileId) =>
params.onSuccessfulAuthProfile?.({
authProfileId: successfulProfileId,
authProfileIdSource: successfulProfileId
? successfulProfileId === authProfileId
? harnessAuthSelection.authProfileIdSource
: "auto"
: undefined,
})
: undefined,
onExecutionStarted: (info) => {
params.opts.onExecutionStarted?.();
if (info?.lifecycleGeneration) {
params.onLifecycleGenerationChanged?.(info.lifecycleGeneration);
}
},
onSessionIdChanged: params.opts.onSessionIdChanged,
bootstrapPromptWarningSignaturesSeen,
bootstrapPromptWarningSignature,
};
setChannelSourceTurnId(embeddedRunParams, readChannelSourceTurnId(params.runContext));
setChannelSourceTurnSameThreadRequired(
embeddedRunParams,
readChannelSourceTurnSameThreadRequired(params.runContext),
);
return runEmbeddedAgent(embeddedRunParams);
}
export function buildAcpResult(params: {
payloadText: string;
terminalReply?: AgentRunTerminalReplySnapshot;
startedAt: number;
stopReason?: string;
resultStatus?: Extract<AcpRuntimeEvent, { type: "done" }>["status"];
abortSignal?: AbortSignal;
}) {
const normalizedFinalPayload = normalizeReplyPayload({
text: params.payloadText,
});
const payloads = normalizedFinalPayload ? [normalizedFinalPayload] : [];
const abortFields = resolveAgentRunAbortLifecycleFields(params.abortSignal);
const resultCancelled = params.resultStatus === "cancelled";
return {
payloads,
meta: {
durationMs: Date.now() - params.startedAt,
aborted: abortFields.aborted ?? resultCancelled,
stopReason: abortFields.stopReason ?? (resultCancelled ? "stop" : params.stopReason),
...(params.terminalReply ? { terminalReply: params.terminalReply } : {}),
},
};
}
export function emitAcpLifecycleStart(params: {
runId: string;
startedAt: number;
sessionKey?: string;
agentId?: string;
lifecycleGeneration?: string;
auditOnly?: boolean;
completionSource?: "reply-dispatch";
}) {
const emit = params.auditOnly ? emitAgentAuditEvent : emitAgentEvent;
emit({
runId: params.runId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
...(params.lifecycleGeneration ? { lifecycleGeneration: params.lifecycleGeneration } : {}),
stream: "lifecycle",
data: {
phase: "start",
...(params.completionSource ? { completionSource: params.completionSource } : {}),
startedAt: params.startedAt,
},
});
}
const ACP_PROXY_ENV_KEYS = [
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
] as const;
type ActiveAcpTool = {
runId: string;
sessionKey?: string;
agentId?: string;
toolCallId: string;
toolName: string;
startedAt: number;
};
export type AcpToolLifecycleTracker = {
active: Map<string, ActiveAcpTool>;
terminalToolCallIds: Set<string>;
saturated: boolean;
};
const MAX_TRACKED_ACP_TOOLS = 4_096;
export function createAcpToolLifecycleTracker(): AcpToolLifecycleTracker {
return {
active: new Map(),
terminalToolCallIds: new Set(),
saturated: false,
};
}
function acpAuditToolName(kind: unknown): string {
switch (kind) {
case "read":
case "edit":
case "delete":
case "move":
case "search":
case "execute":
case "fetch":
case "switch_mode":
case "think":
case "other":
return `acp_${kind}`;
default:
return "acp_tool";
}
}
function resolveAcpToolTerminalReason(
signal: AbortSignal | undefined,
stopReason?: string,
error?: unknown,
resultStatus?: Extract<AcpRuntimeEvent, { type: "done" }>["status"],
): "failed" | "cancelled" | "timed_out" {
const abortFields = resolveAgentRunAbortLifecycleFields(signal);
if (abortFields.aborted) {
return abortFields.stopReason === "timeout" ? "timed_out" : "cancelled";
}
const normalizedStopReason = normalizeOptionalLowercaseString(stopReason);
if (normalizedStopReason === "timeout") {
return "timed_out";
}
if (resultStatus === "cancelled") {
return "cancelled";
}
if (
error instanceof Error &&
(error as Error & { detailCode?: unknown }).detailCode === ACP_TURN_TIMEOUT_DETAIL_CODE
) {
return "timed_out";
}
if (
normalizedStopReason === "cancel" ||
normalizedStopReason === "cancelled" ||
normalizedStopReason === "manual-cancel"
) {
return "cancelled";
}
return "failed";
}
export function resolveAcpLifecycleEndFields(
signal: AbortSignal | undefined,
stopReason?: string,
resultStatus?: Extract<AcpRuntimeEvent, { type: "done" }>["status"],
) {
const abortFields = resolveAgentRunAbortLifecycleFields(signal);
if (abortFields.aborted) {
return abortFields;
}
const terminalReason = resolveAcpToolTerminalReason(
undefined,
stopReason,
undefined,
resultStatus,
);
if (terminalReason === "timed_out") {
return { aborted: true, stopReason: "timeout", status: "timed_out" } as const;
}
if (terminalReason === "cancelled") {
return { aborted: true, stopReason: "stop", status: "cancelled" } as const;
}
return {};
}
function emitAcpToolExecutionEvent(params: {
runId: string;
toolTracker: AcpToolLifecycleTracker;
sessionKey?: string;
agentId?: string;
abortSignal?: AbortSignal;
event: Extract<AcpRuntimeEvent, { type: "tool_call" }>;
}): void {
const { event } = params;
const now = Date.now();
const toolCallId = event.toolCallId?.trim() ? event.toolCallId : undefined;
const activeTool = toolCallId ? params.toolTracker.active.get(toolCallId) : undefined;
const terminalOutcome = resolveAcpToolTerminalOutcome(event.status);
const toolName = acpAuditToolName(event.kind);
// ACP runtimes may replay terminal updates. Keep the closed identity until the run ends so a
// late progress/terminal pair cannot reopen one invocation as a second durable audit action.
if (toolCallId && !activeTool) {
if (params.toolTracker.terminalToolCallIds.has(toolCallId)) {
return;
}
// Never evict an open identity: once this run reaches its bound, ignore new identities until
// lifecycle cleanup releases the complete set. Other runs own independent trackers.
const trackedIdentities =
params.toolTracker.active.size + params.toolTracker.terminalToolCallIds.size;
if (params.toolTracker.saturated || trackedIdentities >= MAX_TRACKED_ACP_TOOLS) {
params.toolTracker.saturated = true;
return;
}
}
// Without an identity, wait for a terminal event so every observed action closes immediately.
// Opening on progress would leave an unmatched audit action if the runtime omits its result.
const startsUnidentifiedTool = toolCallId === undefined && terminalOutcome !== undefined;
if (!activeTool && (toolCallId !== undefined || startsUnidentifiedTool)) {
emitTrustedDiagnosticEvent({
type: "tool.execution.started",
runId: params.runId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
...(toolCallId ? { toolCallId } : {}),
toolName,
toolSource: "core",
toolOwner: "acp",
});
if (toolCallId) {
params.toolTracker.active.set(toolCallId, {
runId: params.runId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
toolCallId,
toolName,
startedAt: now,
});
}
}
if (!terminalOutcome) {
return;
}
const terminalReason = resolveAcpToolTerminalReason(
params.abortSignal,
undefined,
undefined,
terminalOutcome === "cancelled" ? "cancelled" : undefined,
);
const durationMs = Math.max(0, now - (activeTool?.startedAt ?? now));
emitTrustedDiagnosticEvent(
terminalOutcome === "completed"
? {
type: "tool.execution.completed",
runId: params.runId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
...(toolCallId ? { toolCallId } : {}),
toolName: activeTool?.toolName ?? toolName,
toolSource: "core",
toolOwner: "acp",
durationMs,
}
: {
type: "tool.execution.error",
runId: params.runId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
...(toolCallId ? { toolCallId } : {}),
toolName: activeTool?.toolName ?? toolName,
toolSource: "core",
toolOwner: "acp",
durationMs,
errorCategory: terminalReason === "cancelled" ? "aborted" : "acp_tool",
terminalReason,
},
);
if (toolCallId) {
params.toolTracker.active.delete(toolCallId);
params.toolTracker.terminalToolCallIds.add(toolCallId);
}
}
function finalizeAcpToolsForRun(
toolTracker: AcpToolLifecycleTracker,
runId: string,
terminalReason: "failed" | "cancelled" | "timed_out",
): void {
const now = Date.now();
for (const activeTool of toolTracker.active.values()) {
emitTrustedDiagnosticEvent({
type: "tool.execution.error",
runId,
...(activeTool.sessionKey ? { sessionKey: activeTool.sessionKey } : {}),
...(activeTool.agentId ? { agentId: activeTool.agentId } : {}),
toolName: activeTool.toolName,
toolSource: "core",
toolOwner: "acp",
toolCallId: activeTool.toolCallId,
durationMs: Math.max(0, now - activeTool.startedAt),
errorCategory: terminalReason === "cancelled" ? "aborted" : "acp_tool_incomplete",
terminalReason,
});
}
toolTracker.active.clear();
toolTracker.terminalToolCallIds.clear();
toolTracker.saturated = false;
}
function resolvePresentProxyEnvKeys(env: NodeJS.ProcessEnv = process.env): string[] {
return ACP_PROXY_ENV_KEYS.filter((key) => {
const value = env[key];
return typeof value === "string" && value.trim().length > 0;
});
}
function sanitizeAcpDiagnosticText(value: string): string {
return truncateUtf16Safe(redactSensitiveText(value).replace(/\s+/g, " ").trim(), 240);
}
function acpRuntimeEventDiagnostics(event: AcpRuntimeEvent): Record<string, unknown> {
if (event.type === "status") {
return {
eventType: event.type,
text: sanitizeAcpDiagnosticText(event.text),
...(event.tag ? { tag: event.tag } : {}),
};
}
if (event.type === "tool_call") {
return {
eventType: event.type,
text: sanitizeAcpDiagnosticText(event.text),
...(event.tag ? { tag: event.tag } : {}),
...(event.status ? { status: sanitizeAcpDiagnosticText(event.status) } : {}),
...(event.title ? { title: sanitizeAcpDiagnosticText(event.title) } : {}),
...(event.toolCallId ? { toolCallId: sanitizeAcpDiagnosticText(event.toolCallId) } : {}),
};
}
if (event.type === "error") {
return {
eventType: event.type,
message: sanitizeAcpDiagnosticText(event.message),
...(event.code ? { code: sanitizeAcpDiagnosticText(event.code) } : {}),
...(typeof event.retryable === "boolean" ? { retryable: event.retryable } : {}),
};
}
if (event.type === "done") {
return {
eventType: event.type,
...(event.status ? { status: event.status } : {}),
...(event.stopReason ? { stopReason: sanitizeAcpDiagnosticText(event.stopReason) } : {}),
};
}
return {
eventType: event.type,
stream: event.stream ?? "output",
};
}
export function emitAcpPromptSubmitted(params: { runId: string; sessionKey?: string; at: number }) {
emitAgentEvent({
runId: params.runId,
stream: "acp",
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
data: {
phase: "prompt_submitted",
at: params.at,
proxyEnvKeys: resolvePresentProxyEnvKeys(),
},
});
}
export function emitAcpRuntimeEvent(params: {
runId: string;
toolTracker: AcpToolLifecycleTracker;
event: AcpRuntimeEvent;
sessionKey?: string;
agentId?: string;
abortSignal?: AbortSignal;
auditOnly?: boolean;
}) {
if (params.event.type === "tool_call") {
emitAcpToolExecutionEvent({
runId: params.runId,
toolTracker: params.toolTracker,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
...(params.abortSignal ? { abortSignal: params.abortSignal } : {}),
event: params.event,
});
}
if (!params.auditOnly) {
emitAgentEvent({
runId: params.runId,
stream: "acp",
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
data: {
phase: "runtime_event",
...acpRuntimeEventDiagnostics(params.event),
},
});
}
}
function emitAcpTerminalLifecycle(
params: {
runId: string;
sessionKey?: string;
agentId?: string;
lifecycleGeneration?: string;
auditOnly?: boolean;
completionSource?: "reply-dispatch";
},
terminal: Record<string, unknown> & { phase: "end" | "error"; endedAt: number },
) {
const data = {
...terminal,
executionSettled: true,
...(params.completionSource ? { completionSource: params.completionSource } : {}),
};
const emit = params.auditOnly ? emitAgentAuditEvent : emitAgentEvent;
emit({
runId: params.runId,
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
...(params.agentId ? { agentId: params.agentId } : {}),
...(params.lifecycleGeneration ? { lifecycleGeneration: params.lifecycleGeneration } : {}),
stream: "lifecycle",
data,
});
return buildAgentRunTerminalOutcomeFromLifecycleEvent({
phase: terminal.phase,
data,
endedAt: terminal.endedAt,
});
}
export function emitAcpLifecycleEnd(params: {
runId: string;
toolTracker: AcpToolLifecycleTracker;
sessionKey?: string;
agentId?: string;
lifecycleGeneration?: string;
endFields: ReturnType<typeof resolveAcpLifecycleEndFields>;
terminalReply?: AgentRunTerminalReplySnapshot;
auditOnly?: boolean;
completionSource?: "reply-dispatch";
}) {
finalizeAcpToolsForRun(
params.toolTracker,
params.runId,
params.endFields.stopReason === "timeout"
? "timed_out"
: params.endFields.aborted
? "cancelled"
: "failed",
);
return emitAcpTerminalLifecycle(params, {
phase: "end",
endedAt: Date.now(),
...params.endFields,
...(params.terminalReply ? { terminalReply: params.terminalReply } : {}),
});
}
export function emitAcpLifecycleError(params: {
runId: string;
toolTracker: AcpToolLifecycleTracker;
error: unknown;
sessionKey?: string;
agentId?: string;
lifecycleGeneration?: string;
abortSignal?: AbortSignal;
terminalOutcome?: "blocked";
auditOnly?: boolean;
completionSource?: "reply-dispatch";
}) {
const terminalReason = resolveAcpToolTerminalReason(params.abortSignal, undefined, params.error);
finalizeAcpToolsForRun(params.toolTracker, params.runId, terminalReason);
const lifecycleFields =
params.terminalOutcome === "blocked"
? ({ livenessState: "blocked" } as const)
: terminalReason === "timed_out"
? ({ aborted: true, stopReason: "timeout", status: "timed_out" } as const)
: resolveAgentRunAbortLifecycleFields(params.abortSignal);
return emitAcpTerminalLifecycle(params, {
phase: "error",
...(!params.auditOnly ? { error: formatAcpErrorChain(params.error) } : {}),
endedAt: Date.now(),
...lifecycleFields,
});
}
export function emitAcpAssistantDelta(params: { runId: string; text: string; delta: string }) {
emitAgentEvent({
runId: params.runId,
stream: "assistant",
data: {
text: params.text,
delta: params.delta,
},
});
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */