feat: display tool calls and reasoning in channel output (#302)
This commit is contained in:
@@ -74,6 +74,10 @@ features:
|
||||
intervalMin: 30
|
||||
# skipRecentUserMin: 5 # Skip auto-heartbeats for N minutes after user message (0 disables)
|
||||
# memfs: true # Enable memory filesystem (git-backed context repository). Syncs memory blocks to local files.
|
||||
# display:
|
||||
# showToolCalls: false # Show tool invocations in chat (e.g. "Using tool: Read (file_path: ...)")
|
||||
# showReasoning: false # Show agent reasoning/thinking in chat
|
||||
# reasoningMaxChars: 0 # Truncate reasoning to N chars (0 = no limit, default)
|
||||
|
||||
# Attachment handling (defaults to 20MB if omitted)
|
||||
# attachments:
|
||||
|
||||
@@ -25,6 +25,18 @@ export function serverModeLabel(mode?: ServerMode): string {
|
||||
return canonicalizeServerMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Display configuration for tool calls and reasoning in channel output.
|
||||
*/
|
||||
export interface DisplayConfig {
|
||||
/** Show tool invocations in channel output (default: false) */
|
||||
showToolCalls?: boolean;
|
||||
/** Show agent reasoning/thinking in channel output (default: false) */
|
||||
showReasoning?: boolean;
|
||||
/** Truncate reasoning to N characters (default: 0 = no limit) */
|
||||
reasoningMaxChars?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for a single agent in multi-agent mode.
|
||||
* Each agent has its own name, channels, and features.
|
||||
@@ -65,6 +77,7 @@ export interface AgentConfig {
|
||||
};
|
||||
memfs?: boolean; // Enable memory filesystem (git-backed context repository) for SDK sessions
|
||||
maxToolCalls?: number;
|
||||
display?: DisplayConfig;
|
||||
};
|
||||
/** Polling config */
|
||||
polling?: PollingYamlConfig;
|
||||
@@ -138,6 +151,7 @@ export interface LettaBotConfig {
|
||||
inlineImages?: boolean; // Send images directly to the LLM (default: true). Set false to only send file paths.
|
||||
memfs?: boolean; // Enable memory filesystem (git-backed context repository) for SDK sessions
|
||||
maxToolCalls?: number; // Abort if agent calls this many tools in one turn (default: 100)
|
||||
display?: DisplayConfig; // Show tool calls / reasoning in channel output
|
||||
};
|
||||
|
||||
// Polling - system-level background checks (Gmail, etc.)
|
||||
|
||||
@@ -211,6 +211,50 @@ export class LettaBot implements AgentSession {
|
||||
return `${this.config.displayName}: ${text}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a tool call for channel display.
|
||||
* Shows tool name + abbreviated key parameters.
|
||||
*/
|
||||
private formatToolCallDisplay(streamMsg: StreamMsg): string {
|
||||
const name = streamMsg.toolName || 'unknown';
|
||||
const params = this.abbreviateToolInput(streamMsg);
|
||||
return params ? `> **Tool:** ${name} (${params})` : `> **Tool:** ${name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a brief parameter summary from a tool call's input.
|
||||
*/
|
||||
private abbreviateToolInput(streamMsg: StreamMsg): string {
|
||||
const input = streamMsg.toolInput as Record<string, unknown> | undefined;
|
||||
if (!input || typeof input !== 'object') return '';
|
||||
const entries = Object.entries(input).slice(0, 2);
|
||||
return entries
|
||||
.map(([k, v]) => {
|
||||
let str: string;
|
||||
try {
|
||||
str = typeof v === 'string' ? v : (JSON.stringify(v) ?? String(v));
|
||||
} catch {
|
||||
str = String(v);
|
||||
}
|
||||
const truncated = str.length > 80 ? str.slice(0, 77) + '...' : str;
|
||||
return `${k}: ${truncated}`;
|
||||
})
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Format reasoning text for channel display, respecting truncation config.
|
||||
*/
|
||||
private formatReasoningDisplay(text: string): string {
|
||||
const maxChars = this.config.display?.reasoningMaxChars ?? 0;
|
||||
const truncated = maxChars > 0 && text.length > maxChars
|
||||
? text.slice(0, maxChars) + '...'
|
||||
: text;
|
||||
// Prefix every line with "> " so the whole block renders as a blockquote
|
||||
const lines = truncated.split('\n').map(line => `> ${line}`);
|
||||
return `> **Thinking**\n${lines.join('\n')}`;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Session options (shared by processMessage and sendToAgent)
|
||||
// =========================================================================
|
||||
@@ -1064,6 +1108,7 @@ export class LettaBot implements AgentSession {
|
||||
let sawNonAssistantSinceLastUuid = false;
|
||||
let lastErrorDetail: { message: string; stopReason: string; apiError?: Record<string, unknown> } | null = null;
|
||||
let retryInfo: { attempt: number; maxAttempts: number; reason: string } | null = null;
|
||||
let reasoningBuffer = '';
|
||||
const msgTypeCounts: Record<string, number> = {};
|
||||
|
||||
const finalizeMessage = async () => {
|
||||
@@ -1122,6 +1167,20 @@ export class LettaBot implements AgentSession {
|
||||
if (lastMsgType && lastMsgType !== streamMsg.type && response.trim() && streamMsg.type !== 'result') {
|
||||
await finalizeMessage();
|
||||
}
|
||||
|
||||
// Flush reasoning buffer when type changes away from reasoning
|
||||
if (lastMsgType === 'reasoning' && streamMsg.type !== 'reasoning' && reasoningBuffer.trim()) {
|
||||
if (this.config.display?.showReasoning && !suppressDelivery) {
|
||||
try {
|
||||
const text = this.formatReasoningDisplay(reasoningBuffer);
|
||||
await adapter.sendMessage({ chatId: msg.chatId, text, threadId: msg.threadId });
|
||||
sentAnyMessage = true;
|
||||
} catch (err) {
|
||||
console.warn('[Bot] Failed to send reasoning display:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
reasoningBuffer = '';
|
||||
}
|
||||
|
||||
// Tool loop detection
|
||||
const maxToolCalls = this.config.maxToolCalls ?? 100;
|
||||
@@ -1137,14 +1196,30 @@ export class LettaBot implements AgentSession {
|
||||
this.syncTodoToolCall(streamMsg);
|
||||
console.log(`[Stream] >>> TOOL CALL: ${streamMsg.toolName || 'unknown'} (id: ${streamMsg.toolCallId?.slice(0, 12) || '?'})`);
|
||||
sawNonAssistantSinceLastUuid = true;
|
||||
// Display tool call in channel if configured
|
||||
if (this.config.display?.showToolCalls && !suppressDelivery) {
|
||||
try {
|
||||
const text = this.formatToolCallDisplay(streamMsg);
|
||||
await adapter.sendMessage({ chatId: msg.chatId, text, threadId: msg.threadId });
|
||||
sentAnyMessage = true;
|
||||
} catch (err) {
|
||||
console.warn('[Bot] Failed to send tool call display:', err instanceof Error ? err.message : err);
|
||||
}
|
||||
}
|
||||
} else if (streamMsg.type === 'tool_result') {
|
||||
console.log(`[Stream] <<< TOOL RESULT: error=${streamMsg.isError}, len=${(streamMsg as any).content?.length || 0}`);
|
||||
sawNonAssistantSinceLastUuid = true;
|
||||
} else if (streamMsg.type === 'assistant' && lastMsgType !== 'assistant') {
|
||||
console.log(`[Bot] Generating response...`);
|
||||
} else if (streamMsg.type === 'reasoning' && lastMsgType !== 'reasoning') {
|
||||
console.log(`[Bot] Reasoning...`);
|
||||
} else if (streamMsg.type === 'reasoning') {
|
||||
if (lastMsgType !== 'reasoning') {
|
||||
console.log(`[Bot] Reasoning...`);
|
||||
}
|
||||
sawNonAssistantSinceLastUuid = true;
|
||||
// Accumulate reasoning content for display
|
||||
if (this.config.display?.showReasoning) {
|
||||
reasoningBuffer += streamMsg.content || '';
|
||||
}
|
||||
} else if (streamMsg.type === 'error') {
|
||||
// SDK now surfaces error detail that was previously dropped.
|
||||
// Store for use in the user-facing error message.
|
||||
|
||||
@@ -128,6 +128,11 @@ export interface BotConfig {
|
||||
|
||||
// Display
|
||||
displayName?: string; // Prefix outbound messages (e.g. "💜 Signo")
|
||||
display?: {
|
||||
showToolCalls?: boolean; // Show tool invocations in channel output
|
||||
showReasoning?: boolean; // Show agent reasoning/thinking in channel output
|
||||
reasoningMaxChars?: number; // Truncate reasoning to N chars (default: 0 = no limit)
|
||||
};
|
||||
|
||||
// Skills
|
||||
skills?: SkillsConfig;
|
||||
|
||||
@@ -531,6 +531,7 @@ async function main() {
|
||||
displayName: agentConfig.displayName,
|
||||
maxToolCalls: agentConfig.features?.maxToolCalls,
|
||||
memfs: resolvedMemfs,
|
||||
display: agentConfig.features?.display,
|
||||
conversationMode: agentConfig.conversations?.mode || 'shared',
|
||||
heartbeatConversation: agentConfig.conversations?.heartbeat || 'last-active',
|
||||
skills: {
|
||||
|
||||
Reference in New Issue
Block a user