Skip to content

Commit c5af09e

Browse files
committed
refactor: route agent end side effects through harness
1 parent 3037646 commit c5af09e

5 files changed

Lines changed: 179 additions & 32 deletions

File tree

src/agents/cli-runner.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ import { claudeCliSessionTranscriptHasContent as claudeCliSessionTranscriptHasCo
1515
import { classifyFailoverReason, isFailoverErrorMessage } from "./embedded-agent-helpers.js";
1616
import type { EmbeddedAgentRunResult } from "./embedded-agent-runner.js";
1717
import { FailoverError, isFailoverError, resolveFailoverStatus } from "./failover-error.js";
18+
import {
19+
awaitAgentEndSideEffects,
20+
runAgentEndSideEffects,
21+
} from "./harness/agent-end-side-effects.js";
1822
import {
1923
bootstrapHarnessContextEngine,
2024
finalizeHarnessContextEngineTurn,
@@ -23,8 +27,6 @@ import {
2327
import { buildAgentHookContext } from "./harness/hook-context.js";
2428
import { buildAgentHookConversationMessages } from "./harness/hook-history.js";
2529
import {
26-
awaitAgentHarnessAgentEndHook,
27-
runAgentHarnessAgentEndHook,
2830
runAgentHarnessLlmInputHook,
2931
runAgentHarnessLlmOutputHook,
3032
} from "./harness/lifecycle-hook-helpers.js";
@@ -149,7 +151,7 @@ function buildCliContextEngineAssistantMessage(params: {
149151
return buildCliHookAssistantMessage(params) as AgentMessage;
150152
}
151153

152-
type CliAgentEndHookParams = Parameters<typeof runAgentHarnessAgentEndHook>[0];
154+
type CliAgentEndHookParams = Parameters<typeof runAgentEndSideEffects>[0];
153155

154156
function shouldAwaitCliAgentEndHook(params: RunCliAgentParams): boolean {
155157
return !params.messageChannel && !params.messageProvider;
@@ -160,10 +162,10 @@ async function runCliAgentEndHook(
160162
hookParams: CliAgentEndHookParams,
161163
): Promise<void> {
162164
if (shouldAwaitCliAgentEndHook(params)) {
163-
await awaitAgentHarnessAgentEndHook(hookParams);
165+
await awaitAgentEndSideEffects(hookParams);
164166
return;
165167
}
166-
runAgentHarnessAgentEndHook(hookParams);
168+
runAgentEndSideEffects(hookParams);
167169
}
168170

169171
async function persistApprovedCliUserTurnTranscript(params: RunCliAgentParams): Promise<void> {
@@ -358,6 +360,7 @@ export async function runPreparedCliAgent(
358360
sessionId: params.sessionId,
359361
workspaceDir: params.workspaceDir,
360362
trigger: params.trigger,
363+
...(params.config ? { config: params.config } : {}),
361364
...(context.contextWindowInfo?.tokens
362365
? { contextTokenBudget: context.contextWindowInfo.tokens }
363366
: {}),

src/agents/embedded-agent-runner/run/attempt.ts

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ import {
146146
import { countActiveToolExecutions } from "../../embedded-agent-subscribe.handlers.tools.js";
147147
import { subscribeEmbeddedAgentSession } from "../../embedded-agent-subscribe.js";
148148
import { isTimeoutError } from "../../failover-error.js";
149+
import { runAgentEndSideEffects } from "../../harness/agent-end-side-effects.js";
149150
import { resolveHeartbeatPromptForSystemPrompt } from "../../heartbeat-system-prompt.js";
150151
import { resolveImageSanitizationLimits } from "../../image-sanitization.js";
151152
import { filterLocalModelLeanTools, isLocalModelLeanEnabled } from "../../local-model-lean.js";
@@ -4393,33 +4394,26 @@ export async function runEmbeddedAttempt(
43934394
});
43944395
anthropicPayloadLogger?.recordUsage(messagesSnapshot, promptError);
43954396

4396-
// Run agent_end hooks to allow plugins to analyze the conversation
4397-
// This is fire-and-forget, so we don't await
4398-
// Run even on compaction timeout so plugins can log/cleanup
4399-
if (hookRunner?.hasHooks("agent_end")) {
4400-
hookRunner
4401-
.runAgentEnd(
4402-
{
4403-
messages: messagesSnapshot,
4404-
success: !aborted && !promptError,
4405-
error: promptError ? formatErrorMessage(promptError) : undefined,
4406-
durationMs: Date.now() - promptStartedAt,
4407-
},
4408-
{
4409-
runId: params.runId,
4410-
trace: freezeDiagnosticTraceContext(diagnosticTrace),
4411-
agentId: hookAgentId,
4412-
sessionKey: params.sessionKey,
4413-
sessionId: params.sessionId,
4414-
workspaceDir: params.workspaceDir,
4415-
trigger: params.trigger,
4416-
...buildAgentHookContextChannelFields(params),
4417-
},
4418-
)
4419-
.catch((err) => {
4420-
log.warn(`agent_end hook failed: ${err}`);
4421-
});
4422-
}
4397+
runAgentEndSideEffects({
4398+
event: {
4399+
messages: messagesSnapshot,
4400+
success: !aborted && !promptError,
4401+
error: promptError ? formatErrorMessage(promptError) : undefined,
4402+
durationMs: Date.now() - promptStartedAt,
4403+
},
4404+
ctx: {
4405+
runId: params.runId,
4406+
trace: freezeDiagnosticTraceContext(diagnosticTrace),
4407+
agentId: hookAgentId,
4408+
sessionKey: params.sessionKey,
4409+
sessionId: params.sessionId,
4410+
workspaceDir: params.workspaceDir,
4411+
trigger: params.trigger,
4412+
...(params.config ? { config: params.config } : {}),
4413+
...buildAgentHookContextChannelFields(params),
4414+
},
4415+
hookRunner,
4416+
});
44234417
} finally {
44244418
clearTimeout(abortTimer);
44254419
if (abortWarnTimer) {
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { runSkillWorkshopAutoCapture } from "../../skills/workshop/autocapture.js";
3+
import { awaitAgentEndSideEffects, runAgentEndSideEffects } from "./agent-end-side-effects.js";
4+
import {
5+
awaitAgentHarnessAgentEndHook,
6+
runAgentHarnessAgentEndHook,
7+
} from "./lifecycle-hook-helpers.js";
8+
9+
vi.mock("../../skills/workshop/autocapture.js", () => ({
10+
runSkillWorkshopAutoCapture: vi.fn(),
11+
}));
12+
13+
vi.mock("./lifecycle-hook-helpers.js", () => ({
14+
awaitAgentHarnessAgentEndHook: vi.fn(),
15+
runAgentHarnessAgentEndHook: vi.fn(),
16+
}));
17+
18+
const mockAutoCapture = vi.mocked(runSkillWorkshopAutoCapture);
19+
const mockAwaitAgentEndHook = vi.mocked(awaitAgentHarnessAgentEndHook);
20+
const mockRunAgentEndHook = vi.mocked(runAgentHarnessAgentEndHook);
21+
22+
describe("agent end side effects", () => {
23+
beforeEach(() => {
24+
mockAutoCapture.mockReset();
25+
mockAwaitAgentEndHook.mockReset();
26+
mockRunAgentEndHook.mockReset();
27+
});
28+
29+
it("fires plugin agent_end hooks without waiting for Skill Workshop auto-capture", async () => {
30+
let resolveCapture: (() => void) | undefined;
31+
mockAutoCapture.mockReturnValueOnce(
32+
new Promise<void>((resolve) => {
33+
resolveCapture = resolve;
34+
}),
35+
);
36+
37+
runAgentEndSideEffects({
38+
event: {
39+
messages: [],
40+
success: true,
41+
},
42+
ctx: {
43+
runId: "run-1",
44+
workspaceDir: "/workspace",
45+
config: {
46+
skills: {
47+
workshop: {
48+
autonomous: {
49+
enabled: true,
50+
},
51+
},
52+
},
53+
},
54+
},
55+
});
56+
57+
expect(mockRunAgentEndHook).toHaveBeenCalledTimes(1);
58+
expect(mockAutoCapture).toHaveBeenCalledWith({
59+
event: {
60+
messages: [],
61+
success: true,
62+
},
63+
ctx: {
64+
runId: "run-1",
65+
workspaceDir: "/workspace",
66+
config: {
67+
skills: {
68+
workshop: {
69+
autonomous: {
70+
enabled: true,
71+
},
72+
},
73+
},
74+
},
75+
},
76+
config: {
77+
skills: {
78+
workshop: {
79+
autonomous: {
80+
enabled: true,
81+
},
82+
},
83+
},
84+
},
85+
});
86+
87+
resolveCapture?.();
88+
});
89+
90+
it("still runs agent_end hooks when Skill Workshop auto-capture fails", async () => {
91+
mockAutoCapture.mockRejectedValueOnce(new Error("capture failed"));
92+
93+
await awaitAgentEndSideEffects({
94+
event: {
95+
messages: [],
96+
success: true,
97+
},
98+
ctx: {
99+
runId: "run-1",
100+
workspaceDir: "/workspace",
101+
},
102+
});
103+
104+
expect(mockAutoCapture).toHaveBeenCalledWith({
105+
event: {
106+
messages: [],
107+
success: true,
108+
},
109+
ctx: {
110+
runId: "run-1",
111+
workspaceDir: "/workspace",
112+
},
113+
});
114+
expect(mockAwaitAgentEndHook).toHaveBeenCalledTimes(1);
115+
});
116+
});
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { createSubsystemLogger } from "../../logging/subsystem.js";
2+
import { runSkillWorkshopAutoCapture } from "../../skills/workshop/autocapture.js";
3+
import {
4+
awaitAgentHarnessAgentEndHook,
5+
runAgentHarnessAgentEndHook,
6+
} from "./lifecycle-hook-helpers.js";
7+
8+
const log = createSubsystemLogger("agents/harness");
9+
10+
type AgentEndSideEffectsParams = Parameters<typeof runAgentHarnessAgentEndHook>[0];
11+
12+
async function runCoreAgentEndSideEffects(params: AgentEndSideEffectsParams): Promise<void> {
13+
try {
14+
await runSkillWorkshopAutoCapture({
15+
event: params.event,
16+
ctx: params.ctx,
17+
...(params.ctx.config ? { config: params.ctx.config } : {}),
18+
});
19+
} catch (error) {
20+
log.warn(`skill workshop auto-capture failed: ${String(error)}`);
21+
}
22+
}
23+
24+
export function runAgentEndSideEffects(params: AgentEndSideEffectsParams): void {
25+
void runCoreAgentEndSideEffects(params);
26+
runAgentHarnessAgentEndHook(params);
27+
}
28+
29+
export async function awaitAgentEndSideEffects(params: AgentEndSideEffectsParams): Promise<void> {
30+
await runCoreAgentEndSideEffects(params);
31+
await awaitAgentHarnessAgentEndHook(params);
32+
}

src/agents/harness/hook-context.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { OpenClawConfig } from "../../config/types.openclaw.js";
12
import type {
23
PluginHookAgentContext,
34
PluginHookContextWindowSource,
@@ -18,6 +19,7 @@ export type AgentHarnessHookContext = {
1819
contextTokenBudget?: number;
1920
contextWindowSource?: PluginHookContextWindowSource;
2021
contextWindowReferenceTokens?: number;
22+
config?: OpenClawConfig;
2123
};
2224

2325
export function buildAgentHookContext(params: AgentHarnessHookContext): PluginHookAgentContext {

0 commit comments

Comments
 (0)