mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:00:42 +00:00
fix: match codex verbose tool logs to pi (#70966)
This commit is contained in:
@@ -30,6 +30,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- Voice-call/Telnyx: preserve inbound/outbound callback metadata and read transcription text from Telnyx's current `transcription_data` payload.
|
||||
- Codex harness: send verbose tool progress to chat channels for native app-server runs, matching the Pi harness `/verbose on` and `/verbose full` behavior.
|
||||
- Codex harness: route native `request_user_input` prompts back to the originating chat, preserve queued follow-up answers, and honor newer app-server command approval amendment decisions.
|
||||
- Codex status: report Codex CLI OAuth as `oauth (codex-cli)` for native `codex/*` sessions instead of showing unknown auth. Fixes #70688. Thanks @jb510.
|
||||
- Codex harness/context-engine: redact context-engine assembly failures before logging, so fallback warnings do not serialize raw error objects. (#70809) Thanks @jalehman.
|
||||
|
||||
@@ -189,7 +189,7 @@ For the plugin authoring guide, see [Plugin SDK overview](/plugins/sdk-overview)
|
||||
| `plugin-sdk/models-provider-runtime` | `/models` command/provider reply helpers |
|
||||
| `plugin-sdk/skill-commands-runtime` | Skill command listing helpers |
|
||||
| `plugin-sdk/native-command-registry` | Native command registry/build/serialize helpers |
|
||||
| `plugin-sdk/agent-harness` | Experimental trusted-plugin surface for low-level agent harnesses: harness types, active-run steer/abort helpers, OpenClaw tool bridge helpers, and attempt result utilities |
|
||||
| `plugin-sdk/agent-harness` | Experimental trusted-plugin surface for low-level agent harnesses: harness types, active-run steer/abort helpers, OpenClaw tool bridge helpers, tool progress formatting/detail helpers, and attempt result utilities |
|
||||
| `plugin-sdk/provider-zai-endpoint` | Z.AI endpoint detection helpers |
|
||||
| `plugin-sdk/infra-runtime` | System event/heartbeat helpers |
|
||||
| `plugin-sdk/collection-runtime` | Small bounded cache helpers |
|
||||
|
||||
@@ -490,7 +490,7 @@ describe("CodexAppServerEventProjector", () => {
|
||||
data: expect.objectContaining({ phase: "start", itemId: "compact-1" }),
|
||||
}),
|
||||
);
|
||||
expect(result.toolMetas).toEqual([{ toolName: "sessions_send", meta: "completed" }]);
|
||||
expect(result.toolMetas).toEqual([{ toolName: "sessions_send" }]);
|
||||
expect(result.messagesSnapshot.map((message) => message.role)).toEqual([
|
||||
"user",
|
||||
"assistant",
|
||||
@@ -501,6 +501,101 @@ describe("CodexAppServerEventProjector", () => {
|
||||
expect(result.itemLifecycle).toMatchObject({ compactionCount: 1 });
|
||||
});
|
||||
|
||||
it("emits verbose tool summaries through onToolResult", async () => {
|
||||
const onToolResult = vi.fn();
|
||||
const projector = await createProjector({
|
||||
...(await createParams()),
|
||||
verboseLevel: "on",
|
||||
onToolResult,
|
||||
});
|
||||
|
||||
await projector.handleNotification(
|
||||
forCurrentTurn("item/started", {
|
||||
item: {
|
||||
type: "commandExecution",
|
||||
id: "cmd-1",
|
||||
command: "pnpm test extensions/codex",
|
||||
cwd: "/workspace",
|
||||
processId: null,
|
||||
source: "agent",
|
||||
status: "inProgress",
|
||||
commandActions: [],
|
||||
aggregatedOutput: null,
|
||||
exitCode: null,
|
||||
durationMs: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onToolResult).toHaveBeenCalledTimes(1);
|
||||
expect(onToolResult).toHaveBeenCalledWith({
|
||||
text: "🛠️ Bash: `pnpm test extensions/codex`",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses argument details instead of lifecycle status in verbose tool summaries", async () => {
|
||||
const onToolResult = vi.fn();
|
||||
const projector = await createProjector({
|
||||
...(await createParams()),
|
||||
verboseLevel: "on",
|
||||
onToolResult,
|
||||
});
|
||||
|
||||
await projector.handleNotification(
|
||||
forCurrentTurn("item/started", {
|
||||
item: {
|
||||
type: "dynamicToolCall",
|
||||
id: "tool-1",
|
||||
namespace: null,
|
||||
tool: "lcm_grep",
|
||||
arguments: { query: "inProgress text" },
|
||||
status: "inProgress",
|
||||
contentItems: null,
|
||||
success: null,
|
||||
durationMs: null,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(onToolResult).toHaveBeenCalledTimes(1);
|
||||
expect(onToolResult).toHaveBeenCalledWith({
|
||||
text: "🧩 Lcm Grep: `inProgress text`",
|
||||
});
|
||||
});
|
||||
|
||||
it("emits completed tool output only when verbose full is enabled", async () => {
|
||||
const onToolResult = vi.fn();
|
||||
const projector = await createProjector({
|
||||
...(await createParams()),
|
||||
verboseLevel: "full",
|
||||
onToolResult,
|
||||
});
|
||||
|
||||
await projector.handleNotification(
|
||||
turnCompleted([
|
||||
{
|
||||
type: "dynamicToolCall",
|
||||
id: "tool-1",
|
||||
namespace: null,
|
||||
tool: "read",
|
||||
arguments: { path: "README.md" },
|
||||
status: "completed",
|
||||
contentItems: [{ type: "inputText", text: "file contents" }],
|
||||
success: true,
|
||||
durationMs: 12,
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
expect(onToolResult).toHaveBeenCalledTimes(2);
|
||||
expect(onToolResult).toHaveBeenNthCalledWith(1, {
|
||||
text: "📖 Read: `from README.md`",
|
||||
});
|
||||
expect(onToolResult).toHaveBeenNthCalledWith(2, {
|
||||
text: "📖 Read: `from README.md`\n```txt\nfile contents\n```",
|
||||
});
|
||||
});
|
||||
|
||||
it("continues projecting turn completion when an event consumer throws", async () => {
|
||||
const onAgentEvent = vi.fn(() => {
|
||||
throw new Error("consumer failed");
|
||||
|
||||
@@ -3,13 +3,16 @@ import type { AssistantMessage, Usage } from "@mariozechner/pi-ai";
|
||||
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
||||
import {
|
||||
formatErrorMessage,
|
||||
inferToolMetaFromArgs,
|
||||
normalizeUsage,
|
||||
runAgentHarnessAfterCompactionHook,
|
||||
runAgentHarnessBeforeCompactionHook,
|
||||
type EmbeddedRunAttemptParams,
|
||||
type EmbeddedRunAttemptResult,
|
||||
formatToolAggregate,
|
||||
type MessagingToolSend,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { readCodexTurn } from "./protocol-validators.js";
|
||||
import {
|
||||
isJsonObject,
|
||||
type CodexServerNotification,
|
||||
@@ -18,7 +21,6 @@ import {
|
||||
type JsonObject,
|
||||
type JsonValue,
|
||||
} from "./protocol.js";
|
||||
import { readCodexTurn } from "./protocol-validators.js";
|
||||
|
||||
export type CodexAppServerToolTelemetry = {
|
||||
didSendViaMessagingTool: boolean;
|
||||
@@ -62,6 +64,8 @@ export class CodexAppServerEventProjector {
|
||||
private readonly activeItemIds = new Set<string>();
|
||||
private readonly completedItemIds = new Set<string>();
|
||||
private readonly activeCompactionItemIds = new Set<string>();
|
||||
private readonly toolResultSummaryItemIds = new Set<string>();
|
||||
private readonly toolResultOutputItemIds = new Set<string>();
|
||||
private readonly toolMetas = new Map<string, { toolName: string; meta?: string }>();
|
||||
private assistantStarted = false;
|
||||
private reasoningStarted = false;
|
||||
@@ -106,6 +110,12 @@ export class CodexAppServerEventProjector {
|
||||
case "item/completed":
|
||||
await this.handleItemCompleted(params);
|
||||
break;
|
||||
case "item/commandExecution/outputDelta":
|
||||
this.handleOutputDelta(params, "bash");
|
||||
break;
|
||||
case "item/fileChange/outputDelta":
|
||||
this.handleOutputDelta(params, "apply_patch");
|
||||
break;
|
||||
case "item/autoApprovalReview/started":
|
||||
case "item/autoApprovalReview/completed":
|
||||
this.handleGuardianReviewNotification(notification.method, params);
|
||||
@@ -307,6 +317,7 @@ export class CodexAppServerEventProjector {
|
||||
});
|
||||
}
|
||||
this.emitStandardItemEvent({ phase: "start", item });
|
||||
this.emitToolResultSummary(item);
|
||||
this.emitAgentEvent({
|
||||
stream: "codex_app_server.item",
|
||||
data: { phase: "started", itemId, type: item?.type },
|
||||
@@ -359,6 +370,8 @@ export class CodexAppServerEventProjector {
|
||||
}
|
||||
this.recordToolMeta(item);
|
||||
this.emitStandardItemEvent({ phase: "end", item });
|
||||
this.emitToolResultSummary(item);
|
||||
this.emitToolResultOutput(item);
|
||||
this.emitAgentEvent({
|
||||
stream: "codex_app_server.item",
|
||||
data: { phase: "completed", itemId, type: item?.type },
|
||||
@@ -423,11 +436,26 @@ export class CodexAppServerEventProjector {
|
||||
this.emitPlanUpdate({ explanation: undefined, steps: splitPlanText(item.text) });
|
||||
}
|
||||
this.recordToolMeta(item);
|
||||
this.emitToolResultSummary(item);
|
||||
this.emitToolResultOutput(item);
|
||||
}
|
||||
this.activeCompactionItemIds.clear();
|
||||
await this.maybeEndReasoning();
|
||||
}
|
||||
|
||||
private handleOutputDelta(params: JsonObject, toolName: string): void {
|
||||
const itemId = readString(params, "itemId");
|
||||
const delta = readString(params, "delta");
|
||||
if (!itemId || !delta || !this.shouldEmitToolOutput()) {
|
||||
return;
|
||||
}
|
||||
this.emitToolResultMessage({
|
||||
itemId,
|
||||
text: formatToolOutput(toolName, undefined, delta),
|
||||
output: true,
|
||||
});
|
||||
}
|
||||
|
||||
private handleRawResponseItemCompleted(params: JsonObject): void {
|
||||
const item = isJsonObject(params.item) ? params.item : undefined;
|
||||
if (!item || readString(item, "role") !== "assistant") {
|
||||
@@ -492,6 +520,71 @@ export class CodexAppServerEventProjector {
|
||||
});
|
||||
}
|
||||
|
||||
private emitToolResultSummary(item: CodexThreadItem | undefined): void {
|
||||
if (!item || !this.params.onToolResult || !this.shouldEmitToolResult()) {
|
||||
return;
|
||||
}
|
||||
const itemId = item.id;
|
||||
if (this.toolResultSummaryItemIds.has(itemId)) {
|
||||
return;
|
||||
}
|
||||
const toolName = itemName(item);
|
||||
if (!toolName) {
|
||||
return;
|
||||
}
|
||||
this.toolResultSummaryItemIds.add(itemId);
|
||||
const meta = itemMeta(item);
|
||||
this.emitToolResultMessage({
|
||||
itemId,
|
||||
text: formatToolSummary(toolName, meta),
|
||||
});
|
||||
}
|
||||
|
||||
private emitToolResultOutput(item: CodexThreadItem | undefined): void {
|
||||
if (!item || !this.params.onToolResult || !this.shouldEmitToolOutput()) {
|
||||
return;
|
||||
}
|
||||
const itemId = item.id;
|
||||
if (this.toolResultOutputItemIds.has(itemId)) {
|
||||
return;
|
||||
}
|
||||
const toolName = itemName(item);
|
||||
const output = itemOutputText(item);
|
||||
if (!toolName || !output) {
|
||||
return;
|
||||
}
|
||||
this.emitToolResultMessage({
|
||||
itemId,
|
||||
text: formatToolOutput(toolName, itemMeta(item), output),
|
||||
output: true,
|
||||
});
|
||||
}
|
||||
|
||||
private emitToolResultMessage(params: { itemId: string; text: string; output?: boolean }): void {
|
||||
if (params.output) {
|
||||
this.toolResultOutputItemIds.add(params.itemId);
|
||||
}
|
||||
try {
|
||||
void Promise.resolve(this.params.onToolResult?.({ text: params.text })).catch(() => {
|
||||
// Tool progress delivery is best-effort and should not affect the turn.
|
||||
});
|
||||
} catch {
|
||||
// Tool progress delivery is best-effort and should not affect the turn.
|
||||
}
|
||||
}
|
||||
|
||||
private shouldEmitToolResult(): boolean {
|
||||
return typeof this.params.shouldEmitToolResult === "function"
|
||||
? this.params.shouldEmitToolResult()
|
||||
: this.params.verboseLevel === "on" || this.params.verboseLevel === "full";
|
||||
}
|
||||
|
||||
private shouldEmitToolOutput(): boolean {
|
||||
return typeof this.params.shouldEmitToolOutput === "function"
|
||||
? this.params.shouldEmitToolOutput()
|
||||
: this.params.verboseLevel === "full";
|
||||
}
|
||||
|
||||
private recordToolMeta(item: CodexThreadItem | undefined): void {
|
||||
if (!item) {
|
||||
return;
|
||||
@@ -777,7 +870,67 @@ function itemMeta(item: CodexThreadItem): string | undefined {
|
||||
if (item.type === "webSearch" && typeof item.query === "string") {
|
||||
return item.query;
|
||||
}
|
||||
return readItemString(item, "status");
|
||||
const toolName = itemName(item);
|
||||
if ((item.type === "dynamicToolCall" || item.type === "mcpToolCall") && toolName) {
|
||||
return inferToolMetaFromArgs(toolName, item.arguments);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function itemOutputText(item: CodexThreadItem): string | undefined {
|
||||
if (item.type === "commandExecution") {
|
||||
return item.aggregatedOutput?.trim() || undefined;
|
||||
}
|
||||
if (item.type === "dynamicToolCall") {
|
||||
return collectDynamicToolContentText(item.contentItems).trim() || undefined;
|
||||
}
|
||||
if (item.type === "mcpToolCall") {
|
||||
if (item.error) {
|
||||
return stringifyJsonValue(item.error);
|
||||
}
|
||||
return item.result ? stringifyJsonValue(item.result) : undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectDynamicToolContentText(
|
||||
contentItems: Extract<CodexThreadItem, { type: "dynamicToolCall" }>["contentItems"],
|
||||
): string {
|
||||
if (!Array.isArray(contentItems)) {
|
||||
return "";
|
||||
}
|
||||
return contentItems
|
||||
.flatMap((entry) => {
|
||||
if (!isJsonObject(entry)) {
|
||||
return [];
|
||||
}
|
||||
const text = readString(entry, "text");
|
||||
return text ? [text] : [];
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function stringifyJsonValue(value: unknown): string | undefined {
|
||||
try {
|
||||
return JSON.stringify(value, null, 2);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function formatToolSummary(toolName: string, meta?: string): string {
|
||||
const trimmedMeta = meta?.trim();
|
||||
return formatToolAggregate(toolName, trimmedMeta ? [trimmedMeta] : undefined, {
|
||||
markdown: true,
|
||||
});
|
||||
}
|
||||
|
||||
function formatToolOutput(toolName: string, meta: string | undefined, output: string): string {
|
||||
const trimmed = output.trim();
|
||||
if (!trimmed) {
|
||||
return formatToolSummary(toolName, meta);
|
||||
}
|
||||
return `${formatToolSummary(toolName, meta)}\n\`\`\`txt\n${trimmed}\n\`\`\``;
|
||||
}
|
||||
|
||||
function readItemString(item: CodexThreadItem, key: string): string | undefined {
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Keep heavyweight tool construction out of this module so harness imports can
|
||||
// register quickly inside gateway startup and Docker e2e runs.
|
||||
|
||||
import { formatToolDetail, resolveToolDisplay } from "../agents/tool-display.js";
|
||||
|
||||
export type {
|
||||
AgentHarness,
|
||||
AgentHarnessAttemptParams,
|
||||
@@ -37,6 +39,7 @@ export { log as embeddedAgentLog } from "../agents/pi-embedded-runner/logger.js"
|
||||
export { resolveEmbeddedAgentRuntime } from "../agents/pi-embedded-runner/runtime.js";
|
||||
export { resolveUserPath } from "../utils.js";
|
||||
export { callGatewayTool } from "../agents/tools/gateway.js";
|
||||
export { formatToolAggregate } from "../auto-reply/tool-meta.js";
|
||||
export { isMessagingTool, isMessagingToolSendAction } from "../agents/pi-embedded-messaging.js";
|
||||
export {
|
||||
extractToolResultMediaArtifact,
|
||||
@@ -85,3 +88,11 @@ export {
|
||||
runAgentHarnessLlmInputHook,
|
||||
runAgentHarnessLlmOutputHook,
|
||||
} from "../agents/harness/lifecycle-hook-helpers.js";
|
||||
|
||||
/**
|
||||
* Derive the same compact user-facing tool detail that Pi uses for progress logs.
|
||||
*/
|
||||
export function inferToolMetaFromArgs(toolName: string, args: unknown): string | undefined {
|
||||
const display = resolveToolDisplay({ name: toolName, args });
|
||||
return formatToolDetail(display);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user