fix(approval): include allow comments in tool return payloads (#1443)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-03-18 16:59:30 -07:00
committed by GitHub
parent 3b99b7d3b9
commit 59abc529de
3 changed files with 101 additions and 5 deletions

View File

@@ -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,
};
}
}

View File

@@ -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;
});
}

View File

@@ -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", () => {