A React Native app for the ultimate thinking partner.

docs(api): add comprehensive comments on message transformation

- Document API message structure for different message_types
- Explain why tool_call and tool_return fields need special handling
- Add examples showing tool call/return content construction
- Clarify simple transformation architecture (no filtering/grouping)
- Prevent future regressions where tool messages display as empty

+43 -4
+43 -4
src/api/lettaApi.ts
··· 586 586 const response = await this.client.agents.messages.list(agentId, params); 587 587 console.log('listMessages - response count:', response?.length || 0); 588 588 589 - // Simple transformation - just convert format, no grouping or attaching 589 + /** 590 + * MESSAGE TRANSFORMATION ARCHITECTURE 591 + * 592 + * This transformation is intentionally simple: we return ALL messages from the API 593 + * without filtering, grouping, or combining them. The UI handles rendering based on 594 + * message_type. This prevents data loss and keeps the logic maintainable. 595 + * 596 + * CRITICAL: API Message Structure 597 + * 598 + * The Letta API returns messages with different structures based on message_type: 599 + * 600 + * 1. TOOL CALL MESSAGES (type: 'tool_call_message') 601 + * - message.content: Often EMPTY or null 602 + * - message.tool_call: { name: string, arguments: string (JSON) } 603 + * - We MUST construct content from tool_call.name + tool_call.arguments 604 + * - Example: tool_call = { name: "memory_replace", arguments: "{\"label\":\"you\"...}" } 605 + * → content = "memory_replace({\"label\":\"you\"...})" 606 + * 607 + * 2. TOOL RETURN MESSAGES (type: 'tool_return_message') 608 + * - message.content: Often EMPTY or null 609 + * - message.tool_return: string | { tool_return: string } 610 + * - We MUST extract content from tool_return field 611 + * - Example: tool_return = "The core memory block has been edited" 612 + * → content = "The core memory block has been edited" 613 + * 614 + * 3. ASSISTANT/REASONING MESSAGES (type: 'assistant_message' | 'reasoning_message') 615 + * - message.content: Contains the actual text 616 + * - No special handling needed 617 + * 618 + * 4. USER/SYSTEM MESSAGES (type: 'user_message' | 'system_message') 619 + * - message.content: Contains the actual text 620 + * - No special handling needed 621 + * 622 + * WHY THIS MATTERS: 623 + * If we don't construct content from tool_call/tool_return fields, tool messages will 624 + * display as empty blocks like "tool({})" which is meaningless to the user. 625 + */ 590 626 const transformedMessages: LettaMessage[] = response.map((message: any) => { 591 627 const type = message.messageType as string; 628 + 629 + // Extract tool call/return data (try multiple field name variants for SDK compatibility) 592 630 const toolCall = message.tool_call || message.toolCall || (message.tool_calls && message.tool_calls[0]); 593 631 const toolReturn = message.tool_response || message.toolResponse || message.tool_return || message.toolReturn; 594 632 633 + // Map message type to role 595 634 let role: 'user' | 'assistant' | 'system' | 'tool' = 'assistant'; 596 635 if (type === 'user_message') { 597 636 role = 'user'; ··· 603 642 role = 'tool'; 604 643 } 605 644 606 - // Get content - construct from tool call if needed 645 + // Start with content from API (may be empty for tool messages) 607 646 let content: string = message.content || message.reasoning || ''; 608 647 609 - // For tool call messages, construct content from tool_call data if content is empty 648 + // CONSTRUCT content for tool call messages (if content is empty) 610 649 if ((type === 'tool_call_message' || type === 'tool_call') && !content && toolCall) { 611 650 const name = toolCall.name || 'tool'; 612 651 const args = toolCall.arguments || '{}'; 613 652 content = `${name}(${args})`; 614 653 } 615 654 616 - // For tool return messages, use the tool_return or tool_response value 655 + // EXTRACT content for tool return messages (if content is empty) 617 656 if ((type === 'tool_return_message' || type === 'tool_response') && !content && toolReturn) { 618 657 content = typeof toolReturn === 'string' ? toolReturn : (toolReturn.tool_return || toolReturn.content || ''); 619 658 }