mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 12:21:36 +00:00
* fix(sdk): preserve zero values in event ids * test(sdk): isolate zero event ID components --------- Co-authored-by: destire-mio <248462155+destire-mio@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
198 lines
6.1 KiB
TypeScript
198 lines
6.1 KiB
TypeScript
// OpenClaw SDK helper module supports normalize behavior.
|
|
import type { GatewayEvent, JsonObject, OpenClawEvent, OpenClawEventType } from "./types.js";
|
|
|
|
// Normalize raw Gateway events into stable SDK event types and common metadata.
|
|
function asRecord(value: unknown): JsonObject {
|
|
return typeof value === "object" && value !== null ? (value as JsonObject) : {};
|
|
}
|
|
|
|
function readString(value: unknown): string | undefined {
|
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
}
|
|
|
|
function readNumber(value: unknown): number | undefined {
|
|
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
}
|
|
|
|
function readLowerString(value: unknown): string | undefined {
|
|
return readString(value)?.toLowerCase();
|
|
}
|
|
|
|
function hasHardTimeoutMetadata(data: JsonObject, statusAlreadyTimeoutAttributed = false): boolean {
|
|
const timeoutPhase = readLowerString(data.timeoutPhase);
|
|
return (
|
|
(statusAlreadyTimeoutAttributed && data.providerStarted === true) ||
|
|
timeoutPhase === "preflight" ||
|
|
timeoutPhase === "provider" ||
|
|
timeoutPhase === "post_turn"
|
|
);
|
|
}
|
|
|
|
function isLifecycleCancellation(data: JsonObject): boolean {
|
|
const status = readLowerString(data.status);
|
|
const stopReason = readLowerString(data.stopReason);
|
|
return (
|
|
status === "aborted" ||
|
|
status === "cancelled" ||
|
|
status === "canceled" ||
|
|
status === "killed" ||
|
|
stopReason === "aborted" ||
|
|
stopReason === "cancelled" ||
|
|
stopReason === "canceled" ||
|
|
stopReason === "killed" ||
|
|
stopReason === "auth-revoked" ||
|
|
stopReason === "restart" ||
|
|
stopReason === "rpc" ||
|
|
stopReason === "user" ||
|
|
(data.aborted === true && stopReason === "stop")
|
|
);
|
|
}
|
|
|
|
function normalizeLifecycleEndEventType(data: JsonObject): OpenClawEventType {
|
|
const status = readLowerString(data.status);
|
|
const stopReason = readLowerString(data.stopReason);
|
|
const statusAlreadyTimeoutAttributed =
|
|
stopReason !== "restart" &&
|
|
(status === "timeout" || status === "timed_out" || data.aborted === true);
|
|
if (hasHardTimeoutMetadata(data, statusAlreadyTimeoutAttributed)) {
|
|
return "run.timed_out";
|
|
}
|
|
if (isLifecycleCancellation(data)) {
|
|
return "run.cancelled";
|
|
}
|
|
if (
|
|
status === "timeout" ||
|
|
status === "timed_out" ||
|
|
stopReason === "timeout" ||
|
|
stopReason === "timed_out"
|
|
) {
|
|
return "run.timed_out";
|
|
}
|
|
if (data.aborted === true) {
|
|
return "run.timed_out";
|
|
}
|
|
return "run.completed";
|
|
}
|
|
|
|
function normalizeAgentEventType(payload: JsonObject): OpenClawEventType {
|
|
const stream = readString(payload.stream);
|
|
const data = asRecord(payload.data);
|
|
const phase = readString(data.phase);
|
|
const status = readString(data.status);
|
|
|
|
if (stream === "assistant") {
|
|
return data.delta === true || typeof data.delta === "string"
|
|
? "assistant.delta"
|
|
: "assistant.message";
|
|
}
|
|
if (stream === "thinking" || stream === "plan") {
|
|
return "thinking.delta";
|
|
}
|
|
if (stream === "lifecycle") {
|
|
if (phase === "start") {
|
|
return "run.started";
|
|
}
|
|
if (phase === "end") {
|
|
return normalizeLifecycleEndEventType(data);
|
|
}
|
|
if (phase === "error") {
|
|
if (hasHardTimeoutMetadata(data, false)) {
|
|
return "run.timed_out";
|
|
}
|
|
if (isLifecycleCancellation(data)) {
|
|
return "run.cancelled";
|
|
}
|
|
return "run.failed";
|
|
}
|
|
}
|
|
if (stream === "tool" || stream === "item" || stream === "command_output") {
|
|
if (phase === "start" || status === "running") {
|
|
return "tool.call.started";
|
|
}
|
|
if (phase === "delta" || phase === "update") {
|
|
return "tool.call.delta";
|
|
}
|
|
// Terminal tool/item events carry phase:"end" together with the real status, so a failed or
|
|
// blocked tool must be classified before the end/completed branch — otherwise phase:"end" wins
|
|
// and failures are reported as tool.call.completed.
|
|
if (status === "failed" || status === "blocked") {
|
|
return "tool.call.failed";
|
|
}
|
|
if (phase === "end" || status === "completed") {
|
|
return "tool.call.completed";
|
|
}
|
|
return "tool.call.delta";
|
|
}
|
|
if (stream === "approval") {
|
|
return phase === "resolved" ? "approval.resolved" : "approval.requested";
|
|
}
|
|
if (stream === "patch") {
|
|
return "artifact.updated";
|
|
}
|
|
if (stream === "error") {
|
|
return "run.failed";
|
|
}
|
|
return "raw";
|
|
}
|
|
|
|
function normalizeNamedEventType(event: GatewayEvent): OpenClawEventType {
|
|
const payload = asRecord(event.payload);
|
|
switch (event.event) {
|
|
case "agent":
|
|
return normalizeAgentEventType(payload);
|
|
case "sessions.changed": {
|
|
const reason = readString(payload.reason);
|
|
if (reason === "create") {
|
|
return "session.created";
|
|
}
|
|
if (reason === "compact") {
|
|
return "session.compacted";
|
|
}
|
|
return "session.updated";
|
|
}
|
|
case "session.message":
|
|
return "assistant.message";
|
|
case "session.tool":
|
|
return "tool.call.delta";
|
|
case "exec.approval.requested":
|
|
case "plugin.approval.requested":
|
|
return "approval.requested";
|
|
case "exec.approval.resolved":
|
|
case "plugin.approval.resolved":
|
|
return "approval.resolved";
|
|
case "task.updated":
|
|
case "tasks.changed":
|
|
return "task.updated";
|
|
default:
|
|
return "raw";
|
|
}
|
|
}
|
|
|
|
/** Normalize a raw Gateway event into the public SDK event shape. */
|
|
export function normalizeGatewayEvent(event: GatewayEvent): OpenClawEvent {
|
|
const payload = asRecord(event.payload);
|
|
const runId = readString(payload.runId);
|
|
const sessionId = readString(payload.sessionId);
|
|
const sessionKey = readString(payload.sessionKey);
|
|
const taskId = readString(payload.taskId);
|
|
const agentId = readString(payload.agentId);
|
|
const ts = readNumber(payload.ts) ?? Date.now();
|
|
const idParts = [event.seq ?? "local", event.event, runId, sessionKey, ts].filter(
|
|
(part) => part !== undefined,
|
|
);
|
|
|
|
return {
|
|
version: 1,
|
|
id: idParts.join(":"),
|
|
ts,
|
|
type: normalizeNamedEventType(event),
|
|
...(runId ? { runId } : {}),
|
|
...(sessionId ? { sessionId } : {}),
|
|
...(sessionKey ? { sessionKey } : {}),
|
|
...(taskId ? { taskId } : {}),
|
|
...(agentId ? { agentId } : {}),
|
|
data: payload.data ?? payload,
|
|
raw: event,
|
|
};
|
|
}
|