Auto-reply: add /verbose directives and tool result replies

This commit is contained in:
Peter Steinberger
2025-12-03 09:04:37 +00:00
parent 8ba35a2dc3
commit 086dd284d6
10 changed files with 242 additions and 8 deletions

View File

@@ -34,6 +34,7 @@ type CommandReplyParams = {
commandRunner: typeof runCommandWithTimeout;
enqueue?: EnqueueRunner;
thinkLevel?: ThinkLevel;
verboseLevel?: "off" | "on";
};
export type CommandReplyMeta = {
@@ -141,6 +142,7 @@ export async function runCommandReply(
commandRunner,
enqueue = enqueueCommand,
thinkLevel,
verboseLevel,
} = params;
if (!reply.command?.length) {
@@ -301,6 +303,8 @@ export async function runCommandReply(
// Collect one message per assistant text from parseOutput (tau RPC can emit many).
const parsedTexts =
parsed?.texts?.map((t) => t.trim()).filter(Boolean) ?? [];
const parsedToolResults =
parsed?.toolResults?.map((t) => t.trim()).filter(Boolean) ?? [];
type ReplyItem = { text: string; media?: string[] };
const replyItems: ReplyItem[] = [];
@@ -314,6 +318,18 @@ export async function runCommandReply(
});
}
if (verboseLevel === "on") {
for (const tr of parsedToolResults) {
const prefixed = `🛠️ ${tr}`;
const { text: cleanedText, mediaUrls: mediaFound } =
splitMediaFromOutput(prefixed);
replyItems.push({
text: cleanedText,
media: mediaFound?.length ? mediaFound : undefined,
});
}
}
// If parser gave nothing, fall back to raw stdout as a single message.
if (replyItems.length === 0 && trimmed && !parserProvided) {
const { text: cleanedText, mediaUrls: mediaFound } =

View File

@@ -34,6 +34,7 @@ const ABORT_TRIGGERS = new Set(["stop", "esc", "abort", "wait", "exit"]);
const ABORT_MEMORY = new Map<string, boolean>();
type ThinkLevel = "off" | "minimal" | "low" | "medium" | "high";
type VerboseLevel = "off" | "on";
function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined {
if (!raw) return undefined;
@@ -50,6 +51,14 @@ function normalizeThinkLevel(raw?: string | null): ThinkLevel | undefined {
return undefined;
}
function normalizeVerboseLevel(raw?: string | null): VerboseLevel | undefined {
if (!raw) return undefined;
const key = raw.toLowerCase();
if (["off", "false", "no", "0"].includes(key)) return "off";
if (["on", "full", "true", "yes", "1"].includes(key)) return "on";
return undefined;
}
function extractThinkDirective(body?: string): {
cleaned: string;
thinkLevel?: ThinkLevel;
@@ -73,6 +82,26 @@ function extractThinkDirective(body?: string): {
};
}
function extractVerboseDirective(body?: string): {
cleaned: string;
verboseLevel?: VerboseLevel;
rawLevel?: string;
hasDirective: boolean;
} {
if (!body) return { cleaned: "", hasDirective: false };
const match = body.match(/\/(?:verbose|v)\s*:?\s*([a-zA-Z-]+)\b/i);
const verboseLevel = normalizeVerboseLevel(match?.[1]);
const cleaned = match
? body.replace(match[0], "").replace(/\s+/g, " ").trim()
: body.trim();
return {
cleaned,
verboseLevel,
rawLevel: match?.[1],
hasDirective: !!match,
};
}
function isAbortTrigger(text?: string): boolean {
if (!text) return false;
const normalized = text.trim().toLowerCase();
@@ -156,6 +185,7 @@ export async function getReplyFromConfig(
let abortedLastRun = false;
let persistedThinking: string | undefined;
let persistedVerbose: string | undefined;
if (sessionCfg) {
const trimmedBody = (ctx.Body ?? "").trim();
@@ -185,6 +215,7 @@ export async function getReplyFromConfig(
systemSent = entry.systemSent ?? false;
abortedLastRun = entry.abortedLastRun ?? false;
persistedThinking = entry.thinkingLevel;
persistedVerbose = entry.verboseLevel;
} else {
sessionId = crypto.randomUUID();
isNewSession = true;
@@ -198,6 +229,7 @@ export async function getReplyFromConfig(
systemSent,
abortedLastRun,
thinkingLevel: persistedThinking,
verboseLevel: persistedVerbose,
};
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
@@ -216,14 +248,25 @@ export async function getReplyFromConfig(
rawLevel: rawThinkLevel,
hasDirective: hasThinkDirective,
} = extractThinkDirective(sessionCtx.BodyStripped ?? sessionCtx.Body ?? "");
sessionCtx.Body = thinkCleaned;
sessionCtx.BodyStripped = thinkCleaned;
const {
cleaned: verboseCleaned,
verboseLevel: inlineVerbose,
rawLevel: rawVerboseLevel,
hasDirective: hasVerboseDirective,
} = extractVerboseDirective(thinkCleaned);
sessionCtx.Body = verboseCleaned;
sessionCtx.BodyStripped = verboseCleaned;
let resolvedThinkLevel =
inlineThink ??
(sessionEntry?.thinkingLevel as ThinkLevel | undefined) ??
(reply?.thinkingDefault as ThinkLevel | undefined);
let resolvedVerboseLevel =
inlineVerbose ??
(sessionEntry?.verboseLevel as VerboseLevel | undefined) ??
(reply?.verboseDefault as VerboseLevel | undefined);
const directiveOnly = (() => {
if (!hasThinkDirective) return false;
if (!thinkCleaned) return true;
@@ -258,6 +301,38 @@ export async function getReplyFromConfig(
return { text: ack };
}
const verboseDirectiveOnly = (() => {
if (!hasVerboseDirective) return false;
if (!verboseCleaned) return true;
const stripped = verboseCleaned.replace(/\[[^\]]+\]\s*/g, "").trim();
return stripped.length === 0;
})();
if (verboseDirectiveOnly) {
if (!inlineVerbose) {
cleanupTyping();
return {
text: `Unrecognized verbose level "${rawVerboseLevel ?? ""}". Valid levels: off, on.`,
};
}
if (sessionEntry && sessionStore && sessionKey) {
if (inlineVerbose === "off") {
delete sessionEntry.verboseLevel;
} else {
sessionEntry.verboseLevel = inlineVerbose;
}
sessionEntry.updatedAt = Date.now();
sessionStore[sessionKey] = sessionEntry;
await saveSessionStore(storePath, sessionStore);
}
const ack =
inlineVerbose === "off"
? "Verbose logging disabled."
: "Verbose logging enabled.";
cleanupTyping();
return { text: ack };
}
// Optional allowlist by origin number (E.164 without whatsapp: prefix)
const allowFrom = cfg.inbound?.allowFrom;
const from = (ctx.From ?? "").replace(/^whatsapp:/, "");
@@ -445,6 +520,7 @@ export async function getReplyFromConfig(
timeoutSeconds,
commandRunner,
thinkLevel: resolvedThinkLevel,
verboseLevel: resolvedVerboseLevel,
});
const payloadArray = runResult.payloads ?? [];
const meta = runResult.meta;