docs: document harness hook helpers

This commit is contained in:
Peter Steinberger
2026-06-04 00:12:10 -04:00
parent 87b5796649
commit 46f3efe7ce
4 changed files with 42 additions and 0 deletions

View File

@@ -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<void> {
await runCoreAgentEndSideEffects(params);
await awaitAgentHarnessAgentEndHook(params);

View File

@@ -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 {

View File

@@ -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<string, unknown>)
@@ -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;

View File

@@ -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<typeof getGlobalHookRunner>;
type FinalizeRetryBudget = Map<string, Map<string, number>>;
/** 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<string, number>();
const nextCount = (runBudget.get(retryKey) ?? 0) + 1;