From 59abc529deaf0771c9dadb7bd4824a6d6719dde2 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Wed, 18 Mar 2026 16:59:30 -0700 Subject: [PATCH] fix(approval): include allow comments in tool return payloads (#1443) Co-authored-by: Letta Code --- src/agent/approval-execution.ts | 9 +++- src/agent/approval-result-normalization.ts | 43 +++++++++++++-- .../approval-result-normalization.test.ts | 54 +++++++++++++++++++ 3 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/agent/approval-execution.ts b/src/agent/approval-execution.ts index a2ef2a7..b37dfb0 100644 --- a/src/agent/approval-execution.ts +++ b/src/agent/approval-execution.ts @@ -176,8 +176,12 @@ export type ApprovalDecision = } | { type: "deny"; approval: ApprovalRequest; reason: string }; +export type ApprovalToolResult = ToolReturn & { + reason?: string; +}; + // Align result type with the SDK's expected union for approvals payloads -export type ApprovalResult = ToolReturn | ApprovalReturn; +export type ApprovalResult = ApprovalToolResult | ApprovalReturn; /** * Execute a single approval decision and return the result. @@ -226,6 +230,7 @@ async function executeSingleDecision( status: decision.precomputedResult.status, stdout: decision.precomputedResult.stdout, stderr: decision.precomputedResult.stderr, + reason: decision.reason, }; } @@ -284,6 +289,7 @@ async function executeSingleDecision( status: toolResult.status, stdout: toolResult.stdout, stderr: toolResult.stderr, + reason: decision.reason, }; } catch (e) { const isAbortError = @@ -309,6 +315,7 @@ async function executeSingleDecision( tool_call_id: decision.approval.toolCallId, tool_return: errorMessage, status: "error", + reason: decision.reason, }; } } diff --git a/src/agent/approval-result-normalization.ts b/src/agent/approval-result-normalization.ts index 4bf3698..97b7839 100644 --- a/src/agent/approval-result-normalization.ts +++ b/src/agent/approval-result-normalization.ts @@ -1,13 +1,13 @@ import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents"; import type { ApprovalCreate } from "@letta-ai/letta-client/resources/agents/messages"; import { INTERRUPTED_BY_USER } from "../constants"; +import type { ToolReturnContent } from "../tools/manager"; import type { ApprovalResult } from "./approval-execution"; type OutgoingMessage = MessageCreate | ApprovalCreate; -type ToolReturnContent = Extract< - ApprovalResult, - { type: "tool" } ->["tool_return"]; + +const APPROVAL_COMMENT_PREFIX = + "The user approved the tool execution with the following comment:"; export type ApprovalNormalizationOptions = { /** @@ -71,6 +71,27 @@ function isToolReturnContent(value: unknown): value is ToolReturnContent { ); } +function prependApprovalComment( + toolReturn: ToolReturnContent, + reason: string | undefined, +): ToolReturnContent { + const trimmedReason = reason?.trim(); + if (!trimmedReason) { + return toolReturn; + } + + const commentPart = { + type: "text" as const, + text: `${APPROVAL_COMMENT_PREFIX} "${trimmedReason}"`, + }; + + if (typeof toolReturn === "string") { + return [commentPart, { type: "text" as const, text: toolReturn }]; + } + + return [commentPart, ...toolReturn]; +} + export function normalizeApprovalResultsForPersistence( approvals: ApprovalResult[] | null | undefined, options: ApprovalNormalizationOptions = {}, @@ -126,6 +147,12 @@ export function normalizeApprovalResultsForPersistence( "tool_call_id" in approval && typeof approval.tool_call_id === "string" ? approval.tool_call_id : ""; + const toolReturn = prependApprovalComment( + approval.tool_return as ToolReturnContent, + "reason" in approval && typeof approval.reason === "string" + ? approval.reason + : undefined, + ); const interruptedByStructuredId = toolCallId.length > 0 && interruptedSet.has(toolCallId); @@ -142,10 +169,18 @@ export function normalizeApprovalResultsForPersistence( ) { return { ...approval, + tool_return: toolReturn, status: "error" as const, }; } + if (toolReturn !== approval.tool_return) { + return { + ...approval, + tool_return: toolReturn, + }; + } + return approval; }); } diff --git a/src/tests/agent/approval-result-normalization.test.ts b/src/tests/agent/approval-result-normalization.test.ts index 18d9425..8043624 100644 --- a/src/tests/agent/approval-result-normalization.test.ts +++ b/src/tests/agent/approval-result-normalization.test.ts @@ -90,6 +90,60 @@ describe("normalizeApprovalResultsForPersistence", () => { status: "error", }); }); + + test("prepends verbose approval comment to string tool returns", () => { + const approvals: ApprovalResult[] = [ + { + type: "tool", + tool_call_id: "call-4", + tool_return: "bash output", + status: "success", + reason: "Ship it", + } as ApprovalResult, + ]; + + const normalized = normalizeApprovalResultsForPersistence(approvals); + + expect(normalized[0]).toMatchObject({ + type: "tool", + tool_call_id: "call-4", + status: "success", + tool_return: [ + { + type: "text", + text: 'The user approved the tool execution with the following comment: "Ship it"', + }, + { type: "text", text: "bash output" }, + ], + }); + }); + + test("prepends verbose approval comment to structured tool returns", () => { + const approvals: ApprovalResult[] = [ + { + type: "tool", + tool_call_id: "call-5", + tool_return: [{ type: "text", text: "line 1" }], + status: "success", + reason: "Run exactly this", + } as ApprovalResult, + ]; + + const normalized = normalizeApprovalResultsForPersistence(approvals); + + expect(normalized[0]).toMatchObject({ + type: "tool", + tool_call_id: "call-5", + status: "success", + tool_return: [ + { + type: "text", + text: 'The user approved the tool execution with the following comment: "Run exactly this"', + }, + { type: "text", text: "line 1" }, + ], + }); + }); }); describe("normalizeOutgoingApprovalMessages", () => {