mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:40:44 +00:00
perf(agents): keep attempt execution runtime cold
This commit is contained in:
@@ -4,6 +4,7 @@ import { LiveSessionModelSwitchError } from "./live-model-switch.js";
|
||||
const state = vi.hoisted(() => ({
|
||||
runWithModelFallbackMock: vi.fn(),
|
||||
runAgentAttemptMock: vi.fn(),
|
||||
resolveEffectiveModelFallbacksMock: vi.fn().mockReturnValue(undefined),
|
||||
emitAgentEventMock: vi.fn(),
|
||||
registerAgentRunContextMock: vi.fn(),
|
||||
clearAgentRunContextMock: vi.fn(),
|
||||
@@ -15,7 +16,7 @@ vi.mock("./model-fallback.js", () => ({
|
||||
runWithModelFallback: (params: unknown) => state.runWithModelFallbackMock(params),
|
||||
}));
|
||||
|
||||
vi.mock("./command/attempt-execution.js", () => ({
|
||||
vi.mock("./command/attempt-execution.runtime.js", () => ({
|
||||
buildAcpResult: vi.fn(),
|
||||
createAcpVisibleTextAccumulator: vi.fn(),
|
||||
emitAcpAssistantDelta: vi.fn(),
|
||||
@@ -107,7 +108,7 @@ vi.mock("../cli/deps.js", () => ({
|
||||
createDefaultDeps: () => ({}),
|
||||
}));
|
||||
|
||||
vi.mock("../config/config.js", () => ({
|
||||
vi.mock("../config/io.js", () => ({
|
||||
loadConfig: () => ({
|
||||
agents: {
|
||||
defaults: {
|
||||
@@ -122,6 +123,9 @@ vi.mock("../config/config.js", () => ({
|
||||
readConfigFileSnapshotForWrite: async () => ({
|
||||
snapshot: { valid: false },
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../config/runtime-snapshot.js", () => ({
|
||||
setRuntimeConfigSnapshot: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -136,7 +140,7 @@ vi.mock("../config/sessions.js", () => ({
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("../config/sessions/transcript.js", () => ({
|
||||
vi.mock("../config/sessions/transcript-resolve.runtime.js", () => ({
|
||||
resolveSessionTranscriptFile: async () => ({
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
sessionEntry: { sessionId: "session-1", updatedAt: Date.now() },
|
||||
@@ -146,6 +150,7 @@ vi.mock("../config/sessions/transcript.js", () => ({
|
||||
vi.mock("../infra/agent-events.js", () => ({
|
||||
clearAgentRunContext: (...args: unknown[]) => state.clearAgentRunContextMock(...args),
|
||||
emitAgentEvent: (...args: unknown[]) => state.emitAgentEventMock(...args),
|
||||
onAgentEvent: vi.fn(),
|
||||
registerAgentRunContext: (...args: unknown[]) => state.registerAgentRunContextMock(...args),
|
||||
}));
|
||||
|
||||
@@ -158,12 +163,18 @@ vi.mock("../infra/skills-remote.js", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../logging/subsystem.js", () => ({
|
||||
createSubsystemLogger: () => ({
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
}),
|
||||
createSubsystemLogger: () => {
|
||||
const logger = {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
trace: vi.fn(),
|
||||
raw: vi.fn(),
|
||||
child: vi.fn(() => logger),
|
||||
};
|
||||
return logger;
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("../routing/session-key.js", () => ({
|
||||
@@ -198,12 +209,11 @@ vi.mock("../utils/message-channel.js", () => ({
|
||||
resolveMessageChannel: () => "test",
|
||||
}));
|
||||
|
||||
const resolveEffectiveModelFallbacksMock = vi.fn().mockReturnValue(undefined);
|
||||
vi.mock("./agent-scope.js", () => ({
|
||||
listAgentIds: () => ["default"],
|
||||
resolveAgentConfig: () => undefined,
|
||||
resolveAgentDir: () => "/tmp/agent",
|
||||
resolveEffectiveModelFallbacks: resolveEffectiveModelFallbacksMock,
|
||||
resolveEffectiveModelFallbacks: state.resolveEffectiveModelFallbacksMock,
|
||||
resolveSessionAgentId: () => "default",
|
||||
resolveAgentSkillsFilter: () => undefined,
|
||||
resolveAgentWorkspaceDir: () => "/tmp/workspace",
|
||||
@@ -468,7 +478,7 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
});
|
||||
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "gpt-5.4"));
|
||||
|
||||
resolveEffectiveModelFallbacksMock.mockClear();
|
||||
state.resolveEffectiveModelFallbacksMock.mockClear();
|
||||
|
||||
const agentCommand = await getAgentCommand();
|
||||
await agentCommand({
|
||||
@@ -477,11 +487,11 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expect(resolveEffectiveModelFallbacksMock).toHaveBeenCalledTimes(2);
|
||||
expect(resolveEffectiveModelFallbacksMock.mock.calls[0][0]).toMatchObject({
|
||||
expect(state.resolveEffectiveModelFallbacksMock).toHaveBeenCalledTimes(2);
|
||||
expect(state.resolveEffectiveModelFallbacksMock.mock.calls[0][0]).toMatchObject({
|
||||
hasSessionModelOverride: false,
|
||||
});
|
||||
expect(resolveEffectiveModelFallbacksMock.mock.calls[1][0]).toMatchObject({
|
||||
expect(state.resolveEffectiveModelFallbacksMock.mock.calls[1][0]).toMatchObject({
|
||||
hasSessionModelOverride: true,
|
||||
});
|
||||
});
|
||||
@@ -508,7 +518,7 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
});
|
||||
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("anthropic", "claude"));
|
||||
|
||||
resolveEffectiveModelFallbacksMock.mockClear();
|
||||
state.resolveEffectiveModelFallbacksMock.mockClear();
|
||||
|
||||
const agentCommand = await getAgentCommand();
|
||||
await agentCommand({
|
||||
@@ -517,11 +527,11 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expect(resolveEffectiveModelFallbacksMock).toHaveBeenCalledTimes(2);
|
||||
expect(resolveEffectiveModelFallbacksMock.mock.calls[0][0]).toMatchObject({
|
||||
expect(state.resolveEffectiveModelFallbacksMock).toHaveBeenCalledTimes(2);
|
||||
expect(state.resolveEffectiveModelFallbacksMock.mock.calls[0][0]).toMatchObject({
|
||||
hasSessionModelOverride: false,
|
||||
});
|
||||
expect(resolveEffectiveModelFallbacksMock.mock.calls[1][0]).toMatchObject({
|
||||
expect(state.resolveEffectiveModelFallbacksMock.mock.calls[1][0]).toMatchObject({
|
||||
hasSessionModelOverride: false,
|
||||
});
|
||||
});
|
||||
@@ -546,7 +556,7 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
});
|
||||
state.runAgentAttemptMock.mockResolvedValue(makeSuccessResult("openai", "claude"));
|
||||
|
||||
resolveEffectiveModelFallbacksMock.mockClear();
|
||||
state.resolveEffectiveModelFallbacksMock.mockClear();
|
||||
|
||||
const agentCommand = await getAgentCommand();
|
||||
await agentCommand({
|
||||
@@ -555,11 +565,11 @@ describe("agentCommand – LiveSessionModelSwitchError retry", () => {
|
||||
senderIsOwner: true,
|
||||
});
|
||||
|
||||
expect(resolveEffectiveModelFallbacksMock).toHaveBeenCalledTimes(2);
|
||||
expect(resolveEffectiveModelFallbacksMock.mock.calls[0][0]).toMatchObject({
|
||||
expect(state.resolveEffectiveModelFallbacksMock).toHaveBeenCalledTimes(2);
|
||||
expect(state.resolveEffectiveModelFallbacksMock.mock.calls[0][0]).toMatchObject({
|
||||
hasSessionModelOverride: false,
|
||||
});
|
||||
expect(resolveEffectiveModelFallbacksMock.mock.calls[1][0]).toMatchObject({
|
||||
expect(state.resolveEffectiveModelFallbacksMock.mock.calls[1][0]).toMatchObject({
|
||||
hasSessionModelOverride: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@ import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-tar
|
||||
import { type CliDeps, createDefaultDeps } from "../cli/deps.js";
|
||||
import { loadConfig, readConfigFileSnapshotForWrite } from "../config/io.js";
|
||||
import { setRuntimeConfigSnapshot } from "../config/runtime-snapshot.js";
|
||||
import { resolveSessionTranscriptFile } from "../config/sessions/transcript.js";
|
||||
import type { SessionEntry } from "../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { isSecretRef } from "../config/types.secrets.js";
|
||||
@@ -48,18 +47,9 @@ import {
|
||||
import { ensureAuthProfileStore } from "./auth-profiles.js";
|
||||
import { clearSessionAuthProfileOverride } from "./auth-profiles/session-override.js";
|
||||
import {
|
||||
buildAcpResult,
|
||||
createAcpVisibleTextAccumulator,
|
||||
emitAcpAssistantDelta,
|
||||
emitAcpLifecycleEnd,
|
||||
emitAcpLifecycleError,
|
||||
emitAcpLifecycleStart,
|
||||
persistAcpTurnTranscript,
|
||||
persistSessionEntry as persistSessionEntryBase,
|
||||
prependInternalEventContext,
|
||||
runAgentAttempt,
|
||||
sessionFileHasContent,
|
||||
} from "./command/attempt-execution.js";
|
||||
} from "./command/attempt-execution.shared.js";
|
||||
import { deliverAgentCommandResult } from "./command/delivery.js";
|
||||
import { resolveAgentRunContext } from "./command/run-context.js";
|
||||
import { updateSessionStoreAfterAgentRun } from "./command/session-store.js";
|
||||
@@ -88,6 +78,21 @@ import { resolveAgentTimeoutMs } from "./timeout.js";
|
||||
import { ensureAgentWorkspace } from "./workspace.js";
|
||||
|
||||
const log = createSubsystemLogger("agents/agent-command");
|
||||
type AttemptExecutionRuntime = typeof import("./command/attempt-execution.runtime.js");
|
||||
type TranscriptResolveRuntime = typeof import("../config/sessions/transcript-resolve.runtime.js");
|
||||
|
||||
let attemptExecutionRuntimePromise: Promise<AttemptExecutionRuntime> | undefined;
|
||||
let transcriptResolveRuntimePromise: Promise<TranscriptResolveRuntime> | undefined;
|
||||
|
||||
function loadAttemptExecutionRuntime(): Promise<AttemptExecutionRuntime> {
|
||||
attemptExecutionRuntimePromise ??= import("./command/attempt-execution.runtime.js");
|
||||
return attemptExecutionRuntimePromise;
|
||||
}
|
||||
|
||||
function loadTranscriptResolveRuntime(): Promise<TranscriptResolveRuntime> {
|
||||
transcriptResolveRuntimePromise ??= import("../config/sessions/transcript-resolve.runtime.js");
|
||||
return transcriptResolveRuntimePromise;
|
||||
}
|
||||
|
||||
type PersistSessionEntryParams = {
|
||||
sessionStore: Record<string, SessionEntry>;
|
||||
@@ -455,13 +460,14 @@ async function agentCommandInternal(
|
||||
}
|
||||
|
||||
if (acpResolution?.kind === "ready" && sessionKey) {
|
||||
const attemptExecutionRuntime = await loadAttemptExecutionRuntime();
|
||||
const startedAt = Date.now();
|
||||
registerAgentRunContext(runId, {
|
||||
sessionKey,
|
||||
});
|
||||
emitAcpLifecycleStart({ runId, startedAt });
|
||||
attemptExecutionRuntime.emitAcpLifecycleStart({ runId, startedAt });
|
||||
|
||||
const visibleTextAccumulator = createAcpVisibleTextAccumulator();
|
||||
const visibleTextAccumulator = attemptExecutionRuntime.createAcpVisibleTextAccumulator();
|
||||
let stopReason: string | undefined;
|
||||
try {
|
||||
const dispatchPolicyError = resolveAcpDispatchPolicyError(cfg);
|
||||
@@ -501,7 +507,7 @@ async function agentCommandInternal(
|
||||
if (!visibleUpdate) {
|
||||
return;
|
||||
}
|
||||
emitAcpAssistantDelta({
|
||||
attemptExecutionRuntime.emitAcpAssistantDelta({
|
||||
runId,
|
||||
text: visibleUpdate.text,
|
||||
delta: visibleUpdate.delta,
|
||||
@@ -514,19 +520,19 @@ async function agentCommandInternal(
|
||||
fallbackCode: "ACP_TURN_FAILED",
|
||||
fallbackMessage: "ACP turn failed before completion.",
|
||||
});
|
||||
emitAcpLifecycleError({
|
||||
attemptExecutionRuntime.emitAcpLifecycleError({
|
||||
runId,
|
||||
message: acpError.message,
|
||||
});
|
||||
throw acpError;
|
||||
}
|
||||
|
||||
emitAcpLifecycleEnd({ runId });
|
||||
attemptExecutionRuntime.emitAcpLifecycleEnd({ runId });
|
||||
|
||||
const finalTextRaw = visibleTextAccumulator.finalizeRaw();
|
||||
const finalText = visibleTextAccumulator.finalize();
|
||||
try {
|
||||
sessionEntry = await persistAcpTurnTranscript({
|
||||
sessionEntry = await attemptExecutionRuntime.persistAcpTurnTranscript({
|
||||
body,
|
||||
finalText: finalTextRaw,
|
||||
sessionId,
|
||||
@@ -544,7 +550,7 @@ async function agentCommandInternal(
|
||||
);
|
||||
}
|
||||
|
||||
const result = buildAcpResult({
|
||||
const result = attemptExecutionRuntime.buildAcpResult({
|
||||
payloadText: finalText,
|
||||
startedAt,
|
||||
stopReason,
|
||||
@@ -792,6 +798,7 @@ async function agentCommandInternal(
|
||||
});
|
||||
}
|
||||
}
|
||||
const { resolveSessionTranscriptFile } = await loadTranscriptResolveRuntime();
|
||||
let sessionFile: string | undefined;
|
||||
if (sessionStore && sessionKey) {
|
||||
const resolvedSessionFile = await resolveSessionTranscriptFile({
|
||||
@@ -821,8 +828,9 @@ async function agentCommandInternal(
|
||||
|
||||
const startedAt = Date.now();
|
||||
let lifecycleEnded = false;
|
||||
const attemptExecutionRuntime = await loadAttemptExecutionRuntime();
|
||||
|
||||
let result: Awaited<ReturnType<typeof runAgentAttempt>>;
|
||||
let result: Awaited<ReturnType<AttemptExecutionRuntime["runAgentAttempt"]>>;
|
||||
let fallbackProvider = provider;
|
||||
let fallbackModel = model;
|
||||
const MAX_LIVE_SWITCH_RETRIES = 5;
|
||||
@@ -852,7 +860,7 @@ async function agentCommandInternal(
|
||||
run: async (providerOverride, modelOverride, runOptions) => {
|
||||
const isFallbackRetry = fallbackAttemptIndex > 0;
|
||||
fallbackAttemptIndex += 1;
|
||||
return runAgentAttempt({
|
||||
return attemptExecutionRuntime.runAgentAttempt({
|
||||
providerOverride,
|
||||
modelOverride,
|
||||
cfg,
|
||||
@@ -878,7 +886,8 @@ async function agentCommandInternal(
|
||||
sessionStore,
|
||||
storePath,
|
||||
allowTransientCooldownProbe: runOptions?.allowTransientCooldownProbe,
|
||||
sessionHasHistory: !isNewSession || (await sessionFileHasContent(sessionFile)),
|
||||
sessionHasHistory:
|
||||
!isNewSession || (await attemptExecutionRuntime.sessionFileHasContent(sessionFile)),
|
||||
onAgentEvent: (evt) => {
|
||||
if (
|
||||
evt.stream === "lifecycle" &&
|
||||
|
||||
11
src/agents/command/attempt-execution.runtime.ts
Normal file
11
src/agents/command/attempt-execution.runtime.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export {
|
||||
buildAcpResult,
|
||||
createAcpVisibleTextAccumulator,
|
||||
emitAcpAssistantDelta,
|
||||
emitAcpLifecycleEnd,
|
||||
emitAcpLifecycleError,
|
||||
emitAcpLifecycleStart,
|
||||
persistAcpTurnTranscript,
|
||||
runAgentAttempt,
|
||||
sessionFileHasContent,
|
||||
} from "./attempt-execution.js";
|
||||
41
src/agents/command/attempt-execution.shared.ts
Normal file
41
src/agents/command/attempt-execution.shared.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { updateSessionStore } from "../../config/sessions/store.js";
|
||||
import { mergeSessionEntry, type SessionEntry } from "../../config/sessions/types.js";
|
||||
import { formatAgentInternalEventsForPrompt } from "../internal-events.js";
|
||||
import { hasInternalRuntimeContext } from "../internal-runtime-context.js";
|
||||
import type { AgentCommandOpts } from "./types.js";
|
||||
|
||||
export type PersistSessionEntryParams = {
|
||||
sessionStore: Record<string, SessionEntry>;
|
||||
sessionKey: string;
|
||||
storePath: string;
|
||||
entry: SessionEntry;
|
||||
clearedFields?: string[];
|
||||
};
|
||||
|
||||
export async function persistSessionEntry(params: PersistSessionEntryParams): Promise<void> {
|
||||
const persisted = await updateSessionStore(params.storePath, (store) => {
|
||||
const merged = mergeSessionEntry(store[params.sessionKey], params.entry);
|
||||
for (const field of params.clearedFields ?? []) {
|
||||
if (!Object.hasOwn(params.entry, field)) {
|
||||
Reflect.deleteProperty(merged, field);
|
||||
}
|
||||
}
|
||||
store[params.sessionKey] = merged;
|
||||
return merged;
|
||||
});
|
||||
params.sessionStore[params.sessionKey] = persisted;
|
||||
}
|
||||
|
||||
export function prependInternalEventContext(
|
||||
body: string,
|
||||
events: AgentCommandOpts["internalEvents"],
|
||||
): string {
|
||||
if (hasInternalRuntimeContext(body)) {
|
||||
return body;
|
||||
}
|
||||
const renderedEvents = formatAgentInternalEventsForPrompt(events);
|
||||
if (!renderedEvents) {
|
||||
return body;
|
||||
}
|
||||
return [renderedEvents, body].filter(Boolean).join("\n\n");
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import fs from "node:fs/promises";
|
||||
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import { normalizeReplyPayload } from "../../auto-reply/reply/normalize-reply.js";
|
||||
import type { ThinkLevel, VerboseLevel } from "../../auto-reply/thinking.js";
|
||||
import { mergeSessionEntry, type SessionEntry, updateSessionStore } from "../../config/sessions.js";
|
||||
import { resolveSessionTranscriptFile } from "../../config/sessions/transcript.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { emitAgentEvent } from "../../infra/agent-events.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
@@ -14,13 +14,12 @@ import { resolveBootstrapWarningSignaturesSeen } from "../bootstrap-budget.js";
|
||||
import { runCliAgent } from "../cli-runner.js";
|
||||
import { clearCliSession, getCliSessionBinding, setCliSessionBinding } from "../cli-session.js";
|
||||
import { FailoverError } from "../failover-error.js";
|
||||
import { formatAgentInternalEventsForPrompt } from "../internal-events.js";
|
||||
import { hasInternalRuntimeContext } from "../internal-runtime-context.js";
|
||||
import { isCliProvider } from "../model-selection.js";
|
||||
import { prepareSessionManagerForRun } from "../pi-embedded-runner/session-manager-init.js";
|
||||
import { runEmbeddedPiAgent } from "../pi-embedded.js";
|
||||
import { buildWorkspaceSkillSnapshot } from "../skills.js";
|
||||
import { resolveFallbackRetryPrompt } from "./attempt-execution.helpers.js";
|
||||
import { persistSessionEntry } from "./attempt-execution.shared.js";
|
||||
import { resolveAgentRunContext } from "./run-context.js";
|
||||
import type { AgentCommandOpts } from "./types.js";
|
||||
|
||||
@@ -32,42 +31,6 @@ export {
|
||||
|
||||
const log = createSubsystemLogger("agents/agent-command");
|
||||
|
||||
export type PersistSessionEntryParams = {
|
||||
sessionStore: Record<string, SessionEntry>;
|
||||
sessionKey: string;
|
||||
storePath: string;
|
||||
entry: SessionEntry;
|
||||
clearedFields?: string[];
|
||||
};
|
||||
|
||||
export async function persistSessionEntry(params: PersistSessionEntryParams): Promise<void> {
|
||||
const persisted = await updateSessionStore(params.storePath, (store) => {
|
||||
const merged = mergeSessionEntry(store[params.sessionKey], params.entry);
|
||||
for (const field of params.clearedFields ?? []) {
|
||||
if (!Object.hasOwn(params.entry, field)) {
|
||||
Reflect.deleteProperty(merged, field);
|
||||
}
|
||||
}
|
||||
store[params.sessionKey] = merged;
|
||||
return merged;
|
||||
});
|
||||
params.sessionStore[params.sessionKey] = persisted;
|
||||
}
|
||||
|
||||
export function prependInternalEventContext(
|
||||
body: string,
|
||||
events: AgentCommandOpts["internalEvents"],
|
||||
): string {
|
||||
if (hasInternalRuntimeContext(body)) {
|
||||
return body;
|
||||
}
|
||||
const renderedEvents = formatAgentInternalEventsForPrompt(events);
|
||||
if (!renderedEvents) {
|
||||
return body;
|
||||
}
|
||||
return [renderedEvents, body].filter(Boolean).join("\n\n");
|
||||
}
|
||||
|
||||
const ACP_TRANSCRIPT_USAGE = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
|
||||
1
src/config/sessions/transcript-resolve.runtime.ts
Normal file
1
src/config/sessions/transcript-resolve.runtime.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { resolveSessionTranscriptFile } from "./transcript.js";
|
||||
Reference in New Issue
Block a user