Merge a39811caca into da71eaebd2
This commit is contained in:
commit
366ccc0586
@ -52,6 +52,7 @@ import {
|
|||||||
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
|
import { DEFAULT_BOOTSTRAP_FILENAME } from "../../workspace.js";
|
||||||
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
import { buildSystemPromptReport } from "../../system-prompt-report.js";
|
||||||
import { resolveDefaultModelForAgent } from "../../model-selection.js";
|
import { resolveDefaultModelForAgent } from "../../model-selection.js";
|
||||||
|
import { sanitizeToolUseResultPairing } from "../../session-transcript-repair.js";
|
||||||
|
|
||||||
import { isAbortError } from "../abort.js";
|
import { isAbortError } from "../abort.js";
|
||||||
import { buildEmbeddedExtensionPaths } from "../extensions.js";
|
import { buildEmbeddedExtensionPaths } from "../extensions.js";
|
||||||
@ -535,9 +536,11 @@ export async function runEmbeddedAttempt(
|
|||||||
validated,
|
validated,
|
||||||
getDmHistoryLimitFromSessionKey(params.sessionKey, params.config),
|
getDmHistoryLimitFromSessionKey(params.sessionKey, params.config),
|
||||||
);
|
);
|
||||||
cacheTrace?.recordStage("session:limited", { messages: limited });
|
// Fix: Repair tool_use/tool_result pairings AFTER truncation (issue #4367)
|
||||||
if (limited.length > 0) {
|
const repaired = sanitizeToolUseResultPairing(limited);
|
||||||
activeSession.agent.replaceMessages(limited);
|
cacheTrace?.recordStage("session:limited", { messages: repaired });
|
||||||
|
if (repaired.length > 0) {
|
||||||
|
activeSession.agent.replaceMessages(repaired);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
sessionManager.flushPendingToolResults?.();
|
sessionManager.flushPendingToolResults?.();
|
||||||
|
|||||||
@ -192,6 +192,41 @@ async function maybeQueueSubagentAnnounce(params: {
|
|||||||
return "none";
|
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: {
|
async function buildSubagentStatsLine(params: {
|
||||||
sessionKey: string;
|
sessionKey: string;
|
||||||
startedAt?: number;
|
startedAt?: number;
|
||||||
@ -299,6 +334,8 @@ export function buildSubagentSystemPrompt(params: {
|
|||||||
export type SubagentRunOutcome = {
|
export type SubagentRunOutcome = {
|
||||||
status: "ok" | "error" | "timeout" | "unknown";
|
status: "ok" | "error" | "timeout" | "unknown";
|
||||||
error?: string;
|
error?: string;
|
||||||
|
errorType?: "model" | "tool" | "network" | "config" | "timeout" | "unknown";
|
||||||
|
errorHint?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function runSubagentAnnounceFlow(params: {
|
export async function runSubagentAnnounceFlow(params: {
|
||||||
@ -380,7 +417,7 @@ export async function runSubagentAnnounceFlow(params: {
|
|||||||
: outcome.status === "timeout"
|
: outcome.status === "timeout"
|
||||||
? "timed out"
|
? "timed out"
|
||||||
: outcome.status === "error"
|
: outcome.status === "error"
|
||||||
? `failed: ${outcome.error || "unknown error"}`
|
? buildErrorStatusLabel(outcome)
|
||||||
: "finished with unknown status";
|
: "finished with unknown status";
|
||||||
|
|
||||||
// Build instructional message for main agent
|
// Build instructional message for main agent
|
||||||
|
|||||||
@ -184,7 +184,16 @@ function ensureListener() {
|
|||||||
entry.endedAt = endedAt;
|
entry.endedAt = endedAt;
|
||||||
if (phase === "error") {
|
if (phase === "error") {
|
||||||
const error = typeof evt.data?.error === "string" ? (evt.data.error as string) : undefined;
|
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 {
|
} else {
|
||||||
entry.outcome = { status: "ok" };
|
entry.outcome = { status: "ok" };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,85 @@ export type AgentRunLoopResult =
|
|||||||
}
|
}
|
||||||
| { kind: "final"; payload: ReplyPayload };
|
| { kind: "final"; payload: ReplyPayload };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Categorize errors to provide better error messages to users.
|
||||||
|
* Returns error message, type, and optional hint for remediation.
|
||||||
|
*/
|
||||||
|
export 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: {
|
export async function runAgentTurnWithFallback(params: {
|
||||||
commandBody: string;
|
commandBody: string;
|
||||||
followupRun: FollowupRun;
|
followupRun: FollowupRun;
|
||||||
@ -204,6 +283,7 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
return result;
|
return result;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
|
const { message, type, hint } = categorizeError(err);
|
||||||
emitAgentEvent({
|
emitAgentEvent({
|
||||||
runId,
|
runId,
|
||||||
stream: "lifecycle",
|
stream: "lifecycle",
|
||||||
@ -211,7 +291,9 @@ export async function runAgentTurnWithFallback(params: {
|
|||||||
phase: "error",
|
phase: "error",
|
||||||
startedAt,
|
startedAt,
|
||||||
endedAt: Date.now(),
|
endedAt: Date.now(),
|
||||||
error: err instanceof Error ? err.message : String(err),
|
error: message,
|
||||||
|
errorType: type,
|
||||||
|
errorHint: hint,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
318
src/auto-reply/reply/categorize-error.test.ts
Normal file
318
src/auto-reply/reply/categorize-error.test.ts
Normal file
@ -0,0 +1,318 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { categorizeError } from "./agent-runner-execution.js";
|
||||||
|
|
||||||
|
describe("categorizeError", () => {
|
||||||
|
describe("timeout errors", () => {
|
||||||
|
it("categorizes lowercase 'timeout' as timeout type", () => {
|
||||||
|
const error = new Error("Request timeout after 30s");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("timeout");
|
||||||
|
expect(result.message).toBe("Request timeout after 30s");
|
||||||
|
expect(result.hint).toBe("Operation took too long - try increasing timeout");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes 'timed out' as timeout type", () => {
|
||||||
|
const error = new Error("Connection timed out");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("timeout");
|
||||||
|
expect(result.hint).toBe("Operation took too long - try increasing timeout");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes ETIMEDOUT as network type (network error code takes precedence)", () => {
|
||||||
|
const error = new Error("ETIMEDOUT: socket hang up");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
// ETIMEDOUT is caught by network errors before timeout section
|
||||||
|
expect(result.type).toBe("network");
|
||||||
|
expect(result.hint).toBe("Connection failed - check network connectivity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles uppercase TIMEOUT", () => {
|
||||||
|
const error = new Error("TIMEOUT ERROR");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("timeout");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("authentication errors", () => {
|
||||||
|
it("categorizes 401 as config type", () => {
|
||||||
|
const error = new Error("HTTP 401: Unauthorized");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
expect(result.message).toBe("HTTP 401: Unauthorized");
|
||||||
|
expect(result.hint).toBe("Check API credentials and permissions");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes 'unauthorized' as config type", () => {
|
||||||
|
const error = new Error("Request failed: unauthorized access");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
expect(result.hint).toBe("Check API credentials and permissions");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes 'authentication' errors as config type (case-sensitive)", () => {
|
||||||
|
const error = new Error("authentication failed for API key");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
expect(result.hint).toBe("Check API credentials and permissions");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes 403 forbidden as config type", () => {
|
||||||
|
const error = new Error("HTTP 403 forbidden");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
expect(result.hint).toBe("Access denied - check permissions");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes 'forbidden' keyword as config type", () => {
|
||||||
|
const error = new Error("Access forbidden to resource");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rate limit errors", () => {
|
||||||
|
it("categorizes 'rate limit' as model type", () => {
|
||||||
|
const error = new Error("rate limit exceeded");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("model");
|
||||||
|
expect(result.message).toBe("rate limit exceeded");
|
||||||
|
expect(result.hint).toBe("Rate limit exceeded - retry in a few moments");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes HTTP 429 as model type", () => {
|
||||||
|
const error = new Error("HTTP 429: Too Many Requests");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("model");
|
||||||
|
expect(result.hint).toBe("Rate limit exceeded - retry in a few moments");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles rate limit with mixed case", () => {
|
||||||
|
const error = new Error("rate limit exceeded");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("model");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("unknown errors", () => {
|
||||||
|
it("categorizes unrecognized error as unknown type", () => {
|
||||||
|
const error = new Error("Something weird happened");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("unknown");
|
||||||
|
expect(result.message).toBe("Something weird happened");
|
||||||
|
expect(result.hint).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes generic error message as unknown", () => {
|
||||||
|
const error = new Error("An unexpected error occurred");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("unknown");
|
||||||
|
expect(result.hint).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles non-Error objects", () => {
|
||||||
|
const result = categorizeError("plain string error");
|
||||||
|
|
||||||
|
expect(result.type).toBe("unknown");
|
||||||
|
expect(result.message).toBe("plain string error");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles null/undefined errors", () => {
|
||||||
|
const result = categorizeError(null);
|
||||||
|
|
||||||
|
expect(result.type).toBe("unknown");
|
||||||
|
expect(result.message).toBe("null");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("API/model errors", () => {
|
||||||
|
it("categorizes HTTP 400 as model type", () => {
|
||||||
|
const error = new Error("HTTP 400: Bad Request");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("model");
|
||||||
|
expect(result.hint).toBe("Invalid request parameters");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes 'invalid request' as model type", () => {
|
||||||
|
const error = new Error("invalid request format");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("model");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes HTTP 500 as model type", () => {
|
||||||
|
const error = new Error("HTTP 500: Internal Server Error");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("model");
|
||||||
|
expect(result.hint).toBe("API service error - try again later");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes HTTP 503 as model type", () => {
|
||||||
|
const error = new Error("HTTP 503: Service Unavailable");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("model");
|
||||||
|
expect(result.hint).toBe("API service error - try again later");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes quota errors as config type", () => {
|
||||||
|
const error = new Error("quota exceeded for this account");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
expect(result.hint).toBe("Check billing and API quota limits");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes billing errors as config type", () => {
|
||||||
|
const error = new Error("billing issue detected");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
expect(result.hint).toBe("Check billing and API quota limits");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("network errors", () => {
|
||||||
|
it("categorizes ECONNREFUSED as network type", () => {
|
||||||
|
const error = new Error("ECONNREFUSED: Connection refused");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("network");
|
||||||
|
expect(result.hint).toBe("Connection failed - check network connectivity");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes ENOTFOUND as network type", () => {
|
||||||
|
const error = new Error("ENOTFOUND: DNS lookup failed");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("network");
|
||||||
|
expect(result.hint).toBe("DNS resolution failed - check hostname");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes DNS errors as network type", () => {
|
||||||
|
const error = new Error("DNS resolution error");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("network");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes EAI_AGAIN as network type", () => {
|
||||||
|
const error = new Error("EAI_AGAIN: temporary failure");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("network");
|
||||||
|
expect(result.hint).toBe("DNS resolution failed - check hostname");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes ENETUNREACH as network type", () => {
|
||||||
|
const error = new Error("ENETUNREACH: Network is unreachable");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("network");
|
||||||
|
expect(result.hint).toBe("Network unreachable - check connection");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes EHOSTUNREACH as network type", () => {
|
||||||
|
const error = new Error("EHOSTUNREACH: No route to host");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("network");
|
||||||
|
expect(result.hint).toBe("Network unreachable - check connection");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("file system errors (tool type)", () => {
|
||||||
|
it("categorizes ENOENT as tool type", () => {
|
||||||
|
const error = new Error("ENOENT: no such file or directory");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("tool");
|
||||||
|
expect(result.hint).toBe("File or directory not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes ENOTDIR as tool type", () => {
|
||||||
|
const error = new Error("ENOTDIR: not a directory");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("tool");
|
||||||
|
expect(result.hint).toBe("File or directory not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes EACCES as tool type", () => {
|
||||||
|
const error = new Error("EACCES: permission denied");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("tool");
|
||||||
|
expect(result.hint).toBe("Permission denied");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes EPERM as tool type", () => {
|
||||||
|
const error = new Error("EPERM: operation not permitted");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("tool");
|
||||||
|
expect(result.hint).toBe("Permission denied");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes EISDIR as tool type", () => {
|
||||||
|
const error = new Error("EISDIR: illegal operation on a directory");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("tool");
|
||||||
|
expect(result.hint).toBe("Expected file but found directory");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("configuration errors", () => {
|
||||||
|
it("categorizes missing API key as config type", () => {
|
||||||
|
const error = new Error("missing API key");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
expect(result.hint).toBe("Missing required configuration or credentials");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes missing token as config type", () => {
|
||||||
|
const error = new Error("missing authentication token");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
// "authentication" keyword triggers auth error hint first
|
||||||
|
expect(result.hint).toBe("Check API credentials and permissions");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("categorizes missing API token without authentication keyword", () => {
|
||||||
|
const error = new Error("missing API token for request");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("config");
|
||||||
|
expect(result.hint).toBe("Missing required configuration or credentials");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("context/memory errors", () => {
|
||||||
|
it("categorizes context too large as model type", () => {
|
||||||
|
const error = new Error("context window too large");
|
||||||
|
const result = categorizeError(error);
|
||||||
|
|
||||||
|
expect(result.type).toBe("model");
|
||||||
|
expect(result.hint).toBe("Conversation too long - try clearing history");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -12,6 +12,7 @@ import {
|
|||||||
resolveAgentWorkspaceDir,
|
resolveAgentWorkspaceDir,
|
||||||
resolveAgentDir,
|
resolveAgentDir,
|
||||||
} from "../agents/agent-scope.js";
|
} from "../agents/agent-scope.js";
|
||||||
|
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate a short 1-2 word filename slug from session content using LLM
|
* Generate a short 1-2 word filename slug from session content using LLM
|
||||||
@ -38,6 +39,11 @@ ${params.sessionContent.slice(0, 2000)}
|
|||||||
|
|
||||||
Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", "bug-fix"`;
|
Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design", "bug-fix"`;
|
||||||
|
|
||||||
|
// Resolve user's configured default model instead of hardcoded Opus
|
||||||
|
const { provider, model } = resolveDefaultModelForAgent({
|
||||||
|
cfg: params.cfg,
|
||||||
|
});
|
||||||
|
|
||||||
const result = await runEmbeddedPiAgent({
|
const result = await runEmbeddedPiAgent({
|
||||||
sessionId: `slug-generator-${Date.now()}`,
|
sessionId: `slug-generator-${Date.now()}`,
|
||||||
sessionKey: "temp:slug-generator",
|
sessionKey: "temp:slug-generator",
|
||||||
@ -46,6 +52,8 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
|
|||||||
agentDir,
|
agentDir,
|
||||||
config: params.cfg,
|
config: params.cfg,
|
||||||
prompt,
|
prompt,
|
||||||
|
provider,
|
||||||
|
model,
|
||||||
timeoutMs: 15_000, // 15 second timeout
|
timeoutMs: 15_000, // 15 second timeout
|
||||||
runId: `slug-gen-${Date.now()}`,
|
runId: `slug-gen-${Date.now()}`,
|
||||||
});
|
});
|
||||||
@ -75,7 +83,10 @@ Reply with ONLY the slug, nothing else. Examples: "vendor-pitch", "api-design",
|
|||||||
// Clean up temporary session file
|
// Clean up temporary session file
|
||||||
if (tempSessionFile) {
|
if (tempSessionFile) {
|
||||||
try {
|
try {
|
||||||
await fs.rm(path.dirname(tempSessionFile), { recursive: true, force: true });
|
await fs.rm(path.dirname(tempSessionFile), {
|
||||||
|
recursive: true,
|
||||||
|
force: true,
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user