refactor: canonicalize binary operation results (#106049)

* refactor(normalization-core): promote shared result type

* refactor(agent-core): reuse canonical error coercion

* refactor: discriminate internal operation results

* docs: establish shared result convention

* fix(agent-core): preserve error coercion facade

* build: derive package entries from exports

* fix(plugins): resolve shared result subpath

* fix(ci): align Result integration guards

* build: refresh plugin SDK API baseline

* fix(agent-core): preserve toError compatibility
This commit is contained in:
Peter Steinberger
2026-07-13 00:39:43 -07:00
committed by GitHub
parent 2fc91410e8
commit 6df498c3cd
35 changed files with 186 additions and 222 deletions
@@ -1,4 +1,5 @@
// Agent Core module implements agent harness behavior.
import { toErrorObject } from "@openclaw/normalization-core/error-coercion";
import type {
AssistantMessage,
ImageContent,
@@ -52,13 +53,7 @@ import type {
Session,
Skill,
} from "./types.js";
import {
AgentHarnessError,
BranchSummaryError,
CompactionError,
SessionError,
toError,
} from "./types.js";
import { AgentHarnessError, BranchSummaryError, CompactionError, SessionError } from "./types.js";
// CoreAgentHarness coordinates session state, resources, tools, compaction, and
// streaming callbacks around the lower-level agent loop.
@@ -164,7 +159,7 @@ function normalizeHarnessError(
if (error instanceof AgentHarnessError) {
return error;
}
const cause = toError(error);
const cause = toErrorObject(error, "Non-Error thrown");
if (cause instanceof SessionError) {
return new AgentHarnessError("session", cause.message, cause);
}
@@ -672,7 +667,7 @@ export class CoreAgentHarness<
);
} catch (failureError) {
const cause = new AggregateError(
[toError(error), toError(failureError)],
[error, failureError].map((value) => toErrorObject(value, "Non-Error thrown")),
"Agent run failed and failure reporting failed",
);
throw new AgentHarnessError("unknown", cause.message, cause);
@@ -1130,17 +1125,17 @@ export class CoreAgentHarness<
try {
await this.emitQueueUpdate();
} catch (error) {
errors.push(toError(error));
errors.push(toErrorObject(error, "Non-Error thrown"));
}
try {
await this.waitForIdle();
} catch (error) {
errors.push(toError(error));
errors.push(toErrorObject(error, "Non-Error thrown"));
}
try {
await this.emitOwn({ type: "abort", clearedSteer, clearedFollowUp });
} catch (error) {
errors.push(toError(error));
errors.push(toErrorObject(error, "Non-Error thrown"));
}
if (errors.length > 0) {
const cause =
+5 -5
View File
@@ -17,6 +17,7 @@ import {
import { tmpdir } from "node:os";
import { basename, isAbsolute, join, resolve } from "node:path";
import { createInterface } from "node:readline";
import { toErrorObject } from "@openclaw/normalization-core/error-coercion";
import {
type ExecutionEnv,
ExecutionError,
@@ -26,7 +27,6 @@ import {
type FileKind,
ok,
type Result,
toError,
} from "../types.js";
import { killProcessTree } from "./kill-tree.js";
@@ -100,7 +100,7 @@ function toFileError(error: unknown, path?: string): FileError {
if (error instanceof FileError) {
return error;
}
const cause = toError(error);
const cause = toErrorObject(error, "Non-Error thrown");
if (isNodeError(error)) {
const message = error.message;
switch (error.code) {
@@ -342,7 +342,7 @@ export class NodeExecutionEnv implements ExecutionEnv {
windowsHide: true,
});
} catch (error) {
const cause = toError(error);
const cause = toErrorObject(error, "Non-Error thrown");
settle(err(new ExecutionError("spawn_error", cause.message, cause)));
return;
}
@@ -373,7 +373,7 @@ export class NodeExecutionEnv implements ExecutionEnv {
try {
options?.onStdout?.(chunk);
} catch (error) {
const cause = toError(error);
const cause = toErrorObject(error, "Non-Error thrown");
callbackError = new ExecutionError("callback_error", cause.message, cause);
onAbort();
}
@@ -383,7 +383,7 @@ export class NodeExecutionEnv implements ExecutionEnv {
try {
options?.onStderr?.(chunk);
} catch (error) {
const cause = toError(error);
const cause = toErrorObject(error, "Non-Error thrown");
callbackError = new ExecutionError("callback_error", cause.message, cause);
onAbort();
}
@@ -1,4 +1,5 @@
// Agent Core module implements jsonl storage behavior.
import { toErrorObject } from "@openclaw/normalization-core/error-coercion";
import type {
FileError,
FileSystem,
@@ -6,7 +7,7 @@ import type {
Result,
SessionTreeEntry,
} from "../types.js";
import { SessionError, toError } from "../types.js";
import { SessionError } from "../types.js";
import {
appendParentIdAfterEntry,
BaseSessionStorage,
@@ -69,7 +70,11 @@ function parseHeaderLine(line: string, filePath: string): SessionHeader {
try {
parsed = JSON.parse(line);
} catch (error) {
throw invalidSession(filePath, "first line is not a valid session header", toError(error));
throw invalidSession(
filePath,
"first line is not a valid session header",
toErrorObject(error, "Non-Error thrown"),
);
}
if (!isRecord(parsed)) {
throw invalidSession(filePath, "first line is not a valid session header");
@@ -110,7 +115,12 @@ function parseEntryLine(line: string, filePath: string, lineNumber: number): Ses
try {
parsed = JSON.parse(line);
} catch (error) {
throw invalidEntry(filePath, lineNumber, "is not valid JSON", toError(error));
throw invalidEntry(
filePath,
lineNumber,
"is not valid JSON",
toErrorObject(error, "Non-Error thrown"),
);
}
if (!isRecord(parsed)) {
throw invalidEntry(filePath, lineNumber, "is not a valid session entry");
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { toError } from "./types.js";
describe("toError", () => {
it("preserves the shipped facade semantics", () => {
const thrown = { code: "E_TEST", detail: "structured" };
const error = toError(thrown);
expect(error.message).toBe('{"code":"E_TEST","detail":"structured"}');
expect(error).not.toHaveProperty("cause");
});
});
+7 -13
View File
@@ -1,4 +1,5 @@
// Agent Core type module defines shared TypeScript contracts.
import type { Result } from "@openclaw/normalization-core/result";
import type {
ImageContent,
Model,
@@ -11,20 +12,13 @@ import type { AgentEvent, AgentMessage, AgentTool, QueueMode, ThinkingLevel } fr
import type { AgentCoreCompletionRuntimeDeps, AgentCoreRuntimeDeps } from "../runtime-deps.js";
import type { Session } from "./session/session.js";
/** Result of a fallible operation. Expected failures are returned as `ok: false` instead of thrown. */
export type Result<TValue, TError> = { ok: true; value: TValue } | { ok: false; error: TError };
export { err, ok } from "@openclaw/normalization-core/result";
export type { Result } from "@openclaw/normalization-core/result";
/** Create a successful {@link Result}. */
export function ok<TValue, TError>(value: TValue): Result<TValue, TError> {
return { ok: true, value };
}
/** Create a failed {@link Result}. */
export function err<TValue, TError>(error: TError): Result<TValue, TError> {
return { ok: false, error };
}
/** Normalize unknown thrown values into Error instances before using them as typed error causes. */
/**
* @deprecated Use `toErrorObject` from `@openclaw/normalization-core/error-coercion`.
* Kept through the next major release for the shipped agent-core plugin API.
*/
export function toError(error: unknown): Error {
if (error instanceof Error) {
return error;