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
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
agent.subscribe(async (event: AgentEvent) => {
switch (event.type) {
@ -405,7 +421,7 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
});
// Show label in main message only
await ctx.respond(`_${label}_`);
queue.enqueue(() => ctx.respond(`_${label}_`));
break;
}
@ -453,11 +469,11 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
threadMessage += "*Result:*\n```\n" + threadResult + "\n```";
await ctx.respondInThread(threadMessage);
queue.enqueue(() => ctx.respondInThread(threadMessage));
// Show brief error in main message if failed
if (event.isError) {
await ctx.respond(`_Error: ${truncate(resultStr, 200)}_`);
queue.enqueue(() => ctx.respond(`_Error: ${truncate(resultStr, 200)}_`));
}
break;
}
@ -495,17 +511,32 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
totalUsage.cost.total += assistantMsg.usage.cost.total;
}
// Extract text from assistant message
// Extract thinking and text from assistant message
const content = event.message.content;
let text = "";
const thinkingParts: string[] = [];
const textParts: string[] = [];
for (const part of content) {
if (part.type === "text") {
text += part.text;
if (part.type === "thinking") {
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()) {
await ctx.respond(text);
log.logResponseComplete(logCtx, text.length);
log.logResponse(logCtx, text);
queue.enqueue(() => ctx.respond(text));
queue.enqueue(() => ctx.respondInThread(text));
}
}
break;
@ -523,10 +554,26 @@ export function createAgentRunner(sandboxConfig: SandboxConfig): AgentRunner {
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
if (totalUsage.cost.total > 0) {
const summary = log.logUsageSummary(logCtx, totalUsage);
await ctx.respondInThread(summary);
queue.enqueue(() => ctx.respondInThread(summary));
await queue.flush();
}
return { stopReason };

View file

@ -118,10 +118,24 @@ export function logResponseStart(ctx: LogContext): void {
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} → Streaming response...`));
}
export function logResponseComplete(ctx: LogContext, charCount: number): void {
console.log(
chalk.yellow(`${timestamp()} ${formatContext(ctx)} ✓ Response sent (${charCount.toLocaleString()} chars)`),
);
export function logThinking(ctx: LogContext, thinking: string): void {
console.log(chalk.yellow(`${timestamp()} ${formatContext(ctx)} 💭 Thinking`));
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