refactor: extract shared approval batch execution logic (#82)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2025-11-07 18:23:16 -08:00
committed by GitHub
parent ac0b929119
commit d762859963
3 changed files with 166 additions and 195 deletions

View File

@@ -0,0 +1,125 @@
// src/agent/approval-execution.ts
// Shared logic for executing approval batches (used by both interactive and headless modes)
import type { ApprovalRequest } from "../cli/helpers/stream";
import { executeTool } from "../tools/manager";
export type ApprovalDecision =
| { type: "approve"; approval: ApprovalRequest }
| { type: "deny"; approval: ApprovalRequest; reason: string };
export type ApprovalResult = {
type: "tool" | "approval";
tool_call_id: string;
tool_return?: string;
status?: "success" | "error";
stdout?: string[];
stderr?: string[];
approve?: boolean;
reason?: string;
};
/**
* Execute a batch of approval decisions and format results for the backend.
*
* This function handles:
* - Executing approved tools (with error handling)
* - Formatting denials
* - Combining all results into a single batch
*
* Used by both interactive (App.tsx) and headless (headless.ts) modes.
*
* @param decisions - Array of approve/deny decisions for each tool
* @param onChunk - Optional callback to update UI with tool results (for interactive mode)
* @returns Array of formatted results ready to send to backend
*/
export async function executeApprovalBatch(
decisions: ApprovalDecision[],
onChunk?: (chunk: any) => void,
): Promise<ApprovalResult[]> {
const results: ApprovalResult[] = [];
for (const decision of decisions) {
if (decision.type === "approve") {
// Execute the approved tool
try {
const parsedArgs =
typeof decision.approval.toolArgs === "string"
? JSON.parse(decision.approval.toolArgs)
: decision.approval.toolArgs || {};
const toolResult = await executeTool(
decision.approval.toolName,
parsedArgs,
);
// Update UI if callback provided (interactive mode)
if (onChunk) {
onChunk({
message_type: "tool_return_message",
id: "dummy",
date: new Date().toISOString(),
tool_call_id: decision.approval.toolCallId,
tool_return: toolResult.toolReturn,
status: toolResult.status,
stdout: toolResult.stdout,
stderr: toolResult.stderr,
});
}
results.push({
type: "tool",
tool_call_id: decision.approval.toolCallId,
tool_return: toolResult.toolReturn,
status: toolResult.status,
stdout: toolResult.stdout,
stderr: toolResult.stderr,
});
} catch (e) {
// Still need to send error result to backend for this tool
const errorMessage = `Error executing tool: ${String(e)}`;
// Update UI if callback provided
if (onChunk) {
onChunk({
message_type: "tool_return_message",
id: "dummy",
date: new Date().toISOString(),
tool_call_id: decision.approval.toolCallId,
tool_return: errorMessage,
status: "error",
});
}
results.push({
type: "tool",
tool_call_id: decision.approval.toolCallId,
tool_return: errorMessage,
status: "error",
});
}
} else {
// Format denial for backend
// Update UI if callback provided
if (onChunk) {
onChunk({
message_type: "tool_return_message",
id: "dummy",
date: new Date().toISOString(),
tool_call_id: decision.approval.toolCallId,
tool_return: `Error: request to call tool denied. User reason: ${decision.reason}`,
status: "error",
});
}
results.push({
type: "approval",
tool_call_id: decision.approval.toolCallId,
approve: false,
reason: decision.reason,
});
}
}
return results;
}

View File

@@ -1169,94 +1169,28 @@ export default function App({
...(additionalDecision ? [additionalDecision] : []),
];
// Execute approved tools and format results
const executedResults: Array<{
type: "tool" | "approval";
tool_call_id: string;
tool_return?: string;
status?: "success" | "error";
stdout?: string[];
stderr?: string[];
approve?: boolean;
reason?: string;
}> = [];
for (const decision of allDecisions) {
if (decision.type === "approve") {
// Execute the approved tool
try {
const parsedArgs = safeJsonParseOr<Record<string, unknown>>(
decision.approval.toolArgs,
{},
// Execute approved tools and format results using shared function
const { executeApprovalBatch } = await import(
"../agent/approval-execution"
);
const executedResults = await executeApprovalBatch(
allDecisions,
(chunk) => {
onChunk(buffersRef.current, chunk);
// Also log errors to the UI error display
if (
chunk.status === "error" &&
chunk.message_type === "tool_return_message"
) {
const isToolError = chunk.tool_return?.startsWith(
"Error executing tool:",
);
const toolResult = await executeTool(
decision.approval.toolName,
parsedArgs,
);
// Update buffers with tool return for UI
onChunk(buffersRef.current, {
message_type: "tool_return_message",
id: "dummy",
date: new Date().toISOString(),
tool_call_id: decision.approval.toolCallId,
tool_return: toolResult.toolReturn,
status: toolResult.status,
stdout: toolResult.stdout,
stderr: toolResult.stderr,
});
executedResults.push({
type: "tool",
tool_call_id: decision.approval.toolCallId,
tool_return: toolResult.toolReturn,
status: toolResult.status,
stdout: toolResult.stdout,
stderr: toolResult.stderr,
});
} catch (e) {
appendError(String(e));
// Still need to send error result to backend for this tool
const errorMessage = `Error executing tool: ${String(e)}`;
// Update buffers with error for UI
onChunk(buffersRef.current, {
message_type: "tool_return_message",
id: "dummy",
date: new Date().toISOString(),
tool_call_id: decision.approval.toolCallId,
tool_return: errorMessage,
status: "error",
});
executedResults.push({
type: "tool",
tool_call_id: decision.approval.toolCallId,
tool_return: errorMessage,
status: "error",
});
if (isToolError) {
appendError(chunk.tool_return);
}
}
} else {
// Format denial for backend
// Update buffers with denial for UI
onChunk(buffersRef.current, {
message_type: "tool_return_message",
id: "dummy",
date: new Date().toISOString(),
tool_call_id: decision.approval.toolCallId,
tool_return: `Error: request to call tool denied. User reason: ${decision.reason}`,
status: "error",
});
executedResults.push({
type: "approval",
tool_call_id: decision.approval.toolCallId,
approve: false,
reason: decision.reason,
});
}
}
},
);
// Combine with auto-handled and auto-denied results
const allResults = [

View File

@@ -258,68 +258,28 @@ export async function handleHeadlessCommand(
});
}
// Phase 2: Execute approved tools and format results
const executedResults: Array<{
type: "tool" | "approval";
tool_call_id: string;
tool_return?: string;
status?: "success" | "error";
stdout?: string[];
stderr?: string[];
approve?: boolean;
reason?: string;
}> = [];
// Phase 2: Execute approved tools and format results using shared function
const { executeApprovalBatch } = await import(
"./agent/approval-execution"
);
for (const decision of decisions) {
if (decision.type === "approve") {
try {
const parsedArgs = safeJsonParseOr<Record<string, unknown>>(
decision.approval.toolArgs || "{}",
{},
// Emit auto_approval events for stream-json format
if (outputFormat === "stream-json") {
for (const decision of decisions) {
if (decision.type === "approve") {
console.log(
JSON.stringify({
type: "auto_approval",
tool_name: decision.approval.toolName,
tool_call_id: decision.approval.toolCallId,
}),
);
const toolResult = await executeTool(
decision.approval.toolName,
parsedArgs,
);
// Emit auto_approval event for stream-json for visibility
if (outputFormat === "stream-json") {
console.log(
JSON.stringify({
type: "auto_approval",
tool_name: decision.approval.toolName,
tool_call_id: decision.approval.toolCallId,
}),
);
}
executedResults.push({
type: "tool",
tool_call_id: decision.approval.toolCallId,
tool_return: toolResult.toolReturn,
status: toolResult.status,
stdout: toolResult.stdout,
stderr: toolResult.stderr,
});
} catch (e) {
const errorMessage = `Error executing tool: ${String(e)}`;
executedResults.push({
type: "tool",
tool_call_id: decision.approval.toolCallId,
tool_return: errorMessage,
status: "error",
});
}
} else {
executedResults.push({
type: "approval",
tool_call_id: decision.approval.toolCallId,
approve: false,
reason: decision.reason,
});
}
}
const executedResults = await executeApprovalBatch(decisions);
// Send all results in one batch
const approvalInput: ApprovalCreate = {
type: "approval",
@@ -683,59 +643,11 @@ export async function handleHeadlessCommand(
});
}
// Phase 2: Execute all approved tools and format results
const executedResults: Array<{
type: "tool" | "approval";
tool_call_id: string;
tool_return?: string;
status?: "success" | "error";
stdout?: string[];
stderr?: string[];
approve?: boolean;
reason?: string;
}> = [];
for (const decision of decisions) {
if (decision.type === "approve") {
// Execute the approved tool
try {
const parsedArgs = safeJsonParseOr<Record<string, unknown>>(
decision.approval.toolArgs,
{},
);
const toolResult = await executeTool(
decision.approval.toolName,
parsedArgs,
);
executedResults.push({
type: "tool",
tool_call_id: decision.approval.toolCallId,
tool_return: toolResult.toolReturn,
status: toolResult.status,
stdout: toolResult.stdout,
stderr: toolResult.stderr,
});
} catch (e) {
// Still need to send error result to backend for this tool
const errorMessage = `Error executing tool: ${String(e)}`;
executedResults.push({
type: "tool",
tool_call_id: decision.approval.toolCallId,
tool_return: errorMessage,
status: "error",
});
}
} else {
// Format denial for backend
executedResults.push({
type: "approval",
tool_call_id: decision.approval.toolCallId,
approve: false,
reason: decision.reason,
});
}
}
// Phase 2: Execute all approved tools and format results using shared function
const { executeApprovalBatch } = await import(
"./agent/approval-execution"
);
const executedResults = await executeApprovalBatch(decisions);
// Send all results in one batch
currentInput = [