feat(mom): reduce tool verbosity in main Slack messages

- During execution: show tool labels, thinking, and text in main message
- After completion: replace main message with only final assistant text
- Post thinking (italic) and text to thread for full audit trail
- Add promise queue to ensure ctx.respond calls execute sequentially
- Add log.logThinking() and log.logResponse() for console output
- Get final text from agent.state.messages instead of tracking

Closes #65
This commit is contained in:
Mario Zechner 2025-11-27 14:07:21 +01:00
parent 318254bff4
commit effb4d0b7c
2 changed files with 75 additions and 14 deletions

View file

@ -377,6 +377,22 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
// Track stop reason // Track stop reason
let stopReason = "stop"; let stopReason = "stop";
// Promise queue to ensure ctx.respond/respondInThread calls execute in order
const queue = {
chain: Promise.resolve(),
enqueue<T>(fn: () => Promise<T>): Promise<T> {
const result = this.chain.then(fn);
this.chain = result.then(
() => {},
() => {},
); // swallow errors for chain
return result;
},
flush(): Promise<void> {
return this.chain;
},
};
// Subscribe to events // Subscribe to events
agent.subscribe(async (event: AgentEvent) => { agent.subscribe(async (event: AgentEvent) => {
switch (event.type) { switch (event.type) {
@ -405,7 +421,7 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
}); });
// Show label in main message only // Show label in main message only
await ctx.respond(`_${label}_`); queue.enqueue(() => ctx.respond(`_${label}_`));
break; break;
} }
@ -453,11 +469,11 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
threadMessage += "*Result:*\n```\n" + threadResult + "\n```"; threadMessage += "*Result:*\n```\n" + threadResult + "\n```";
await ctx.respondInThread(threadMessage); queue.enqueue(() => ctx.respondInThread(threadMessage));
// Show brief error in main message if failed // Show brief error in main message if failed
if (event.isError) { if (event.isError) {
await ctx.respond(`_Error: ${truncate(resultStr, 200)}_`); queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`));
} }
break; break;
} }
@ -495,17 +511,32 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
totalUsage.cost.total += assistantMsg.usage.cost.total; totalUsage.cost.total += assistantMsg.usage.cost.total;
} }
// Extract text from assistant message // Extract thinking and text from assistant message
const content = event.message.content; const content = event.message.content;
let text = ""; const thinkingParts: string[] = [];
const textParts: string[] = [];
for (const part of content) { for (const part of content) {
if (part.type === "text") { if (part.type === "thinking") {
text += part.text; thinkingParts.push(part.thinking);
} else if (part.type === "text") {
textParts.push(part.text);
} }
} }
const text = textParts.join("\n");
// Post thinking to main message and thread
for (const thinking of thinkingParts) {
log.logThinking(logCtx, thinking);
queue.enqueue(() => ctx.respond(`_${thinking}_`));
queue.enqueue(() => ctx.respondInThread(`_${thinking}_`));
}
// Post text to main message and thread
if (text.trim()) { if (text.trim()) {
await ctx.respond(text); log.logResponse(logCtx, text);
log.logResponseComplete(logCtx, text.length); queue.enqueue(() => ctx.respond(text));
queue.enqueue(() => ctx.respondInThread(text));
} }
} }
break; break;
@ -523,10 +554,26 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
await agent.prompt(userPrompt); await agent.prompt(userPrompt);
// Wait for all queued respond calls to complete
await queue.flush();
// Get final assistant message text from agent state and replace main message
const messages = agent.state.messages;
const lastAssistant = messages.filter((m) => m.role === "assistant").pop();
const finalText =
lastAssistant?.content
.filter((c): c is { type: "text"; text: string } => c.type === "text")
.map((c) => c.text)
.join("\n") || "";
if (finalText.trim()) {
await ctx.replaceMessage(finalText);
}
// Log usage summary if there was any usage // Log usage summary if there was any usage
if (totalUsage.cost.total > 0) { if (totalUsage.cost.total > 0) {
const summary = log.logUsageSummary(logCtx, totalUsage); const summary = log.logUsageSummary(logCtx, totalUsage);
await ctx.respondInThread(summary); queue.enqueue(() => ctx.respondInThread(summary));
await queue.flush();
} }
return { stopReason }; return { stopReason };

View file

@ -118,10 +118,24 @@ export function logResponseStart(ctx: LogContext): void {
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`)); console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));
} }
export function logResponseComplete(ctx: LogContext, charCount: number): void { export function logThinking(ctx: LogContext, thinking: string): void {
console.log( console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));
chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Response sent (${charCount.toLocaleString()} chars)`), const truncated = truncate(thinking, 1000);
); const indented = truncated
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
console.log(chalk.dim(indented));
}
export function logResponse(ctx: LogContext, text: string): void {
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💬 Response`));
const truncated = truncate(text, 1000);
const indented = truncated
.split("\n")
.map((line) => ` ${line}`)
.join("\n");
console.log(chalk.dim(indented));
} }
// Attachments // Attachments