File size: 4,005 Bytes
4440aec | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 | import { asFiniteNumber as readFiniteNumberValue } from "@openclaw/normalization-core/number-coercion";
import { asOptionalRecord as readRecordValue } from "@openclaw/normalization-core/record-coerce";
import { readStringValue } from "@openclaw/normalization-core/string-coerce";
import type { EmbeddedAgentEvent } from "../../agents/embedded-agent-subscribe.shared-types.js";
import { inferToolMetaFromArgsCore } from "../../agents/tool-display.js";
import type { GetReplyOptions } from "../types.js";
/**
* CLI backends report a tool result as its raw content: a string, or the text
* blocks the harness streamed. Structured runners send a record instead, so the
* command projection has to read both or every CLI command result is dropped.
*/
function readToolResultText(value: unknown): string | undefined {
const direct = readStringValue(value);
if (direct !== undefined) {
return direct;
}
if (!Array.isArray(value)) {
return undefined;
}
const text = value
.map((block) => readStringValue(readRecordValue(block)?.text))
.filter((part): part is string => part !== undefined)
.join("\n")
.trim();
return text || undefined;
}
function readNullableNumberValue(value: unknown): number | null | undefined {
if (value === null) {
return null;
}
return readFiniteNumberValue(value);
}
function isCommandToolName(name: string | undefined): boolean {
const normalized = name?.trim().toLowerCase();
return normalized === "exec" || normalized === "bash" || normalized === "shell";
}
/** Projects a completed command-tool event into the channel command-output contract. */
export function buildCommandOutputFromToolResultEvent(
evt: EmbeddedAgentEvent,
): Parameters<NonNullable<GetReplyOptions["onCommandOutput"]>>[0] | undefined {
if (evt.stream !== "tool" || evt.data.phase !== "result") {
return undefined;
}
const name = evt.data.name;
const commandBearing = evt.data.commandBearing === true;
if (!name || (!commandBearing && !isCommandToolName(name))) {
return undefined;
}
const result = readRecordValue(evt.data.result);
const details = readRecordValue(result?.details);
const output =
evt.data.output ??
readStringValue(result?.output) ??
readStringValue(details?.output) ??
readToolResultText(evt.data.result);
const explicitStatus =
evt.data.status ?? readStringValue(result?.status) ?? readStringValue(details?.status);
const exitCode = readNullableNumberValue(
result?.exitCode ?? details?.exitCode ?? evt.data.exitCode,
);
const durationMs = readFiniteNumberValue(
result?.durationMs ?? details?.durationMs ?? evt.data.durationMs,
);
const cwd = evt.data.cwd;
const errorStatus =
evt.data.isError === true ? "failed" : evt.data.isError === false ? "completed" : undefined;
// A bare result carries no outcome of its own: runners that report one send a
// separate command_output event, and synthesizing here would duplicate it.
// A CLI result is different because its content *is* the outcome, which
// readToolResultText surfaces as output above.
const hasConcreteCommandResult =
output !== undefined ||
explicitStatus !== undefined ||
exitCode !== undefined ||
durationMs !== undefined ||
cwd !== undefined ||
(commandBearing && typeof evt.data.isError === "boolean") ||
(result !== undefined && Object.keys(result).length > 0);
if (!hasConcreteCommandResult) {
return undefined;
}
// Keep the line describing the command, not its output: without a title the
// terminal line would replace the request with whatever the tool printed.
const args = evt.data.args;
const title =
evt.data.title ??
(args ? inferToolMetaFromArgsCore(name, args, { detailMode: "explain" }) : undefined);
return {
itemId: evt.data.itemId,
phase: "end",
title,
toolCallId: evt.data.toolCallId,
name,
output,
status: explicitStatus ?? errorStatus,
exitCode,
durationMs,
cwd,
};
}
|