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 }; | { type: "deny"; approval: ApprovalRequest; reason: string };
export type ApprovalToolResult = ToolReturn & {
reason?: string;
};
// Align result type with the SDK's expected union for approvals payloads // 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. * Execute a single approval decision and return the result.
@@ -226,6 +230,7 @@ async function executeSingleDecision(
status: decision.precomputedResult.status, status: decision.precomputedResult.status,
stdout: decision.precomputedResult.stdout, stdout: decision.precomputedResult.stdout,
stderr: decision.precomputedResult.stderr, stderr: decision.precomputedResult.stderr,
reason: decision.reason,
}; };
} }
@@ -284,6 +289,7 @@ async function executeSingleDecision(
status: toolResult.status, status: toolResult.status,
stdout: toolResult.stdout, stdout: toolResult.stdout,
stderr: toolResult.stderr, stderr: toolResult.stderr,
reason: decision.reason,
}; };
} catch (e) { } catch (e) {
const isAbortError = const isAbortError =
@@ -309,6 +315,7 @@ async function executeSingleDecision(
tool_call_id: decision.approval.toolCallId, tool_call_id: decision.approval.toolCallId,
tool_return: errorMessage, tool_return: errorMessage,
status: "error", status: "error",
reason: decision.reason,
}; };
} }
} }

View File

@@ -1,13 +1,13 @@
import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents"; import type { MessageCreate } from "@letta-ai/letta-client/resources/agents/agents";
import type { ApprovalCreate } from "@letta-ai/letta-client/resources/agents/messages"; import type { ApprovalCreate } from "@letta-ai/letta-client/resources/agents/messages";
import { INTERRUPTED_BY_USER } from "../constants"; import { INTERRUPTED_BY_USER } from "../constants";
import type { ToolReturnContent } from "../tools/manager";
import type { ApprovalResult } from "./approval-execution"; import type { ApprovalResult } from "./approval-execution";
type OutgoingMessage = MessageCreate | ApprovalCreate; type OutgoingMessage = MessageCreate | ApprovalCreate;
type ToolReturnContent = Extract<
ApprovalResult, const APPROVAL_COMMENT_PREFIX =
{ type: "tool" } "The user approved the tool execution with the following comment:";
>["tool_return"];
export type ApprovalNormalizationOptions = { 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( export function normalizeApprovalResultsForPersistence(
approvals: ApprovalResult[] | null | undefined, approvals: ApprovalResult[] | null | undefined,
options: ApprovalNormalizationOptions = {}, options: ApprovalNormalizationOptions = {},
@@ -126,6 +147,12 @@ export function normalizeApprovalResultsForPersistence(
"tool_call_id" in approval && typeof approval.tool_call_id === "string" "tool_call_id" in approval && typeof approval.tool_call_id === "string"
? approval.tool_call_id ? approval.tool_call_id
: ""; : "";
const toolReturn = prependApprovalComment(
approval.tool_return as ToolReturnContent,
"reason" in approval && typeof approval.reason === "string"
? approval.reason
: undefined,
);
const interruptedByStructuredId = const interruptedByStructuredId =
toolCallId.length > 0 && interruptedSet.has(toolCallId); toolCallId.length > 0 && interruptedSet.has(toolCallId);
@@ -142,10 +169,18 @@ export function normalizeApprovalResultsForPersistence(
) { ) {
return { return {
...approval, ...approval,
tool_return: toolReturn,
status: "error" as const, status: "error" as const,
}; };
} }
if (toolReturn !== approval.tool_return) {
return {
...approval,
tool_return: toolReturn,
};
}
return approval; return approval;
}); });
} }

View File

@@ -90,6 +90,60 @@ describe("normalizeApprovalResultsForPersistence", () => {
status: "error", 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", () => { describe("normalizeOutgoingApprovalMessages", () => {