Skip to content

Commit 76d1a7e

Browse files
committed
fix(codex): complete dynamic tool diagnostics
Co-authored-by: 0x505badc0de <32790662+rozmiarD@users.noreply.github.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
1 parent f0b43bf commit 76d1a7e

11 files changed

Lines changed: 1069 additions & 111 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
4444

4545
- CLI: enforce the documented Node.js 22.19 runtime floor in the source launcher.
4646
- Agents/replies: persist queued follow-up user messages and assistant error stubs only once across model-fallback retries, preventing repeated provider rejections from corrupted same-role session transcripts. Fixes #83404. (#83417) Thanks @yetval.
47+
- Codex app-server: complete OpenClaw dynamic tool diagnostics at the request boundary so successful, failed, timed out, aborted, and blocked tool calls do not leave active tool state behind. Fixes #83474. Thanks @rozmiarD.
4748
- Gateway/config: keep config writes from failing on unrelated unresolved auth-profile SecretRefs while preserving live auth-profile runtime snapshots.
4849
- Gateway/sessions: clear stored CLI provider resume bindings on non-subagent `/reset` so the next turn starts a fresh provider-side CLI conversation instead of resuming old context. (#83448) Thanks @jasonyliu.
4950
- Discord/OpenAI: keep realtime Discord voice sessions hearing follow-up turns with OpenAI realtime and prebuffer assistant playback to avoid choppy starts. (#80505) Thanks @Solvely-Colin.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
2+
import type { CodexDynamicToolCallParams, CodexDynamicToolCallResponse } from "./protocol.js";
3+
4+
type DynamicToolDiagnosticContext = {
5+
call: CodexDynamicToolCallParams;
6+
runId?: string | undefined;
7+
sessionId?: string | undefined;
8+
sessionKey?: string | undefined;
9+
};
10+
11+
export function emitDynamicToolStartedDiagnostic(params: DynamicToolDiagnosticContext): void {
12+
emitTrustedDiagnosticEvent({
13+
type: "tool.execution.started",
14+
runId: params.runId,
15+
sessionId: params.sessionId,
16+
sessionKey: params.sessionKey,
17+
toolName: params.call.tool,
18+
toolCallId: params.call.callId,
19+
});
20+
}
21+
22+
export function emitDynamicToolErrorDiagnostic(
23+
params: DynamicToolDiagnosticContext & {
24+
durationMs: number;
25+
},
26+
): void {
27+
emitTrustedDiagnosticEvent({
28+
type: "tool.execution.error",
29+
runId: params.runId,
30+
sessionId: params.sessionId,
31+
sessionKey: params.sessionKey,
32+
toolName: params.call.tool,
33+
toolCallId: params.call.callId,
34+
durationMs: params.durationMs,
35+
errorCategory: "codex_dynamic_tool_error",
36+
});
37+
}
38+
39+
export function emitDynamicToolTerminalDiagnostic(
40+
params: DynamicToolDiagnosticContext & {
41+
response: CodexDynamicToolCallResponse;
42+
durationMs: number;
43+
},
44+
): void {
45+
const terminalType =
46+
params.response.diagnosticTerminalType ?? (params.response.success ? "completed" : "error");
47+
if (terminalType === "completed") {
48+
emitTrustedDiagnosticEvent({
49+
type: "tool.execution.completed",
50+
runId: params.runId,
51+
sessionId: params.sessionId,
52+
sessionKey: params.sessionKey,
53+
toolName: params.call.tool,
54+
toolCallId: params.call.callId,
55+
durationMs: params.durationMs,
56+
});
57+
return;
58+
}
59+
if (terminalType === "blocked") {
60+
emitTrustedDiagnosticEvent({
61+
type: "tool.execution.blocked",
62+
runId: params.runId,
63+
sessionId: params.sessionId,
64+
sessionKey: params.sessionKey,
65+
toolName: params.call.tool,
66+
toolCallId: params.call.callId,
67+
deniedReason: "plugin-before-tool-call",
68+
reason: "Tool call blocked",
69+
});
70+
return;
71+
}
72+
emitDynamicToolErrorDiagnostic(params);
73+
}

extensions/codex/src/app-server/dynamic-tools.ts

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
isMessagingToolSendAction,
1313
normalizeHeartbeatToolResponse,
1414
runAgentHarnessAfterToolCallHook,
15+
setBeforeToolCallDiagnosticsEnabled,
1516
type AnyAgentTool,
1617
type HeartbeatToolResponse,
1718
type MessagingToolSend,
@@ -25,6 +26,7 @@ import {
2526
type CodexDynamicToolCallOutputContentItem,
2627
type CodexDynamicToolCallParams,
2728
type CodexDynamicToolCallResponse,
29+
type CodexDynamicToolDiagnosticTerminalType,
2830
type CodexDynamicToolSpec,
2931
type JsonValue,
3032
} from "./protocol.js";
@@ -75,11 +77,13 @@ export function createCodexDynamicToolBridge(params: {
7577
}): CodexDynamicToolBridge {
7678
const toolResultHookContext = toToolResultHookContext(params.hookContext);
7779
const toolResultMaxChars = resolveCodexDynamicToolResultMaxChars(params.hookContext);
78-
const tools = params.tools.map((tool) =>
79-
isToolWrappedWithBeforeToolCallHook(tool)
80-
? tool
81-
: wrapToolWithBeforeToolCallHook(tool, params.hookContext),
82-
);
80+
const tools = params.tools.map((tool) => {
81+
if (isToolWrappedWithBeforeToolCallHook(tool)) {
82+
setBeforeToolCallDiagnosticsEnabled(tool, false);
83+
return tool;
84+
}
85+
return wrapToolWithBeforeToolCallHook(tool, params.hookContext, { emitDiagnostics: false });
86+
});
8387
const toolMap = new Map(tools.map((tool) => [tool.name, tool]));
8488
const telemetry: CodexDynamicToolBridge["telemetry"] = {
8589
didSendViaMessagingTool: false,
@@ -163,10 +167,13 @@ export function createCodexDynamicToolBridge(params: {
163167
result,
164168
startedAt,
165169
});
166-
return {
167-
contentItems: convertToolContents(result.content, toolResultMaxChars),
168-
success: !resultIsError,
169-
};
170+
return withDiagnosticTerminalType(
171+
{
172+
contentItems: convertToolContents(result.content, toolResultMaxChars),
173+
success: !resultIsError,
174+
},
175+
inferToolResultDiagnosticTerminalType(result, resultIsError),
176+
);
170177
} catch (error) {
171178
collectToolTelemetry({
172179
toolName: tool.name,
@@ -187,15 +194,18 @@ export function createCodexDynamicToolBridge(params: {
187194
error: error instanceof Error ? error.message : String(error),
188195
startedAt,
189196
});
190-
return {
191-
contentItems: [
192-
{
193-
type: "inputText",
194-
text: error instanceof Error ? error.message : String(error),
195-
},
196-
],
197-
success: false,
198-
};
197+
return withDiagnosticTerminalType(
198+
{
199+
contentItems: [
200+
{
201+
type: "inputText",
202+
text: error instanceof Error ? error.message : String(error),
203+
},
204+
],
205+
success: false,
206+
},
207+
"error",
208+
);
199209
}
200210
},
201211
};
@@ -427,6 +437,32 @@ function isToolResultError(result: AgentToolResult<unknown>): boolean {
427437
);
428438
}
429439

440+
function inferToolResultDiagnosticTerminalType(
441+
result: AgentToolResult<unknown>,
442+
isError: boolean,
443+
): CodexDynamicToolDiagnosticTerminalType {
444+
const details = result.details;
445+
if (isRecord(details) && typeof details.status === "string") {
446+
const status = details.status.trim().toLowerCase();
447+
if (status === "blocked") {
448+
return "blocked";
449+
}
450+
}
451+
return isError ? "error" : "completed";
452+
}
453+
454+
function withDiagnosticTerminalType<T extends CodexDynamicToolCallResponse>(
455+
response: T,
456+
terminalType: CodexDynamicToolDiagnosticTerminalType,
457+
): T {
458+
Object.defineProperty(response, "diagnosticTerminalType", {
459+
configurable: true,
460+
enumerable: false,
461+
value: terminalType,
462+
});
463+
return response;
464+
}
465+
430466
function normalizeToolResultMaxChars(maxChars: number): number {
431467
return typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0
432468
? Math.floor(maxChars)

extensions/codex/src/app-server/protocol.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,9 +255,12 @@ export type CodexDynamicToolCallParams = {
255255

256256
export type CodexDynamicToolCallResponse = {
257257
contentItems: CodexDynamicToolCallOutputContentItem[];
258+
diagnosticTerminalType?: CodexDynamicToolDiagnosticTerminalType;
258259
success: boolean;
259260
};
260261

262+
export type CodexDynamicToolDiagnosticTerminalType = "blocked" | "completed" | "error";
263+
261264
export type CodexDynamicToolCallOutputContentItem =
262265
| {
263266
type: "inputText";

0 commit comments

Comments
 (0)