mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 07:20:44 +00:00
fix: annotate message-tool-only replies in Codex tool spec
Thread sourceReplyDeliveryMode into Codex/OpenClaw tool construction and annotate the message tool description for message-tool-only turns so visible replies use message(action=send).\n\nAlso adds focused regression coverage and a changelog entry.
This commit is contained in:
committed by
GitHub
parent
657d2331b3
commit
b62166301e
@@ -1,3 +1,4 @@
|
||||
import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js";
|
||||
import { selectApplicableRuntimeConfig } from "../config/config.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { callGateway } from "../gateway/call.js";
|
||||
@@ -117,6 +118,8 @@ export function createOpenClawTools(
|
||||
cronSelfRemoveOnlyJobId?: string;
|
||||
/** Require explicit message targets (no implicit last-route sends). */
|
||||
requireExplicitMessageTarget?: boolean;
|
||||
/** Visible source replies must be sent through the message tool when set to message_tool_only. */
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
/** If true, omit the message tool from the tool list. */
|
||||
disableMessageTool?: boolean;
|
||||
/** If true, include the heartbeat response tool for structured heartbeat outcomes. */
|
||||
@@ -294,6 +297,7 @@ export function createOpenClawTools(
|
||||
hasRepliedRef: options?.hasRepliedRef,
|
||||
sandboxRoot: options?.sandboxRoot,
|
||||
requireExplicitTarget: options?.requireExplicitMessageTarget,
|
||||
sourceReplyDeliveryMode: options?.sourceReplyDeliveryMode,
|
||||
requesterSenderId: options?.requesterSenderId ?? undefined,
|
||||
senderIsOwner: options?.senderIsOwner,
|
||||
});
|
||||
|
||||
@@ -721,6 +721,7 @@ async function compactEmbeddedPiSessionDirectOnce(
|
||||
workspaceDir: effectiveWorkspace,
|
||||
config: params.config,
|
||||
abortSignal: runAbortController.signal,
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
modelProvider: model.provider,
|
||||
modelId,
|
||||
modelCompat: extractModelCompat(effectiveModel),
|
||||
|
||||
@@ -901,6 +901,7 @@ export async function runEmbeddedAttempt(
|
||||
modelHasVision: params.model.input?.includes("image") ?? false,
|
||||
requireExplicitMessageTarget:
|
||||
params.requireExplicitMessageTarget ?? isSubagentSessionKey(params.sessionKey),
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
disableMessageTool: params.disableMessageTool,
|
||||
forceMessageTool: params.forceMessageTool,
|
||||
enableHeartbeatTool: params.enableHeartbeatTool,
|
||||
|
||||
@@ -200,6 +200,23 @@ describe("createOpenClawCodingTools", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes source reply delivery mode to OpenClaw tool construction", () => {
|
||||
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
|
||||
createOpenClawToolsMock.mockClear();
|
||||
|
||||
createOpenClawCodingTools({
|
||||
config: testConfig,
|
||||
forceMessageTool: true,
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
});
|
||||
|
||||
expect(createOpenClawToolsMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("skips unrelated tool families when construction is planned from a narrow allowlist", () => {
|
||||
const createOpenClawToolsMock = vi.mocked(createOpenClawTools);
|
||||
createOpenClawToolsMock.mockClear();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createCodingTools, createReadTool } from "@mariozechner/pi-coding-agent";
|
||||
import type { SourceReplyDeliveryMode } from "../auto-reply/get-reply-options.types.js";
|
||||
import { HEARTBEAT_RESPONSE_TOOL_NAME } from "../auto-reply/heartbeat-tool-response.js";
|
||||
import type { ModelCompatConfig } from "../config/types.models.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
@@ -341,6 +342,8 @@ export function createOpenClawCodingTools(options?: {
|
||||
modelHasVision?: boolean;
|
||||
/** Require explicit message targets (no implicit last-route sends). */
|
||||
requireExplicitMessageTarget?: boolean;
|
||||
/** Visible source replies must be sent through the message tool when set to message_tool_only. */
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
/** If true, omit the message tool from the tool list. */
|
||||
disableMessageTool?: boolean;
|
||||
/** Keep the message tool available even when the selected profile omits it. */
|
||||
@@ -743,6 +746,7 @@ export function createOpenClawCodingTools(options?: {
|
||||
hasRepliedRef: options?.hasRepliedRef,
|
||||
modelHasVision: options?.modelHasVision,
|
||||
requireExplicitMessageTarget: options?.requireExplicitMessageTarget,
|
||||
sourceReplyDeliveryMode: options?.sourceReplyDeliveryMode,
|
||||
disableMessageTool: options?.disableMessageTool,
|
||||
enableHeartbeatTool,
|
||||
disablePluginTools: !includePluginTools,
|
||||
|
||||
@@ -325,6 +325,41 @@ async function executeSend(params: {
|
||||
}
|
||||
|
||||
describe("message tool secret scoping", () => {
|
||||
it("marks message-tool-only source replies in the tool description", () => {
|
||||
const scopedTool = createMessageTool({
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
});
|
||||
const explicitTargetTool = createMessageTool({
|
||||
requireExplicitTarget: true,
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
});
|
||||
const defaultTool = createMessageTool();
|
||||
|
||||
expect(scopedTool.description).toContain(
|
||||
'visible replies to the current source conversation must use action="send"',
|
||||
);
|
||||
expect(scopedTool.description).toContain("target defaults to the current source conversation");
|
||||
expect(scopedTool.description).toContain("Normal final answers are private");
|
||||
expect(explicitTargetTool.description).toContain("Include target when sending");
|
||||
expect(explicitTargetTool.description).not.toContain(
|
||||
"target defaults to the current source conversation",
|
||||
);
|
||||
expect(defaultTool.description).not.toContain(
|
||||
"visible replies to the current source conversation",
|
||||
);
|
||||
});
|
||||
|
||||
it("forwards source reply delivery mode through createOpenClawTools", () => {
|
||||
const tool = createOpenClawTools({
|
||||
config: {} as never,
|
||||
sourceReplyDeliveryMode: "message_tool_only",
|
||||
}).find((candidate) => candidate.name === "message");
|
||||
|
||||
expect(tool?.description).toContain(
|
||||
'visible replies to the current source conversation must use action="send"',
|
||||
);
|
||||
});
|
||||
|
||||
it("scopes command-time secret resolution to the selected channel/account", async () => {
|
||||
mockSendResult({ channel: "discord", to: "discord:123" });
|
||||
mocks.getRuntimeConfig.mockReturnValue({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Type, type TSchema } from "typebox";
|
||||
import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js";
|
||||
import { listChannelPlugins } from "../../channels/plugins/index.js";
|
||||
import {
|
||||
channelSupportsMessageCapability,
|
||||
@@ -520,6 +521,7 @@ type MessageToolOptions = {
|
||||
hasRepliedRef?: { value: boolean };
|
||||
sandboxRoot?: string;
|
||||
requireExplicitTarget?: boolean;
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
requesterSenderId?: string;
|
||||
senderIsOwner?: boolean;
|
||||
};
|
||||
@@ -648,6 +650,8 @@ function buildMessageToolDescription(options?: {
|
||||
sessionKey?: string;
|
||||
sessionId?: string;
|
||||
agentId?: string;
|
||||
requireExplicitTarget?: boolean;
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
requesterSenderId?: string;
|
||||
senderIsOwner?: boolean;
|
||||
}): string {
|
||||
@@ -679,13 +683,35 @@ function buildMessageToolDescription(options?: {
|
||||
ChannelMessageActionName | "send"
|
||||
>;
|
||||
return appendMessageToolReadHint(
|
||||
`${baseDescription} Supports actions: ${sortedActions.join(", ")}.`,
|
||||
appendMessageToolVisibleReplyHint(
|
||||
`${baseDescription} Supports actions: ${sortedActions.join(", ")}.`,
|
||||
resolvedOptions.sourceReplyDeliveryMode,
|
||||
resolvedOptions.requireExplicitTarget,
|
||||
),
|
||||
sortedActions,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return `${baseDescription} Supports actions: send, delete, react, poll, pin, threads, and more.`;
|
||||
return appendMessageToolVisibleReplyHint(
|
||||
`${baseDescription} Supports actions: send, delete, react, poll, pin, threads, and more.`,
|
||||
resolvedOptions.sourceReplyDeliveryMode,
|
||||
resolvedOptions.requireExplicitTarget,
|
||||
);
|
||||
}
|
||||
|
||||
function appendMessageToolVisibleReplyHint(
|
||||
description: string,
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode,
|
||||
requireExplicitTarget?: boolean,
|
||||
): string {
|
||||
if (sourceReplyDeliveryMode !== "message_tool_only") {
|
||||
return description;
|
||||
}
|
||||
const targetGuidance = requireExplicitTarget
|
||||
? "Include target when sending."
|
||||
: "The target defaults to the current source conversation, so omit target unless sending elsewhere.";
|
||||
return `${description} For this turn, visible replies to the current source conversation must use action="send" with message. ${targetGuidance} Normal final answers are private and are not posted.`;
|
||||
}
|
||||
|
||||
function appendMessageToolReadHint(
|
||||
@@ -743,6 +769,8 @@ export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
sessionKey: options?.agentSessionKey,
|
||||
sessionId: options?.sessionId,
|
||||
agentId: resolvedAgentId,
|
||||
requireExplicitTarget: options?.requireExplicitTarget,
|
||||
sourceReplyDeliveryMode: options?.sourceReplyDeliveryMode,
|
||||
requesterSenderId: options?.requesterSenderId,
|
||||
senderIsOwner: options?.senderIsOwner,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user