fix(approval): include allow comments in tool return payloads (#1443)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user