mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 10:00:42 +00:00
Add structured heartbeat responses and Codex tool replies
* Add structured heartbeat response tool * agents: default codex replies to tools * agents: use flat heartbeat tool enums
This commit is contained in:
@@ -23,6 +23,9 @@ export function createCodexAppServerAgentHarness(options?: {
|
||||
return {
|
||||
id: options?.id ?? "codex",
|
||||
label: options?.label ?? "Codex agent harness",
|
||||
deliveryDefaults: {
|
||||
sourceVisibleReplies: "message_tool",
|
||||
},
|
||||
supports: (ctx) => {
|
||||
const provider = ctx.provider.trim().toLowerCase();
|
||||
if (providerIds.has(provider)) {
|
||||
|
||||
@@ -44,6 +44,7 @@ describe("codex plugin", () => {
|
||||
expect(registerAgentHarness.mock.calls[0]?.[0]).toMatchObject({
|
||||
id: "codex",
|
||||
label: "Codex agent harness",
|
||||
deliveryDefaults: { sourceVisibleReplies: "message_tool" },
|
||||
dispose: expect.any(Function),
|
||||
});
|
||||
expect(registerMediaUnderstandingProvider.mock.calls[0]?.[0]).toMatchObject({
|
||||
@@ -89,6 +90,7 @@ describe("codex plugin", () => {
|
||||
it("only claims the codex provider by default", () => {
|
||||
const harness = createCodexAppServerAgentHarness();
|
||||
|
||||
expect(harness.deliveryDefaults?.sourceVisibleReplies).toBe("message_tool");
|
||||
expect(
|
||||
harness.supports({ provider: "codex", modelId: "gpt-5.4", requestedRuntime: "auto" })
|
||||
.supported,
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { AnyAgentTool } from "openclaw/plugin-sdk/agent-harness";
|
||||
import { wrapToolWithBeforeToolCallHook } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
HEARTBEAT_RESPONSE_TOOL_NAME,
|
||||
wrapToolWithBeforeToolCallHook,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import {
|
||||
initializeGlobalHookRunner,
|
||||
resetGlobalHookRunner,
|
||||
@@ -212,6 +215,38 @@ describe("createCodexDynamicToolBridge", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("records heartbeat response tool outcomes", async () => {
|
||||
const bridge = createBridgeWithToolResult(
|
||||
HEARTBEAT_RESPONSE_TOOL_NAME,
|
||||
textToolResult("Recorded.", {
|
||||
status: "recorded",
|
||||
outcome: "needs_attention",
|
||||
notify: true,
|
||||
summary: "Build is blocked.",
|
||||
notificationText: "Build is blocked on missing credentials.",
|
||||
priority: "high",
|
||||
}),
|
||||
);
|
||||
|
||||
const result = await bridge.handleToolCall({
|
||||
threadId: "thread-1",
|
||||
turnId: "turn-1",
|
||||
callId: "call-1",
|
||||
namespace: null,
|
||||
tool: HEARTBEAT_RESPONSE_TOOL_NAME,
|
||||
arguments: {},
|
||||
});
|
||||
|
||||
expect(result).toEqual(expectInputText("Recorded."));
|
||||
expect(bridge.telemetry.heartbeatToolResponse).toEqual({
|
||||
outcome: "needs_attention",
|
||||
notify: true,
|
||||
summary: "Build is blocked.",
|
||||
notificationText: "Build is blocked on missing credentials.",
|
||||
priority: "high",
|
||||
});
|
||||
});
|
||||
|
||||
it("applies agent tool result middleware from the active plugin registry", async () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const handler = vi.fn(
|
||||
|
||||
@@ -5,11 +5,14 @@ import {
|
||||
createCodexAppServerToolResultExtensionRunner,
|
||||
extractToolResultMediaArtifact,
|
||||
filterToolResultMediaUrls,
|
||||
HEARTBEAT_RESPONSE_TOOL_NAME,
|
||||
isToolWrappedWithBeforeToolCallHook,
|
||||
isMessagingTool,
|
||||
isMessagingToolSendAction,
|
||||
normalizeHeartbeatToolResponse,
|
||||
runAgentHarnessAfterToolCallHook,
|
||||
type AnyAgentTool,
|
||||
type HeartbeatToolResponse,
|
||||
type MessagingToolSend,
|
||||
wrapToolWithBeforeToolCallHook,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
@@ -32,6 +35,7 @@ export type CodexDynamicToolBridge = {
|
||||
messagingToolSentTexts: string[];
|
||||
messagingToolSentMediaUrls: string[];
|
||||
messagingToolSentTargets: MessagingToolSend[];
|
||||
heartbeatToolResponse?: HeartbeatToolResponse;
|
||||
toolMediaUrls: string[];
|
||||
toolAudioAsVoice: boolean;
|
||||
successfulCronAdds?: number;
|
||||
@@ -190,6 +194,12 @@ function collectToolTelemetry(params: {
|
||||
if (!params.isError && params.toolName === "cron" && isCronAddAction(params.args)) {
|
||||
params.telemetry.successfulCronAdds = (params.telemetry.successfulCronAdds ?? 0) + 1;
|
||||
}
|
||||
if (!params.isError && params.toolName === HEARTBEAT_RESPONSE_TOOL_NAME) {
|
||||
const response = normalizeHeartbeatToolResponse(params.result?.details);
|
||||
if (response) {
|
||||
params.telemetry.heartbeatToolResponse = response;
|
||||
}
|
||||
}
|
||||
if (!params.isError && params.result) {
|
||||
const media = extractToolResultMediaArtifact(params.result);
|
||||
if (media) {
|
||||
@@ -256,6 +266,7 @@ function isToolResultError(result: AgentToolResult<unknown>): boolean {
|
||||
status !== "ok" &&
|
||||
status !== "success" &&
|
||||
status !== "completed" &&
|
||||
status !== "recorded" &&
|
||||
status !== "running"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
type AgentMessage,
|
||||
type EmbeddedRunAttemptParams,
|
||||
type EmbeddedRunAttemptResult,
|
||||
type HeartbeatToolResponse,
|
||||
type MessagingToolSend,
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { readCodexTurn } from "./protocol-validators.js";
|
||||
@@ -32,6 +33,7 @@ export type CodexAppServerToolTelemetry = {
|
||||
messagingToolSentTexts: string[];
|
||||
messagingToolSentMediaUrls: string[];
|
||||
messagingToolSentTargets: MessagingToolSend[];
|
||||
heartbeatToolResponse?: HeartbeatToolResponse;
|
||||
toolMediaUrls?: string[];
|
||||
toolAudioAsVoice?: boolean;
|
||||
successfulCronAdds?: number;
|
||||
@@ -232,6 +234,7 @@ export class CodexAppServerEventProjector {
|
||||
messagingToolSentTexts: toolTelemetry.messagingToolSentTexts,
|
||||
messagingToolSentMediaUrls: toolTelemetry.messagingToolSentMediaUrls,
|
||||
messagingToolSentTargets: toolTelemetry.messagingToolSentTargets,
|
||||
heartbeatToolResponse: toolTelemetry.heartbeatToolResponse,
|
||||
toolMediaUrls: toolTelemetry.toolMediaUrls,
|
||||
toolAudioAsVoice: toolTelemetry.toolAudioAsVoice,
|
||||
successfulCronAdds: toolTelemetry.successfulCronAdds,
|
||||
|
||||
@@ -360,12 +360,14 @@ describe("runCodexAppServerAttempt", () => {
|
||||
"update_plan",
|
||||
"web_search",
|
||||
"message",
|
||||
"heartbeat_respond",
|
||||
"sessions_spawn",
|
||||
].map((name) => ({ name }));
|
||||
|
||||
expect(__testing.applyCodexDynamicToolProfile(tools, {}).map((tool) => tool.name)).toEqual([
|
||||
"web_search",
|
||||
"message",
|
||||
"heartbeat_respond",
|
||||
"sessions_spawn",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -1398,6 +1398,8 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
requireExplicitMessageTarget:
|
||||
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
|
||||
disableMessageTool: params.disableMessageTool,
|
||||
enableHeartbeatTool: params.trigger === "heartbeat",
|
||||
forceHeartbeatTool: params.trigger === "heartbeat",
|
||||
onYield: (message) => {
|
||||
input.onYieldDetected();
|
||||
emitCodexAppServerEvent(params, {
|
||||
|
||||
Reference in New Issue
Block a user