Heartbeat: allow suppressing tool warnings (#18497)

* Heartbeat: allow suppressing tool warnings

* Changelog: note heartbeat tool-warning suppression
This commit is contained in:
Shadow
2026-02-16 13:29:24 -06:00
committed by GitHub
parent 3238bd78d9
commit 72e228e14b
14 changed files with 36 additions and 2 deletions

View File

@@ -922,6 +922,7 @@ export async function runEmbeddedPiAgent(
verboseLevel: params.verboseLevel,
reasoningLevel: params.reasoningLevel,
toolResultFormat: resolvedToolResultFormat,
suppressToolErrorWarnings: params.suppressToolErrorWarnings,
inlineToolResultsAllowed: false,
});

View File

@@ -74,6 +74,8 @@ export type RunEmbeddedPiAgentParams = {
verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
toolResultFormat?: ToolResultFormat;
/** If true, suppress tool error warning payloads for this run (including mutating tools). */
suppressToolErrorWarnings?: boolean;
execOverrides?: Pick<ExecToolDefaults, "host" | "security" | "ask" | "node">;
bashElevated?: ExecElevatedDefaults;
timeoutMs: number;

View File

@@ -252,6 +252,15 @@ describe("buildEmbeddedRunPayloads", () => {
expect(payloads[0]?.text).toContain("connection timeout");
});
it("suppresses mutating tool errors when suppressToolErrorWarnings is enabled", () => {
const payloads = buildPayloads({
lastToolError: { toolName: "exec", error: "command not found" },
suppressToolErrorWarnings: true,
});
expect(payloads).toHaveLength(0);
});
it("shows recoverable tool errors for mutating tools", () => {
const payloads = buildPayloads({
lastToolError: { toolName: "message", meta: "reply", error: "text required" },

View File

@@ -48,7 +48,11 @@ function shouldShowToolErrorWarning(params: {
lastToolError: LastToolError;
hasUserFacingReply: boolean;
suppressToolErrors: boolean;
suppressToolErrorWarnings?: boolean;
}): boolean {
if (params.suppressToolErrorWarnings) {
return false;
}
const isMutatingToolError =
params.lastToolError.mutatingAction ?? isLikelyMutatingToolName(params.lastToolError.toolName);
if (isMutatingToolError) {
@@ -71,6 +75,7 @@ export function buildEmbeddedRunPayloads(params: {
verboseLevel?: VerboseLevel;
reasoningLevel?: ReasoningLevel;
toolResultFormat?: ToolResultFormat;
suppressToolErrorWarnings?: boolean;
inlineToolResultsAllowed: boolean;
}): Array<{
text?: string;
@@ -247,6 +252,7 @@ export function buildEmbeddedRunPayloads(params: {
lastToolError: params.lastToolError,
hasUserFacingReply: hasUserFacingAssistantReply,
suppressToolErrors: Boolean(params.config?.messages?.suppressToolErrors),
suppressToolErrorWarnings: params.suppressToolErrorWarnings,
});
// Always surface mutating tool failures so we do not silently confirm actions that did not happen.

View File

@@ -312,6 +312,7 @@ export async function runAgentTurnWithFallback(params: {
}
return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
})(),
suppressToolErrorWarnings: params.opts?.suppressToolErrorWarnings,
bashElevated: params.followupRun.run.bashElevated,
timeoutMs: params.followupRun.run.timeoutMs,
runId,

View File

@@ -166,6 +166,7 @@ export function createFollowupRunner(params: {
thinkLevel: queued.run.thinkLevel,
verboseLevel: queued.run.verboseLevel,
reasoningLevel: queued.run.reasoningLevel,
suppressToolErrorWarnings: opts?.suppressToolErrorWarnings,
execOverrides: queued.run.execOverrides,
bashElevated: queued.run.bashElevated,
timeoutMs: queued.run.timeoutMs,

View File

@@ -29,6 +29,8 @@ export type GetReplyOptions = {
isHeartbeat?: boolean;
/** Resolved heartbeat model override (provider/model string from merged per-agent config). */
heartbeatModelOverride?: string;
/** If true, suppress tool error warning payloads for this run. */
suppressToolErrorWarnings?: boolean;
onPartialReply?: (payload: ReplyPayload) => Promise<void> | void;
onReasoningStream?: (payload: ReplyPayload) => Promise<void> | void;
/** Called when a thinking/reasoning block ends. */

View File

@@ -17,6 +17,10 @@ export const FIELD_HELP: Record<string, string> = {
"Optional allowlist of skills for this agent (omit = all skills; empty = no skills).",
"agents.list[].identity.avatar":
"Avatar image path (relative to the agent workspace only) or a remote URL/data URL.",
"agents.defaults.heartbeat.suppressToolErrorWarnings":
"Suppress tool error warning payloads during heartbeat runs.",
"agents.list[].heartbeat.suppressToolErrorWarnings":
"Suppress tool error warning payloads during heartbeat runs.",
"discovery.mdns.mode":
'mDNS broadcast mode ("minimal" default, "full" includes cliPath/sshPort, "off" disables mDNS).',
"gateway.auth.token":

View File

@@ -220,6 +220,8 @@ export type AgentDefaultsConfig = {
prompt?: string;
/** Max chars allowed after HEARTBEAT_OK before delivery (default: 30). */
ackMaxChars?: number;
/** Suppress tool error warning payloads during heartbeat runs. */
suppressToolErrorWarnings?: boolean;
/**
* When enabled, deliver the model's reasoning payload for heartbeat runs (when available)
* as a separate message prefixed with `Reasoning:` (same as `/reasoning on`).

View File

@@ -29,6 +29,7 @@ export const HeartbeatSchema = z
accountId: z.string().optional(),
prompt: z.string().optional(),
ackMaxChars: z.number().int().nonnegative().optional(),
suppressToolErrorWarnings: z.boolean().optional(),
})
.strict()
.superRefine((val, ctx) => {

View File

@@ -540,9 +540,10 @@ export async function runHeartbeatOnce(opts: {
try {
const heartbeatModelOverride = heartbeat?.model?.trim() || undefined;
const suppressToolErrorWarnings = heartbeat?.suppressToolErrorWarnings === true;
const replyOpts = heartbeatModelOverride
? { isHeartbeat: true, heartbeatModelOverride }
: { isHeartbeat: true };
? { isHeartbeat: true, heartbeatModelOverride, suppressToolErrorWarnings }
: { isHeartbeat: true, suppressToolErrorWarnings };
const replyResult = await getReplyFromConfig(ctx, replyOpts, cfg);
const replyPayload = resolveHeartbeatReplyPayload(replyResult);
const includeReasoning = heartbeat?.includeReasoning === true;