fix: format verbose tool output by channel
This commit is contained in:
@@ -54,6 +54,7 @@ Docs: https://docs.clawd.bot
|
||||
|
||||
### Fixes
|
||||
- macOS: drain subprocess pipes before waiting to avoid deadlocks. (#1081) — thanks @thesash.
|
||||
- Verbose: wrap tool summaries/output in markdown only for markdown-capable channels.
|
||||
- Telegram: accept tg/group/telegram prefixes + topic targets for inline button validation. (#1072) — thanks @danielz1z.
|
||||
- Telegram: split long captions into follow-up messages.
|
||||
- Sub-agents: normalize announce delivery origin + queue bucketing by accountId to keep multi-account routing stable. (#1061, #1058) — thanks @adam91holt.
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
|
||||
import type { ThinkLevel } from "../../auto-reply/thinking.js";
|
||||
import { enqueueCommandInLane } from "../../process/command-queue.js";
|
||||
import { resolveUserPath } from "../../utils.js";
|
||||
import { isMarkdownCapableMessageChannel } from "../../utils/message-channel.js";
|
||||
import { resolveClawdbotAgentDir } from "../agent-paths.js";
|
||||
import {
|
||||
markAuthProfileFailure,
|
||||
@@ -58,6 +59,14 @@ export async function runEmbeddedPiAgent(
|
||||
const globalLane = resolveGlobalLane(params.lane);
|
||||
const enqueueGlobal =
|
||||
params.enqueue ?? ((task, opts) => enqueueCommandInLane(globalLane, task, opts));
|
||||
const channelHint = params.messageChannel ?? params.messageProvider;
|
||||
const resolvedToolResultFormat =
|
||||
params.toolResultFormat ??
|
||||
(channelHint
|
||||
? isMarkdownCapableMessageChannel(channelHint)
|
||||
? "markdown"
|
||||
: "plain"
|
||||
: "markdown");
|
||||
|
||||
return enqueueCommandInLane(sessionLane, () =>
|
||||
enqueueGlobal(async () => {
|
||||
@@ -208,6 +217,7 @@ export async function runEmbeddedPiAgent(
|
||||
thinkLevel,
|
||||
verboseLevel: params.verboseLevel,
|
||||
reasoningLevel: params.reasoningLevel,
|
||||
toolResultFormat: resolvedToolResultFormat,
|
||||
bashElevated: params.bashElevated,
|
||||
timeoutMs: params.timeoutMs,
|
||||
runId: params.runId,
|
||||
@@ -408,6 +418,7 @@ export async function runEmbeddedPiAgent(
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
verboseLevel: params.verboseLevel,
|
||||
reasoningLevel: params.reasoningLevel,
|
||||
toolResultFormat: resolvedToolResultFormat,
|
||||
inlineToolResultsAllowed: !params.onPartialReply && !params.onToolResult,
|
||||
});
|
||||
|
||||
|
||||
@@ -365,6 +365,7 @@ export async function runEmbeddedAttempt(
|
||||
runId: params.runId,
|
||||
verboseLevel: params.verboseLevel,
|
||||
reasoningMode: params.reasoningLevel ?? "off",
|
||||
toolResultFormat: params.toolResultFormat,
|
||||
shouldEmitToolResult: params.shouldEmitToolResult,
|
||||
shouldEmitToolOutput: params.shouldEmitToolOutput,
|
||||
onToolResult: params.onToolResult,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../../../auto-rep
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import type { enqueueCommand } from "../../../process/command-queue.js";
|
||||
import type { ExecElevatedDefaults } from "../../bash-tools.js";
|
||||
import type { BlockReplyChunking } from "../../pi-embedded-subscribe.js";
|
||||
import type { BlockReplyChunking, ToolResultFormat } from "../../pi-embedded-subscribe.js";
|
||||
import type { SkillSnapshot } from "../../skills.js";
|
||||
|
||||
export type RunEmbeddedPiAgentParams = {
|
||||
@@ -33,6 +33,7 @@ export type RunEmbeddedPiAgentParams = {
|
||||
thinkLevel?: ThinkLevel;
|
||||
verboseLevel?: VerboseLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
toolResultFormat?: ToolResultFormat;
|
||||
bashElevated?: ExecElevatedDefaults;
|
||||
timeoutMs: number;
|
||||
runId: string;
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
extractAssistantThinking,
|
||||
formatReasoningMessage,
|
||||
} from "../../pi-embedded-utils.js";
|
||||
import type { ToolResultFormat } from "../../pi-embedded-subscribe.js";
|
||||
|
||||
type ToolMetaEntry = { toolName: string; meta?: string };
|
||||
|
||||
@@ -26,6 +27,7 @@ export function buildEmbeddedRunPayloads(params: {
|
||||
sessionKey: string;
|
||||
verboseLevel?: VerboseLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
toolResultFormat?: ToolResultFormat;
|
||||
inlineToolResultsAllowed: boolean;
|
||||
}): Array<{
|
||||
text?: string;
|
||||
@@ -47,6 +49,7 @@ export function buildEmbeddedRunPayloads(params: {
|
||||
replyToCurrent?: boolean;
|
||||
}> = [];
|
||||
|
||||
const useMarkdown = params.toolResultFormat === "markdown";
|
||||
const lastAssistantErrored = params.lastAssistant?.stopReason === "error";
|
||||
const errorText = params.lastAssistant
|
||||
? formatAssistantErrorText(params.lastAssistant, {
|
||||
@@ -71,7 +74,9 @@ export function buildEmbeddedRunPayloads(params: {
|
||||
params.inlineToolResultsAllowed && params.verboseLevel !== "off" && params.toolMetas.length > 0;
|
||||
if (inlineToolResults) {
|
||||
for (const { toolName, meta } of params.toolMetas) {
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : []);
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : [], {
|
||||
markdown: useMarkdown,
|
||||
});
|
||||
const {
|
||||
text: cleanedText,
|
||||
mediaUrls,
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { ReasoningLevel, ThinkLevel, VerboseLevel } from "../../../auto-rep
|
||||
import type { ClawdbotConfig } from "../../../config/config.js";
|
||||
import type { ExecElevatedDefaults } from "../../bash-tools.js";
|
||||
import type { MessagingToolSend } from "../../pi-embedded-messaging.js";
|
||||
import type { BlockReplyChunking } from "../../pi-embedded-subscribe.js";
|
||||
import type { BlockReplyChunking, ToolResultFormat } from "../../pi-embedded-subscribe.js";
|
||||
import type { SkillSnapshot } from "../../skills.js";
|
||||
import type { SessionSystemPromptReport } from "../../../config/sessions/types.js";
|
||||
|
||||
@@ -38,6 +38,7 @@ export type EmbeddedRunAttemptParams = {
|
||||
thinkLevel: ThinkLevel;
|
||||
verboseLevel?: VerboseLevel;
|
||||
reasoningLevel?: ReasoningLevel;
|
||||
toolResultFormat?: ToolResultFormat;
|
||||
bashElevated?: ExecElevatedDefaults;
|
||||
timeoutMs: number;
|
||||
runId: string;
|
||||
|
||||
@@ -23,10 +23,13 @@ const log = createSubsystemLogger("agent/embedded");
|
||||
export type {
|
||||
BlockReplyChunking,
|
||||
SubscribeEmbeddedPiSessionParams,
|
||||
ToolResultFormat,
|
||||
} from "./pi-embedded-subscribe.types.js";
|
||||
|
||||
export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionParams) {
|
||||
const reasoningMode = params.reasoningMode ?? "off";
|
||||
const toolResultFormat = params.toolResultFormat ?? "markdown";
|
||||
const useMarkdown = toolResultFormat === "markdown";
|
||||
const state: EmbeddedPiSubscribeState = {
|
||||
assistantTexts: [],
|
||||
toolMetas: [],
|
||||
@@ -180,11 +183,14 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
|
||||
const formatToolOutputBlock = (text: string) => {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return "(no output)";
|
||||
if (!useMarkdown) return trimmed;
|
||||
return `\`\`\`txt\n${trimmed}\n\`\`\``;
|
||||
};
|
||||
const emitToolSummary = (toolName?: string, meta?: string) => {
|
||||
if (!params.onToolResult) return;
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined);
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
|
||||
markdown: useMarkdown,
|
||||
});
|
||||
const { text: cleanedText, mediaUrls } = parseReplyDirectives(agg);
|
||||
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) return;
|
||||
try {
|
||||
@@ -198,7 +204,9 @@ export function subscribeEmbeddedPiSession(params: SubscribeEmbeddedPiSessionPar
|
||||
};
|
||||
const emitToolOutput = (toolName?: string, meta?: string, output?: string) => {
|
||||
if (!params.onToolResult || !output) return;
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined);
|
||||
const agg = formatToolAggregate(toolName, meta ? [meta] : undefined, {
|
||||
markdown: useMarkdown,
|
||||
});
|
||||
const message = `${agg}\n${formatToolOutputBlock(output)}`;
|
||||
const { text: cleanedText, mediaUrls } = parseReplyDirectives(message);
|
||||
if (!cleanedText && (!mediaUrls || mediaUrls.length === 0)) return;
|
||||
|
||||
@@ -3,11 +3,14 @@ import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
||||
import type { ReasoningLevel, VerboseLevel } from "../auto-reply/thinking.js";
|
||||
import type { BlockReplyChunking } from "./pi-embedded-block-chunker.js";
|
||||
|
||||
export type ToolResultFormat = "markdown" | "plain";
|
||||
|
||||
export type SubscribeEmbeddedPiSessionParams = {
|
||||
session: AgentSession;
|
||||
runId: string;
|
||||
verboseLevel?: VerboseLevel;
|
||||
reasoningMode?: ReasoningLevel;
|
||||
toolResultFormat?: ToolResultFormat;
|
||||
shouldEmitToolResult?: () => boolean;
|
||||
shouldEmitToolOutput?: () => boolean;
|
||||
onToolResult?: (payload: { text?: string; mediaUrls?: string[] }) => void | Promise<void>;
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { emitAgentEvent, registerAgentRunContext } from "../../infra/agent-events.js";
|
||||
import { defaultRuntime } from "../../runtime.js";
|
||||
import {
|
||||
isMarkdownCapableMessageChannel,
|
||||
resolveMessageChannel,
|
||||
} from "../../utils/message-channel.js";
|
||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import type { VerboseLevel } from "../thinking.js";
|
||||
@@ -222,6 +226,14 @@ export async function runAgentTurnWithFallback(params: {
|
||||
thinkLevel: params.followupRun.run.thinkLevel,
|
||||
verboseLevel: params.followupRun.run.verboseLevel,
|
||||
reasoningLevel: params.followupRun.run.reasoningLevel,
|
||||
toolResultFormat: (() => {
|
||||
const channel = resolveMessageChannel(
|
||||
params.sessionCtx.Surface,
|
||||
params.sessionCtx.Provider,
|
||||
);
|
||||
if (!channel) return "markdown";
|
||||
return isMarkdownCapableMessageChannel(channel) ? "markdown" : "plain";
|
||||
})(),
|
||||
bashElevated: params.followupRun.run.bashElevated,
|
||||
timeoutMs: params.followupRun.run.timeoutMs,
|
||||
runId,
|
||||
|
||||
@@ -36,6 +36,12 @@ describe("tool meta formatting", () => {
|
||||
expect(out).toContain("a→b");
|
||||
});
|
||||
|
||||
it("wraps aggregate meta in backticks when markdown is enabled", () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
const out = formatToolAggregate("fs", ["/Users/test/dir/a.txt"], { markdown: true });
|
||||
expect(out).toContain("`~/dir/a.txt`");
|
||||
});
|
||||
|
||||
it("formats prefixes with default labels", () => {
|
||||
vi.stubEnv("HOME", "/Users/test");
|
||||
expect(formatToolPrefix(undefined, undefined)).toBe("🧩 tool");
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { formatToolSummary, resolveToolDisplay } from "../agents/tool-display.js";
|
||||
import { shortenHomeInString, shortenHomePath } from "../utils.js";
|
||||
|
||||
type ToolAggregateOptions = {
|
||||
markdown?: boolean;
|
||||
};
|
||||
|
||||
export function shortenPath(p: string): string {
|
||||
return shortenHomePath(p);
|
||||
}
|
||||
@@ -14,7 +18,11 @@ export function shortenMeta(meta: string): string {
|
||||
return `${shortenHomeInString(base)}${rest}`;
|
||||
}
|
||||
|
||||
export function formatToolAggregate(toolName?: string, metas?: string[]): string {
|
||||
export function formatToolAggregate(
|
||||
toolName?: string,
|
||||
metas?: string[],
|
||||
options?: ToolAggregateOptions,
|
||||
): string {
|
||||
const filtered = (metas ?? []).filter(Boolean).map(shortenMeta);
|
||||
const display = resolveToolDisplay({ name: toolName });
|
||||
const prefix = `${display.emoji} ${display.label}`;
|
||||
@@ -51,7 +59,8 @@ export function formatToolAggregate(toolName?: string, metas?: string[]): string
|
||||
});
|
||||
|
||||
const allSegments = [...rawSegments, ...segments];
|
||||
return `${prefix}: ${allSegments.join("; ")}`;
|
||||
const meta = allSegments.join("; ");
|
||||
return `${prefix}: ${maybeWrapMarkdown(meta, options?.markdown)}`;
|
||||
}
|
||||
|
||||
export function formatToolPrefix(toolName?: string, meta?: string) {
|
||||
@@ -68,3 +77,9 @@ function isPathLike(value: string): boolean {
|
||||
if (value.includes("&&") || value.includes("||")) return false;
|
||||
return /^~?(\/[^\s]+)+$/.test(value);
|
||||
}
|
||||
|
||||
function maybeWrapMarkdown(value: string, markdown?: boolean): string {
|
||||
if (!markdown) return value;
|
||||
if (value.includes("`")) return value;
|
||||
return `\`${value}\``;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,15 @@ import { getActivePluginRegistry } from "../plugins/runtime.js";
|
||||
export const INTERNAL_MESSAGE_CHANNEL = "webchat" as const;
|
||||
export type InternalMessageChannel = typeof INTERNAL_MESSAGE_CHANNEL;
|
||||
|
||||
const MARKDOWN_CAPABLE_CHANNELS = new Set<string>([
|
||||
"slack",
|
||||
"telegram",
|
||||
"signal",
|
||||
"discord",
|
||||
"tui",
|
||||
INTERNAL_MESSAGE_CHANNEL,
|
||||
]);
|
||||
|
||||
export { GATEWAY_CLIENT_NAMES, GATEWAY_CLIENT_MODES };
|
||||
export type { GatewayClientName, GatewayClientMode };
|
||||
export { normalizeGatewayClientName, normalizeGatewayClientMode };
|
||||
@@ -112,3 +121,9 @@ export function resolveMessageChannel(
|
||||
): string | undefined {
|
||||
return normalizeMessageChannel(primary) ?? normalizeMessageChannel(fallback);
|
||||
}
|
||||
|
||||
export function isMarkdownCapableMessageChannel(raw?: string | null): boolean {
|
||||
const channel = normalizeMessageChannel(raw);
|
||||
if (!channel) return false;
|
||||
return MARKDOWN_CAPABLE_CHANNELS.has(channel);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user