mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(ci): restore LOC ratchet headroom (#106063)
* refactor(ci): restore TypeScript LOC ratchet * fix(ci): keep source reply item type internal
This commit is contained in:
committed by
GitHub
parent
9cdf166d2e
commit
6410e33cb0
@@ -0,0 +1,37 @@
|
||||
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import { asOptionalRecord as readRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
/** Resolves an agent override before falling back to the configured default. */
|
||||
export function resolveAgentContextLimitValue(params: {
|
||||
config: EmbeddedRunAttemptParams["config"] | undefined;
|
||||
agentId?: string;
|
||||
key: string;
|
||||
}): number | undefined {
|
||||
const agents = readRecord(params.config?.agents);
|
||||
const defaults = readRecord(readRecord(agents?.defaults)?.contextLimits);
|
||||
const defaultValue = readPositiveInteger(defaults?.[params.key]);
|
||||
if (!params.agentId) {
|
||||
return defaultValue;
|
||||
}
|
||||
const list = agents?.list;
|
||||
if (!Array.isArray(list)) {
|
||||
return defaultValue;
|
||||
}
|
||||
const normalizedAgentId = normalizeAgentId(params.agentId);
|
||||
const agent = list.find((entry) => {
|
||||
const entryId = readRecord(entry)?.id;
|
||||
return typeof entryId === "string" && normalizeAgentId(entryId) === normalizedAgentId;
|
||||
});
|
||||
const agentValue = readPositiveInteger(
|
||||
readRecord(readRecord(agent)?.contextLimits)?.[params.key],
|
||||
);
|
||||
return agentValue ?? defaultValue;
|
||||
}
|
||||
|
||||
function readPositiveInteger(value: unknown): number | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.floor(value);
|
||||
}
|
||||
@@ -28,7 +28,9 @@ import {
|
||||
isForcedPrivateQaCodexRuntime,
|
||||
normalizeCodexDynamicToolName,
|
||||
} from "./dynamic-tool-profile.js";
|
||||
import { addCodexMessageToolOnlyFinalControl } from "./message-tool-final-control.js";
|
||||
import {
|
||||
resolveCodexNodeExecToolOverrides,
|
||||
resolveCodexNativeExecutionPolicy,
|
||||
type CodexNativeExecutionPolicy,
|
||||
} from "./native-execution-policy.js";
|
||||
@@ -40,7 +42,6 @@ import { resolveCodexWebSearchPlan, type CodexNativeWebSearchSupport } from "./w
|
||||
type OpenClawCodingToolsOptions = NonNullable<
|
||||
Parameters<(typeof import("openclaw/plugin-sdk/agent-harness"))["createOpenClawCodingTools"]>[0]
|
||||
>;
|
||||
type OpenClawExecOptions = NonNullable<OpenClawCodingToolsOptions["exec"]>;
|
||||
|
||||
/** Factory seam for constructing OpenClaw runtime tools without eagerly loading agent-harness. */
|
||||
type OpenClawCodingToolsFactory =
|
||||
@@ -231,7 +232,7 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
...buildEmbeddedAttemptToolRunContext(params),
|
||||
exec: {
|
||||
...params.execOverrides,
|
||||
...resolveNodeExecToolOverrides(nativeExecutionPolicy),
|
||||
...resolveCodexNodeExecToolOverrides(nativeExecutionPolicy),
|
||||
config: params.config,
|
||||
elevated: params.bashElevated,
|
||||
},
|
||||
@@ -881,51 +882,6 @@ function hideNodeExecDynamicToolParameters(
|
||||
...(Array.isArray(rawRequired) ? { required: nextRequired } : {}),
|
||||
};
|
||||
}
|
||||
/**
|
||||
* `final` is a Codex-only control for message-tool-only source delivery. Keep
|
||||
* it on the projected Codex schema so other agent runtimes never receive an
|
||||
* API contract they do not implement.
|
||||
*/
|
||||
function addCodexMessageToolOnlyFinalControl(
|
||||
tools: OpenClawDynamicTool[],
|
||||
sourceReplyDeliveryMode: EmbeddedRunAttemptParams["sourceReplyDeliveryMode"],
|
||||
): OpenClawDynamicTool[] {
|
||||
if (sourceReplyDeliveryMode !== "message_tool_only") {
|
||||
return tools;
|
||||
}
|
||||
// allTools is attempt-fresh from createOpenClawCodingTools inside
|
||||
// buildDynamicTools — never a shared/cached instance across attempts or
|
||||
// delivery modes. Project the Codex-only `final` property in place so
|
||||
// WeakMap ownership metadata stays attached without a public SDK clone helper.
|
||||
for (const tool of tools) {
|
||||
if (normalizeCodexDynamicToolName(tool.name) !== "message") {
|
||||
continue;
|
||||
}
|
||||
tool.parameters = addCodexMessageToolOnlyFinalParameter(tool.parameters);
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
function addCodexMessageToolOnlyFinalParameter(parameters: OpenClawDynamicTool["parameters"]) {
|
||||
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
||||
return parameters;
|
||||
}
|
||||
const schema = parameters as Record<string, unknown>;
|
||||
const rawProperties = schema.properties;
|
||||
if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) {
|
||||
return parameters;
|
||||
}
|
||||
return {
|
||||
...schema,
|
||||
properties: {
|
||||
...rawProperties,
|
||||
final: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Set true only when this message is intended to complete the reply to the current source conversation. OpenClaw stops after confirming delivery.",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
function resolveCodexNativeExecutionPolicyForDynamicTools(
|
||||
input: DynamicToolBuildParams,
|
||||
): CodexNativeExecutionPolicy {
|
||||
@@ -939,18 +895,6 @@ function resolveCodexNativeExecutionPolicyForDynamicTools(
|
||||
readRuntimeSessionEntry: true,
|
||||
});
|
||||
}
|
||||
function resolveNodeExecToolOverrides(
|
||||
policy: CodexNativeExecutionPolicy,
|
||||
): Pick<OpenClawExecOptions, "host" | "node"> | undefined {
|
||||
if (policy.effectiveExecHost !== "node") {
|
||||
return undefined;
|
||||
}
|
||||
const node = policy.node?.trim();
|
||||
return {
|
||||
host: "node",
|
||||
...(node ? { node } : {}),
|
||||
};
|
||||
}
|
||||
/** Applies a normalized tool allowlist while preserving shell aliases for exec/process. */
|
||||
function filterCodexDynamicToolsForAllowlist<T extends { name: string }>(
|
||||
tools: T[],
|
||||
|
||||
@@ -44,12 +44,9 @@ import {
|
||||
import { emitTrustedDiagnosticEvent } from "openclaw/plugin-sdk/diagnostic-runtime";
|
||||
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
||||
import type { ImageContent, TextContent } from "openclaw/plugin-sdk/llm";
|
||||
import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
|
||||
import {
|
||||
asOptionalRecord as readRecord,
|
||||
isRecord,
|
||||
} from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { isRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { resolveAgentContextLimitValue } from "./agent-context-limits.js";
|
||||
import type { CodexDynamicToolsLoading } from "./config.js";
|
||||
import { invalidInlineImageText, sanitizeInlineImageDataUrl } from "./image-payload-sanitizer.js";
|
||||
import {
|
||||
@@ -1123,31 +1120,6 @@ function resolveCodexDynamicToolResultMaxChars(
|
||||
});
|
||||
return configured ?? DEFAULT_CODEX_DYNAMIC_TOOL_RESULT_MAX_CHARS;
|
||||
}
|
||||
function resolveAgentContextLimitValue(params: {
|
||||
config: EmbeddedRunAttemptParams["config"] | undefined;
|
||||
agentId?: string;
|
||||
key: string;
|
||||
}): number | undefined {
|
||||
const agents = readRecord(params.config?.agents);
|
||||
const defaults = readRecord(readRecord(agents?.defaults)?.contextLimits);
|
||||
const defaultValue = readPositiveInteger(defaults?.[params.key]);
|
||||
if (!params.agentId) {
|
||||
return defaultValue;
|
||||
}
|
||||
const list = agents?.list;
|
||||
if (!Array.isArray(list)) {
|
||||
return defaultValue;
|
||||
}
|
||||
const normalizedAgentId = normalizeAgentId(params.agentId);
|
||||
const agent = list.find((entry) => {
|
||||
const entryId = readRecord(entry)?.id;
|
||||
return typeof entryId === "string" && normalizeAgentId(entryId) === normalizedAgentId;
|
||||
});
|
||||
const agentValue = readPositiveInteger(
|
||||
readRecord(readRecord(agent)?.contextLimits)?.[params.key],
|
||||
);
|
||||
return agentValue ?? defaultValue;
|
||||
}
|
||||
function composeAbortSignals(...signals: Array<AbortSignal | undefined>): AbortSignal {
|
||||
const activeSignals = signals.filter((signal): signal is AbortSignal => Boolean(signal));
|
||||
if (activeSignals.length === 0) {
|
||||
@@ -1283,12 +1255,6 @@ function extractInternalSourceReplyPayload(
|
||||
? payload
|
||||
: undefined;
|
||||
}
|
||||
function readPositiveInteger(value: unknown): number | undefined {
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return Math.floor(value);
|
||||
}
|
||||
function isCodexToolResultError(result: AgentToolResult<unknown>): boolean {
|
||||
if (isToolResultError(result)) {
|
||||
return true;
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { EmbeddedRunAttemptParams } from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { normalizeCodexDynamicToolName } from "./dynamic-tool-profile.js";
|
||||
|
||||
type MutableDynamicTool = {
|
||||
name: string;
|
||||
parameters?: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* `final` is a Codex-only control for message-tool-only source delivery. Keep
|
||||
* it on the projected Codex schema so other agent runtimes never receive an
|
||||
* API contract they do not implement.
|
||||
*/
|
||||
export function addCodexMessageToolOnlyFinalControl<T extends MutableDynamicTool>(
|
||||
tools: T[],
|
||||
sourceReplyDeliveryMode: EmbeddedRunAttemptParams["sourceReplyDeliveryMode"],
|
||||
): T[] {
|
||||
if (sourceReplyDeliveryMode !== "message_tool_only") {
|
||||
return tools;
|
||||
}
|
||||
// These tools are attempt-fresh. Mutating preserves their WeakMap ownership
|
||||
// metadata without exposing a clone helper through the public plugin SDK.
|
||||
for (const tool of tools) {
|
||||
if (normalizeCodexDynamicToolName(tool.name) === "message") {
|
||||
const mutableTool: MutableDynamicTool = tool;
|
||||
mutableTool.parameters = addCodexMessageToolOnlyFinalParameter(mutableTool.parameters);
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
function addCodexMessageToolOnlyFinalParameter(parameters: unknown): unknown {
|
||||
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) {
|
||||
return parameters;
|
||||
}
|
||||
const schema = parameters as Record<string, unknown>;
|
||||
const rawProperties = schema.properties;
|
||||
if (!rawProperties || typeof rawProperties !== "object" || Array.isArray(rawProperties)) {
|
||||
return parameters;
|
||||
}
|
||||
return {
|
||||
...schema,
|
||||
properties: {
|
||||
...rawProperties,
|
||||
final: {
|
||||
type: "boolean",
|
||||
description:
|
||||
"Set true only when this message is intended to complete the reply to the current source conversation. OpenClaw stops after confirming delivery.",
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -31,6 +31,17 @@ export type CodexNativeExecutionPolicy = {
|
||||
blockReason?: string;
|
||||
};
|
||||
|
||||
/** Projects node execution ownership into the runtime tool factory options. */
|
||||
export function resolveCodexNodeExecToolOverrides(
|
||||
policy: CodexNativeExecutionPolicy,
|
||||
): { host: "node"; node?: string } | undefined {
|
||||
if (policy.effectiveExecHost !== "node") {
|
||||
return undefined;
|
||||
}
|
||||
const node = policy.node?.trim();
|
||||
return { host: "node", ...(node ? { node } : {}) };
|
||||
}
|
||||
|
||||
/** Resolves node/gateway/sandbox execution ownership from overrides, session, agent, and config. */
|
||||
export function resolveCodexNativeExecutionPolicy(params: {
|
||||
config?: OpenClawConfig;
|
||||
|
||||
@@ -45,9 +45,9 @@
|
||||
"extensions/codex/src/app-server/computer-use.ts": 1264,
|
||||
"extensions/codex/src/app-server/config.ts": 2492,
|
||||
"extensions/codex/src/app-server/context-engine-projection.ts": 514,
|
||||
"extensions/codex/src/app-server/dynamic-tool-build.ts": 992,
|
||||
"extensions/codex/src/app-server/dynamic-tool-build.ts": 936,
|
||||
"extensions/codex/src/app-server/dynamic-tool-execution.ts": 583,
|
||||
"extensions/codex/src/app-server/dynamic-tools.ts": 1550,
|
||||
"extensions/codex/src/app-server/dynamic-tools.ts": 1516,
|
||||
"extensions/codex/src/app-server/elicitation-bridge.ts": 993,
|
||||
"extensions/codex/src/app-server/event-projector.ts": 3499,
|
||||
"extensions/codex/src/app-server/native-subagent-monitor.ts": 1631,
|
||||
@@ -551,7 +551,7 @@
|
||||
"src/agents/embedded-agent-runner/model.ts": 1995,
|
||||
"src/agents/embedded-agent-runner/replay-history.ts": 937,
|
||||
"src/agents/embedded-agent-runner/run.overflow-compaction.harness.ts": 953,
|
||||
"src/agents/embedded-agent-runner/run.ts": 5082,
|
||||
"src/agents/embedded-agent-runner/run.ts": 5081,
|
||||
"src/agents/embedded-agent-runner/run/attempt.llm-boundary.ts": 637,
|
||||
"src/agents/embedded-agent-runner/run/attempt.model-diagnostic-events.ts": 894,
|
||||
"src/agents/embedded-agent-runner/run/attempt.prompt-helpers.ts": 721,
|
||||
@@ -563,7 +563,7 @@
|
||||
"src/agents/embedded-agent-runner/run/images.ts": 665,
|
||||
"src/agents/embedded-agent-runner/run/incomplete-turn.ts": 823,
|
||||
"src/agents/embedded-agent-runner/run/llm-idle-timeout.ts": 615,
|
||||
"src/agents/embedded-agent-runner/run/payloads.ts": 1018,
|
||||
"src/agents/embedded-agent-runner/run/payloads.ts": 968,
|
||||
"src/agents/embedded-agent-runner/runs.ts": 950,
|
||||
"src/agents/embedded-agent-runner/thinking.ts": 767,
|
||||
"src/agents/embedded-agent-runner/tool-result-context-guard.ts": 573,
|
||||
@@ -653,7 +653,7 @@
|
||||
"src/agents/tools/image-tool.ts": 1095,
|
||||
"src/agents/tools/media-generate-background-shared.ts": 865,
|
||||
"src/agents/tools/media-tool-shared.ts": 689,
|
||||
"src/agents/tools/message-tool.ts": 1756,
|
||||
"src/agents/tools/message-tool.ts": 1733,
|
||||
"src/agents/tools/music-generate-tool.ts": 853,
|
||||
"src/agents/tools/pdf-tool.ts": 563,
|
||||
"src/agents/tools/session-status-tool.ts": 934,
|
||||
@@ -677,7 +677,7 @@
|
||||
"src/auto-reply/reply/agent-runner-execution.ts": 3419,
|
||||
"src/auto-reply/reply/agent-runner-memory.ts": 1586,
|
||||
"src/auto-reply/reply/agent-runner-payloads.ts": 514,
|
||||
"src/auto-reply/reply/agent-runner.ts": 2858,
|
||||
"src/auto-reply/reply/agent-runner.ts": 2853,
|
||||
"src/auto-reply/reply/commands-acp/lifecycle.ts": 895,
|
||||
"src/auto-reply/reply/commands-acp/shared.ts": 539,
|
||||
"src/auto-reply/reply/commands-allowlist.ts": 607,
|
||||
@@ -693,7 +693,7 @@
|
||||
"src/auto-reply/reply/dispatch-acp-delivery.ts": 542,
|
||||
"src/auto-reply/reply/dispatch-acp.ts": 812,
|
||||
"src/auto-reply/reply/dispatch-from-config.ts": 4562,
|
||||
"src/auto-reply/reply/followup-runner.ts": 2065,
|
||||
"src/auto-reply/reply/followup-runner.ts": 2059,
|
||||
"src/auto-reply/reply/get-reply-directives-apply.ts": 537,
|
||||
"src/auto-reply/reply/get-reply-directives.ts": 738,
|
||||
"src/auto-reply/reply/get-reply-inline-actions.ts": 641,
|
||||
|
||||
@@ -30,6 +30,7 @@ type AgentDeliveryEvidence = {
|
||||
errorMessage?: unknown;
|
||||
};
|
||||
didSendViaMessagingTool?: unknown;
|
||||
didSendDeterministicApprovalPrompt?: unknown;
|
||||
messagingToolSentTexts?: unknown;
|
||||
messagingToolSentMediaUrls?: unknown;
|
||||
messagingToolSentTargets?: unknown;
|
||||
@@ -86,6 +87,18 @@ export function hasCompletedSourceReplyDeliveryEvidence(
|
||||
);
|
||||
}
|
||||
|
||||
/** Returns whether delivery evidence completes the current interactive turn. */
|
||||
export function hasCompletedTerminalDeliveryEvidence(
|
||||
result: AgentDeliveryEvidence & SourceReplyDeliveryEvidence & ExplicitFinalSourceReplyEvidence,
|
||||
): boolean {
|
||||
const explicitFinal = resolveExplicitFinalSourceReplyDeliveryEvidence(result);
|
||||
return (
|
||||
hasCompletedSourceReplyDeliveryEvidence(result) ||
|
||||
(explicitFinal === undefined && hasVisibleOutboundDeliveryEvidence(result)) ||
|
||||
result.didSendDeterministicApprovalPrompt === true
|
||||
);
|
||||
}
|
||||
|
||||
function hasNonEmptyString(value: unknown): value is string {
|
||||
return typeof value === "string" && value.trim().length > 0;
|
||||
}
|
||||
|
||||
@@ -4317,7 +4317,6 @@ async function runEmbeddedAgentInternal(
|
||||
};
|
||||
const finalAssistantVisibleText = resolveFinalAssistantVisibleText(attemptAssistant);
|
||||
const finalAssistantRawText = resolveFinalAssistantRawText(attemptAssistant);
|
||||
|
||||
const payloads = buildEmbeddedRunPayloads({
|
||||
assistantTexts: attempt.assistantTexts,
|
||||
assistantMessageIndex: attempt.lastAssistantTextMessageIndex,
|
||||
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
} from "../../embedded-agent-utils.js";
|
||||
import { isExecLikeToolName, type ToolErrorSummary } from "../../tool-error-summary.js";
|
||||
import { isLikelyMutatingToolName } from "../../tool-mutation.js";
|
||||
import { resolveExplicitFinalSourceReplyDeliveryEvidence } from "../delivery-evidence.js";
|
||||
import { buildSourceReplyPayloadState } from "./source-reply-payloads.js";
|
||||
|
||||
type ToolMetaEntry = { toolName: string; meta?: string };
|
||||
type ToolErrorWarningPolicy = {
|
||||
@@ -586,71 +586,21 @@ export function buildEmbeddedRunPayloads(params: {
|
||||
if (params.heartbeatToolResponse) {
|
||||
return [createHeartbeatToolResponsePayload(params.heartbeatToolResponse)];
|
||||
}
|
||||
const replyItems: Array<{
|
||||
text: string;
|
||||
media?: string[];
|
||||
mediaUrl?: string;
|
||||
isError?: boolean;
|
||||
isReasoning?: boolean;
|
||||
/** Marks pre-tool commentary (💬) — a display lane, suppressed unless the channel opts in. */
|
||||
isCommentary?: boolean;
|
||||
audioAsVoice?: boolean;
|
||||
replyToId?: string;
|
||||
replyToTag?: boolean;
|
||||
replyToCurrent?: boolean;
|
||||
presentation?: ReplyPayload["presentation"];
|
||||
interactive?: ReplyPayload["interactive"];
|
||||
channelData?: Record<string, unknown>;
|
||||
nonTerminalToolErrorWarning?: boolean;
|
||||
sourceReplyMirror?: {
|
||||
idempotencyKey?: string;
|
||||
};
|
||||
}> = [];
|
||||
// Internal source replies always need transcript/UI mirror payloads. Only a
|
||||
// Internal source replies always need transcript/UI mirrors. Only a
|
||||
// message_tool_only run suppresses the separate automatic final answer.
|
||||
const sourceReplyPayloads = params.messagingToolSourceReplyPayloads ?? [];
|
||||
const sourceReplyStartIndex = replyItems.length;
|
||||
sourceReplyPayloads.forEach((payload, index) => {
|
||||
const text = normalizeOptionalString(payload.text) ?? "";
|
||||
const media = Array.from(
|
||||
new Set([...(payload.mediaUrl ? [payload.mediaUrl] : []), ...(payload.mediaUrls ?? [])]),
|
||||
).filter((value) => value.trim().length > 0);
|
||||
if (
|
||||
!text &&
|
||||
media.length === 0 &&
|
||||
!payload.presentation &&
|
||||
!payload.interactive &&
|
||||
!payload.channelData
|
||||
) {
|
||||
return;
|
||||
}
|
||||
// Message-tool-only replies were already sent by the tool. Mirror them into
|
||||
// the transcript while marking payloads so channel delivery suppresses a duplicate send.
|
||||
replyItems.push({
|
||||
text,
|
||||
...(payload.mediaUrl ? { mediaUrl: payload.mediaUrl } : {}),
|
||||
...(media.length ? { media } : {}),
|
||||
...(payload.audioAsVoice ? { audioAsVoice: true } : {}),
|
||||
...(payload.presentation ? { presentation: payload.presentation } : {}),
|
||||
...(payload.interactive ? { interactive: payload.interactive } : {}),
|
||||
...(payload.channelData ? { channelData: payload.channelData } : {}),
|
||||
sourceReplyMirror: {
|
||||
idempotencyKey:
|
||||
payload.idempotencyKey ??
|
||||
(params.runId ? `${params.runId}:internal-source-reply:${index}` : undefined),
|
||||
},
|
||||
});
|
||||
const {
|
||||
replyItems,
|
||||
hasSourceReplyPayload,
|
||||
deliveredSourceReplyViaMessageTool,
|
||||
explicitFinalSourceReply,
|
||||
completedSourceReplyViaMessageTool,
|
||||
} = buildSourceReplyPayloadState({
|
||||
payloads: params.messagingToolSourceReplyPayloads,
|
||||
sentTargets: params.messagingToolSentTargets,
|
||||
sourceReplyDeliveryMode: params.sourceReplyDeliveryMode,
|
||||
didDeliverSourceReplyViaMessageTool: params.didDeliverSourceReplyViaMessageTool,
|
||||
runId: params.runId,
|
||||
});
|
||||
const hasSourceReplyPayload = replyItems.length > sourceReplyStartIndex;
|
||||
const deliveredSourceReplyViaMessageTool =
|
||||
params.sourceReplyDeliveryMode === "message_tool_only" &&
|
||||
params.didDeliverSourceReplyViaMessageTool === true;
|
||||
const explicitFinalSourceReply = resolveExplicitFinalSourceReplyDeliveryEvidence({
|
||||
messagingToolSentTargets: params.messagingToolSentTargets,
|
||||
messagingToolSourceReplyPayloads: sourceReplyPayloads,
|
||||
});
|
||||
const completedSourceReplyViaMessageTool =
|
||||
explicitFinalSourceReply ?? (hasSourceReplyPayload || deliveredSourceReplyViaMessageTool);
|
||||
const useMarkdown = params.toolResultFormat === "markdown";
|
||||
const suppressAssistantArtifacts =
|
||||
params.didSendDeterministicApprovalPrompt === true ||
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import type { SourceReplyDeliveryMode } from "../../../auto-reply/get-reply-options.types.js";
|
||||
import type { ReplyPayload } from "../../../auto-reply/reply-payload.js";
|
||||
import type {
|
||||
MessagingToolSend,
|
||||
MessagingToolSourceReplyPayload,
|
||||
} from "../../embedded-agent-messaging.types.js";
|
||||
import { resolveExplicitFinalSourceReplyDeliveryEvidence } from "../delivery-evidence.js";
|
||||
|
||||
type EmbeddedRunReplyItem = {
|
||||
text: string;
|
||||
media?: string[];
|
||||
mediaUrl?: string;
|
||||
isError?: boolean;
|
||||
isReasoning?: boolean;
|
||||
/** Marks pre-tool commentary (💬) — a display lane, suppressed unless the channel opts in. */
|
||||
isCommentary?: boolean;
|
||||
audioAsVoice?: boolean;
|
||||
replyToId?: string;
|
||||
replyToTag?: boolean;
|
||||
replyToCurrent?: boolean;
|
||||
presentation?: ReplyPayload["presentation"];
|
||||
interactive?: ReplyPayload["interactive"];
|
||||
channelData?: Record<string, unknown>;
|
||||
nonTerminalToolErrorWarning?: boolean;
|
||||
sourceReplyMirror?: { idempotencyKey?: string };
|
||||
};
|
||||
|
||||
/** Builds transcript mirrors and completion evidence for message-tool source replies. */
|
||||
export function buildSourceReplyPayloadState(params: {
|
||||
payloads?: MessagingToolSourceReplyPayload[];
|
||||
sentTargets?: MessagingToolSend[];
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode;
|
||||
didDeliverSourceReplyViaMessageTool?: boolean;
|
||||
runId?: string;
|
||||
}): {
|
||||
replyItems: EmbeddedRunReplyItem[];
|
||||
hasSourceReplyPayload: boolean;
|
||||
deliveredSourceReplyViaMessageTool: boolean;
|
||||
explicitFinalSourceReply: boolean | undefined;
|
||||
completedSourceReplyViaMessageTool: boolean;
|
||||
} {
|
||||
const sourceReplyPayloads = params.payloads ?? [];
|
||||
const replyItems = sourceReplyPayloads.flatMap((payload, index): EmbeddedRunReplyItem[] => {
|
||||
const text = normalizeOptionalString(payload.text) ?? "";
|
||||
const media = Array.from(
|
||||
new Set([...(payload.mediaUrl ? [payload.mediaUrl] : []), ...(payload.mediaUrls ?? [])]),
|
||||
).filter((value) => value.trim().length > 0);
|
||||
if (
|
||||
!text &&
|
||||
media.length === 0 &&
|
||||
!payload.presentation &&
|
||||
!payload.interactive &&
|
||||
!payload.channelData
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
// These replies were already sent by the tool. Mirror them into the
|
||||
// transcript while marking channel delivery to suppress a duplicate send.
|
||||
return [
|
||||
{
|
||||
text,
|
||||
...(payload.mediaUrl ? { mediaUrl: payload.mediaUrl } : {}),
|
||||
...(media.length ? { media } : {}),
|
||||
...(payload.audioAsVoice ? { audioAsVoice: true } : {}),
|
||||
...(payload.presentation ? { presentation: payload.presentation } : {}),
|
||||
...(payload.interactive ? { interactive: payload.interactive } : {}),
|
||||
...(payload.channelData ? { channelData: payload.channelData } : {}),
|
||||
sourceReplyMirror: {
|
||||
idempotencyKey:
|
||||
payload.idempotencyKey ??
|
||||
(params.runId ? `${params.runId}:internal-source-reply:${index}` : undefined),
|
||||
},
|
||||
},
|
||||
];
|
||||
});
|
||||
const hasSourceReplyPayload = replyItems.length > 0;
|
||||
const deliveredSourceReplyViaMessageTool =
|
||||
params.sourceReplyDeliveryMode === "message_tool_only" &&
|
||||
params.didDeliverSourceReplyViaMessageTool === true;
|
||||
const explicitFinalSourceReply = resolveExplicitFinalSourceReplyDeliveryEvidence({
|
||||
messagingToolSentTargets: params.sentTargets,
|
||||
messagingToolSourceReplyPayloads: sourceReplyPayloads,
|
||||
});
|
||||
return {
|
||||
replyItems,
|
||||
hasSourceReplyPayload,
|
||||
deliveredSourceReplyViaMessageTool,
|
||||
explicitFinalSourceReply,
|
||||
completedSourceReplyViaMessageTool:
|
||||
explicitFinalSourceReply ?? (hasSourceReplyPayload || deliveredSourceReplyViaMessageTool),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import type { SourceReplyDeliveryMode } from "../../auto-reply/get-reply-options.types.js";
|
||||
import type { ChannelMessageActionName } from "../../channels/plugins/types.public.js";
|
||||
|
||||
const MESSAGE_TOOL_THREAD_READ_HINT = ' Missing thread context: action="read" + threadId.';
|
||||
|
||||
export function appendMessageToolVisibleReplyHint(
|
||||
description: string,
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode,
|
||||
requireExplicitTarget?: boolean,
|
||||
): string {
|
||||
if (sourceReplyDeliveryMode !== "message_tool_only") {
|
||||
return description;
|
||||
}
|
||||
const targetGuidance = requireExplicitTarget
|
||||
? "send needs target."
|
||||
: "target defaults current source; set only elsewhere.";
|
||||
return `${description} This turn visible reply: action="send" + message; ${targetGuidance} Final answer private.`;
|
||||
}
|
||||
|
||||
export function appendMessageToolReadHint(
|
||||
description: string,
|
||||
actions: Iterable<ChannelMessageActionName | "send">,
|
||||
): string {
|
||||
for (const action of actions) {
|
||||
if (action === "read") {
|
||||
return `${description}${MESSAGE_TOOL_THREAD_READ_HINT}`;
|
||||
}
|
||||
}
|
||||
return description;
|
||||
}
|
||||
@@ -88,10 +88,13 @@ import {
|
||||
resolveMessageActionAgentRuntimeIdentityToken,
|
||||
type GatewayCallOptions,
|
||||
} from "./gateway.js";
|
||||
import {
|
||||
appendMessageToolReadHint,
|
||||
appendMessageToolVisibleReplyHint,
|
||||
} from "./message-tool-description.js";
|
||||
import { isPollVoteEchoText } from "./poll-vote-echo.js";
|
||||
|
||||
const AllMessageActions = CHANNEL_MESSAGE_ACTION_NAMES;
|
||||
const MESSAGE_TOOL_THREAD_READ_HINT = ' Missing thread context: action="read" + threadId.';
|
||||
function actionNeedsExplicitTarget(action: ChannelMessageActionName): boolean {
|
||||
return action === "broadcast" || shouldApplyCrossContextMarker(action);
|
||||
}
|
||||
@@ -1335,32 +1338,6 @@ function buildMessageToolDescription(options?: {
|
||||
);
|
||||
}
|
||||
|
||||
function appendMessageToolVisibleReplyHint(
|
||||
description: string,
|
||||
sourceReplyDeliveryMode?: SourceReplyDeliveryMode,
|
||||
requireExplicitTarget?: boolean,
|
||||
): string {
|
||||
if (sourceReplyDeliveryMode !== "message_tool_only") {
|
||||
return description;
|
||||
}
|
||||
const targetGuidance = requireExplicitTarget
|
||||
? "send needs target."
|
||||
: "target defaults current source; set only elsewhere.";
|
||||
return `${description} This turn visible reply: action="send" + message; ${targetGuidance} Final answer private.`;
|
||||
}
|
||||
|
||||
function appendMessageToolReadHint(
|
||||
description: string,
|
||||
actions: Iterable<ChannelMessageActionName | "send">,
|
||||
): string {
|
||||
for (const action of actions) {
|
||||
if (action === "read") {
|
||||
return `${description}${MESSAGE_TOOL_THREAD_READ_HINT}`;
|
||||
}
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
export function createMessageTool(options?: MessageToolOptions): AnyAgentTool {
|
||||
const loadConfigForTool = options?.getRuntimeConfig ?? getRuntimeConfig;
|
||||
const getScopedSecretTargetsForTool =
|
||||
|
||||
@@ -13,10 +13,10 @@ import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import { isLikelyContextOverflowError } from "../../agents/embedded-agent-helpers/errors.js";
|
||||
import {
|
||||
hasCompletedSourceReplyDeliveryEvidence,
|
||||
hasCompletedTerminalDeliveryEvidence,
|
||||
hasCommittedSourceReplyDeliveryEvidence,
|
||||
hasVisibleCommittedMessagingToolDeliveryEvidence,
|
||||
hasVisibleOutboundDeliveryEvidence,
|
||||
resolveExplicitFinalSourceReplyDeliveryEvidence,
|
||||
} from "../../agents/embedded-agent-runner/delivery-evidence.js";
|
||||
import { hasDeliberateSilentTerminalReply } from "../../agents/embedded-agent-runner/result-fallback-classifier.js";
|
||||
import {
|
||||
@@ -2011,8 +2011,6 @@ export async function runReplyAgent(params: {
|
||||
const committedMessagingToolSourceReplyDelivery =
|
||||
hasCommittedSourceReplyDeliveryEvidence(runResult);
|
||||
const completedSourceReplyDelivery = hasCompletedSourceReplyDeliveryEvidence(runResult);
|
||||
const hasExplicitSourceReplyCompletion =
|
||||
resolveExplicitFinalSourceReplyDeliveryEvidence(runResult) !== undefined;
|
||||
const visibleOutboundDelivery = hasVisibleOutboundDeliveryEvidence(runResult);
|
||||
const successfulSideEffectDelivery =
|
||||
successfulSourceReplyDelivery ||
|
||||
@@ -2023,10 +2021,7 @@ export async function runReplyAgent(params: {
|
||||
hasSuccessfulTerminalSourceReplyDelivery({
|
||||
blockReplyPipeline,
|
||||
directlySentBlockPayloads,
|
||||
}) ||
|
||||
completedSourceReplyDelivery ||
|
||||
(!hasExplicitSourceReplyCompletion && visibleOutboundDelivery) ||
|
||||
runResult.didSendDeterministicApprovalPrompt === true;
|
||||
}) || hasCompletedTerminalDeliveryEvidence(runResult);
|
||||
// Compaction notices are progress, not a terminal reply. Dispatcher-backed
|
||||
// delivery settles after this run returns, so it cannot prove turn completion here.
|
||||
const shouldDeliverTerminalFailure = Boolean(
|
||||
|
||||
@@ -14,9 +14,9 @@ import { resolveContextTokensForModel } from "../../agents/context.js";
|
||||
import { DEFAULT_CONTEXT_TOKENS } from "../../agents/defaults.js";
|
||||
import {
|
||||
hasCompletedSourceReplyDeliveryEvidence,
|
||||
hasCompletedTerminalDeliveryEvidence,
|
||||
hasCommittedSourceReplyDeliveryEvidence,
|
||||
hasVisibleOutboundDeliveryEvidence,
|
||||
resolveExplicitFinalSourceReplyDeliveryEvidence,
|
||||
} from "../../agents/embedded-agent-runner/delivery-evidence.js";
|
||||
import {
|
||||
hasDeliberateSilentTerminalReply,
|
||||
@@ -1801,13 +1801,7 @@ export function createFollowupRunner(params: {
|
||||
hasVisibleOutboundDeliveryEvidence(runResult) ||
|
||||
hasCommittedSourceReplyDeliveryEvidence(runResult) ||
|
||||
runResult.didSendDeterministicApprovalPrompt === true;
|
||||
const completedSourceReplyDelivery = hasCompletedSourceReplyDeliveryEvidence(runResult);
|
||||
const hasExplicitSourceReplyCompletion =
|
||||
resolveExplicitFinalSourceReplyDeliveryEvidence(runResult) !== undefined;
|
||||
const hasCompletedTerminalDelivery =
|
||||
completedSourceReplyDelivery ||
|
||||
(!hasExplicitSourceReplyCompletion && hasVisibleOutboundDeliveryEvidence(runResult)) ||
|
||||
runResult.didSendDeterministicApprovalPrompt === true;
|
||||
const hasCompletedTerminalDelivery = hasCompletedTerminalDeliveryEvidence(runResult);
|
||||
const hasDeliveryDestination = Boolean(
|
||||
(isRoutableChannel(queued.originatingChannel) && queued.originatingTo) ||
|
||||
opts?.onBlockReply,
|
||||
|
||||
Reference in New Issue
Block a user