Improve subagent error messages with categorization and hints
- Enhanced SubagentRunOutcome type with errorType and errorHint fields - Added categorizeError() helper to classify common error patterns: * File system errors (ENOENT, EACCES, etc.) * API/model errors (rate limits, auth failures, invalid requests) * Network errors (connection refused, DNS failures) * Timeout errors * Configuration errors (missing credentials, quota limits) - Updated error emission in agent-runner-execution.ts to categorize errors - Updated subagent-registry.ts to capture and propagate new error fields - Added buildErrorStatusLabel() helper for user-friendly error messages - Error announcements now include error type and remediation hints Example improved messages: - Before: 'failed: unknown error' - After: 'failed (tool error): ENOENT — File or directory not found' This makes subagent failures much easier to understand and debug while maintaining backward compatibility.
This commit is contained in:
parent
c555c00cb3
commit
44aea44fc8
@ -192,6 +192,41 @@ async function maybeQueueSubagentAnnounce(params: {
|
||||
return "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a descriptive error status label from outcome data.
|
||||
* Includes error type, message, and hint if available.
|
||||
*/
|
||||
function buildErrorStatusLabel(outcome: SubagentRunOutcome): string {
|
||||
const parts: string[] = [];
|
||||
|
||||
// Start with "failed"
|
||||
parts.push("failed");
|
||||
|
||||
// Add error type context
|
||||
if (outcome.errorType) {
|
||||
const typeLabel: Record<string, string> = {
|
||||
model: "API error",
|
||||
tool: "tool error",
|
||||
network: "network error",
|
||||
config: "configuration error",
|
||||
timeout: "timeout",
|
||||
};
|
||||
const label = typeLabel[outcome.errorType] || "error";
|
||||
parts.push(`(${label}):`);
|
||||
}
|
||||
|
||||
// Add error message
|
||||
const errorMsg = outcome.error || "unknown error";
|
||||
parts.push(errorMsg);
|
||||
|
||||
// Add hint if available
|
||||
if (outcome.errorHint) {
|
||||
parts.push(`— ${outcome.errorHint}`);
|
||||
}
|
||||
|
||||
return parts.join(" ");
|
||||
}
|
||||
|
||||
async function buildSubagentStatsLine(params: {
|
||||
sessionKey: string;
|
||||
startedAt?: number;
|
||||
@ -299,6 +334,8 @@ export function buildSubagentSystemPrompt(params: {
|
||||
export type SubagentRunOutcome = {
|
||||
status: "ok" | "error" | "timeout" | "unknown";
|
||||
error?: string;
|
||||
errorType?: "model" | "tool" | "network" | "config" | "timeout" | "unknown";
|
||||
errorHint?: string;
|
||||
};
|
||||
|
||||
export async function runSubagentAnnounceFlow(params: {
|
||||
@ -380,7 +417,7 @@ export async function runSubagentAnnounceFlow(params: {
|
||||
: outcome.status === "timeout"
|
||||
? "timed out"
|
||||
: outcome.status === "error"
|
||||
? `failed: ${outcome.error || "unknown error"}`
|
||||
? buildErrorStatusLabel(outcome)
|
||||
: "finished with unknown status";
|
||||
|
||||
// Build instructional message for main agent
|
||||
|
||||
@ -184,7 +184,16 @@ function ensureListener() {
|
||||
entry.endedAt = endedAt;
|
||||
if (phase === "error") {
|
||||
const error = typeof evt.data?.error === "string" ? (evt.data.error as string) : undefined;
|
||||
entry.outcome = { status: "error", error };
|
||||
const errorType =
|
||||
typeof evt.data?.errorType === "string" ? (evt.data.errorType as string) : undefined;
|
||||
const errorHint =
|
||||
typeof evt.data?.errorHint === "string" ? (evt.data.errorHint as string) : undefined;
|
||||
entry.outcome = {
|
||||
status: "error",
|
||||
error,
|
||||
errorType: errorType as SubagentRunOutcome["errorType"],
|
||||
errorHint,
|
||||
};
|
||||
} else {
|
||||
entry.outcome = { status: "ok" };
|
||||
}
|
||||
|
||||
@ -51,6 +51,85 @@ export type AgentRunLoopResult =
|
||||
}
|
||||
| { kind: "final"; payload: ReplyPayload };
|
||||
|
||||
/**
|
||||
* Categorize errors to provide better error messages to users.
|
||||
* Returns error message, type, and optional hint for remediation.
|
||||
*/
|
||||
function categorizeError(err: unknown): {
|
||||
message: string;
|
||||
type: "model" | "tool" | "network" | "config" | "timeout" | "unknown";
|
||||
hint?: string;
|
||||
} {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
|
||||
// File system errors
|
||||
if (message.includes("ENOENT") || message.includes("ENOTDIR")) {
|
||||
return { message, type: "tool", hint: "File or directory not found" };
|
||||
}
|
||||
if (message.includes("EACCES") || message.includes("EPERM")) {
|
||||
return { message, type: "tool", hint: "Permission denied" };
|
||||
}
|
||||
if (message.includes("EISDIR")) {
|
||||
return { message, type: "tool", hint: "Expected file but found directory" };
|
||||
}
|
||||
|
||||
// API/Model errors
|
||||
if (message.includes("rate limit") || message.includes("429")) {
|
||||
return { message, type: "model", hint: "Rate limit exceeded - retry in a few moments" };
|
||||
}
|
||||
if (
|
||||
message.includes("401") ||
|
||||
message.includes("unauthorized") ||
|
||||
message.includes("authentication")
|
||||
) {
|
||||
return { message, type: "config", hint: "Check API credentials and permissions" };
|
||||
}
|
||||
if (message.includes("403") || message.includes("forbidden")) {
|
||||
return { message, type: "config", hint: "Access denied - check permissions" };
|
||||
}
|
||||
if (message.includes("400") || message.includes("invalid request")) {
|
||||
return { message, type: "model", hint: "Invalid request parameters" };
|
||||
}
|
||||
if (message.includes("500") || message.includes("503")) {
|
||||
return { message, type: "model", hint: "API service error - try again later" };
|
||||
}
|
||||
if (message.includes("quota") || message.includes("billing")) {
|
||||
return { message, type: "config", hint: "Check billing and API quota limits" };
|
||||
}
|
||||
|
||||
// Network errors
|
||||
if (message.includes("ECONNREFUSED") || message.includes("ETIMEDOUT")) {
|
||||
return { message, type: "network", hint: "Connection failed - check network connectivity" };
|
||||
}
|
||||
if (message.includes("ENOTFOUND") || message.includes("DNS") || message.includes("EAI_AGAIN")) {
|
||||
return { message, type: "network", hint: "DNS resolution failed - check hostname" };
|
||||
}
|
||||
if (message.includes("ENETUNREACH") || message.includes("EHOSTUNREACH")) {
|
||||
return { message, type: "network", hint: "Network unreachable - check connection" };
|
||||
}
|
||||
|
||||
// Timeout errors
|
||||
if (
|
||||
message.toLowerCase().includes("timeout") ||
|
||||
message.toLowerCase().includes("timed out") ||
|
||||
message.includes("ETIMEDOUT")
|
||||
) {
|
||||
return { message, type: "timeout", hint: "Operation took too long - try increasing timeout" };
|
||||
}
|
||||
|
||||
// Context/memory errors
|
||||
if (message.includes("context") && message.includes("too large")) {
|
||||
return { message, type: "model", hint: "Conversation too long - try clearing history" };
|
||||
}
|
||||
|
||||
// Missing environment/config
|
||||
if (message.includes("missing") && (message.includes("key") || message.includes("token"))) {
|
||||
return { message, type: "config", hint: "Missing required configuration or credentials" };
|
||||
}
|
||||
|
||||
return { message, type: "unknown" };
|
||||
}
|
||||
|
||||
export async function runAgentTurnWithFallback(params: {
|
||||
commandBody: string;
|
||||
followupRun: FollowupRun;
|
||||
@ -204,6 +283,7 @@ export async function runAgentTurnWithFallback(params: {
|
||||
return result;
|
||||
})
|
||||
.catch((err) => {
|
||||
const { message, type, hint } = categorizeError(err);
|
||||
emitAgentEvent({
|
||||
runId,
|
||||
stream: "lifecycle",
|
||||
@ -211,7 +291,9 @@ export async function runAgentTurnWithFallback(params: {
|
||||
phase: "error",
|
||||
startedAt,
|
||||
endedAt: Date.now(),
|
||||
error: err instanceof Error ? err.message : String(err),
|
||||
error: message,
|
||||
errorType: type,
|
||||
errorHint: hint,
|
||||
},
|
||||
});
|
||||
throw err;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user