@letta-ai/letta-code 0.33.6 → 0.33.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agent-presets.js.map +1 -1
  2. package/dist/channels-public.js.map +1 -1
  3. package/dist/mcp-client.js +53 -11
  4. package/dist/mcp-client.js.map +3 -3
  5. package/dist/mcp-oauth.js +18545 -0
  6. package/dist/mcp-oauth.js.map +155 -0
  7. package/dist/types/agent/client-skills.d.ts.map +1 -1
  8. package/dist/types/agent/skills.d.ts +5 -0
  9. package/dist/types/agent/skills.d.ts.map +1 -1
  10. package/dist/types/backend/backend.d.ts +2 -0
  11. package/dist/types/backend/backend.d.ts.map +1 -1
  12. package/dist/types/backend/dev/fake-headless-backend.d.ts +2 -0
  13. package/dist/types/backend/dev/fake-headless-backend.d.ts.map +1 -1
  14. package/dist/types/backend/dev/pi-ollama-provider.d.ts +7 -16
  15. package/dist/types/backend/dev/pi-ollama-provider.d.ts.map +1 -1
  16. package/dist/types/constants.d.ts +1 -1
  17. package/dist/types/constants.d.ts.map +1 -1
  18. package/dist/types/experiments/manager.d.ts.map +1 -1
  19. package/dist/types/experiments/types.d.ts +1 -1
  20. package/dist/types/experiments/types.d.ts.map +1 -1
  21. package/dist/types/mcp-client.d.ts +6 -0
  22. package/dist/types/mcp-client.d.ts.map +1 -1
  23. package/dist/types/mcp-oauth-callback.d.ts +11 -0
  24. package/dist/types/mcp-oauth-callback.d.ts.map +1 -0
  25. package/dist/types/mcp-oauth-public.d.ts +55 -0
  26. package/dist/types/mcp-oauth-public.d.ts.map +1 -0
  27. package/dist/types/mcp-oauth.d.ts +56 -0
  28. package/dist/types/mcp-oauth.d.ts.map +1 -0
  29. package/dist/types/tools/toolset-catalog.d.ts +1 -1
  30. package/dist/types/types/protocol_v2.d.ts +1 -1
  31. package/dist/types/types/protocol_v2.d.ts.map +1 -1
  32. package/dist/types/utils/secrets.d.ts.map +1 -1
  33. package/letta.js +868 -237
  34. package/package.json +11 -1
  35. package/skills/curating-memory-palace/SKILL.md +105 -0
@@ -3,7 +3,7 @@
3
3
  "sources": ["../src/agent/agent-tags.ts", "../src/constants.ts", "../src/agent/memory-constants.ts", "../src/agent/prompt-assets.ts", "../src/agent/memory.ts", "../src/agent/model-catalog.ts", "../src/agent/personality-presets.ts", "../src/agent/create-agent-request.ts"],
4
4
  "sourcesContent": [
5
5
  "/**\n * Tags that identify Letta Code agents and their capabilities.\n *\n * This module must stay free of Node/backend imports: it is bundled into the\n * browser-safe `@letta-ai/letta-code/agent-presets` package export.\n */\n\n/** Marks an agent as created/managed by Letta Code. */\nexport const LETTA_CODE_ORIGIN_TAG = \"origin:letta-code\";\n\n/** Marks an agent as created by a first-run onboarding flow. */\nexport const ONBOARDING_ORIGIN_TAG = \"origin:onboarding\";\n\n/** Marks an agent as a Letta Code subagent (excluded from prompt management). */\nexport const LETTA_CODE_SUBAGENT_TAG = \"role:subagent\";\n\n/** Marks an agent as using git-backed memory (MemFS). */\nexport const GIT_MEMORY_ENABLED_TAG = \"git-memory-enabled\";\n\nexport interface BuildCreatedAgentTagsOptions {\n tags?: string[] | null;\n isSubagent?: boolean;\n enableMemfs?: boolean;\n}\n\nexport function buildCreatedAgentTags(\n options: BuildCreatedAgentTagsOptions = {},\n): string[] {\n const tags = [LETTA_CODE_ORIGIN_TAG];\n if (options.isSubagent) {\n tags.push(LETTA_CODE_SUBAGENT_TAG);\n }\n if (options.enableMemfs) {\n tags.push(GIT_MEMORY_ENABLED_TAG);\n }\n if (options.tags && Array.isArray(options.tags)) {\n tags.push(...options.tags);\n }\n return Array.from(new Set(tags));\n}\n",
6
- "/**\n * Application-wide constants\n */\n\n/**\n * Default model ID to use when no model is specified\n */\nexport const DEFAULT_MODEL_ID = \"auto\";\n\n/**\n * Default model handle to use for conversation compaction / summarization.\n */\nexport const DEFAULT_SUMMARIZATION_MODEL = \"letta/auto\";\n\n/**\n * Default model handle for lightweight conversation title generation.\n */\nexport const DEFAULT_TITLE_SUMMARIZATION_MODEL = \"openai/gpt-5.6-luna\";\n\n/**\n * Default agent name when creating a new agent\n */\nexport const DEFAULT_AGENT_NAME = \"Letta Code\";\n\n/**\n * Message displayed when user interrupts tool execution\n */\nexport const INTERRUPTED_BY_USER = \"Interrupted by user\";\n\n/**\n * Synthetic tool result injected when a turn ended without completing all tool calls\n * (e.g. process crash or unhandled stream error). Settled automatically at the start\n * of the next turn so the conversation history stays valid for the provider.\n */\nexport const TURN_DID_NOT_COMPLETE = \"Turn did not complete\";\n\n/**\n * XML tag used to wrap system reminder content injected into messages\n */\nexport const SYSTEM_REMINDER_TAG = \"system-reminder\";\nexport const SYSTEM_REMINDER_OPEN = `<${SYSTEM_REMINDER_TAG}>`;\nexport const SYSTEM_REMINDER_CLOSE = `</${SYSTEM_REMINDER_TAG}>`;\n// Legacy tag kept for parsing/backward compatibility with older saved messages.\nexport const SYSTEM_ALERT_TAG = \"system-alert\";\nexport const SYSTEM_ALERT_OPEN = `<${SYSTEM_ALERT_TAG}>`;\nexport const SYSTEM_ALERT_CLOSE = `</${SYSTEM_ALERT_TAG}>`;\n\n/**\n * How often (in turns) to check for memfs sync conflicts, even without\n * filesystem change events. Catches block-only changes (e.g. ADE/API edits).\n */\nexport const MEMFS_CONFLICT_CHECK_INTERVAL = 5;\n\n/**\n * Header displayed before compaction summary when conversation context is truncated\n */\nexport const COMPACTION_SUMMARY_HEADER =\n \"(Earlier messages in this conversation have been compacted to free up context, summarized below)\";\n\n/**\n * Status bar thresholds - only show indicators when values exceed these\n */\n// Show token count after 100 estimated tokens (shows exact count until 1k, then compact)\nexport const TOKEN_DISPLAY_THRESHOLD = 100;\n// Show elapsed time after 2 minutes (in ms)\nexport const ELAPSED_DISPLAY_THRESHOLD_MS = 60 * 1000;\n",
6
+ "/**\n * Application-wide constants\n */\n\n/**\n * Default model ID to use when no model is specified\n */\nexport const DEFAULT_MODEL_ID = \"auto\";\n\n/**\n * Default model handle to use for conversation compaction / summarization.\n */\nexport const DEFAULT_SUMMARIZATION_MODEL = \"letta/auto\";\n\n/**\n * Default model handle for lightweight conversation title generation.\n */\nexport const DEFAULT_TITLE_SUMMARIZATION_MODEL = \"openai/gpt-6-luna\";\n\n/**\n * Default agent name when creating a new agent\n */\nexport const DEFAULT_AGENT_NAME = \"Letta Code\";\n\n/**\n * Message displayed when user interrupts tool execution\n */\nexport const INTERRUPTED_BY_USER = \"Interrupted by user\";\n\n/**\n * Synthetic tool result injected when a turn ended without completing all tool calls\n * (e.g. process crash or unhandled stream error). Settled automatically at the start\n * of the next turn so the conversation history stays valid for the provider.\n */\nexport const TURN_DID_NOT_COMPLETE = \"Turn did not complete\";\n\n/**\n * XML tag used to wrap system reminder content injected into messages\n */\nexport const SYSTEM_REMINDER_TAG = \"system-reminder\";\nexport const SYSTEM_REMINDER_OPEN = `<${SYSTEM_REMINDER_TAG}>`;\nexport const SYSTEM_REMINDER_CLOSE = `</${SYSTEM_REMINDER_TAG}>`;\n// Legacy tag kept for parsing/backward compatibility with older saved messages.\nexport const SYSTEM_ALERT_TAG = \"system-alert\";\nexport const SYSTEM_ALERT_OPEN = `<${SYSTEM_ALERT_TAG}>`;\nexport const SYSTEM_ALERT_CLOSE = `</${SYSTEM_ALERT_TAG}>`;\n\n/**\n * How often (in turns) to check for memfs sync conflicts, even without\n * filesystem change events. Catches block-only changes (e.g. ADE/API edits).\n */\nexport const MEMFS_CONFLICT_CHECK_INTERVAL = 5;\n\n/**\n * Header displayed before compaction summary when conversation context is truncated\n */\nexport const COMPACTION_SUMMARY_HEADER =\n \"(Earlier messages in this conversation have been compacted to free up context, summarized below)\";\n\n/**\n * Status bar thresholds - only show indicators when values exceed these\n */\n// Show token count after 100 estimated tokens (shows exact count until 1k, then compact)\nexport const TOKEN_DISPLAY_THRESHOLD = 100;\n// Show elapsed time after 2 minutes (in ms)\nexport const ELAPSED_DISPLAY_THRESHOLD_MS = 60 * 1000;\n",
7
7
  "export const READ_ONLY_BLOCK_LABELS = [\"memory_filesystem\"] as const;\n",
8
8
  "// Additional system prompts for /system command\n\nimport approvalRecoveryAlert from \"./prompts/approval_recovery_alert.txt\";\nimport humanPrompt from \"./prompts/human.mdx\";\nimport humanKawaiiPrompt from \"./prompts/human_kawaii.mdx\";\nimport humanLinusPrompt from \"./prompts/human_linus.mdx\";\nimport humanMemoPrompt from \"./prompts/human_memo.mdx\";\nimport humanTutorialPrompt from \"./prompts/human_tutorial.mdx\";\nimport interruptRecoveryAlert from \"./prompts/interrupt_recovery_alert.txt\";\nimport lettaMemfsPrompt from \"./prompts/letta.md\";\nimport lettaLocalMemfsPrompt from \"./prompts/letta_local_memfs.md\";\nimport lettaNoMemfsPrompt from \"./prompts/letta_no_memfs.md\";\nimport lettaRootMemfsPrompt from \"./prompts/letta_root_memfs.md\";\nimport memoryFilesystemPrompt from \"./prompts/memory_filesystem.mdx\";\nimport onboardingPrompt from \"./prompts/onboarding.mdx\";\nimport onboardingLocalPrompt from \"./prompts/onboarding_local.mdx\";\nimport personaPrompt from \"./prompts/persona.mdx\";\nimport personaBlankPrompt from \"./prompts/persona_blank.mdx\";\nimport personaKawaiiPrompt from \"./prompts/persona_kawaii.mdx\";\nimport personaLinusPrompt from \"./prompts/persona_linus.mdx\";\nimport personaMemoPrompt from \"./prompts/persona_memo.mdx\";\nimport personaTutorialPrompt from \"./prompts/persona_tutorial.mdx\";\nimport projectPrompt from \"./prompts/project.mdx\";\nimport skillCreatorModePrompt from \"./prompts/skill_creator_mode.md\";\nimport sourceClaudePrompt from \"./prompts/source_claude.md\";\nimport sourceCodexPrompt from \"./prompts/source_codex.md\";\nimport sourceGeminiPrompt from \"./prompts/source_gemini.md\";\n\nimport stylePrompt from \"./prompts/style.mdx\";\n\nexport const SYSTEM_PROMPT = lettaNoMemfsPrompt;\n\nexport const SKILL_CREATOR_PROMPT = skillCreatorModePrompt;\nexport const APPROVAL_RECOVERY_PROMPT = approvalRecoveryAlert;\nexport const INTERRUPT_RECOVERY_ALERT = interruptRecoveryAlert;\n\nexport const MEMORY_PROMPTS: Record<string, string> = {\n \"persona.mdx\": personaPrompt,\n \"persona_blank.mdx\": personaBlankPrompt,\n \"persona_kawaii.mdx\": personaKawaiiPrompt,\n \"persona_linus.mdx\": personaLinusPrompt,\n \"persona_memo.mdx\": personaMemoPrompt,\n \"persona_tutorial.mdx\": personaTutorialPrompt,\n \"human.mdx\": humanPrompt,\n \"human_kawaii.mdx\": humanKawaiiPrompt,\n \"human_linus.mdx\": humanLinusPrompt,\n \"human_memo.mdx\": humanMemoPrompt,\n \"human_tutorial.mdx\": humanTutorialPrompt,\n \"project.mdx\": projectPrompt,\n\n \"memory_filesystem.mdx\": memoryFilesystemPrompt,\n \"onboarding.mdx\": onboardingPrompt,\n \"onboarding_local.mdx\": onboardingLocalPrompt,\n \"style.mdx\": stylePrompt,\n};\n\n// System prompt options for /system command\nexport interface SystemPromptOption {\n id: string;\n label: string;\n description: string;\n content: string;\n memfsContent?: string;\n rootMemfsContent?: string;\n localMemfsContent?: string;\n isDefault?: boolean;\n isFeatured?: boolean;\n}\n\nexport const SYSTEM_PROMPTS: SystemPromptOption[] = [\n {\n id: \"default\",\n label: \"Default\",\n description: \"Alias for letta\",\n content: lettaNoMemfsPrompt,\n memfsContent: lettaMemfsPrompt,\n rootMemfsContent: lettaRootMemfsPrompt,\n localMemfsContent: lettaLocalMemfsPrompt,\n isDefault: true,\n isFeatured: true,\n },\n {\n id: \"letta\",\n label: \"Letta Code\",\n description: \"Full Letta Code system prompt\",\n content: lettaNoMemfsPrompt,\n memfsContent: lettaMemfsPrompt,\n rootMemfsContent: lettaRootMemfsPrompt,\n localMemfsContent: lettaLocalMemfsPrompt,\n isFeatured: true,\n },\n {\n id: \"source-claude\",\n label: \"Claude Code\",\n description: \"Source-faithful Claude Code prompt (for benchmarking)\",\n content: sourceClaudePrompt,\n },\n {\n id: \"source-codex\",\n label: \"Codex\",\n description: \"Source-faithful OpenAI Codex prompt (for benchmarking)\",\n content: sourceCodexPrompt,\n },\n {\n id: \"source-gemini\",\n label: \"Gemini CLI\",\n description: \"Source-faithful Gemini CLI prompt (for benchmarking)\",\n content: sourceGeminiPrompt,\n },\n];\n\nexport type MemoryPromptMode =\n | \"standard\"\n | \"memfs\"\n | \"root-memfs\"\n | \"local-memfs\";\n\nexport function getSystemPromptVariantContents(\n prompt: SystemPromptOption,\n): string[] {\n return [\n prompt.content,\n prompt.memfsContent,\n prompt.rootMemfsContent,\n prompt.localMemfsContent,\n ].filter((content): content is string => typeof content === \"string\");\n}\n\n/**\n * Check if a preset ID exists in SYSTEM_PROMPTS.\n */\nexport function isKnownPreset(id: string): boolean {\n return SYSTEM_PROMPTS.some((p) => p.id === id);\n}\n\n/**\n * Deterministic rebuild of a system prompt from a known preset + memory mode.\n * Throws on unknown preset (prevents stale/renamed presets from silently rewriting prompts).\n */\nexport function buildSystemPrompt(\n presetId: string,\n memoryMode: MemoryPromptMode,\n): string {\n const preset = SYSTEM_PROMPTS.find((p) => p.id === presetId);\n if (!preset) {\n throw new Error(\n `Unknown preset \"${presetId}\" — cannot rebuild system prompt`,\n );\n }\n if (memoryMode === \"local-memfs\") {\n return (\n preset.localMemfsContent ??\n preset.memfsContent ??\n preset.content\n ).trim();\n }\n if (memoryMode === \"root-memfs\") {\n return (\n preset.rootMemfsContent ??\n preset.memfsContent ??\n preset.content\n ).trim();\n }\n if (memoryMode === \"memfs\") {\n return (preset.memfsContent ?? preset.content).trim();\n }\n\n return preset.content.trim();\n}\n\n/**\n * Returns true if the agent is not on the current default preset\n * and would benefit from switching to `/system default`.\n */\nexport function shouldRecommendDefaultPrompt(\n currentPrompt: string,\n memoryMode: MemoryPromptMode,\n): boolean {\n const defaultPrompt = buildSystemPrompt(\"default\", memoryMode);\n return currentPrompt !== defaultPrompt;\n}\n",
9
9
  "/**\n * Agent memory block management\n * Loads memory blocks from .mdx files in src/agent/prompts\n */\n\nimport type { CreateBlock } from \"@letta-ai/letta-client/resources/blocks/blocks\";\nimport { READ_ONLY_BLOCK_LABELS } from \"./memory-constants\";\nimport { MEMORY_PROMPTS } from \"./prompt-assets\";\n\n/**\n * The memory block labels every standard (non-MemFS) agent is created with.\n * Each maps to a `<label>.mdx` file in src/agent/prompts. Per-project blocks\n * (skills/loaded_skills) were removed in LET-7353 — skills are now injected\n * via system reminders, leaving only these defaults.\n */\nexport const MEMORY_BLOCK_LABELS = [\"persona\", \"human\"] as const;\n\n/**\n * Block labels that should be read-only (agent cannot modify via memory tools).\n */\nexport { READ_ONLY_BLOCK_LABELS };\n\n/**\n * Parse frontmatter and content from an .mdx file\n */\nexport function parseMdxFrontmatter(content: string): {\n frontmatter: Record<string, string>;\n body: string;\n} {\n const frontmatterRegex = /^---\\n([\\s\\S]*?)\\n---\\n([\\s\\S]*)$/;\n const match = content.match(frontmatterRegex);\n\n if (!match || !match[1] || !match[2]) {\n return { frontmatter: {}, body: content };\n }\n\n const frontmatterText = match[1];\n const body = match[2];\n const frontmatter: Record<string, string> = {};\n\n // Parse YAML-like frontmatter (simple key: value pairs)\n for (const line of frontmatterText.split(\"\\n\")) {\n const colonIndex = line.indexOf(\":\");\n if (colonIndex > 0) {\n const key = line.slice(0, colonIndex).trim();\n const value = line.slice(colonIndex + 1).trim();\n frontmatter[key] = value;\n }\n }\n\n return { frontmatter, body: body.trim() };\n}\n\n/**\n * Load memory blocks from .mdx files in src/agent/prompts\n */\nasync function loadMemoryBlocksFromMdx(): Promise<CreateBlock[]> {\n const memoryBlocks: CreateBlock[] = [];\n\n const mdxFiles = MEMORY_BLOCK_LABELS.map((label) => `${label}.mdx`);\n\n for (const filename of mdxFiles) {\n try {\n const content = MEMORY_PROMPTS[filename];\n if (!content) {\n console.warn(`Missing embedded prompt file: ${filename}`);\n continue;\n }\n const { frontmatter, body } = parseMdxFrontmatter(content);\n\n const label = frontmatter.label || filename.replace(\".mdx\", \"\");\n const block: CreateBlock = {\n label,\n value: body,\n };\n\n if (frontmatter.description) {\n block.description = frontmatter.description;\n }\n\n // Set read-only for blocks managed by specific tools (not memory tools)\n if ((READ_ONLY_BLOCK_LABELS as readonly string[]).includes(label)) {\n block.read_only = true;\n }\n\n memoryBlocks.push(block);\n } catch (error) {\n console.error(`Error loading ${filename}:`, error);\n }\n }\n\n return memoryBlocks;\n}\n\n// Cache for loaded memory blocks\nlet cachedMemoryBlocks: CreateBlock[] | null = null;\n\n/**\n * Get default starter memory blocks for new agents\n */\nexport async function getDefaultMemoryBlocks(): Promise<CreateBlock[]> {\n if (!cachedMemoryBlocks) {\n cachedMemoryBlocks = await loadMemoryBlocksFromMdx();\n }\n return cachedMemoryBlocks;\n}\n",
@@ -7,7 +7,7 @@
7
7
  "import type { ChannelTurnProgressUpdate } from \"./types\";\n\nexport interface LettaStreamErrorParams {\n errorType?: string;\n message: string;\n detail?: string;\n runId?: string;\n}\n\nexport interface CollectLettaSseAssistantTextResult {\n text: string;\n chunkCount: number;\n stopReason?: string;\n}\n\nexport interface CollectLettaSseAssistantTextOptions {\n onDelta?: (delta: unknown) => void | Promise<void>;\n onProgressUpdate?: (\n update: ChannelTurnProgressUpdate,\n ) => void | Promise<void>;\n progressBuilder?: {\n buildUpdates(delta: unknown): ChannelTurnProgressUpdate[];\n };\n}\n\nexport interface FormatLettaStreamCoreErrorOptions {\n includeDetail?: boolean;\n}\n\ninterface LettaSseTextContentPart {\n type: \"text\";\n text: string;\n signature?: string;\n}\n\ninterface LettaSseAssistantMessage {\n message_type: \"assistant_message\";\n content?: string | LettaSseTextContentPart[];\n}\n\ninterface LettaSseErrorMessage {\n message_type: \"error_message\";\n error_type?: string;\n message?: string;\n detail?: string;\n run_id?: string;\n}\n\ninterface LettaSseStopReasonMessage {\n message_type: \"stop_reason\";\n stop_reason?: string;\n}\n\nexport const LETTA_STREAM_NO_ASSISTANT_MESSAGE_ERROR =\n \"No assistant message received in stream\";\n\nexport class LettaStreamCoreError extends Error {\n readonly errorType?: string;\n readonly detail?: string;\n readonly runId?: string;\n\n constructor(params: LettaStreamErrorParams) {\n super(params.message);\n this.name = \"LettaStreamCoreError\";\n this.errorType = params.errorType;\n this.detail = params.detail;\n this.runId = params.runId;\n }\n}\n\nexport class LettaStreamNoAssistantMessageError extends Error {\n constructor() {\n super(LETTA_STREAM_NO_ASSISTANT_MESSAGE_ERROR);\n this.name = \"LettaStreamNoAssistantMessageError\";\n }\n}\n\nfunction normalizeErrorLine(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nexport function formatLettaStreamCoreErrorForChannel(\n error: LettaStreamCoreError,\n options: FormatLettaStreamCoreErrorOptions = {},\n): string {\n const message =\n normalizeErrorLine(error.message) ?? \"Core failed to generate a response.\";\n const detail = normalizeErrorLine(error.detail);\n\n if (options.includeDetail === false || !detail || detail === message) {\n return message;\n }\n\n return `${message}\\n${detail}`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\";\n}\n\nfunction isOptionalString(value: unknown): value is string | undefined {\n return typeof value === \"undefined\" || typeof value === \"string\";\n}\n\nfunction isLettaSseTextContentPart(\n value: unknown,\n): value is LettaSseTextContentPart {\n if (!isRecord(value)) {\n return false;\n }\n return (\n value.type === \"text\" &&\n typeof value.text === \"string\" &&\n isOptionalString(value.signature)\n );\n}\n\nfunction isLettaSseAssistantMessage(\n value: unknown,\n): value is LettaSseAssistantMessage {\n if (!isRecord(value) || value.message_type !== \"assistant_message\") {\n return false;\n }\n const content = value.content;\n return (\n typeof content === \"undefined\" ||\n typeof content === \"string\" ||\n (Array.isArray(content) && content.every(isLettaSseTextContentPart))\n );\n}\n\nfunction isLettaSseErrorMessage(value: unknown): value is LettaSseErrorMessage {\n if (!isRecord(value) || value.message_type !== \"error_message\") {\n return false;\n }\n return (\n isOptionalString(value.error_type) &&\n isOptionalString(value.message) &&\n isOptionalString(value.detail) &&\n isOptionalString(value.run_id)\n );\n}\n\nfunction isLettaSseStopReasonMessage(\n value: unknown,\n): value is LettaSseStopReasonMessage {\n if (!isRecord(value) || value.message_type !== \"stop_reason\") {\n return false;\n }\n return isOptionalString(value.stop_reason);\n}\n\nfunction coreErrorFromMessage(\n message: LettaSseErrorMessage,\n): LettaStreamCoreError {\n return new LettaStreamCoreError({\n errorType: message.error_type,\n message: message.message ?? \"Core failed to generate a response.\",\n detail: message.detail,\n runId: message.run_id,\n });\n}\n\nfunction collectAssistantContent(message: LettaSseAssistantMessage): string[] {\n if (typeof message.content === \"string\") {\n return [message.content];\n }\n if (Array.isArray(message.content)) {\n return message.content.map((part) => part.text);\n }\n return [];\n}\n\nexport async function collectLettaSseAssistantText(\n body: ReadableStream<Uint8Array>,\n options: CollectLettaSseAssistantTextOptions = {},\n): Promise<CollectLettaSseAssistantTextResult> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n const assistantMessages: string[] = [];\n let stopReason: string | undefined;\n\n async function processData(data: string): Promise<void> {\n if (data === \"[DONE]\") {\n return;\n }\n\n const parsed: unknown = JSON.parse(data);\n await options.onDelta?.(parsed);\n const progressUpdates = options.progressBuilder?.buildUpdates(parsed) ?? [];\n for (const update of progressUpdates) {\n await options.onProgressUpdate?.(update);\n }\n\n if (isLettaSseErrorMessage(parsed)) {\n throw coreErrorFromMessage(parsed);\n }\n if (isLettaSseAssistantMessage(parsed)) {\n assistantMessages.push(...collectAssistantContent(parsed));\n return;\n }\n if (isLettaSseStopReasonMessage(parsed)) {\n stopReason = parsed.stop_reason;\n }\n }\n\n async function processLine(line: string): Promise<void> {\n if (!line.trim() || !line.startsWith(\"data: \")) {\n return;\n }\n await processData(line.slice(6));\n }\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n\n if (done) {\n break;\n }\n\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\");\n buffer = lines.pop() ?? \"\";\n\n for (const line of lines) {\n await processLine(line);\n }\n }\n\n const flushed = decoder.decode();\n if (flushed) {\n buffer += flushed;\n }\n if (buffer) {\n await processLine(buffer);\n }\n } finally {\n reader.releaseLock();\n }\n\n const text = assistantMessages.join(\" \").trim();\n if (!text) {\n throw new LettaStreamNoAssistantMessageError();\n }\n\n return {\n text,\n chunkCount: assistantMessages.length,\n stopReason,\n };\n}\n",
8
8
  "const CONVERSATION_BUSY_ERROR_PATTERNS = [\n /another request is (?:currently )?(?:being )?processed/i,\n /currently being processed for this conversation/i,\n /busy with another active run/i,\n /already processing for this conversation/i,\n /turn still running/i,\n];\n\nexport const CONVERSATION_BUSY_TITLE = \"Turn still running\";\n\nexport function isConversationBusyErrorText(\n errorText: string | null | undefined,\n): boolean {\n if (!errorText) return false;\n return CONVERSATION_BUSY_ERROR_PATTERNS.some((pattern) =>\n pattern.test(errorText),\n );\n}\n\nexport function buildConversationBusyErrorBody(\n automaticRetry: boolean,\n): string {\n return automaticRetry\n ? \"Another request is already processing for this conversation. I’ll wait for it to finish and retry automatically.\"\n : \"Another request is already processing for this conversation. Please wait for it to finish, then try again.\";\n}\n\nexport function formatConversationBusyErrorMessage(options: {\n automaticRetry?: boolean;\n runId?: string;\n}): string {\n const lines = [\n CONVERSATION_BUSY_TITLE,\n buildConversationBusyErrorBody(options.automaticRetry ?? false),\n ];\n if (options.runId) {\n lines.push(\"\", `Run ID: ${options.runId}`);\n }\n return lines.join(\"\\n\");\n}\n",
9
9
  "import {\n buildConversationBusyErrorBody,\n CONVERSATION_BUSY_TITLE,\n isConversationBusyErrorText,\n} from \"@/utils/conversation-busy-error\";\n\nconst RAW_LOOP_ERROR_PATTERN = /^Unexpected stop reason:\\s*error$/i;\nconst APP_CHAT_URL_PATTERN = /https:\\/\\/app\\.letta\\.com\\/chat\\/\\S+/i;\nconst ESCAPE_CHARACTER = String.fromCharCode(27);\nconst OSC8_PREFIX = `${ESCAPE_CHARACTER}]8;;`;\nconst OSC8_TERMINATOR = `${ESCAPE_CHARACTER}\\\\`;\n\nconst APPROVAL_PENDING_ERROR_PATTERNS = [\n /waiting for approval/i,\n /pending request before continuing/i,\n /approve or deny the pending request/i,\n];\n\nconst DATABASE_LOCK_TIMEOUT_PATTERN =\n /\\bcancel(?:l)?ing statement due to lock timeout\\b/i;\nconst POSTGRES_LOCK_NOT_AVAILABLE_CODE_PATTERN = /\\b55P03\\b/i;\nconst POSTGRES_LOCK_NOT_AVAILABLE_SQLSTATE_PATTERN =\n /\\b(?:sqlstate|pgcode)\\s*[:=]?\\s*[\"']?55P03\\b/i;\nconst POSTGRES_LOCK_NOT_AVAILABLE_CONTEXT_PATTERN =\n /\\b(?:postgres(?:ql)?|psycopg|sqlalchemy|lock[_ -]?not[_ -]?available|lock timeout)\\b/i;\n\nconst RUN_ID_PATTERNS = [\n /\"run_id\"\\s*:\\s*\"([^\"\\\\]+)\"/i,\n /\\brun[_\\s-]?id\\b[\"']?\\s*[:=]\\s*[\"']?([A-Za-z0-9_-]+)/i,\n /\\(run:\\s*([A-Za-z0-9_-]+)\\)/i,\n];\n\nexport type ChannelLifecycleErrorKind =\n | \"approval_pending\"\n | \"conversation_busy\"\n | \"database_lock_timeout\"\n | \"generic\";\n\nexport interface ChannelLifecycleErrorDisplayOptions {\n automaticRetry?: boolean;\n runId?: string | null;\n}\n\nexport interface ChannelLifecycleErrorDisplay {\n kind: ChannelLifecycleErrorKind;\n title: string;\n body: string;\n runId?: string;\n}\n\nexport interface ChannelLifecycleErrorFormatOptions\n extends ChannelLifecycleErrorDisplayOptions {\n codeBlock?: boolean;\n maxLength?: number;\n}\n\nexport const CHANNEL_LIFECYCLE_FALLBACK_ERROR_MESSAGE =\n \"Something went wrong while processing that message. Please try again.\";\n\nexport const CHANNEL_LIFECYCLE_APPROVAL_PENDING_MESSAGE =\n \"The agent is still waiting on a tool approval from an earlier turn. Please approve or deny that pending request, then send your message again.\";\n\nexport const CHANNEL_LIFECYCLE_TRANSIENT_ERROR_MESSAGE =\n \"A temporary error interrupted this turn. Please try again.\";\n\nexport const CHANNEL_LIFECYCLE_CONVERSATION_BUSY_TITLE =\n CONVERSATION_BUSY_TITLE;\n\nfunction stripTrailingRunIdPunctuation(runId: string): string {\n return runId.replace(/[)\"'.,;]+$/g, \"\");\n}\n\nexport function extractChannelLifecycleRunId(\n errorText: string | null | undefined,\n): string | undefined {\n if (!errorText) return undefined;\n for (const pattern of RUN_ID_PATTERNS) {\n const match = errorText.match(pattern);\n const runId = match?.[1]?.trim();\n if (runId) {\n return stripTrailingRunIdPunctuation(runId);\n }\n }\n return undefined;\n}\n\nfunction stripOsc8TerminalLinks(errorText: string): string {\n let output = \"\";\n let cursor = 0;\n\n while (cursor < errorText.length) {\n const linkStart = errorText.indexOf(OSC8_PREFIX, cursor);\n if (linkStart === -1) {\n output += errorText.slice(cursor);\n break;\n }\n\n const labelStart = errorText.indexOf(\n OSC8_TERMINATOR,\n linkStart + OSC8_PREFIX.length,\n );\n if (labelStart === -1) {\n output += errorText.slice(cursor);\n break;\n }\n\n const closeStart = errorText.indexOf(\n OSC8_PREFIX,\n labelStart + OSC8_TERMINATOR.length,\n );\n if (closeStart === -1) {\n output += errorText.slice(cursor);\n break;\n }\n\n const closeEnd = errorText.indexOf(\n OSC8_TERMINATOR,\n closeStart + OSC8_PREFIX.length,\n );\n if (closeEnd === -1) {\n output += errorText.slice(cursor);\n break;\n }\n\n output += errorText.slice(cursor, linkStart);\n output += errorText.slice(labelStart + OSC8_TERMINATOR.length, closeStart);\n cursor = closeEnd + OSC8_TERMINATOR.length;\n }\n\n return output;\n}\n\nexport function sanitizeChannelLifecycleErrorText(\n errorText: string | null | undefined,\n): string {\n if (!errorText) return \"\";\n\n const withoutTerminalLinks = stripOsc8TerminalLinks(errorText);\n\n return withoutTerminalLinks\n .split(\"\\n\")\n .filter((line) => {\n const trimmed = line.trim();\n if (/^View agent:/i.test(trimmed)) return false;\n if (APP_CHAT_URL_PATTERN.test(trimmed)) return false;\n return true;\n })\n .join(\"\\n\")\n .replace(/[ \\t]+\\n/g, \"\\n\")\n .trim();\n}\n\nfunction truncateLifecycleMessage(text: string, maxLength: number): string {\n if (text.length <= maxLength) return text;\n return `${text.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;\n}\n\nfunction isDatabaseLockErrorText(errorText: string): boolean {\n return (\n DATABASE_LOCK_TIMEOUT_PATTERN.test(errorText) ||\n POSTGRES_LOCK_NOT_AVAILABLE_SQLSTATE_PATTERN.test(errorText) ||\n (POSTGRES_LOCK_NOT_AVAILABLE_CODE_PATTERN.test(errorText) &&\n POSTGRES_LOCK_NOT_AVAILABLE_CONTEXT_PATTERN.test(errorText))\n );\n}\n\nexport function getChannelLifecycleErrorDisplay(\n errorText: string | null | undefined,\n options: ChannelLifecycleErrorDisplayOptions = {},\n): ChannelLifecycleErrorDisplay {\n const normalized = sanitizeChannelLifecycleErrorText(errorText);\n const optionsRunId = options.runId?.trim();\n const runId = optionsRunId || extractChannelLifecycleRunId(errorText);\n\n if (!normalized || RAW_LOOP_ERROR_PATTERN.test(normalized)) {\n return {\n kind: \"generic\",\n title: \"Turn failed\",\n body: CHANNEL_LIFECYCLE_FALLBACK_ERROR_MESSAGE,\n runId,\n };\n }\n\n if (isDatabaseLockErrorText(normalized)) {\n return {\n kind: \"database_lock_timeout\",\n title: \"Turn failed\",\n body: CHANNEL_LIFECYCLE_TRANSIENT_ERROR_MESSAGE,\n runId,\n };\n }\n\n if (isConversationBusyErrorText(normalized)) {\n return {\n kind: \"conversation_busy\",\n title: CHANNEL_LIFECYCLE_CONVERSATION_BUSY_TITLE,\n body: buildConversationBusyErrorBody(options.automaticRetry ?? false),\n runId,\n };\n }\n\n if (\n APPROVAL_PENDING_ERROR_PATTERNS.some((pattern) => pattern.test(normalized))\n ) {\n return {\n kind: \"approval_pending\",\n title: \"Turn failed\",\n body: CHANNEL_LIFECYCLE_APPROVAL_PENDING_MESSAGE,\n runId,\n };\n }\n\n return {\n kind: \"generic\",\n title: \"Turn failed\",\n body: normalized,\n runId,\n };\n}\n\nexport function normalizeChannelLifecycleErrorMessage(\n errorText: string | null | undefined,\n options: ChannelLifecycleErrorDisplayOptions = {},\n): string {\n return getChannelLifecycleErrorDisplay(errorText, options).body;\n}\n\nexport function formatChannelLifecycleErrorMessage(\n errorText: string | null | undefined,\n options: ChannelLifecycleErrorFormatOptions = {},\n): string {\n const display = getChannelLifecycleErrorDisplay(errorText, options);\n const maxLength = options.maxLength ?? Number.POSITIVE_INFINITY;\n const body = truncateLifecycleMessage(display.body, maxLength);\n\n if (display.kind === \"conversation_busy\") {\n const lines = [display.title, body];\n if (display.runId) {\n lines.push(\"\", `Run ID: ${display.runId}`);\n }\n return lines.join(\"\\n\");\n }\n\n if (display.kind === \"database_lock_timeout\") {\n const lines = [`${display.title}:`, body];\n if (display.runId) {\n lines.push(\"\", `Run ID: ${display.runId}`);\n }\n return lines.join(\"\\n\");\n }\n\n if (options.codeBlock) {\n const escaped = body.replace(/```/g, \"``\\u200b`\");\n const lines = [`${display.title}:`, \"```\", escaped, \"```\"];\n if (display.runId) {\n lines.push(\"\", `Run ID: ${display.runId}`);\n }\n return lines.join(\"\\n\");\n }\n\n const lines = [`${display.title}:`, body];\n if (display.runId) {\n lines.push(\"\", `Run ID: ${display.runId}`);\n }\n return lines.join(\"\\n\");\n}\n",
10
- "/**\n * Application-wide constants\n */\n\n/**\n * Default model ID to use when no model is specified\n */\nexport const DEFAULT_MODEL_ID = \"auto\";\n\n/**\n * Default model handle to use for conversation compaction / summarization.\n */\nexport const DEFAULT_SUMMARIZATION_MODEL = \"letta/auto\";\n\n/**\n * Default model handle for lightweight conversation title generation.\n */\nexport const DEFAULT_TITLE_SUMMARIZATION_MODEL = \"openai/gpt-5.6-luna\";\n\n/**\n * Default agent name when creating a new agent\n */\nexport const DEFAULT_AGENT_NAME = \"Letta Code\";\n\n/**\n * Message displayed when user interrupts tool execution\n */\nexport const INTERRUPTED_BY_USER = \"Interrupted by user\";\n\n/**\n * Synthetic tool result injected when a turn ended without completing all tool calls\n * (e.g. process crash or unhandled stream error). Settled automatically at the start\n * of the next turn so the conversation history stays valid for the provider.\n */\nexport const TURN_DID_NOT_COMPLETE = \"Turn did not complete\";\n\n/**\n * XML tag used to wrap system reminder content injected into messages\n */\nexport const SYSTEM_REMINDER_TAG = \"system-reminder\";\nexport const SYSTEM_REMINDER_OPEN = `<${SYSTEM_REMINDER_TAG}>`;\nexport const SYSTEM_REMINDER_CLOSE = `</${SYSTEM_REMINDER_TAG}>`;\n// Legacy tag kept for parsing/backward compatibility with older saved messages.\nexport const SYSTEM_ALERT_TAG = \"system-alert\";\nexport const SYSTEM_ALERT_OPEN = `<${SYSTEM_ALERT_TAG}>`;\nexport const SYSTEM_ALERT_CLOSE = `</${SYSTEM_ALERT_TAG}>`;\n\n/**\n * How often (in turns) to check for memfs sync conflicts, even without\n * filesystem change events. Catches block-only changes (e.g. ADE/API edits).\n */\nexport const MEMFS_CONFLICT_CHECK_INTERVAL = 5;\n\n/**\n * Header displayed before compaction summary when conversation context is truncated\n */\nexport const COMPACTION_SUMMARY_HEADER =\n \"(Earlier messages in this conversation have been compacted to free up context, summarized below)\";\n\n/**\n * Status bar thresholds - only show indicators when values exceed these\n */\n// Show token count after 100 estimated tokens (shows exact count until 1k, then compact)\nexport const TOKEN_DISPLAY_THRESHOLD = 100;\n// Show elapsed time after 2 minutes (in ms)\nexport const ELAPSED_DISPLAY_THRESHOLD_MS = 60 * 1000;\n",
10
+ "/**\n * Application-wide constants\n */\n\n/**\n * Default model ID to use when no model is specified\n */\nexport const DEFAULT_MODEL_ID = \"auto\";\n\n/**\n * Default model handle to use for conversation compaction / summarization.\n */\nexport const DEFAULT_SUMMARIZATION_MODEL = \"letta/auto\";\n\n/**\n * Default model handle for lightweight conversation title generation.\n */\nexport const DEFAULT_TITLE_SUMMARIZATION_MODEL = \"openai/gpt-6-luna\";\n\n/**\n * Default agent name when creating a new agent\n */\nexport const DEFAULT_AGENT_NAME = \"Letta Code\";\n\n/**\n * Message displayed when user interrupts tool execution\n */\nexport const INTERRUPTED_BY_USER = \"Interrupted by user\";\n\n/**\n * Synthetic tool result injected when a turn ended without completing all tool calls\n * (e.g. process crash or unhandled stream error). Settled automatically at the start\n * of the next turn so the conversation history stays valid for the provider.\n */\nexport const TURN_DID_NOT_COMPLETE = \"Turn did not complete\";\n\n/**\n * XML tag used to wrap system reminder content injected into messages\n */\nexport const SYSTEM_REMINDER_TAG = \"system-reminder\";\nexport const SYSTEM_REMINDER_OPEN = `<${SYSTEM_REMINDER_TAG}>`;\nexport const SYSTEM_REMINDER_CLOSE = `</${SYSTEM_REMINDER_TAG}>`;\n// Legacy tag kept for parsing/backward compatibility with older saved messages.\nexport const SYSTEM_ALERT_TAG = \"system-alert\";\nexport const SYSTEM_ALERT_OPEN = `<${SYSTEM_ALERT_TAG}>`;\nexport const SYSTEM_ALERT_CLOSE = `</${SYSTEM_ALERT_TAG}>`;\n\n/**\n * How often (in turns) to check for memfs sync conflicts, even without\n * filesystem change events. Catches block-only changes (e.g. ADE/API edits).\n */\nexport const MEMFS_CONFLICT_CHECK_INTERVAL = 5;\n\n/**\n * Header displayed before compaction summary when conversation context is truncated\n */\nexport const COMPACTION_SUMMARY_HEADER =\n \"(Earlier messages in this conversation have been compacted to free up context, summarized below)\";\n\n/**\n * Status bar thresholds - only show indicators when values exceed these\n */\n// Show token count after 100 estimated tokens (shows exact count until 1k, then compact)\nexport const TOKEN_DISPLAY_THRESHOLD = 100;\n// Show elapsed time after 2 minutes (in ms)\nexport const ELAPSED_DISPLAY_THRESHOLD_MS = 60 * 1000;\n",
11
11
  "/**\n * XML formatting for channel notifications.\n *\n * Produces structured XML that the agent receives as message content.\n * Follows the same escaping patterns used in taskNotifications.ts.\n */\n\nimport type { MessageCreate } from \"@letta-ai/letta-client/resources/agents/agents\";\nimport type { ChannelUserMention } from \"@/channels/message-references\";\nimport { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from \"@/constants\";\nimport type {\n ChannelMessageAttachment,\n ChannelThreadContextEntry,\n InboundChannelMessage,\n} from \"./types\";\n\n/**\n * Escape XML text-node content without over-escaping quotes that should remain\n * readable inside the rendered message body.\n */\nfunction escapeXmlText(text: string): string {\n return text\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\");\n}\n\n/**\n * Escape XML attribute values, including quotes.\n */\nfunction escapeXmlAttribute(text: string): string {\n return escapeXmlText(text).replace(/\"/g, \"&quot;\").replace(/'/g, \"&apos;\");\n}\n\nfunction sanitizeMentionDisplayName(mention: ChannelUserMention): string {\n const name = mention.displayName\n .replace(/[\\p{Cc}\\p{Cf}\\p{Zl}\\p{Zp}]+/gu, \" \")\n .replace(/\\s+/gu, \" \")\n .trim()\n .slice(0, 80)\n .trim();\n return name || mention.userId;\n}\n\nfunction buildTextWithUserMentions(\n text: string,\n mentions: ChannelUserMention[] | undefined,\n): string {\n if (!mentions || mentions.length === 0) return escapeXmlText(text);\n let cursor = 0;\n const parts: string[] = [];\n for (const mention of mentions) {\n if (\n !Number.isInteger(mention.start) ||\n !Number.isInteger(mention.end) ||\n mention.start < cursor ||\n mention.end <= mention.start ||\n mention.end > text.length ||\n !mention.userId.trim()\n ) {\n return escapeXmlText(text);\n }\n parts.push(escapeXmlText(text.slice(cursor, mention.start)));\n const displayName = sanitizeMentionDisplayName(mention);\n parts.push(\n `<mention id=\"${escapeXmlAttribute(mention.userId)}\">@${escapeXmlText(displayName)}</mention>`,\n );\n cursor = mention.end;\n }\n parts.push(escapeXmlText(text.slice(cursor)));\n return parts.join(\"\");\n}\n\nfunction formatMebibytes(bytes: number): string {\n const mebibytes = bytes / (1024 * 1024);\n const rounded =\n mebibytes >= 100 ? Math.round(mebibytes).toString() : mebibytes.toFixed(1);\n return `${rounded.replace(/\\.0$/, \"\")} MiB`;\n}\n\nfunction hasNotificationAttachmentPaths(msg: InboundChannelMessage): boolean {\n if (msg.attachments?.some((attachment) => attachment.localPath)) {\n return true;\n }\n if (\n msg.threadContext?.starter?.attachments?.some(\n (attachment) => attachment.localPath,\n )\n ) {\n return true;\n }\n return Boolean(\n msg.threadContext?.history?.some((entry) =>\n entry.attachments?.some((attachment) => attachment.localPath),\n ),\n );\n}\n\nfunction buildChannelAttachmentReminderText(\n msg: InboundChannelMessage,\n): string | undefined {\n if (!hasNotificationAttachmentPaths(msg)) return undefined;\n return [\n SYSTEM_REMINDER_OPEN,\n \"If this notification includes attachment local_path values, you may be able to inspect those files using local file or image tools available in your current toolset (for example Read or ViewImage), using the local_path.\",\n SYSTEM_REMINDER_CLOSE,\n ].join(\"\\n\");\n}\n\ntype AttachmentXmlContext = {\n channel: string;\n accountId?: string;\n chatId: string;\n messageId?: string;\n};\n\nfunction buildAttachmentXml(\n attachment: ChannelMessageAttachment,\n context: AttachmentXmlContext,\n): string {\n const attrs = [`kind=\"${escapeXmlAttribute(attachment.kind)}\"`];\n\n if (attachment.localPath) {\n attrs.push(`local_path=\"${escapeXmlAttribute(attachment.localPath)}\"`);\n } else {\n attrs.push('download_status=\"not_downloaded\"');\n }\n\n if (attachment.id) {\n attrs.push(`attachment_id=\"${escapeXmlAttribute(attachment.id)}\"`);\n }\n if (attachment.name) {\n attrs.push(`name=\"${escapeXmlAttribute(attachment.name)}\"`);\n }\n if (attachment.mimeType) {\n attrs.push(`mime_type=\"${escapeXmlAttribute(attachment.mimeType)}\"`);\n }\n if (typeof attachment.sizeBytes === \"number\") {\n attrs.push(`size_bytes=\"${attachment.sizeBytes}\"`);\n }\n const sourceMessageId = attachment.sourceMessageId ?? context.messageId;\n if (!attachment.localPath && sourceMessageId) {\n attrs.push(`source_message_id=\"${escapeXmlAttribute(sourceMessageId)}\"`);\n }\n if (!attachment.localPath && attachment.sourceThreadId) {\n attrs.push(\n `source_thread_id=\"${escapeXmlAttribute(attachment.sourceThreadId)}\"`,\n );\n }\n if (attachment.downloadReason) {\n attrs.push(\n `download_reason=\"${escapeXmlAttribute(attachment.downloadReason)}\"`,\n );\n }\n if (typeof attachment.autoDownloadLimitBytes === \"number\") {\n attrs.push(\n `auto_download_limit_bytes=\"${attachment.autoDownloadLimitBytes}\"`,\n );\n }\n\n const children: string[] = [];\n if (attachment.transcription) {\n children.push(\n `<attempted_transcription>${escapeXmlText(attachment.transcription)}</attempted_transcription>`,\n );\n }\n if (attachment.transcriptionError) {\n children.push(\n `<attempted_transcription_error>${escapeXmlText(attachment.transcriptionError)}</attempted_transcription_error>`,\n );\n }\n if (\n !attachment.localPath &&\n context.channel === \"slack\" &&\n attachment.id &&\n sourceMessageId\n ) {\n const accountArg = context.accountId\n ? `, accountId=\"${escapeXmlAttribute(context.accountId)}\"`\n : \"\";\n const threadArg = attachment.sourceThreadId\n ? `, threadId=\"${escapeXmlAttribute(attachment.sourceThreadId)}\"`\n : \"\";\n const action = `MessageChannel with action=\"download-file\", channel=\"slack\", chat_id=\"${escapeXmlAttribute(context.chatId)}\"${accountArg}${threadArg}, attachmentId=\"${escapeXmlAttribute(attachment.id)}\", and messageId=\"${escapeXmlAttribute(sourceMessageId)}\"`;\n if (attachment.downloadReason === \"exceeds_auto_download_limit\") {\n const sizeNote =\n typeof attachment.sizeBytes === \"number\"\n ? `This file is ${formatMebibytes(attachment.sizeBytes)}${\n typeof attachment.autoDownloadLimitBytes === \"number\"\n ? `, above the ${formatMebibytes(attachment.autoDownloadLimitBytes)} automatic download limit`\n : \"\"\n }. `\n : \"\";\n children.push(\n `<download-instruction>${sizeNote}Call ${action}. The tool downloads the file into the same Slack inbound attachment directory and returns its local_path. Large downloads continue in the background instead of blocking; a task notification delivers the local_path when they finish. Do not ask the sender to reattach it.</download-instruction>`,\n );\n } else {\n children.push(\n `<download-retry>Automatic download did not complete. Call ${action} to retry. The action may return a precise error if Slack still cannot provide the file.</download-retry>`,\n );\n }\n }\n\n if (children.length > 0) {\n return `<attachment ${attrs.join(\" \")}>\\n ${children.join(\"\\n \")}\\n</attachment>`;\n }\n\n return `<attachment ${attrs.join(\" \")} />`;\n}\n\nfunction canEmitInlineImageContentPart(mimeType: string): boolean {\n const normalized = mimeType.split(\";\")[0]?.trim().toLowerCase();\n return (\n !!normalized &&\n normalized.startsWith(\"image/\") &&\n normalized !== \"image/svg+xml\"\n );\n}\n\nfunction buildReactionXml(msg: InboundChannelMessage): string | null {\n if (!msg.reaction) {\n return null;\n }\n\n const attrs = [\n `action=\"${escapeXmlAttribute(msg.reaction.action)}\"`,\n `emoji=\"${escapeXmlAttribute(msg.reaction.emoji)}\"`,\n `target_message_id=\"${escapeXmlAttribute(msg.reaction.targetMessageId)}\"`,\n ];\n\n if (msg.reaction.targetSenderId) {\n attrs.push(\n `target_sender_id=\"${escapeXmlAttribute(msg.reaction.targetSenderId)}\"`,\n );\n }\n\n return `<reaction ${attrs.join(\" \")} />`;\n}\n\nfunction buildReplyContextXml(msg: InboundChannelMessage): string | null {\n const replyContext = msg.replyContext;\n if (!replyContext) {\n return null;\n }\n\n const attrs: string[] = [];\n if (replyContext.messageId) {\n attrs.push(`message_id=\"${escapeXmlAttribute(replyContext.messageId)}\"`);\n }\n if (replyContext.senderId) {\n attrs.push(`sender_id=\"${escapeXmlAttribute(replyContext.senderId)}\"`);\n }\n if (replyContext.senderName) {\n attrs.push(`sender_name=\"${escapeXmlAttribute(replyContext.senderName)}\"`);\n }\n\n const attrString = attrs.length > 0 ? ` ${attrs.join(\" \")}` : \"\";\n if (replyContext.text?.trim()) {\n const text = buildTextWithUserMentions(\n replyContext.text,\n replyContext.userMentions,\n );\n return `<reply-context${attrString}>\\n${text}\\n</reply-context>`;\n }\n return `<reply-context${attrString} />`;\n}\n\nfunction buildThreadContextEntryXml(\n tagName: string,\n entry: ChannelThreadContextEntry,\n context: Omit<AttachmentXmlContext, \"messageId\">,\n): string {\n const attrs: string[] = [];\n if (entry.senderId) {\n attrs.push(`sender_id=\"${escapeXmlAttribute(entry.senderId)}\"`);\n }\n if (entry.senderName) {\n attrs.push(`sender_name=\"${escapeXmlAttribute(entry.senderName)}\"`);\n }\n if (entry.messageId) {\n attrs.push(`message_id=\"${escapeXmlAttribute(entry.messageId)}\"`);\n }\n\n const attrString = attrs.length > 0 ? ` ${attrs.join(\" \")}` : \"\";\n const body = [\n ...(entry.text\n ? [buildTextWithUserMentions(entry.text, entry.userMentions)]\n : []),\n ...(entry.attachments ?? []).map((attachment) =>\n buildAttachmentXml(attachment, {\n ...context,\n messageId: entry.messageId,\n }),\n ),\n ].join(\"\\n\");\n return `<${tagName}${attrString}>\\n${body}\\n</${tagName}>`;\n}\n\nfunction buildThreadContextXml(msg: InboundChannelMessage): string | null {\n const threadContext = msg.threadContext;\n if (!threadContext) {\n return null;\n }\n\n const parts: string[] = [];\n if (threadContext.starter) {\n parts.push(\n buildThreadContextEntryXml(\"thread-starter\", threadContext.starter, {\n channel: msg.channel,\n accountId: msg.accountId,\n chatId: msg.chatId,\n }),\n );\n }\n const historyEntries = threadContext.history ?? [];\n if (historyEntries.length > 0) {\n parts.push(\n [\n \"<thread-history>\",\n ...historyEntries.map((entry) =>\n buildThreadContextEntryXml(\"thread-message\", entry, {\n channel: msg.channel,\n accountId: msg.accountId,\n chatId: msg.chatId,\n }),\n ),\n \"</thread-history>\",\n ].join(\"\\n\"),\n );\n }\n\n if (parts.length === 0) {\n return null;\n }\n\n const attrs = threadContext.label\n ? ` label=\"${escapeXmlAttribute(threadContext.label)}\"`\n : \"\";\n return [`<thread-context${attrs}>`, ...parts, \"</thread-context>\"].join(\"\\n\");\n}\n\n/**\n * Format an inbound channel message as XML for the agent.\n *\n * Example output:\n * ```xml\n * <channel-notification source=\"telegram\" chat_id=\"12345\" sender_id=\"67890\" sender_name=\"John\">\n * Hello from Telegram!\n * </channel-notification>\n * ```\n */\nexport function buildChannelNotificationXml(\n msg: InboundChannelMessage,\n): string {\n const attrs: string[] = [\n `source=\"${escapeXmlAttribute(msg.channel)}\"`,\n `chat_id=\"${escapeXmlAttribute(msg.chatId)}\"`,\n `sender_id=\"${escapeXmlAttribute(msg.senderId)}\"`,\n ];\n\n if (msg.accountId) {\n attrs.push(`account_id=\"${escapeXmlAttribute(msg.accountId)}\"`);\n }\n\n if (msg.senderName) {\n attrs.push(`sender_name=\"${escapeXmlAttribute(msg.senderName)}\"`);\n }\n\n if (msg.messageId) {\n attrs.push(`message_id=\"${escapeXmlAttribute(msg.messageId)}\"`);\n }\n\n if (msg.threadId) {\n attrs.push(`thread_id=\"${escapeXmlAttribute(msg.threadId)}\"`);\n }\n if (msg.routedBy) {\n attrs.push(`routed_by=\"${escapeXmlAttribute(msg.routedBy)}\"`);\n }\n\n const attrString = attrs.join(\" \");\n const escapedText = msg.text\n ? buildTextWithUserMentions(msg.text, msg.userMentions)\n : \"\";\n const reactionXml = buildReactionXml(msg);\n const replyContextXml = buildReplyContextXml(msg);\n const threadContextXml = buildThreadContextXml(msg);\n const attachmentXml = (msg.attachments ?? []).map((attachment) =>\n buildAttachmentXml(attachment, {\n channel: msg.channel,\n accountId: msg.accountId,\n chatId: msg.chatId,\n messageId: msg.messageId,\n }),\n );\n const body = [\n threadContextXml,\n replyContextXml,\n reactionXml,\n ...attachmentXml,\n escapedText,\n ]\n .filter(Boolean)\n .join(\"\\n\");\n\n return `<channel-notification ${attrString}>\\n${body}\\n</channel-notification>`;\n}\n\n/**\n * Format an inbound channel message as structured content parts.\n *\n * Attachment guidance is emitted only when this event contains inspectable\n * local paths. Reply semantics live in the scoped MessageChannel definition.\n */\nexport function formatChannelNotification(\n msg: InboundChannelMessage,\n): MessageCreate[\"content\"] {\n const attachmentReminder = buildChannelAttachmentReminderText(msg);\n return [\n ...(attachmentReminder\n ? [{ type: \"text\" as const, text: attachmentReminder }]\n : []),\n { type: \"text\", text: buildChannelNotificationXml(msg) },\n ...(msg.attachments ?? []).flatMap((attachment) => {\n if (\n attachment.kind !== \"image\" ||\n typeof attachment.imageDataBase64 !== \"string\" ||\n attachment.imageDataBase64.length === 0 ||\n typeof attachment.mimeType !== \"string\" ||\n !canEmitInlineImageContentPart(attachment.mimeType)\n ) {\n return [];\n }\n\n return [\n {\n type: \"image\" as const,\n source: {\n type: \"base64\" as const,\n media_type: attachment.mimeType,\n data: attachment.imageDataBase64,\n },\n },\n ];\n }),\n ] as MessageCreate[\"content\"];\n}\n",
12
12
  "import type {\n ChannelRoute,\n ChannelTurnSource,\n InboundChannelMessage,\n OutboundChannelMessage,\n} from \"./types\";\nimport { buildChannelNotificationXml } from \"./xml\";\n\nexport interface BuildChannelTurnSourceParams {\n message: InboundChannelMessage;\n route: ChannelRoute;\n}\n\nexport interface FormatInboundChannelMessageParams {\n message: InboundChannelMessage;\n}\n\nexport interface ChannelBatchMessage {\n text: string;\n senderId?: string;\n senderName?: string;\n timestamp?: string | number;\n channelTurnSource?: ChannelTurnSource;\n}\n\nexport interface FormatBatchedChannelMessagesParams {\n messages: ChannelBatchMessage[];\n}\n\nexport interface BuildOutboundChannelMessageFromTurnSourceParams {\n turnSource: ChannelTurnSource;\n text: string;\n}\n\nexport function buildChannelTurnSource(\n params: BuildChannelTurnSourceParams,\n): ChannelTurnSource {\n const { message, route } = params;\n return {\n channel: message.channel,\n accountId: message.accountId ?? route.accountId,\n chatId: message.chatId,\n chatType: message.chatType ?? route.chatType,\n senderId: message.senderId,\n senderTeamId: message.senderTeamId,\n messageId: message.messageId,\n threadId: message.threadId ?? route.threadId ?? null,\n agentId: route.agentId,\n conversationId: route.conversationId,\n };\n}\n\nexport function formatInboundChannelMessageForAgent(\n params: FormatInboundChannelMessageParams,\n): string {\n return buildChannelNotificationXml(params.message);\n}\n\nfunction formatLegacyBatchedSender(message: ChannelBatchMessage): string {\n const senderName = message.senderName?.trim();\n const senderId = message.senderId?.trim() ?? \"unknown\";\n if (senderName && senderName !== senderId) {\n return `${senderName}:${senderId}`;\n }\n return senderId;\n}\n\nexport function formatBatchedChannelMessagesForAgent(\n params: FormatBatchedChannelMessagesParams,\n): string {\n const { messages } = params;\n if (messages.length === 0) {\n return \"\";\n }\n const firstMessage = messages[0];\n if (messages.length === 1 && firstMessage) {\n return firstMessage.text;\n }\n\n if (messages.every((message) => message.channelTurnSource)) {\n const formatted = messages.map((message) => message.text).join(\"\\n\");\n return `--- Batched Channel Messages (${messages.length}) ---\\n${formatted}\\n--- End Batched Channel Messages ---`;\n }\n\n const formatted = messages\n .map((message) => {\n const timestamp = message.timestamp ?? \"unknown\";\n return `[user@${formatLegacyBatchedSender(message)}]<${timestamp}>:${message.text}`;\n })\n .join(\"\\n\");\n\n return `--- Batched Messages (${messages.length}) ---\\n${formatted}\\n--- End Batched Messages ---`;\n}\n\nexport function buildOutboundChannelMessageFromTurnSource(\n params: BuildOutboundChannelMessageFromTurnSourceParams,\n): OutboundChannelMessage {\n const { turnSource } = params;\n return {\n channel: turnSource.channel,\n accountId: turnSource.accountId,\n chatId: turnSource.chatId,\n threadId: turnSource.threadId,\n text: params.text,\n agentId: turnSource.agentId,\n conversationId: turnSource.conversationId,\n };\n}\n",
13
13
  "const MAX_PROGRESS_TEXT_LENGTH = 140;\nexport const MAX_PROGRESS_DETAILS_LENGTH = 180;\nconst MAX_SHELL_PROGRESS_DETAILS_LENGTH = 64;\nconst MAX_SUBAGENT_PROGRESS_DETAILS_LENGTH = 180;\nconst ESCAPE_CODE = String.fromCharCode(27);\nconst ANSI_ESCAPE_RE = new RegExp(`${ESCAPE_CODE}\\\\[[0-9;?]*[ -/]*[@-~]`, \"g\");\nconst SECRET_ASSIGNMENT_RE =\n /\\b([A-Z0-9_]*(?:TOKEN|SECRET|PASSWORD|PASS|API[_-]?KEY|ACCESS[_-]?KEY)[A-Z0-9_]*)\\s*=\\s*(\"[^\"]*\"|'[^']*'|\\S+)/gi;\nconst SECRET_JSON_RE =\n /([\"']?(?:token|secret|password|api[_-]?key|access[_-]?key)[\"']?\\s*[:=]\\s*)(\"[^\"]*\"|'[^']*'|\\S+)/gi;\n\nfunction isTaskTool(name: string): boolean {\n return (\n name === \"Task\" || name === \"task\" || name === \"Agent\" || name === \"agent\"\n );\n}\n\nfunction isShellTool(name: string): boolean {\n const normalized = name.toLowerCase();\n return normalized === \"bash\" || normalized === \"exec_command\";\n}\n\nfunction isWebSearchTool(name: string): boolean {\n const normalized = name.toLowerCase();\n return normalized === \"web_search\" || normalized === \"websearch\";\n}\n\nfunction isSearchTool(name: string): boolean {\n return name.toLowerCase() === \"grep\";\n}\n\nfunction isGlobTool(name: string): boolean {\n return name.toLowerCase() === \"glob\";\n}\n\nfunction isFileReadTool(name: string): boolean {\n return name.toLowerCase() === \"read\";\n}\n\nfunction isFileWriteTool(name: string): boolean {\n const normalized = name.toLowerCase();\n return normalized === \"write\";\n}\n\nfunction isFileEditTool(name: string): boolean {\n const normalized = name.toLowerCase();\n return normalized === \"edit\" || normalized === \"applypatch\";\n}\n\nfunction getPathBaseName(filePath: string): string {\n return filePath.split(/[\\\\/]/).filter(Boolean).pop() ?? filePath;\n}\n\nexport function asRecord(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\"\n ? (value as Record<string, unknown>)\n : null;\n}\n\nexport function firstNonEmptyString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === \"string\" && value.trim().length > 0) {\n return value;\n }\n }\n return undefined;\n}\n\nexport function truncateChannelProgressText(\n value: string,\n maxLength: number,\n marker = \"...\",\n): string {\n if (value.length <= maxLength) {\n return value;\n }\n if (maxLength <= marker.length) {\n return marker.slice(0, Math.max(0, maxLength));\n }\n return `${value.slice(0, maxLength - marker.length).trimEnd()}${marker}`;\n}\n\nfunction replaceControlCharacters(value: string): string {\n let result = \"\";\n for (let index = 0; index < value.length; index += 1) {\n const character = value[index] ?? \"\";\n const code = character.charCodeAt(0);\n result +=\n code <= 8 ||\n code === 11 ||\n code === 12 ||\n (code >= 14 && code <= 31) ||\n code === 127\n ? \" \"\n : character;\n }\n return result;\n}\n\n/**\n * Shared sanitization core for channel-facing progress text: strips ANSI\n * escapes and control characters, redacts secret-looking assignments, and\n * neutralizes platform mentions. Platform adapters layer their own escaping\n * (and truncation marker) on top of this instead of maintaining parallel\n * redaction rules.\n */\nexport function sanitizeChannelProgressCore(value: unknown): string {\n const raw = typeof value === \"string\" ? value : String(value ?? \"\");\n const redacted = raw\n .replace(ANSI_ESCAPE_RE, \"\")\n .replace(SECRET_ASSIGNMENT_RE, \"$1=[redacted]\")\n .replace(SECRET_JSON_RE, \"$1[redacted]\");\n return replaceControlCharacters(redacted)\n .replace(/[\\r\\n\\t]+/g, \" \")\n .replace(/@(?=channel|here|everyone|[A-Za-z0-9._-]+)/gi, \"@\\u200b\")\n .replace(/\\s+/g, \" \")\n .trim();\n}\n\nexport function sanitizeChannelProgressText(\n value: unknown,\n maxLength: number = MAX_PROGRESS_TEXT_LENGTH,\n): string {\n return truncateChannelProgressText(\n sanitizeChannelProgressCore(value),\n maxLength,\n );\n}\n\nexport function sanitizeChannelProgressIdentifier(\n value: unknown,\n fallback: string,\n): string {\n const text = sanitizeChannelProgressText(value, 64);\n if (!text) {\n return fallback;\n }\n const cleaned = text.replace(/[^A-Za-z0-9_.:/ -]/g, \"\").trim();\n return cleaned || fallback;\n}\n\nfunction summarizeShellCommand(command: string): string {\n const normalized = sanitizeChannelProgressText(command, 10_000);\n if (!normalized) {\n return \"\";\n }\n\n // Preview the first command segment (or two short ones) and drop pipelines\n // so multi-step shell invocations stay readable at Slack-row lengths.\n const segments = normalized\n .split(/\\s*;\\s*/)\n .map((segment) => segment.trim())\n .filter(Boolean);\n const firstTwoSegments = segments.slice(0, 2).join(\"; \");\n const previewSource =\n firstTwoSegments.length > 0 && firstTwoSegments.length <= 70\n ? firstTwoSegments\n : (segments[0] ?? normalized);\n const withoutPipeline = previewSource.split(/\\s*\\|\\s*/)[0] ?? previewSource;\n\n return sanitizeChannelProgressText(\n withoutPipeline.trim() || normalized,\n MAX_SHELL_PROGRESS_DETAILS_LENGTH,\n );\n}\n\ntype SkillDescriptionLookup =\n | ReadonlyMap<string, string>\n | Readonly<Record<string, string | undefined>>;\n\nexport type ChannelTurnProgressBuilderOptions = {\n skillDescriptionsByName?: SkillDescriptionLookup;\n};\n\nexport type ToolCallSummary = {\n id?: string;\n name?: string;\n argumentsText?: string;\n};\n\nfunction getSkillNameFromArguments(\n parsedArguments: Record<string, unknown>,\n): string | undefined {\n return firstNonEmptyString(parsedArguments.skill, parsedArguments.skillName);\n}\n\nfunction getFragmentedSkillName(summary: ToolCallSummary): string | undefined {\n const skillMatch = summary.argumentsText?.match(\n /\"(?:skill|skillName)\"\\s*:\\s*\"([^\"]+)\"/,\n );\n return skillMatch?.[1];\n}\n\nfunction resolveSkillDescription(\n skillName: string | undefined,\n options: ChannelTurnProgressBuilderOptions | undefined,\n): string | undefined {\n const lookup = options?.skillDescriptionsByName;\n if (!skillName || !lookup) {\n return undefined;\n }\n if (\"get\" in lookup && typeof lookup.get === \"function\") {\n return firstNonEmptyString(lookup.get(skillName));\n }\n return firstNonEmptyString(\n (lookup as Readonly<Record<string, string | undefined>>)[skillName],\n );\n}\n\nfunction formatSkillProgressTitleFromName(\n skillName: string | undefined,\n): string | undefined {\n const sanitized = sanitizeChannelProgressText(\n skillName,\n MAX_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized ? `Skill: ${sanitized}` : undefined;\n}\n\nfunction formatShellProgressDetailsFromArguments(\n parsedArguments: Record<string, unknown>,\n): string | undefined {\n const description = firstNonEmptyString(parsedArguments.description);\n if (description) {\n return (\n sanitizeChannelProgressText(description, MAX_PROGRESS_DETAILS_LENGTH) ||\n undefined\n );\n }\n\n const commandPreview = firstNonEmptyString(\n parsedArguments.command,\n parsedArguments.cmd,\n );\n return summarizeShellCommand(commandPreview ?? \"\") || undefined;\n}\n\nfunction formatFragmentedShellProgressDetails(\n summary: ToolCallSummary,\n): string | undefined {\n const descriptionMatch = summary.argumentsText?.match(\n /\"description\"\\s*:\\s*\"([^\"]+)\"/,\n );\n if (descriptionMatch?.[1]) {\n return (\n sanitizeChannelProgressText(\n descriptionMatch[1],\n MAX_PROGRESS_DETAILS_LENGTH,\n ) || undefined\n );\n }\n\n // Do not expose command previews from incomplete shell-tool JSON. Slack's\n // task stream appends, rather than replaces, changed details for the same\n // task id; sending a provisional command before the streamed description\n // arrives leaves both strings glued together in the preview.\n return undefined;\n}\n\nfunction formatSubagentProgressDetailsFromArguments(\n parsedArguments: Record<string, unknown>,\n): string | undefined {\n const preview = firstNonEmptyString(\n parsedArguments.prompt,\n parsedArguments.description,\n parsedArguments.subject,\n );\n const sanitized = sanitizeChannelProgressText(\n preview,\n MAX_SUBAGENT_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n}\n\nfunction formatFragmentedSubagentProgressDetails(\n summary: ToolCallSummary,\n): string | undefined {\n const previewMatch = summary.argumentsText?.match(\n /\"(?:prompt|description|subject)\"\\s*:\\s*\"([^\"]+)\"/,\n );\n if (!previewMatch?.[1]) {\n return undefined;\n }\n const sanitized = sanitizeChannelProgressText(\n previewMatch[1],\n MAX_SUBAGENT_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n}\n\nexport type ToolReturnSummary = {\n summary: ToolCallSummary;\n status: \"completed\" | \"error\";\n errorDetails?: string;\n};\n\nexport function parseToolArguments(\n value: string | undefined,\n): Record<string, unknown> | null {\n if (!value) {\n return null;\n }\n try {\n return asRecord(JSON.parse(value));\n } catch {\n return null;\n }\n}\n\nfunction isFetchWebpageToolName(name: string | undefined): boolean {\n return (\n name === \"fetch_webpage\" ||\n name === \"FetchWebpage\" ||\n name === \"fetchWebpage\"\n );\n}\n\nexport function isSkillToolName(name: string | undefined): boolean {\n if (!name) {\n return false;\n }\n const normalized = name.includes(\".\")\n ? name.slice(name.lastIndexOf(\".\") + 1)\n : name;\n return normalized === \"Skill\" || normalized === \"skill\";\n}\n\nfunction isFilePathToolName(name: string): boolean {\n return isFileReadTool(name) || isFileWriteTool(name) || isFileEditTool(name);\n}\n\nfunction getFileToolKind(name: string): \"read\" | \"write\" | \"update\" | null {\n if (isFileReadTool(name)) {\n return \"read\";\n }\n if (isFileWriteTool(name)) {\n return \"write\";\n }\n if (isFileEditTool(name)) {\n return \"update\";\n }\n return null;\n}\n\nfunction countProgressLines(value: string | undefined): number {\n return value ? value.split(\"\\n\").length : 0;\n}\n\ntype LineChangeSummary = {\n additions?: number;\n deletions?: number;\n};\n\nfunction formatLineChangeSummary(summary: LineChangeSummary | null): string {\n if (!summary) {\n return \"\";\n }\n const parts: string[] = [];\n if (summary.additions !== undefined) {\n parts.push(`+${summary.additions}`);\n }\n if (summary.deletions !== undefined) {\n parts.push(`-${summary.deletions}`);\n }\n return parts.length > 0 ? ` ${parts.join(\" \")}` : \"\";\n}\n\nfunction extractFilePathFromArguments(\n parsedArguments: Record<string, unknown>,\n): string | undefined {\n return firstNonEmptyString(\n parsedArguments.file_path,\n parsedArguments.filePath,\n parsedArguments.path,\n );\n}\n\nfunction formatProgressFileName(\n filePath: string | undefined,\n): string | undefined {\n if (!filePath) {\n return undefined;\n }\n const fileName = getPathBaseName(filePath) || filePath;\n return (\n sanitizeChannelProgressText(fileName, MAX_PROGRESS_DETAILS_LENGTH) ||\n undefined\n );\n}\n\nfunction getEditLineChangeSummary(\n parsedArguments: Record<string, unknown>,\n): LineChangeSummary | null {\n const oldString = firstNonEmptyString(parsedArguments.old_string);\n const newString = firstNonEmptyString(parsedArguments.new_string);\n if (oldString === undefined || newString === undefined) {\n return null;\n }\n return {\n additions: countProgressLines(newString),\n deletions: countProgressLines(oldString),\n };\n}\n\nfunction getWriteLineChangeSummary(\n parsedArguments: Record<string, unknown>,\n): LineChangeSummary | null {\n const content = firstNonEmptyString(parsedArguments.content);\n if (content === undefined) {\n return null;\n }\n return {\n additions: countProgressLines(content),\n };\n}\n\nfunction getFileLineChangeSummary(\n name: string,\n parsedArguments: Record<string, unknown>,\n): LineChangeSummary | null {\n if (isFileWriteTool(name)) {\n return getWriteLineChangeSummary(parsedArguments);\n }\n if (isFileEditTool(name)) {\n return getEditLineChangeSummary(parsedArguments);\n }\n return null;\n}\n\nfunction getFileToolVerb(\n kind: \"read\" | \"write\" | \"update\",\n status: \"started\" | \"completed\" | \"error\",\n): string {\n if (status === \"error\") {\n if (kind === \"read\") {\n return \"Tried to read\";\n }\n if (kind === \"write\") {\n return \"Tried to write\";\n }\n return \"Tried to update\";\n }\n if (status === \"started\") {\n if (kind === \"read\") {\n return \"Reading\";\n }\n if (kind === \"write\") {\n return \"Writing\";\n }\n return \"Updating\";\n }\n if (kind === \"read\") {\n return \"Read\";\n }\n if (kind === \"write\") {\n return \"Wrote\";\n }\n return \"Updated\";\n}\n\nfunction formatFileToolProgressTitle(\n name: string,\n parsedArguments: Record<string, unknown>,\n status: \"started\" | \"completed\" | \"error\",\n): string | undefined {\n const kind = getFileToolKind(name);\n if (!kind) {\n return undefined;\n }\n const fileName = formatProgressFileName(\n extractFilePathFromArguments(parsedArguments),\n );\n if (!fileName) {\n return undefined;\n }\n const stats =\n status === \"completed\"\n ? formatLineChangeSummary(getFileLineChangeSummary(name, parsedArguments))\n : \"\";\n return `${getFileToolVerb(kind, status)} ${fileName}${stats}`;\n}\n\nfunction formatFragmentedFileToolProgressTitle(\n summary: ToolCallSummary,\n status: \"started\" | \"completed\" | \"error\",\n): string | undefined {\n if (!summary.name || !summary.argumentsText) {\n return undefined;\n }\n const kind = getFileToolKind(summary.name);\n if (!kind) {\n return undefined;\n }\n const filePathMatch = summary.argumentsText.match(\n /\"(?:file_path|filePath|path)\"\\s*:\\s*\"([^\"]+)\"/,\n );\n const fileName = formatProgressFileName(filePathMatch?.[1]);\n if (!fileName) {\n return undefined;\n }\n return `${getFileToolVerb(kind, status)} ${fileName}`;\n}\n\nexport function formatToolProgressTitle(\n summary: ToolCallSummary,\n status: \"started\" | \"completed\" | \"error\",\n): string | undefined {\n if (!summary.name || !summary.argumentsText) {\n return undefined;\n }\n\n const parsedArguments = parseToolArguments(summary.argumentsText);\n if (parsedArguments) {\n if (isSkillToolName(summary.name)) {\n return formatSkillProgressTitleFromName(\n getSkillNameFromArguments(parsedArguments),\n );\n }\n if (isFilePathToolName(summary.name)) {\n return formatFileToolProgressTitle(summary.name, parsedArguments, status);\n }\n return undefined;\n }\n\n if (isSkillToolName(summary.name)) {\n return formatSkillProgressTitleFromName(getFragmentedSkillName(summary));\n }\n if (isFilePathToolName(summary.name)) {\n return formatFragmentedFileToolProgressTitle(summary, status);\n }\n return undefined;\n}\n\nfunction formatSkillProgressDetailsFromArguments(\n parsedArguments: Record<string, unknown>,\n options: ChannelTurnProgressBuilderOptions | undefined,\n): string | undefined {\n const skillName = getSkillNameFromArguments(parsedArguments);\n const detail = resolveSkillDescription(skillName, options) ?? skillName;\n const sanitized = sanitizeChannelProgressText(\n detail,\n MAX_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n}\n\nfunction formatFragmentedSkillProgressDetails(\n summary: ToolCallSummary,\n options: ChannelTurnProgressBuilderOptions | undefined,\n): string | undefined {\n const skillName = getFragmentedSkillName(summary);\n const detail = resolveSkillDescription(skillName, options) ?? skillName;\n const sanitized = sanitizeChannelProgressText(\n detail,\n MAX_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n}\n\nexport function formatToolProgressDetails(\n summary: ToolCallSummary,\n options?: ChannelTurnProgressBuilderOptions,\n): string | undefined {\n if (!summary.name || !summary.argumentsText) {\n return undefined;\n }\n\n // Try JSON parse first (complete arguments)\n const parsedArguments = parseToolArguments(summary.argumentsText);\n if (parsedArguments) {\n if (isWebSearchTool(summary.name)) {\n const query = firstNonEmptyString(parsedArguments.query);\n const sanitized = sanitizeChannelProgressText(\n query,\n MAX_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n }\n\n if (isFetchWebpageToolName(summary.name)) {\n const url = firstNonEmptyString(parsedArguments.url);\n const sanitized = sanitizeChannelProgressText(\n url,\n MAX_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n }\n\n if (isSkillToolName(summary.name)) {\n return formatSkillProgressDetailsFromArguments(parsedArguments, options);\n }\n\n if (isTaskTool(summary.name)) {\n return formatSubagentProgressDetailsFromArguments(parsedArguments);\n }\n\n if (isShellTool(summary.name)) {\n return formatShellProgressDetailsFromArguments(parsedArguments);\n }\n\n if (isFilePathToolName(summary.name)) {\n const filePath = firstNonEmptyString(\n parsedArguments.file_path,\n parsedArguments.filePath,\n parsedArguments.path,\n );\n const sanitized = sanitizeChannelProgressText(\n filePath,\n MAX_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n }\n\n if (isGlobTool(summary.name) || isSearchTool(summary.name)) {\n const pattern = firstNonEmptyString(parsedArguments.pattern);\n const sanitized = sanitizeChannelProgressText(\n pattern,\n MAX_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n }\n\n return undefined;\n }\n\n // Fallback: extract known preview fields from fragmented/incomplete JSON.\n if (isShellTool(summary.name)) {\n return formatFragmentedShellProgressDetails(summary);\n }\n\n if (isSkillToolName(summary.name)) {\n return formatFragmentedSkillProgressDetails(summary, options);\n }\n\n if (isTaskTool(summary.name)) {\n return formatFragmentedSubagentProgressDetails(summary);\n }\n\n if (isFilePathToolName(summary.name)) {\n const filePathMatch = summary.argumentsText.match(\n /\"file_path\"\\s*:\\s*\"([^\"]+)\"/,\n );\n if (filePathMatch?.[1]) {\n const sanitized = sanitizeChannelProgressText(\n filePathMatch[1],\n MAX_PROGRESS_DETAILS_LENGTH,\n );\n return sanitized || undefined;\n }\n }\n\n return undefined;\n}\n",
@@ -16867,28 +16867,29 @@ class StreamableHTTPClientTransport {
16867
16867
  // src/mcp-client.ts
16868
16868
  var DEFAULT_CLIENT_INFO = {
16869
16869
  name: "letta-code",
16870
- version: "0.33.6"
16870
+ version: "0.33.8"
16871
16871
  };
16872
16872
  async function connectMcpServer(config2, options = {}) {
16873
16873
  let client = new Client(options.clientInfo ?? DEFAULT_CLIENT_INFO);
16874
16874
  try {
16875
+ options.signal?.throwIfAborted();
16875
16876
  const transport = createTransport(config2, options);
16876
16877
  try {
16877
- await client.connect(transport);
16878
+ await client.connect(transport, options.signal ? { signal: options.signal } : undefined);
16878
16879
  } catch (error2) {
16879
16880
  if (!(error2 instanceof UnauthorizedError) || !options.oauth?.waitForAuthorizationCode || !supportsOAuthCompletion(transport)) {
16880
16881
  throw error2;
16881
16882
  }
16882
- const authorizationCode = await options.oauth.waitForAuthorizationCode();
16883
- await transport.finishAuth(authorizationCode);
16883
+ const authorizationCode = await withAbort(options.oauth.waitForAuthorizationCode(), options.signal, () => options.oauth?.close());
16884
+ await withAbort(transport.finishAuth(authorizationCode), options.signal, () => transport.close());
16884
16885
  await client.close().catch(() => transport.close().catch(() => {
16885
16886
  return;
16886
16887
  }));
16887
16888
  client = new Client(options.clientInfo ?? DEFAULT_CLIENT_INFO);
16888
- await client.connect(createTransport(config2, options));
16889
+ await client.connect(createTransport(config2, options), options.signal ? { signal: options.signal } : undefined);
16889
16890
  }
16890
- await options.oauth?.close();
16891
- const response = await client.listTools();
16891
+ await (options.oauth?.closeCallback?.() ?? options.oauth?.close() ?? Promise.resolve());
16892
+ const response = await client.listTools(undefined, options.signal ? { signal: options.signal } : undefined);
16892
16893
  const tools = response.tools.map((tool) => ({
16893
16894
  ...tool,
16894
16895
  inputSchema: normalizeInputSchema(tool.inputSchema),
@@ -16911,7 +16912,13 @@ async function connectMcpServer(config2, options = {}) {
16911
16912
  if (closed)
16912
16913
  return;
16913
16914
  closed = true;
16914
- await client.close();
16915
+ const results = await Promise.allSettled([
16916
+ client.close(),
16917
+ options.oauth?.close()
16918
+ ]);
16919
+ const failure = results.find((result) => result.status === "rejected");
16920
+ if (failure)
16921
+ throw failure.reason;
16915
16922
  }
16916
16923
  };
16917
16924
  } catch (error2) {
@@ -16922,6 +16929,27 @@ async function connectMcpServer(config2, options = {}) {
16922
16929
  throw error2;
16923
16930
  }
16924
16931
  }
16932
+ async function withAbort(operation, signal, onAbort) {
16933
+ if (!signal)
16934
+ return operation;
16935
+ signal.throwIfAborted();
16936
+ let abort;
16937
+ const cancellation = new Promise((_resolve, reject) => {
16938
+ abort = () => {
16939
+ Promise.resolve(onAbort?.()).catch(() => {
16940
+ return;
16941
+ });
16942
+ reject(signal.reason ?? new DOMException("Operation aborted", "AbortError"));
16943
+ };
16944
+ signal.addEventListener("abort", abort, { once: true });
16945
+ });
16946
+ try {
16947
+ return await Promise.race([operation, cancellation]);
16948
+ } finally {
16949
+ if (abort)
16950
+ signal.removeEventListener("abort", abort);
16951
+ }
16952
+ }
16925
16953
  function connectStdioMcpServer(config2, options = {}) {
16926
16954
  return connectMcpServer({ ...config2, transport: "stdio" }, options);
16927
16955
  }
@@ -16930,7 +16958,8 @@ function createTransport(config2, options) {
16930
16958
  const headers = resolveHeaderEnvironment(config2.headers);
16931
16959
  return new StreamableHTTPClientTransport(new URL(config2.url), {
16932
16960
  requestInit: headersRequestInit(headers),
16933
- authProvider: options.oauth?.authProvider
16961
+ authProvider: options.oauth?.authProvider,
16962
+ fetch: abortAwareFetch(options.signal, undefined, options.fetch)
16934
16963
  });
16935
16964
  }
16936
16965
  if (config2.transport === "sse") {
@@ -16939,7 +16968,7 @@ function createTransport(config2, options) {
16939
16968
  return new SSEClientTransport(new URL(config2.url), {
16940
16969
  requestInit,
16941
16970
  authProvider: options.oauth?.authProvider,
16942
- fetch: headers ? (url2, init) => fetch(url2, mergeHeaders(init, headers)) : undefined
16971
+ fetch: abortAwareFetch(options.signal, headers, options.fetch)
16943
16972
  });
16944
16973
  }
16945
16974
  return new StdioClientTransport({
@@ -16950,6 +16979,19 @@ function createTransport(config2, options) {
16950
16979
  stderr: options.stderr ?? "inherit"
16951
16980
  });
16952
16981
  }
16982
+ function abortAwareFetch(operationSignal, headers, fetchFn) {
16983
+ if (!operationSignal && !headers)
16984
+ return fetchFn;
16985
+ const request = fetchFn ?? fetch;
16986
+ return (url2, init) => {
16987
+ const requestInit = headers ? mergeHeaders(init, headers) : { ...init };
16988
+ const requestSignal = requestInit.signal ?? undefined;
16989
+ return request(url2, {
16990
+ ...requestInit,
16991
+ signal: operationSignal && requestSignal ? AbortSignal.any([operationSignal, requestSignal]) : operationSignal ?? requestSignal
16992
+ });
16993
+ };
16994
+ }
16953
16995
  function supportsOAuthCompletion(transport) {
16954
16996
  return "finishAuth" in transport && typeof transport.finishAuth === "function";
16955
16997
  }
@@ -16992,4 +17034,4 @@ export {
16992
17034
  connectMcpServer
16993
17035
  };
16994
17036
 
16995
- //# debugId=EA35533720B42BA264756E2164756E21
17037
+ //# debugId=76725E87859D45A764756E2164756E21