fix(agents): snapshot harness registrations

This commit is contained in:
Vincent Koc
2026-06-05 15:37:10 +02:00
parent ec1cd7bcec
commit 4c78b7babb
4 changed files with 291 additions and 16 deletions

View File

@@ -79,6 +79,72 @@ describe("agent harness registry", () => {
expect(listAgentHarnessIds()).toEqual(["custom"]);
});
it("snapshots harness fields before registry lookup", async () => {
let idReads = 0;
let labelReads = 0;
let supportsReads = 0;
let resetReads = 0;
const events: string[] = [];
registerAgentHarness(
{
marker: "original",
get id() {
idReads += 1;
if (idReads > 1) {
throw new Error("agent harness id getter re-read");
}
return " volatile ";
},
get label() {
labelReads += 1;
if (labelReads > 1) {
throw new Error("agent harness label getter re-read");
}
return "Volatile";
},
get supports() {
supportsReads += 1;
if (supportsReads > 1) {
throw new Error("agent harness supports getter re-read");
}
return function (this: { marker?: string }) {
events.push(`supports:${this.marker ?? "missing"}`);
return { supported: true as const, priority: 20 };
};
},
async runAttempt() {
throw new Error("not used");
},
get reset() {
resetReads += 1;
if (resetReads > 1) {
throw new Error("agent harness reset getter re-read");
}
return async function (this: { marker?: string }) {
events.push(`reset:${this.marker ?? "missing"}`);
};
},
} as AgentHarness & { marker: string },
{ ownerPluginId: "plugin-a" },
);
const harness = getAgentHarness("volatile");
expect(harness?.id).toBe("volatile");
expect(harness?.label).toBe("Volatile");
expect(harness?.pluginId).toBe("plugin-a");
expect(harness?.supports({ provider: "codex", requestedRuntime: "auto" })).toEqual({
supported: true,
priority: 20,
});
await harness?.reset?.({ reason: "reset" });
expect(events).toEqual(["supports:original", "reset:original"]);
expect(idReads).toBe(1);
expect(labelReads).toBe(1);
expect(supportsReads).toBe(1);
expect(resetReads).toBe(1);
});
it("restores a registry snapshot", () => {
registerAgentHarness(makeHarness("a"));
const snapshot = listRegisteredAgentHarnesses();

View File

@@ -2,7 +2,12 @@
* Registry for native agent harness implementations and lifecycle cleanup.
*/
import { createSubsystemLogger } from "../../logging/subsystem.js";
import type { AgentHarness, AgentHarnessResetParams, RegisteredAgentHarness } from "./types.js";
import type {
AgentHarness,
AgentHarnessDeliveryDefaults,
AgentHarnessResetParams,
RegisteredAgentHarness,
} from "./types.js";
/**
* Process-wide registry for agent harnesses contributed by core and runtime plugins.
@@ -17,6 +22,20 @@ type AgentHarnessRegistryState = {
harnesses: Map<string, RegisteredAgentHarness>;
};
const agentHarnessSnapshotFields = [
"label",
"pluginId",
"contextEngineHostCapabilities",
"deliveryDefaults",
"supports",
"runAttempt",
"runSideQuestion",
"classify",
"compact",
"reset",
"dispose",
] as const satisfies readonly (keyof AgentHarness)[];
function getAgentHarnessRegistryState(): AgentHarnessRegistryState {
const globalState = globalThis as typeof globalThis & {
[AGENT_HARNESS_REGISTRY_STATE]?: AgentHarnessRegistryState;
@@ -27,18 +46,80 @@ function getAgentHarnessRegistryState(): AgentHarnessRegistryState {
return globalState[AGENT_HARNESS_REGISTRY_STATE];
}
function bindAgentHarnessFunction<TFunction>(harness: AgentHarness, fn: TFunction): TFunction {
if (typeof fn !== "function") {
return fn;
}
return function (this: unknown, ...args: unknown[]) {
return Reflect.apply(fn as (...args: unknown[]) => unknown, harness, args);
} as TFunction;
}
function snapshotAgentHarnessDeliveryDefaults(
value: unknown,
): AgentHarnessDeliveryDefaults | undefined {
if (value === undefined) {
return undefined;
}
if (!value || typeof value !== "object") {
return value as AgentHarnessDeliveryDefaults;
}
const source = value as Partial<AgentHarnessDeliveryDefaults>;
if (source.sourceVisibleReplies === undefined) {
return {};
}
return { sourceVisibleReplies: source.sourceVisibleReplies };
}
function snapshotAgentHarnessValue(
harness: AgentHarness,
key: (typeof agentHarnessSnapshotFields)[number],
value: unknown,
): unknown {
if (typeof value === "function") {
return bindAgentHarnessFunction(harness, value);
}
if (key === "contextEngineHostCapabilities" && Array.isArray(value)) {
return [...value];
}
if (key === "deliveryDefaults") {
return snapshotAgentHarnessDeliveryDefaults(value);
}
return value;
}
export function snapshotAgentHarness(
harness: AgentHarness,
options: { ownerPluginId?: string } = {},
): AgentHarness {
const rawId = harness.id;
const id = typeof rawId === "string" ? rawId.trim() : "";
const snapshot = { id } as Partial<AgentHarness>;
for (const key of agentHarnessSnapshotFields) {
const value = harness[key];
if (value === undefined) {
continue;
}
snapshot[key] = snapshotAgentHarnessValue(harness, key, value) as never;
}
if (!snapshot.pluginId && options.ownerPluginId) {
snapshot.pluginId = options.ownerPluginId;
}
return snapshot as AgentHarness;
}
/** Registers or replaces an agent harness under its trimmed id. */
export function registerAgentHarness(
harness: AgentHarness,
options?: { ownerPluginId?: string },
): void {
const id = harness.id.trim();
const snapshot = snapshotAgentHarness(harness, options);
const id = snapshot.id;
if (!id) {
throw new Error("agent harness registration missing id");
}
getAgentHarnessRegistryState().harnesses.set(id, {
harness: {
...harness,
id,
pluginId: harness.pluginId ?? options?.ownerPluginId,
},
harness: snapshot,
ownerPluginId: options?.ownerPluginId,
});
}

View File

@@ -0,0 +1,120 @@
// Agent harness registration contracts cover plugin-owned runtime harness snapshots.
import {
createPluginRegistryFixture,
registerVirtualTestPlugin,
} from "openclaw/plugin-sdk/plugin-test-contracts";
import { afterEach, describe, expect, it } from "vitest";
import { clearAgentHarnesses, getRegisteredAgentHarness } from "../../agents/harness/registry.js";
import type { AgentHarness } from "../../agents/harness/types.js";
afterEach(() => {
clearAgentHarnesses();
});
describe("agent harness registration", () => {
it("snapshots harness fields before runtime lookup", async () => {
let idReads = 0;
let labelReads = 0;
let supportsReads = 0;
let runAttemptReads = 0;
let resetReads = 0;
let disposeReads = 0;
const events: string[] = [];
const { config, registry } = createPluginRegistryFixture();
registerVirtualTestPlugin({
registry,
config,
id: "volatile-harness-owner",
name: "Volatile Harness Owner",
register(api) {
api.registerAgentHarness({
marker: "original",
get id() {
idReads += 1;
if (idReads > 1) {
throw new Error("agent harness id getter re-read");
}
return " volatile-harness ";
},
get label() {
labelReads += 1;
if (labelReads > 1) {
throw new Error("agent harness label getter re-read");
}
return "Volatile Harness";
},
get supports() {
supportsReads += 1;
if (supportsReads > 1) {
throw new Error("agent harness supports getter re-read");
}
return function (this: { marker?: string }) {
events.push(`supports:${this.marker ?? "missing"}`);
return { supported: true as const, priority: 50 };
};
},
get runAttempt() {
runAttemptReads += 1;
if (runAttemptReads > 1) {
throw new Error("agent harness runAttempt getter re-read");
}
return async function (this: { marker?: string }) {
events.push(`run:${this.marker ?? "missing"}`);
return { ok: false as const, error: "unused" };
};
},
get reset() {
resetReads += 1;
if (resetReads > 1) {
throw new Error("agent harness reset getter re-read");
}
return async function (this: { marker?: string }) {
events.push(`reset:${this.marker ?? "missing"}`);
};
},
get dispose() {
disposeReads += 1;
if (disposeReads > 1) {
throw new Error("agent harness dispose getter re-read");
}
return async function (this: { marker?: string }) {
events.push(`dispose:${this.marker ?? "missing"}`);
};
},
} as AgentHarness & { marker: string });
},
});
expect(registry.registry.diagnostics).toEqual([]);
const localHarness = registry.registry.agentHarnesses[0]?.harness;
const globalHarness = getRegisteredAgentHarness("volatile-harness")?.harness;
expect(localHarness?.id).toBe("volatile-harness");
expect(localHarness?.pluginId).toBe("volatile-harness-owner");
expect(globalHarness?.id).toBe("volatile-harness");
expect(globalHarness?.pluginId).toBe("volatile-harness-owner");
expect(localHarness?.label).toBe("Volatile Harness");
expect(localHarness?.supports({ provider: "codex", requestedRuntime: "auto" })).toEqual({
supported: true,
priority: 50,
});
await expect(localHarness?.runAttempt({} as never)).resolves.toEqual({
ok: false,
error: "unused",
});
await globalHarness?.reset?.({ reason: "reset" });
await globalHarness?.dispose?.();
expect(events).toEqual([
"supports:original",
"run:original",
"reset:original",
"dispose:original",
]);
expect(idReads).toBe(1);
expect(labelReads).toBe(1);
expect(supportsReads).toBe(1);
expect(runAttemptReads).toBe(1);
expect(resetReads).toBe(1);
expect(disposeReads).toBe(1);
});
});

View File

@@ -13,6 +13,7 @@ import { clearCodeModeNamespacesForPlugin } from "../agents/code-mode-namespaces
import {
getRegisteredAgentHarness,
registerAgentHarness as registerGlobalAgentHarness,
snapshotAgentHarness,
} from "../agents/harness/registry.js";
import type { AgentHarness } from "../agents/harness/types.js";
import type { AnyAgentTool } from "../agents/tools/common.js";
@@ -1077,7 +1078,19 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
};
const registerAgentHarness = (record: PluginRecord, harness: AgentHarness) => {
const id = normalizeOptionalString((harness as Partial<AgentHarness> | undefined)?.id) ?? "";
let snapshot: AgentHarness;
try {
snapshot = snapshotAgentHarness(harness, { ownerPluginId: record.id });
} catch (error) {
pushDiagnostic({
level: "error",
pluginId: record.id,
source: record.source,
message: `agent harness registration has unreadable fields: ${formatErrorMessage(error)}`,
});
return;
}
const id = normalizeOptionalString(snapshot.id) ?? "";
if (!id) {
pushDiagnostic({
level: "error",
@@ -1087,7 +1100,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
return;
}
if (typeof harness.supports !== "function" || typeof harness.runAttempt !== "function") {
if (typeof snapshot.supports !== "function" || typeof snapshot.runAttempt !== "function") {
pushDiagnostic({
level: "error",
pluginId: record.id,
@@ -1116,19 +1129,14 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
});
return;
}
const normalizedHarness = {
...harness,
id,
pluginId: harness.pluginId ?? record.id,
};
if (registryParams.activateGlobalSideEffects !== false) {
registerGlobalAgentHarness(normalizedHarness, { ownerPluginId: record.id });
registerGlobalAgentHarness(snapshot, { ownerPluginId: record.id });
}
record.agentHarnessIds.push(id);
registry.agentHarnesses.push({
pluginId: record.id,
pluginName: record.name,
harness: normalizedHarness,
harness: snapshot,
source: record.source,
rootDir: record.rootDir,
});