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 };
|
| { 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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user