fix: improve subagent UI display and interruption handling (#330)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type { ExecOptions } from "node:child_process";
|
||||
import { exec, spawn } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { INTERRUPTED_BY_USER } from "../../constants";
|
||||
import { backgroundProcesses, getNextBashId } from "./process_manager.js";
|
||||
import { getShellEnv } from "./shellEnv.js";
|
||||
import { LIMITS, truncateByChars } from "./truncation.js";
|
||||
@@ -156,7 +157,7 @@ export async function bash(args: BashArgs): Promise<BashResult> {
|
||||
|
||||
let errorMessage = "";
|
||||
if (isAbort) {
|
||||
errorMessage = "User interrupted tool execution";
|
||||
errorMessage = INTERRUPTED_BY_USER;
|
||||
} else {
|
||||
if (err.killed && err.signal === "SIGTERM")
|
||||
errorMessage = `Command timed out after ${effectiveTimeout}ms\n`;
|
||||
|
||||
@@ -20,6 +20,7 @@ interface TaskArgs {
|
||||
description: string;
|
||||
model?: string;
|
||||
toolCallId?: string; // Injected by executeTool for linking subagent to parent tool call
|
||||
signal?: AbortSignal; // Injected by executeTool for interruption handling
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -33,7 +34,8 @@ export async function task(args: TaskArgs): Promise<string> {
|
||||
"Task",
|
||||
);
|
||||
|
||||
const { subagent_type, prompt, description, model, toolCallId } = args;
|
||||
const { subagent_type, prompt, description, model, toolCallId, signal } =
|
||||
args;
|
||||
|
||||
// Get all available subagent configs (built-in + custom)
|
||||
const allConfigs = await getAllSubagentConfigs();
|
||||
@@ -54,12 +56,14 @@ export async function task(args: TaskArgs): Promise<string> {
|
||||
prompt,
|
||||
model,
|
||||
subagentId,
|
||||
signal,
|
||||
);
|
||||
|
||||
// Mark subagent as completed in state store
|
||||
completeSubagent(subagentId, {
|
||||
success: result.success,
|
||||
error: result.error,
|
||||
totalTokens: result.totalTokens,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
} from "@letta-ai/letta-client";
|
||||
import { getModelInfo } from "../agent/model";
|
||||
import { getAllSubagentConfigs } from "../agent/subagents";
|
||||
import { INTERRUPTED_BY_USER } from "../constants";
|
||||
import { telemetry } from "../telemetry";
|
||||
import { TOOL_DEFINITIONS, type ToolName } from "./toolDefinitions";
|
||||
|
||||
@@ -911,9 +912,14 @@ export async function executeTool(
|
||||
enhancedArgs = { ...enhancedArgs, signal: options.signal };
|
||||
}
|
||||
|
||||
// Inject toolCallId for Task tool (for linking subagents to their parent tool call)
|
||||
if (internalName === "Task" && options?.toolCallId) {
|
||||
enhancedArgs = { ...enhancedArgs, toolCallId: options.toolCallId };
|
||||
// Inject toolCallId and abort signal for Task tool
|
||||
if (internalName === "Task") {
|
||||
if (options?.toolCallId) {
|
||||
enhancedArgs = { ...enhancedArgs, toolCallId: options.toolCallId };
|
||||
}
|
||||
if (options?.signal) {
|
||||
enhancedArgs = { ...enhancedArgs, signal: options.signal };
|
||||
}
|
||||
}
|
||||
|
||||
const result = await tool.fn(enhancedArgs);
|
||||
@@ -925,23 +931,27 @@ export async function executeTool(
|
||||
const stderrValue = recordResult?.stderr;
|
||||
const stdout = isStringArray(stdoutValue) ? stdoutValue : undefined;
|
||||
const stderr = isStringArray(stderrValue) ? stderrValue : undefined;
|
||||
|
||||
// Check if tool returned a status (e.g., Bash returns status: "error" on abort)
|
||||
const toolStatus = recordResult?.status === "error" ? "error" : "success";
|
||||
|
||||
// Flatten the response to plain text
|
||||
const flattenedResponse = flattenToolResponse(result);
|
||||
|
||||
// Track tool usage (success path - we're in the try block)
|
||||
// Track tool usage
|
||||
telemetry.trackToolUsage(
|
||||
internalName,
|
||||
true, // Hardcoded to true since tool execution succeeded
|
||||
toolStatus === "success",
|
||||
duration,
|
||||
flattenedResponse.length,
|
||||
undefined, // no error_type on success
|
||||
toolStatus === "error" ? "tool_error" : undefined,
|
||||
stderr ? stderr.join("\n") : undefined,
|
||||
);
|
||||
|
||||
// Return the full response (truncation happens in UI layer only)
|
||||
return {
|
||||
toolReturn: flattenedResponse,
|
||||
status: "success",
|
||||
status: toolStatus,
|
||||
...(stdout && { stdout }),
|
||||
...(stderr && { stderr }),
|
||||
};
|
||||
@@ -959,7 +969,7 @@ export async function executeTool(
|
||||
? error.name
|
||||
: "unknown";
|
||||
const errorMessage = isAbort
|
||||
? "User interrupted tool execution"
|
||||
? INTERRUPTED_BY_USER
|
||||
: error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
|
||||
Reference in New Issue
Block a user