From 46f3efe7ceec0b2cc2aa98f0e86b5d92189f3a96 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 4 Jun 2026 00:12:10 -0400 Subject: [PATCH] docs: document harness hook helpers --- src/agents/harness/agent-end-side-effects.ts | 9 +++++++++ src/agents/harness/compaction-recovery.ts | 8 ++++++++ src/agents/harness/hook-helpers.ts | 9 +++++++++ src/agents/harness/lifecycle-hook-helpers.ts | 16 ++++++++++++++++ 4 files changed, 42 insertions(+) diff --git a/src/agents/harness/agent-end-side-effects.ts b/src/agents/harness/agent-end-side-effects.ts index 8565495d23e..92ec1a89c6a 100644 --- a/src/agents/harness/agent-end-side-effects.ts +++ b/src/agents/harness/agent-end-side-effects.ts @@ -1,3 +1,9 @@ +/** + * Agent-end side effect runner. + * + * Harnesses use this to trigger core research capture and plugin agent_end hooks + * either fire-and-forget or awaited during tests/shutdown. + */ import { createSubsystemLogger } from "../../logging/subsystem.js"; import { runSkillResearchAutoCapture } from "../../skills/research/autocapture.js"; import { @@ -17,15 +23,18 @@ async function runCoreAgentEndSideEffects(params: AgentEndSideEffectsParams): Pr ...(params.ctx.config ? { config: params.ctx.config } : {}), }); } catch (error) { + // Side effects are observational; failures must not change the completed run result. log.warn(`skill research auto-capture failed: ${String(error)}`); } } +/** Starts agent-end side effects without waiting for completion. */ export function runAgentEndSideEffects(params: AgentEndSideEffectsParams): void { void runCoreAgentEndSideEffects(params); runAgentHarnessAgentEndHook(params); } +/** Runs agent-end side effects and waits for plugin/core completion. */ export async function awaitAgentEndSideEffects(params: AgentEndSideEffectsParams): Promise { await runCoreAgentEndSideEffects(params); await awaitAgentHarnessAgentEndHook(params); diff --git a/src/agents/harness/compaction-recovery.ts b/src/agents/harness/compaction-recovery.ts index 5075f855fe4..a5b08cb6c81 100644 --- a/src/agents/harness/compaction-recovery.ts +++ b/src/agents/harness/compaction-recovery.ts @@ -1,5 +1,12 @@ +/** + * Native harness compaction recovery helpers. + * + * CLI compaction uses these guards to recognize thread-binding failures that can + * fall back to context-engine compaction after clearing stale session bindings. + */ import type { EmbeddedAgentCompactResult } from "../embedded-agent-runner/types.js"; +/** Returns whether a native harness failure reason indicates a recoverable binding issue. */ export function isRecoverableNativeHarnessBindingReason(reason: unknown): boolean { if (typeof reason !== "string") { return false; @@ -13,6 +20,7 @@ export function isRecoverableNativeHarnessBindingReason(reason: unknown): boolea ); } +/** Returns whether a compact result failed due to a recoverable native binding issue. */ export function isRecoverableNativeHarnessBindingFailure( result: EmbeddedAgentCompactResult | undefined, ): boolean { diff --git a/src/agents/harness/hook-helpers.ts b/src/agents/harness/hook-helpers.ts index cf13fa16932..25e8b4196a0 100644 --- a/src/agents/harness/hook-helpers.ts +++ b/src/agents/harness/hook-helpers.ts @@ -1,3 +1,9 @@ +/** + * Agent harness tool/message hook helpers. + * + * Harnesses use this to dispatch after-tool-call and before-message-write hooks + * while isolating hook failures from the runtime path. + */ import { createSubsystemLogger } from "../../logging/subsystem.js"; import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js"; import { consumeAdjustedParamsForToolCall } from "../agent-tools.before-tool-call.js"; @@ -5,6 +11,7 @@ import type { AgentMessage } from "../runtime/index.js"; const log = createSubsystemLogger("agents/harness"); +/** Runs best-effort after-tool-call hooks for a completed tool invocation. */ export async function runAgentHarnessAfterToolCallHook(params: { toolName: string; toolCallId: string; @@ -23,6 +30,7 @@ export async function runAgentHarnessAfterToolCallHook(params: { return; } const adjustedArgs = consumeAdjustedParamsForToolCall(params.toolCallId, params.runId); + // Hooks should see adjusted tool params when before_tool_call rewrote them. const eventArgs = adjustedArgs && typeof adjustedArgs === "object" ? (adjustedArgs as Record) @@ -53,6 +61,7 @@ export async function runAgentHarnessAfterToolCallHook(params: { } } +/** Runs before-message-write hooks and returns the possibly rewritten message. */ export function runAgentHarnessBeforeMessageWriteHook(params: { message: AgentMessage; agentId?: string; diff --git a/src/agents/harness/lifecycle-hook-helpers.ts b/src/agents/harness/lifecycle-hook-helpers.ts index 41df893d765..e7fb4a15cb4 100644 --- a/src/agents/harness/lifecycle-hook-helpers.ts +++ b/src/agents/harness/lifecycle-hook-helpers.ts @@ -1,3 +1,9 @@ +/** + * Agent harness lifecycle hook helpers. + * + * This module dispatches LLM/agent lifecycle plugin hooks and normalizes + * before-finalize retry/finalize decisions with bounded retry accounting. + */ import { createHash } from "node:crypto"; import { normalizeOptionalString as normalizeTrimmedString } from "@openclaw/normalization-core/string-coerce"; import { createSubsystemLogger } from "../../logging/subsystem.js"; @@ -20,6 +26,7 @@ const FINALIZE_RETRY_BUDGET_MAX_ENTRIES = 2048; type AgentHarnessHookRunner = ReturnType; type FinalizeRetryBudget = Map>; +/** Returns the current global hook runner for harness lifecycle hooks. */ export function getAgentHarnessHookRunner(): AgentHarnessHookRunner { return getGlobalHookRunner(); } @@ -57,6 +64,7 @@ function buildFinalizeRetryInstructionKey(instruction: string): string { return `instruction:${createHash("sha256").update(instruction).digest("hex")}`; } +/** Clears before-finalize retry budgets globally or for one run. */ export function clearAgentHarnessFinalizeRetryBudget(params?: { runId?: string }): void { const budget = getFinalizeRetryBudget(); if (!params?.runId) { @@ -66,6 +74,7 @@ export function clearAgentHarnessFinalizeRetryBudget(params?: { runId?: string } budget.delete(params.runId); } +/** Dispatches best-effort LLM input hooks for a harness attempt. */ export function runAgentHarnessLlmInputHook(params: { event: PluginHookLlmInputEvent; ctx: AgentHarnessHookContext; @@ -82,6 +91,7 @@ export function runAgentHarnessLlmInputHook(params: { }); } +/** Dispatches best-effort LLM output hooks for a harness attempt. */ export function runAgentHarnessLlmOutputHook(params: { event: PluginHookLlmOutputEvent; ctx: AgentHarnessHookContext; @@ -116,6 +126,7 @@ async function executeAgentHarnessAgentEndHook(params: { } } +/** Starts agent_end hooks with unref timeout behavior. */ export function runAgentHarnessAgentEndHook(params: { event: PluginHookAgentEndEvent; ctx: AgentHarnessHookContext; @@ -124,6 +135,7 @@ export function runAgentHarnessAgentEndHook(params: { void executeAgentHarnessAgentEndHook({ ...params, unrefTimeout: true }); } +/** Runs agent_end hooks and waits for completion. */ export async function awaitAgentHarnessAgentEndHook(params: { event: PluginHookAgentEndEvent; ctx: AgentHarnessHookContext; @@ -132,11 +144,13 @@ export async function awaitAgentHarnessAgentEndHook(params: { await executeAgentHarnessAgentEndHook({ ...params, unrefTimeout: false }); } +/** Normalized before-finalize hook decision consumed by harness loops. */ export type AgentHarnessBeforeAgentFinalizeOutcome = | { action: "continue" } | { action: "revise"; reason: string } | { action: "finalize"; reason?: string }; +/** Runs before-finalize hooks and normalizes finalize/revise/continue decisions. */ export async function runAgentHarnessBeforeAgentFinalizeHook(params: { event: PluginHookBeforeAgentFinalizeEvent; ctx: AgentHarnessHookContext; @@ -192,6 +206,8 @@ function normalizeBeforeAgentFinalizeResult( const retryKey = normalizeTrimmedString(retry.idempotencyKey) || buildFinalizeRetryInstructionKey(retryInstruction); + // Track retry attempts per run+instruction to prevent finalize hooks + // from creating an unbounded revise loop. const budget = getFinalizeRetryBudget(); const runBudget = budget.get(retryRunId) ?? new Map(); const nextCount = (runBudget.get(retryKey) ?? 0) + 1;