Files
letta-code/src/cli/helpers/errorFormatter.ts
2026-02-27 17:02:05 -08:00

762 lines
26 KiB
TypeScript

import { APIError } from "@letta-ai/letta-client/core/error";
import { getErrorContext } from "./errorContext";
import { checkZaiError } from "./zaiErrors";
const LETTA_USAGE_URL = "https://app.letta.com/settings/organization/usage";
const LETTA_AGENTS_URL =
"https://app.letta.com/projects/default-project/agents";
function extractReasonList(value: unknown): string[] {
if (!Array.isArray(value)) return [];
return value
.filter((reason): reason is string => typeof reason === "string")
.map((reason) => reason.toLowerCase());
}
interface CloudflareEdgeErrorInfo {
code?: string;
statusText?: string;
host?: string;
rayId?: string;
}
const CLOUDFLARE_EDGE_5XX_MARKER_PATTERN =
/(^|\s)(502|52[0-6])\s*<!doctype html|error code\s*(502|52[0-6])/i;
const CLOUDFLARE_EDGE_5XX_TITLE_PATTERN = /\|\s*(502|52[0-6])\s*:/i;
export function isCloudflareEdge52xHtmlError(text: string): boolean {
const normalized = text.toLowerCase();
const hasCloudflare = normalized.includes("cloudflare");
const hasHtml =
normalized.includes("<!doctype html") ||
normalized.includes("<html") ||
normalized.includes("error code");
const has52xCode =
CLOUDFLARE_EDGE_5XX_MARKER_PATTERN.test(text) ||
CLOUDFLARE_EDGE_5XX_TITLE_PATTERN.test(text);
return hasCloudflare && hasHtml && has52xCode;
}
function parseCloudflareEdgeError(
text: string,
): CloudflareEdgeErrorInfo | undefined {
if (!isCloudflareEdge52xHtmlError(text)) return undefined;
const code =
text.match(/^\s*(502|52[0-6])\s*<!doctype html/i)?.[1] ??
text.match(/error code\s*(502|52[0-6])/i)?.[1] ??
text.match(/\|\s*(502|52[0-6])\s*:/i)?.[1];
const statusText =
text
.match(/<title>[^<|]*\|\s*(?:502|52[0-6])\s*:\s*([^<]+)/i)?.[1]
?.trim() ??
text.match(/<span\s+class="inline-block">([^<]+)<\/span>/i)?.[1]?.trim();
const host =
text.match(/utm_campaign=([a-z0-9.-]+)/i)?.[1] ??
text.match(/<span[^>]*truncate[^>]*>([a-z0-9.-]+)<\/span>/i)?.[1];
const rayId =
text.match(
/Cloudflare Ray ID:\s*(?:<strong[^>]*>)?([a-z0-9]+)(?:<\/strong>)?/i,
)?.[1] ?? text.match(/Cloudflare Ray ID:\s*([a-z0-9]+)/i)?.[1];
if (!code && !statusText && !host && !rayId) return undefined;
return { code, statusText, host, rayId };
}
export function checkCloudflareEdgeError(text: string): string | undefined {
const info = parseCloudflareEdgeError(text);
if (!info) return undefined;
const codeLabel = info.code ? `Cloudflare ${info.code}` : "Cloudflare";
const statusSegment = info.statusText
? `: ${info.statusText}`
: " upstream error";
const hostSegment = info.host ? ` for ${info.host}` : "";
const raySegment = info.rayId ? ` (Ray ID: ${info.rayId})` : "";
return `${codeLabel}${statusSegment}${hostSegment}${raySegment}. This is usually a temporary edge/origin outage. Please retry in a moment.`;
}
/**
* Normalize raw provider error payloads before sending to telemetry.
* Keeps telemetry concise by collapsing Cloudflare HTML pages into a
* single readable line while preserving non-Cloudflare messages as-is.
*/
export function formatTelemetryErrorMessage(
message: string | null | undefined,
): string {
if (!message) return "Unknown error";
return checkCloudflareEdgeError(message) ?? message;
}
function getErrorReasons(e: APIError): string[] {
const reasons = new Set<string>();
const errorBody = e.error;
if (errorBody && typeof errorBody === "object") {
const body = errorBody as Record<string, unknown>;
for (const reason of extractReasonList(body.reasons)) {
reasons.add(reason);
}
if (body.error && typeof body.error === "object") {
const nested = body.error as Record<string, unknown>;
for (const reason of extractReasonList(nested.reasons)) {
reasons.add(reason);
}
}
}
// Fallback: infer known reasons from message text.
const message = e.message?.toLowerCase() ?? "";
for (const knownReason of [
"not-enough-credits",
"model-unknown",
"byok-not-available-on-free-tier",
"free-usage-exceeded",
"premium-usage-exceeded",
"standard-usage-exceeded",
"basic-usage-exceeded",
"context-window-size-not-supported",
"agents-limit-exceeded",
"exceeded-quota",
]) {
if (message.includes(knownReason)) {
reasons.add(knownReason);
}
}
return Array.from(reasons);
}
function hasErrorReason(
e: APIError,
reason: string,
reasons?: string[],
): boolean {
const allReasons = reasons ?? getErrorReasons(e);
return allReasons.includes(reason.toLowerCase());
}
/**
* Check if the error is a rate limit error (429 with exceeded-quota)
* Returns the timeToQuotaResetMs if it's a rate limit error, undefined otherwise
*/
function getRateLimitResetMs(e: APIError): number | undefined {
if (e.status !== 429) return undefined;
const errorBody = e.error;
if (errorBody && typeof errorBody === "object") {
// Check for reasons array with "exceeded-quota"
if ("reasons" in errorBody && Array.isArray(errorBody.reasons)) {
if (errorBody.reasons.includes("exceeded-quota")) {
if (
"timeToQuotaResetMs" in errorBody &&
typeof errorBody.timeToQuotaResetMs === "number"
) {
return errorBody.timeToQuotaResetMs;
}
// Return 0 to indicate rate limited but no reset time available
return 0;
}
}
}
return undefined;
}
/**
* Walk an error object to find and format Cloudflare HTML 52x pages.
*/
function findAndFormatCloudflareEdgeError(e: unknown): string | undefined {
if (typeof e === "string") return checkCloudflareEdgeError(e);
if (typeof e !== "object" || e === null) return undefined;
if (e instanceof Error) {
const msg = checkCloudflareEdgeError(e.message);
if (msg) return msg;
}
const obj = e as Record<string, unknown>;
if (typeof obj.detail === "string") {
const msg = checkCloudflareEdgeError(obj.detail);
if (msg) return msg;
}
if (typeof obj.message === "string") {
const msg = checkCloudflareEdgeError(obj.message);
if (msg) return msg;
}
if (obj.error && typeof obj.error === "object") {
const errObj = obj.error as Record<string, unknown>;
if (typeof errObj.detail === "string") {
const msg = checkCloudflareEdgeError(errObj.detail);
if (msg) return msg;
}
if (typeof errObj.message === "string") {
const msg = checkCloudflareEdgeError(errObj.message);
if (msg) return msg;
}
if (errObj.error && typeof errObj.error === "object") {
const inner = errObj.error as Record<string, unknown>;
if (typeof inner.detail === "string") {
const msg = checkCloudflareEdgeError(inner.detail);
if (msg) return msg;
}
if (typeof inner.message === "string") {
const msg = checkCloudflareEdgeError(inner.message);
if (msg) return msg;
}
}
}
return undefined;
}
/**
* Format a time duration in milliseconds to a human-readable string
*/
function formatResetTime(ms: number): string {
const now = new Date();
const resetTime = new Date(now.getTime() + ms);
// Format the reset time
const timeStr = resetTime.toLocaleTimeString([], {
hour: "numeric",
minute: "2-digit",
});
// Calculate human-readable duration
const totalMinutes = Math.ceil(ms / 60000);
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
let durationStr: string;
if (hours > 0 && minutes > 0) {
durationStr = `${hours}h ${minutes}m`;
} else if (hours > 0) {
durationStr = `${hours}h`;
} else {
durationStr = `${minutes}m`;
}
return `Resets at ${timeStr} (${durationStr})`;
}
/**
* Check if the error is a resource limit error (402 with "You have reached your limit for X")
* Returns the error message if it matches, undefined otherwise
*/
function getResourceLimitMessage(e: APIError): string | undefined {
if (e.status !== 402) return undefined;
const errorBody = e.error;
if (errorBody && typeof errorBody === "object") {
if (
"error" in errorBody &&
typeof errorBody.error === "string" &&
errorBody.error.includes("You have reached your limit for")
) {
return errorBody.error;
}
}
// Also check the message directly
if (e.message?.includes("You have reached your limit for")) {
// Extract just the error message part, not the full "402 {...}" string
const match = e.message.match(/"error":"([^"]+)"/);
if (match) {
return match[1];
}
}
return undefined;
}
/**
* Check if the error is an agent limit error (429 with agents-limit-exceeded)
*/
function isAgentLimitError(e: APIError, reasons?: string[]): boolean {
if (e.status !== 429) return false;
return hasErrorReason(e, "agents-limit-exceeded", reasons);
}
/**
* Check if the error is a credit exhaustion error (402 with not-enough-credits)
*/
function isCreditExhaustedError(e: APIError, reasons?: string[]): boolean {
// Check status code
if (e.status !== 402) return false;
return hasErrorReason(e, "not-enough-credits", reasons);
}
function getTierUsageLimitMessage(reasons: string[]): string | undefined {
if (reasons.includes("premium-usage-exceeded")) {
return `You've reached your Premium model usage limit. Try switching to Standard or Basic hosted models with /model. View your plan and usage at ${LETTA_USAGE_URL}, or connect your own provider keys with /connect.`;
}
if (reasons.includes("standard-usage-exceeded")) {
return `You've reached your Standard model usage limit. Try switching to Basic hosted models with /model. View your plan and usage at ${LETTA_USAGE_URL}, or connect your own provider keys with /connect.`;
}
if (reasons.includes("basic-usage-exceeded")) {
return `You've reached your Basic model usage limit. Try switching models with /model, view your plan and usage at ${LETTA_USAGE_URL}, or connect your own provider keys with /connect.`;
}
return undefined;
}
const CHATGPT_USAGE_LIMIT_HINT =
"Switch models with /model, or connect your own provider keys with /connect.";
/**
* Check if a string contains a ChatGPT usage_limit_reached error with optional
* reset timing, and return a friendly message.
*
* ChatGPT wraps the error as embedded JSON inside a detail string like:
* RATE_LIMIT_EXCEEDED: ChatGPT rate limit exceeded: {"error":{"type":"usage_limit_reached",...}}
*/
export function checkChatGptUsageLimitError(text: string): string | undefined {
if (!text.includes("usage_limit_reached")) return undefined;
// Try to extract the embedded JSON object
const jsonStart = text.indexOf("{");
if (jsonStart < 0) {
return `ChatGPT usage limit reached. ${CHATGPT_USAGE_LIMIT_HINT}`;
}
try {
const parsed = JSON.parse(text.slice(jsonStart));
const errorObj = parsed.error || parsed;
if (errorObj.type !== "usage_limit_reached") return undefined;
// Extract plan type
const planType = errorObj.plan_type;
const planInfo = planType ? ` (${planType} plan)` : "";
// Extract reset timing — prefer resets_in_seconds, fall back to resets_at
let resetInfo = "Try again later";
if (
typeof errorObj.resets_in_seconds === "number" &&
errorObj.resets_in_seconds > 0
) {
resetInfo = formatResetTime(errorObj.resets_in_seconds * 1000);
} else if (typeof errorObj.resets_at === "number") {
const resetMs = errorObj.resets_at * 1000 - Date.now();
if (resetMs > 0) {
resetInfo = formatResetTime(resetMs);
}
}
return `ChatGPT usage limit reached${planInfo}. ${resetInfo}.\n${CHATGPT_USAGE_LIMIT_HINT}`;
} catch {
// JSON parse failed — return generic message
return `ChatGPT usage limit reached. ${CHATGPT_USAGE_LIMIT_HINT}`;
}
}
/**
* Walk an error object to find a ChatGPT usage_limit_reached detail string
* and format it. Handles APIError, nested run-metadata objects, and strings.
*/
function findAndFormatChatGptUsageLimit(e: unknown): string | undefined {
// Direct string
if (typeof e === "string") return checkChatGptUsageLimitError(e);
if (typeof e !== "object" || e === null) return undefined;
// APIError or Error — check .message
if (e instanceof Error) {
const msg = checkChatGptUsageLimitError(e.message);
if (msg) return msg;
}
const obj = e as Record<string, unknown>;
// Check e.error.error.detail (run-metadata shape)
if (obj.error && typeof obj.error === "object") {
const errObj = obj.error as Record<string, unknown>;
if (errObj.error && typeof errObj.error === "object") {
const inner = errObj.error as Record<string, unknown>;
if (typeof inner.detail === "string") {
const msg = checkChatGptUsageLimitError(inner.detail);
if (msg) return msg;
}
}
// Check e.error.detail
if (typeof errObj.detail === "string") {
const msg = checkChatGptUsageLimitError(errObj.detail);
if (msg) return msg;
}
}
return undefined;
}
const ENCRYPTED_CONTENT_HINT = [
"",
"This occurs when the conversation contains messages with encrypted",
"reasoning from a different OpenAI authentication scope (e.g. switching",
"between ChatGPT OAuth and an OpenAI API key).",
"Use /clear to start a new conversation.",
].join("\n");
/**
* Walk the error object to find the `detail` string containing the encrypted content error.
* Handles both direct (e.detail) and nested (e.error.error.detail) structures.
*/
function findEncryptedContentDetail(e: unknown): string | undefined {
if (typeof e !== "object" || e === null) return undefined;
const obj = e as Record<string, unknown>;
// Check direct: e.detail
if (
typeof obj.detail === "string" &&
obj.detail.includes("invalid_encrypted_content")
) {
return obj.detail;
}
// Check nested: e.error.error.detail or e.error.detail
if (obj.error && typeof obj.error === "object") {
const errObj = obj.error as Record<string, unknown>;
if (errObj.error && typeof errObj.error === "object") {
const inner = errObj.error as Record<string, unknown>;
if (
typeof inner.detail === "string" &&
inner.detail.includes("invalid_encrypted_content")
) {
return inner.detail;
}
}
if (
typeof errObj.detail === "string" &&
errObj.detail.includes("invalid_encrypted_content")
) {
return errObj.detail;
}
}
return undefined;
}
/**
* Check if the error contains an encrypted content organization mismatch from OpenAI/ChatGPT.
* This occurs when switching between ChatGPT OAuth and OpenAI API key auth,
* leaving encrypted reasoning tokens from a different auth scope in the conversation.
*/
function checkEncryptedContentError(e: unknown): string | undefined {
// Walk the object structure first (cheap) before falling back to stringify
const detail = findEncryptedContentDetail(e);
if (!detail) {
// Fallback: stringify for edge cases (e.g. plain string errors)
try {
const errorStr = typeof e === "string" ? e : JSON.stringify(e);
if (!errorStr.includes("invalid_encrypted_content")) return undefined;
} catch {
return undefined;
}
// Detected via stringify but couldn't extract detail — return generic message
return (
"OpenAI error: Encrypted content could not be verified — organization mismatch." +
ENCRYPTED_CONTENT_HINT
);
}
// Try to parse the embedded JSON from the detail string for pretty-printing
try {
const jsonStart = detail.indexOf("{");
if (jsonStart >= 0) {
const parsed = JSON.parse(detail.slice(jsonStart));
const innerError = parsed.error || parsed;
if (innerError.code === "invalid_encrypted_content") {
const msg = String(
innerError.message || "Encrypted content verification failed.",
).replaceAll('"', '\\"');
return [
"OpenAI error:",
" {",
` type: "${innerError.type || "invalid_request_error"}",`,
` code: "${innerError.code}",`,
` message: "${msg}"`,
" }",
ENCRYPTED_CONTENT_HINT,
].join("\n");
}
}
} catch {
// Fall through to generic message
}
return (
"OpenAI error: Encrypted content could not be verified — organization mismatch." +
ENCRYPTED_CONTENT_HINT
);
}
/**
* Returns true if the error is an OpenAI encrypted content org mismatch.
* Used by callers to skip generic error hints for this self-explanatory error.
*/
export function isEncryptedContentError(e: unknown): boolean {
return findEncryptedContentDetail(e) !== undefined;
}
/**
* Extract comprehensive error details from any error object
* Handles APIError, Error, and other error types consistently
* @param e The error object to format
* @param agentId Optional agent ID to create hyperlinks to the Letta dashboard
* @param conversationId Optional conversation ID to include in agent links
*/
export function formatErrorDetails(
e: unknown,
agentId?: string,
conversationId?: string,
): string {
let runId: string | undefined;
// Check for OpenAI encrypted content org mismatch before anything else
const encryptedContentMsg = checkEncryptedContentError(e);
if (encryptedContentMsg) return encryptedContentMsg;
// Check for ChatGPT usage limit errors — walk nested error objects like
// checkEncryptedContentError does, since these arrive both as APIError
// and as plain run-metadata objects ({error: {error: {detail: "..."}}})
const chatGptUsageLimitMsg = findAndFormatChatGptUsageLimit(e);
if (chatGptUsageLimitMsg) return chatGptUsageLimitMsg;
const cloudflareEdgeMsg = findAndFormatCloudflareEdgeError(e);
if (cloudflareEdgeMsg) return cloudflareEdgeMsg;
// Check for Z.ai provider errors (wrapped in generic "OpenAI" messages)
const errorText =
e instanceof APIError
? e.message
: e instanceof Error
? e.message
: typeof e === "string"
? e
: undefined;
if (errorText) {
const zaiMsg = checkZaiError(errorText);
if (zaiMsg) return zaiMsg;
}
// Handle APIError from streaming (event: error)
if (e instanceof APIError) {
const reasons = getErrorReasons(e);
// Check for rate limit error first - provide a friendly message with reset time
const rateLimitResetMs = getRateLimitResetMs(e);
if (rateLimitResetMs !== undefined) {
const resetInfo =
rateLimitResetMs > 0
? formatResetTime(rateLimitResetMs)
: "Try again later";
return `You've hit your usage limit. ${resetInfo}. View usage: ${LETTA_USAGE_URL}`;
}
// Check for agent limit error (free tier agent count limit)
if (isAgentLimitError(e, reasons)) {
const { billingTier } = getErrorContext();
if (billingTier?.toLowerCase() === "free") {
return `You've reached the agent limit (3) for the Free Plan. Delete agents at: ${LETTA_AGENTS_URL}\nOr upgrade to Pro for unlimited agents at: ${LETTA_USAGE_URL}`;
}
// Fallback for paid tiers (shouldn't normally hit this, but just in case)
return `You've reached your agent limit. Delete agents at: ${LETTA_AGENTS_URL}\nOr check your plan at: ${LETTA_USAGE_URL}`;
}
if (hasErrorReason(e, "model-unknown", reasons)) {
return `The selected model is not currently available for this account or provider. Run /model and press R to refresh available models, then choose an available model or connect a provider with /connect.`;
}
if (hasErrorReason(e, "context-window-size-not-supported", reasons)) {
return `The selected context window is not supported for this model. Switch models with /model or pick a model with a larger context window.`;
}
// Check for resource limit error (e.g., "You have reached your limit for agents")
const resourceLimitMsg = getResourceLimitMessage(e);
if (resourceLimitMsg) {
// Extract the resource type (agents, tools, etc.) from the message
const match = resourceLimitMsg.match(/limit for (\w+)/);
const resourceType = match ? match[1] : "resources";
return `${resourceLimitMsg}\nUpgrade at: ${LETTA_USAGE_URL}\nDelete ${resourceType} at: ${LETTA_AGENTS_URL}`;
}
// Check for credit exhaustion error - provide a friendly message
if (isCreditExhaustedError(e, reasons)) {
return `Your account is out of credits for hosted inference. Add credits, enable auto-recharge, or upgrade at ${LETTA_USAGE_URL}. You can also connect your own provider keys with /connect.`;
}
const tierUsageLimitMsg = getTierUsageLimitMessage(reasons);
if (tierUsageLimitMsg) return tierUsageLimitMsg;
if (hasErrorReason(e, "byok-not-available-on-free-tier", reasons)) {
const { modelDisplayName } = getErrorContext();
const modelInfo = modelDisplayName ? ` (${modelDisplayName})` : "";
return `Selected BYOK model${modelInfo} is not available on the Free plan. Switch to a free hosted model with /model (glm-4.7 or minimax-m2.1), or upgrade at ${LETTA_USAGE_URL}.`;
}
if (hasErrorReason(e, "free-usage-exceeded", reasons)) {
return `You've reached the Free plan hosted model usage limit. Switch to free hosted models with /model (glm-4.7 or minimax-m2.1), upgrade at ${LETTA_USAGE_URL}, or connect your own provider keys with /connect.`;
}
// Check for nested error structure: e.error.error
if (e.error && typeof e.error === "object" && "error" in e.error) {
const errorData = e.error.error;
if (errorData && typeof errorData === "object") {
const type = "type" in errorData ? errorData.type : undefined;
const message =
"message" in errorData ? errorData.message : "An error occurred";
const detail = "detail" in errorData ? errorData.detail : undefined;
const errorType = type ? `[${type}] ` : "";
const errorDetail = detail ? `\nDetail: ${detail}` : "";
// Extract run_id from e.error
if ("run_id" in e.error && typeof e.error.run_id === "string") {
runId = e.error.run_id;
}
const baseError = `${errorType}${message}${errorDetail}`;
return runId && agentId
? `${baseError}\n${createAgentLink(runId, agentId, conversationId)}`
: baseError;
}
}
// Handle APIError with direct error structure: e.error.detail
if (e.error && typeof e.error === "object") {
const detail = "detail" in e.error ? e.error.detail : undefined;
if ("run_id" in e.error && typeof e.error.run_id === "string") {
runId = e.error.run_id;
}
// When detail is available, prefer showing just the detail to avoid redundancy
// (e.message often contains the full JSON body like '409 {"detail":"CONFLICT: ..."}')
const baseError =
detail && typeof detail === "string" ? detail : e.message;
return runId && agentId
? `${baseError}\n${createAgentLink(runId, agentId, conversationId)}`
: baseError;
}
// Fallback for APIError with just message
return e.message;
}
// Handle regular Error objects
if (e instanceof Error) {
return e.message;
}
// Fallback for any other type (e.g., plain objects thrown by SDK or other code)
if (typeof e === "object" && e !== null) {
const obj = e as Record<string, unknown>;
// Check common error-like properties
if (typeof obj.message === "string") {
return obj.message;
}
if (typeof obj.error === "string") {
return obj.error;
}
if (typeof obj.detail === "string") {
return obj.detail;
}
// Last resort: JSON stringify
try {
return JSON.stringify(e, null, 2);
} catch {
return "[Error: Unable to serialize error object]";
}
}
return String(e);
}
const DEFAULT_RETRY_MESSAGE =
"Unexpected downstream LLM API error, retrying...";
/**
* Return a user-facing status message for a retriable LLM API error.
* Matches known provider error patterns from the run's error detail and
* returns a specific message; falls back to a generic one otherwise.
*/
export function getRetryStatusMessage(
errorDetail: string | null | undefined,
): string | null {
if (!errorDetail) return DEFAULT_RETRY_MESSAGE;
// Cloudflare edge errors are transient and retried silently — no status line
if (parseCloudflareEdgeError(errorDetail)) return null;
if (checkZaiError(errorDetail)) return "Z.ai API error, retrying...";
if (errorDetail.includes("Anthropic API is overloaded"))
return "Anthropic API is overloaded, retrying...";
if (
errorDetail.includes("ChatGPT API error") ||
errorDetail.includes("ChatGPT server error")
) {
return "OpenAI ChatGPT backend connection failed, retrying...";
}
if (
errorDetail.includes("upstream connect error") ||
errorDetail.includes("Connection error during streaming") ||
errorDetail.includes("incomplete chunked read") ||
errorDetail.includes("connection termination")
) {
const provider = getProviderDisplayName();
return `${provider} streaming connection dropped, retrying...`;
}
if (errorDetail.includes("OpenAI API error"))
return "OpenAI API error, retrying...";
return DEFAULT_RETRY_MESSAGE;
}
const ENDPOINT_TYPE_DISPLAY_NAMES: Record<string, string> = {
openai: "OpenAI",
anthropic: "Anthropic",
chatgpt_oauth: "ChatGPT",
google_ai: "Google AI",
google_vertex: "Google Vertex",
bedrock: "AWS Bedrock",
openrouter: "OpenRouter",
minimax: "MiniMax",
zai: "zAI",
};
function getProviderDisplayName(): string {
const { modelEndpointType } = getErrorContext();
if (!modelEndpointType) return "LLM";
return ENDPOINT_TYPE_DISPLAY_NAMES[modelEndpointType] ?? modelEndpointType;
}
/**
* Create a terminal hyperlink to the agent with run ID displayed
*/
function createAgentLink(
runId: string,
agentId: string,
conversationId?: string,
): string {
const url = `https://app.letta.com/agents/${agentId}${conversationId && conversationId !== "default" ? `?conversation=${conversationId}` : ""}`;
return `View agent: \x1b]8;;${url}\x1b\\${agentId}\x1b]8;;\x1b\\ (run: ${runId})`;
}