mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
refactor: route agent end side effects through harness
This commit is contained in:
@@ -15,6 +15,10 @@ import { claudeCliSessionTranscriptHasContent as claudeCliSessionTranscriptHasCo
|
||||
import { classifyFailoverReason, isFailoverErrorMessage } from "./embedded-agent-helpers.js";
|
||||
import type { EmbeddedAgentRunResult } from "./embedded-agent-runner.js";
|
||||
import { FailoverError, isFailoverError, resolveFailoverStatus } from "./failover-error.js";
|
||||
import {
|
||||
awaitAgentEndSideEffects,
|
||||
runAgentEndSideEffects,
|
||||
} from "./harness/agent-end-side-effects.js";
|
||||
import {
|
||||
bootstrapHarnessContextEngine,
|
||||
finalizeHarnessContextEngineTurn,
|
||||
@@ -23,8 +27,6 @@ import {
|
||||
import { buildAgentHookContext } from "./harness/hook-context.js";
|
||||
import { buildAgentHookConversationMessages } from "./harness/hook-history.js";
|
||||
import {
|
||||
awaitAgentHarnessAgentEndHook,
|
||||
runAgentHarnessAgentEndHook,
|
||||
runAgentHarnessLlmInputHook,
|
||||
runAgentHarnessLlmOutputHook,
|
||||
} from "./harness/lifecycle-hook-helpers.js";
|
||||
@@ -149,7 +151,7 @@ function buildCliContextEngineAssistantMessage(params: {
|
||||
return buildCliHookAssistantMessage(params) as AgentMessage;
|
||||
}
|
||||
|
||||
type CliAgentEndHookParams = Parameters<typeof runAgentHarnessAgentEndHook>[0];
|
||||
type CliAgentEndHookParams = Parameters<typeof runAgentEndSideEffects>[0];
|
||||
|
||||
function shouldAwaitCliAgentEndHook(params: RunCliAgentParams): boolean {
|
||||
return !params.messageChannel && !params.messageProvider;
|
||||
@@ -160,10 +162,10 @@ async function runCliAgentEndHook(
|
||||
hookParams: CliAgentEndHookParams,
|
||||
): Promise<void> {
|
||||
if (shouldAwaitCliAgentEndHook(params)) {
|
||||
await awaitAgentHarnessAgentEndHook(hookParams);
|
||||
await awaitAgentEndSideEffects(hookParams);
|
||||
return;
|
||||
}
|
||||
runAgentHarnessAgentEndHook(hookParams);
|
||||
runAgentEndSideEffects(hookParams);
|
||||
}
|
||||
|
||||
async function persistApprovedCliUserTurnTranscript(params: RunCliAgentParams): Promise<void> {
|
||||
@@ -358,6 +360,7 @@ export async function runPreparedCliAgent(
|
||||
sessionId: params.sessionId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
trigger: params.trigger,
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
...(context.contextWindowInfo?.tokens
|
||||
? { contextTokenBudget: context.contextWindowInfo.tokens }
|
||||
: {}),
|
||||
|
||||
@@ -146,6 +146,7 @@ import {
|
||||
import { countActiveToolExecutions } from "../../embedded-agent-subscribe.handlers.tools.js";
|
||||
import { subscribeEmbeddedAgentSession } from "../../embedded-agent-subscribe.js";
|
||||
import { isTimeoutError } from "../../failover-error.js";
|
||||
import { runAgentEndSideEffects } from "../../harness/agent-end-side-effects.js";
|
||||
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
|
||||
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
|
||||
import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "../../local-model-lean.js";
|
||||
@@ -4393,33 +4394,26 @@ export async function runEmbeddedAttempt(
|
||||
});
|
||||
anthropicPayloadLogger?.recordUsage(messagesSnapshot, promptError);
|
||||
|
||||
// Run agent_end hooks to allow plugins to analyze the conversation
|
||||
// This is fire-and-forget, so we don't await
|
||||
// Run even on compaction timeout so plugins can log/cleanup
|
||||
if (hookRunner?.hasHooks("agent_end")) {
|
||||
hookRunner
|
||||
.runAgentEnd(
|
||||
{
|
||||
messages: messagesSnapshot,
|
||||
success: !aborted && !promptError,
|
||||
error: promptError ? formatErrorMessage(promptError) : undefined,
|
||||
durationMs: Date.now() - promptStartedAt,
|
||||
},
|
||||
{
|
||||
runId: params.runId,
|
||||
trace: freezeDiagnosticTraceContext(diagnosticTrace),
|
||||
agentId: hookAgentId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
trigger: params.trigger,
|
||||
...buildAgentHookContextChannelFields(params),
|
||||
},
|
||||
)
|
||||
.catch((err) => {
|
||||
log.warn(`agent_end hook failed: ${err}`);
|
||||
});
|
||||
}
|
||||
runAgentEndSideEffects({
|
||||
event: {
|
||||
messages: messagesSnapshot,
|
||||
success: !aborted && !promptError,
|
||||
error: promptError ? formatErrorMessage(promptError) : undefined,
|
||||
durationMs: Date.now() - promptStartedAt,
|
||||
},
|
||||
ctx: {
|
||||
runId: params.runId,
|
||||
trace: freezeDiagnosticTraceContext(diagnosticTrace),
|
||||
agentId: hookAgentId,
|
||||
sessionKey: params.sessionKey,
|
||||
sessionId: params.sessionId,
|
||||
workspaceDir: params.workspaceDir,
|
||||
trigger: params.trigger,
|
||||
...(params.config ? { config: params.config } : {}),
|
||||
...buildAgentHookContextChannelFields(params),
|
||||
},
|
||||
hookRunner,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(abortTimer);
|
||||
if (abortWarnTimer) {
|
||||
|
||||
116
src/agents/harness/agent-end-side-effects.test.ts
Normal file
116
src/agents/harness/agent-end-side-effects.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { runSkillWorkshopAutoCapture } from "../../skills/workshop/autocapture.js";
|
||||
import { awaitAgentEndSideEffects, runAgentEndSideEffects } from "./agent-end-side-effects.js";
|
||||
import {
|
||||
awaitAgentHarnessAgentEndHook,
|
||||
runAgentHarnessAgentEndHook,
|
||||
} from "./lifecycle-hook-helpers.js";
|
||||
|
||||
vi.mock("../../skills/workshop/autocapture.js", () => ({
|
||||
runSkillWorkshopAutoCapture: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./lifecycle-hook-helpers.js", () => ({
|
||||
awaitAgentHarnessAgentEndHook: vi.fn(),
|
||||
runAgentHarnessAgentEndHook: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockAutoCapture = vi.mocked(runSkillWorkshopAutoCapture);
|
||||
const mockAwaitAgentEndHook = vi.mocked(awaitAgentHarnessAgentEndHook);
|
||||
const mockRunAgentEndHook = vi.mocked(runAgentHarnessAgentEndHook);
|
||||
|
||||
describe("agent end side effects", () => {
|
||||
beforeEach(() => {
|
||||
mockAutoCapture.mockReset();
|
||||
mockAwaitAgentEndHook.mockReset();
|
||||
mockRunAgentEndHook.mockReset();
|
||||
});
|
||||
|
||||
it("fires plugin agent_end hooks without waiting for Skill Workshop auto-capture", async () => {
|
||||
let resolveCapture: (() => void) | undefined;
|
||||
mockAutoCapture.mockReturnValueOnce(
|
||||
new Promise<void>((resolve) => {
|
||||
resolveCapture = resolve;
|
||||
}),
|
||||
);
|
||||
|
||||
runAgentEndSideEffects({
|
||||
event: {
|
||||
messages: [],
|
||||
success: true,
|
||||
},
|
||||
ctx: {
|
||||
runId: "run-1",
|
||||
workspaceDir: "/workspace",
|
||||
config: {
|
||||
skills: {
|
||||
workshop: {
|
||||
autonomous: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockRunAgentEndHook).toHaveBeenCalledTimes(1);
|
||||
expect(mockAutoCapture).toHaveBeenCalledWith({
|
||||
event: {
|
||||
messages: [],
|
||||
success: true,
|
||||
},
|
||||
ctx: {
|
||||
runId: "run-1",
|
||||
workspaceDir: "/workspace",
|
||||
config: {
|
||||
skills: {
|
||||
workshop: {
|
||||
autonomous: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
config: {
|
||||
skills: {
|
||||
workshop: {
|
||||
autonomous: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
resolveCapture?.();
|
||||
});
|
||||
|
||||
it("still runs agent_end hooks when Skill Workshop auto-capture fails", async () => {
|
||||
mockAutoCapture.mockRejectedValueOnce(new Error("capture failed"));
|
||||
|
||||
await awaitAgentEndSideEffects({
|
||||
event: {
|
||||
messages: [],
|
||||
success: true,
|
||||
},
|
||||
ctx: {
|
||||
runId: "run-1",
|
||||
workspaceDir: "/workspace",
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockAutoCapture).toHaveBeenCalledWith({
|
||||
event: {
|
||||
messages: [],
|
||||
success: true,
|
||||
},
|
||||
ctx: {
|
||||
runId: "run-1",
|
||||
workspaceDir: "/workspace",
|
||||
},
|
||||
});
|
||||
expect(mockAwaitAgentEndHook).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
32
src/agents/harness/agent-end-side-effects.ts
Normal file
32
src/agents/harness/agent-end-side-effects.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { runSkillWorkshopAutoCapture } from "../../skills/workshop/autocapture.js";
|
||||
import {
|
||||
awaitAgentHarnessAgentEndHook,
|
||||
runAgentHarnessAgentEndHook,
|
||||
} from "./lifecycle-hook-helpers.js";
|
||||
|
||||
const log = createSubsystemLogger("agents/harness");
|
||||
|
||||
type AgentEndSideEffectsParams = Parameters<typeof runAgentHarnessAgentEndHook>[0];
|
||||
|
||||
async function runCoreAgentEndSideEffects(params: AgentEndSideEffectsParams): Promise<void> {
|
||||
try {
|
||||
await runSkillWorkshopAutoCapture({
|
||||
event: params.event,
|
||||
ctx: params.ctx,
|
||||
...(params.ctx.config ? { config: params.ctx.config } : {}),
|
||||
});
|
||||
} catch (error) {
|
||||
log.warn(`skill workshop auto-capture failed: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function runAgentEndSideEffects(params: AgentEndSideEffectsParams): void {
|
||||
void runCoreAgentEndSideEffects(params);
|
||||
runAgentHarnessAgentEndHook(params);
|
||||
}
|
||||
|
||||
export async function awaitAgentEndSideEffects(params: AgentEndSideEffectsParams): Promise<void> {
|
||||
await runCoreAgentEndSideEffects(params);
|
||||
await awaitAgentHarnessAgentEndHook(params);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import type {
|
||||
PluginHookAgentContext,
|
||||
PluginHookContextWindowSource,
|
||||
@@ -18,6 +19,7 @@ export type AgentHarnessHookContext = {
|
||||
contextTokenBudget?: number;
|
||||
contextWindowSource?: PluginHookContextWindowSource;
|
||||
contextWindowReferenceTokens?: number;
|
||||
config?: OpenClawConfig;
|
||||
};
|
||||
|
||||
export function buildAgentHookContext(params: AgentHarnessHookContext): PluginHookAgentContext {
|
||||
|
||||
Reference in New Issue
Block a user