fix: approval detection missing include param + /reset command + startup check (#175)
getPendingApprovals() was calling agents.retrieve() without the include: ['agent.pending_approval'] parameter, so the Letta API never returned the pending_approval field. This caused stuck server-side tool approvals (requires_approval=true) to go undetected, leaving the agent permanently stuck with empty responses. Also adds: - Proactive ensureNoToolApprovals() on startup to disable requires_approval - /reset slash command for deployed instances (clears conversation, keeps memory) - Robust parsing for ToolCallDelta and deprecated tool_call field formats Written by Cameron ◯ Letta Code "The only way to do great work is to love what you do." - Steve Jobs
This commit is contained in:
@@ -79,6 +79,13 @@ export class LettaBot {
|
||||
});
|
||||
return '⏰ Heartbeat triggered (silent mode - check server logs)';
|
||||
}
|
||||
case 'reset': {
|
||||
const oldConversationId = this.store.conversationId;
|
||||
this.store.conversationId = null;
|
||||
this.store.resetRecoveryAttempts();
|
||||
console.log(`[Command] /reset - conversation cleared (was: ${oldConversationId})`);
|
||||
return 'Conversation reset. Send a message to start a new conversation. (Agent memory is preserved.)';
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* Shared command parsing and help text for all channels.
|
||||
*/
|
||||
|
||||
export const COMMANDS = ['status', 'heartbeat', 'help', 'start'] as const;
|
||||
export const COMMANDS = ['status', 'heartbeat', 'reset', 'help', 'start'] as const;
|
||||
export type Command = typeof COMMANDS[number];
|
||||
|
||||
export const HELP_TEXT = `LettaBot - AI assistant with persistent memory
|
||||
@@ -12,6 +12,7 @@ export const HELP_TEXT = `LettaBot - AI assistant with persistent memory
|
||||
Commands:
|
||||
/status - Show current status
|
||||
/heartbeat - Trigger heartbeat
|
||||
/reset - Reset conversation (keeps agent memory)
|
||||
/help - Show this message
|
||||
|
||||
Just send a message to get started!`;
|
||||
|
||||
10
src/main.ts
10
src/main.ts
@@ -122,7 +122,7 @@ import { DiscordAdapter } from './channels/discord.js';
|
||||
import { CronService } from './cron/service.js';
|
||||
import { HeartbeatService } from './cron/heartbeat.js';
|
||||
import { PollingService } from './polling/service.js';
|
||||
import { agentExists, findAgentByName } from './tools/letta-api.js';
|
||||
import { agentExists, findAgentByName, ensureNoToolApprovals } from './tools/letta-api.js';
|
||||
// Skills are now installed to agent-scoped location after agent creation (see bot.ts)
|
||||
|
||||
// Check if config exists (skip in Railway/Docker where env vars are used directly)
|
||||
@@ -380,6 +380,14 @@ async function main() {
|
||||
console.log(`[Agent] No agent found - will create "${agentName}" on first message`);
|
||||
}
|
||||
|
||||
// Proactively disable tool approvals for headless operation
|
||||
// Prevents stuck states from server-side requires_approval=true (SDK issue #25)
|
||||
if (initialStatus.agentId) {
|
||||
ensureNoToolApprovals(initialStatus.agentId).catch(err => {
|
||||
console.warn('[Agent] Failed to check tool approvals:', err);
|
||||
});
|
||||
}
|
||||
|
||||
// Register enabled channels
|
||||
if (config.telegram.enabled) {
|
||||
const telegram = new TelegramAdapter({
|
||||
|
||||
@@ -235,20 +235,46 @@ export async function getPendingApprovals(
|
||||
const client = getClient();
|
||||
|
||||
// Prefer agent-level pending approval to avoid scanning stale history.
|
||||
// IMPORTANT: Must include 'agent.pending_approval' or the field won't be returned.
|
||||
try {
|
||||
const agentState = await client.agents.retrieve(agentId);
|
||||
const agentState = await client.agents.retrieve(agentId, {
|
||||
include: ['agent.pending_approval'],
|
||||
});
|
||||
if ('pending_approval' in agentState) {
|
||||
const pending = (agentState as { pending_approval?: { id: string; run_id?: string | null; tool_calls?: Array<{ tool_call_id: string; name: string }>; tool_call?: { tool_call_id: string; name: string } } | null }).pending_approval;
|
||||
const pending = agentState.pending_approval;
|
||||
if (!pending) {
|
||||
console.log('[Letta API] No pending approvals on agent');
|
||||
return [];
|
||||
}
|
||||
const toolCalls = pending.tool_calls || (pending.tool_call ? [pending.tool_call] : []);
|
||||
console.log(`[Letta API] Found pending approval: ${pending.id}, run_id=${pending.run_id}`);
|
||||
|
||||
// Extract tool calls - handle both Array<ToolCall> and ToolCallDelta formats
|
||||
const rawToolCalls = pending.tool_calls;
|
||||
const toolCallsList: Array<{ tool_call_id: string; name: string }> = [];
|
||||
|
||||
if (Array.isArray(rawToolCalls)) {
|
||||
for (const tc of rawToolCalls) {
|
||||
if (tc && 'tool_call_id' in tc && tc.tool_call_id) {
|
||||
toolCallsList.push({ tool_call_id: tc.tool_call_id, name: tc.name || 'unknown' });
|
||||
}
|
||||
}
|
||||
} else if (rawToolCalls && typeof rawToolCalls === 'object' && 'tool_call_id' in rawToolCalls && rawToolCalls.tool_call_id) {
|
||||
// ToolCallDelta case
|
||||
toolCallsList.push({ tool_call_id: rawToolCalls.tool_call_id, name: rawToolCalls.name || 'unknown' });
|
||||
}
|
||||
|
||||
// Fallback to deprecated singular tool_call field
|
||||
if (toolCallsList.length === 0 && pending.tool_call) {
|
||||
const tc = pending.tool_call;
|
||||
if ('tool_call_id' in tc && tc.tool_call_id) {
|
||||
toolCallsList.push({ tool_call_id: tc.tool_call_id, name: tc.name || 'unknown' });
|
||||
}
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const approvals: PendingApproval[] = [];
|
||||
for (const tc of toolCalls) {
|
||||
if (!tc?.tool_call_id || seen.has(tc.tool_call_id)) {
|
||||
continue;
|
||||
}
|
||||
for (const tc of toolCallsList) {
|
||||
if (seen.has(tc.tool_call_id)) continue;
|
||||
seen.add(tc.tool_call_id);
|
||||
approvals.push({
|
||||
runId: pending.run_id || 'unknown',
|
||||
@@ -257,6 +283,7 @@ export async function getPendingApprovals(
|
||||
messageId: pending.id,
|
||||
});
|
||||
}
|
||||
console.log(`[Letta API] Extracted ${approvals.length} pending approval(s): ${approvals.map(a => a.toolName).join(', ')}`);
|
||||
return approvals;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -460,6 +487,24 @@ export async function getAgentTools(agentId: string): Promise<Array<{
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure no tools on the agent require approval.
|
||||
* Call on startup to proactively prevent stuck approval states.
|
||||
*/
|
||||
export async function ensureNoToolApprovals(agentId: string): Promise<void> {
|
||||
try {
|
||||
const tools = await getAgentTools(agentId);
|
||||
const approvalTools = tools.filter(t => t.requiresApproval);
|
||||
if (approvalTools.length > 0) {
|
||||
console.log(`[Letta API] Found ${approvalTools.length} tool(s) requiring approval: ${approvalTools.map(t => t.name).join(', ')}`);
|
||||
console.log('[Letta API] Disabling tool approvals for headless operation...');
|
||||
await disableAllToolApprovals(agentId);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Letta API] Failed to check/disable tool approvals:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable approval requirement for ALL tools on an agent.
|
||||
* Useful for ensuring a headless deployment doesn't get stuck.
|
||||
|
||||
Reference in New Issue
Block a user