titan-agent 5.6.0 → 5.6.2
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.
- package/dist/agent/adapters/codex.js +1 -1
- package/dist/agent/adapters/codex.js.map +1 -1
- package/dist/agent/agent.js +1 -1
- package/dist/agent/agent.js.map +1 -1
- package/dist/agent/agentLoop.js +1 -1
- package/dist/agent/agentLoop.js.map +1 -1
- package/dist/agent/autoVerify.js +1 -1
- package/dist/agent/autoVerify.js.map +1 -1
- package/dist/agent/outputGuardrails.js +1 -1
- package/dist/agent/outputGuardrails.js.map +1 -1
- package/dist/agent/shadowGit.js +1 -1
- package/dist/agent/shadowGit.js.map +1 -1
- package/dist/agent/trajectoryCompressor.js +1 -1
- package/dist/agent/trajectoryCompressor.js.map +1 -1
- package/dist/gateway/openai-compat.js +1 -1
- package/dist/gateway/openai-compat.js.map +1 -1
- package/dist/gateway/routes/paperclip.js.map +1 -1
- package/dist/lib/auto-heal/repair-strategies.js +2 -2
- package/dist/lib/auto-heal/repair-strategies.js.map +1 -1
- package/dist/providers/anthropic.js +21 -1
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/google.js +19 -1
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/openai.js +23 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/utils/constants.js +1 -1
- package/dist/utils/constants.js.map +1 -1
- package/dist/voice/bridge.js.map +1 -1
- package/package.json +1 -1
- package/ui/dist/sw.js +96 -4
package/dist/agent/autoVerify.js
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync, statSync } from "fs";
|
|
|
3
3
|
import { resolve } from "path";
|
|
4
4
|
import logger from "../utils/logger.js";
|
|
5
5
|
const COMPONENT = "AutoVerify";
|
|
6
|
-
function verifyFileWrite(toolName, args,
|
|
6
|
+
function verifyFileWrite(toolName, args, _toolResult) {
|
|
7
7
|
if (toolName !== "write_file" && toolName !== "append_file") {
|
|
8
8
|
return { passed: true };
|
|
9
9
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/autoVerify.ts"],"sourcesContent":["/**\n * TITAN — Auto-Verify\n * Automatically verifies tool results after write operations.\n * Catches truncated files, broken syntax, and incomplete outputs.\n */\nimport { existsSync, readFileSync, statSync } from 'fs';\nimport { resolve } from 'path';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'AutoVerify';\n\nexport interface VerifyResult {\n passed: boolean;\n issue?: string;\n suggestion?: string;\n}\n\n/**\n * Verify a file after a write/append operation.\n * Returns pass/fail with optional fix suggestion.\n */\nexport function verifyFileWrite(toolName: string, args: Record<string, unknown>,
|
|
1
|
+
{"version":3,"sources":["../../src/agent/autoVerify.ts"],"sourcesContent":["/**\n * TITAN — Auto-Verify\n * Automatically verifies tool results after write operations.\n * Catches truncated files, broken syntax, and incomplete outputs.\n */\nimport { existsSync, readFileSync, statSync } from 'fs';\nimport { resolve } from 'path';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'AutoVerify';\n\nexport interface VerifyResult {\n passed: boolean;\n issue?: string;\n suggestion?: string;\n}\n\n/**\n * Verify a file after a write/append operation.\n * Returns pass/fail with optional fix suggestion.\n */\nexport function verifyFileWrite(toolName: string, args: Record<string, unknown>, _toolResult: string): VerifyResult {\n // Only verify write_file and append_file\n if (toolName !== 'write_file' && toolName !== 'append_file') {\n return { passed: true };\n }\n\n const filePath = args.path as string;\n if (!filePath) return { passed: true };\n\n const resolved = resolve(filePath);\n\n // Check file exists\n if (!existsSync(resolved)) {\n return {\n passed: false,\n issue: `File ${filePath} does not exist after write`,\n suggestion: `The write_file call may have failed silently. Try calling write_file again.`,\n };\n }\n\n // Check file is not empty\n const stat = statSync(resolved);\n if (stat.size === 0) {\n return {\n passed: false,\n issue: `File ${filePath} is empty (0 bytes) after write`,\n suggestion: `The file was created but no content was written. Call write_file again with the content.`,\n };\n }\n\n // For HTML files, check structural completeness\n if (filePath.endsWith('.html') || filePath.endsWith('.htm')) {\n try {\n const content = readFileSync(resolved, 'utf-8');\n if (content.includes('<html') && !content.includes('</html>')) {\n return {\n passed: false,\n issue: `HTML file ${filePath} is truncated — has <html> but no </html>`,\n suggestion: `The file is incomplete. Use append_file to add the missing closing sections (</main>, </body>, </html>) and any remaining content.`,\n };\n }\n if (content.includes('<body') && !content.includes('</body>')) {\n return {\n passed: false,\n issue: `HTML file ${filePath} is truncated — has <body> but no </body>`,\n suggestion: `The file is incomplete. Use append_file to add the remaining body content and closing tags.`,\n };\n }\n if (content.includes('<script') && !content.includes('</script>')) {\n return {\n passed: false,\n issue: `HTML file ${filePath} has unclosed <script> tag`,\n suggestion: `The JavaScript section is incomplete. Use append_file to complete the script and add </script></body></html>.`,\n };\n }\n } catch { /* can't read, skip */ }\n }\n\n // For JSON files, check valid JSON\n if (filePath.endsWith('.json')) {\n try {\n const content = readFileSync(resolved, 'utf-8');\n JSON.parse(content);\n } catch {\n return {\n passed: false,\n issue: `JSON file ${filePath} contains invalid JSON`,\n suggestion: `The JSON is malformed. Read the file and fix the syntax error.`,\n };\n }\n }\n\n // Check reasonable file size (warn if suspiciously small for written content)\n const writtenContent = args.content as string;\n if (writtenContent && stat.size < writtenContent.length * 0.5) {\n logger.warn(COMPONENT, `File ${filePath} is smaller than expected: ${stat.size} bytes vs ${writtenContent.length} written`);\n }\n\n logger.info(COMPONENT, `Verified ${filePath}: ${stat.size} bytes, OK`);\n return { passed: true };\n}\n"],"mappings":";AAKA,SAAS,YAAY,cAAc,gBAAgB;AACnD,SAAS,eAAe;AACxB,OAAO,YAAY;AAEnB,MAAM,YAAY;AAYX,SAAS,gBAAgB,UAAkB,MAA+B,aAAmC;AAEhH,MAAI,aAAa,gBAAgB,aAAa,eAAe;AACzD,WAAO,EAAE,QAAQ,KAAK;AAAA,EAC1B;AAEA,QAAM,WAAW,KAAK;AACtB,MAAI,CAAC,SAAU,QAAO,EAAE,QAAQ,KAAK;AAErC,QAAM,WAAW,QAAQ,QAAQ;AAGjC,MAAI,CAAC,WAAW,QAAQ,GAAG;AACvB,WAAO;AAAA,MACH,QAAQ;AAAA,MACR,OAAO,QAAQ,QAAQ;AAAA,MACvB,YAAY;AAAA,IAChB;AAAA,EACJ;AAGA,QAAM,OAAO,SAAS,QAAQ;AAC9B,MAAI,KAAK,SAAS,GAAG;AACjB,WAAO;AAAA,MACH,QAAQ;AAAA,MACR,OAAO,QAAQ,QAAQ;AAAA,MACvB,YAAY;AAAA,IAChB;AAAA,EACJ;AAGA,MAAI,SAAS,SAAS,OAAO,KAAK,SAAS,SAAS,MAAM,GAAG;AACzD,QAAI;AACA,YAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,UAAI,QAAQ,SAAS,OAAO,KAAK,CAAC,QAAQ,SAAS,SAAS,GAAG;AAC3D,eAAO;AAAA,UACH,QAAQ;AAAA,UACR,OAAO,aAAa,QAAQ;AAAA,UAC5B,YAAY;AAAA,QAChB;AAAA,MACJ;AACA,UAAI,QAAQ,SAAS,OAAO,KAAK,CAAC,QAAQ,SAAS,SAAS,GAAG;AAC3D,eAAO;AAAA,UACH,QAAQ;AAAA,UACR,OAAO,aAAa,QAAQ;AAAA,UAC5B,YAAY;AAAA,QAChB;AAAA,MACJ;AACA,UAAI,QAAQ,SAAS,SAAS,KAAK,CAAC,QAAQ,SAAS,WAAW,GAAG;AAC/D,eAAO;AAAA,UACH,QAAQ;AAAA,UACR,OAAO,aAAa,QAAQ;AAAA,UAC5B,YAAY;AAAA,QAChB;AAAA,MACJ;AAAA,IACJ,QAAQ;AAAA,IAAyB;AAAA,EACrC;AAGA,MAAI,SAAS,SAAS,OAAO,GAAG;AAC5B,QAAI;AACA,YAAM,UAAU,aAAa,UAAU,OAAO;AAC9C,WAAK,MAAM,OAAO;AAAA,IACtB,QAAQ;AACJ,aAAO;AAAA,QACH,QAAQ;AAAA,QACR,OAAO,aAAa,QAAQ;AAAA,QAC5B,YAAY;AAAA,MAChB;AAAA,IACJ;AAAA,EACJ;AAGA,QAAM,iBAAiB,KAAK;AAC5B,MAAI,kBAAkB,KAAK,OAAO,eAAe,SAAS,KAAK;AAC3D,WAAO,KAAK,WAAW,QAAQ,QAAQ,8BAA8B,KAAK,IAAI,aAAa,eAAe,MAAM,UAAU;AAAA,EAC9H;AAEA,SAAO,KAAK,WAAW,YAAY,QAAQ,KAAK,KAAK,IAAI,YAAY;AACrE,SAAO,EAAE,QAAQ,KAAK;AAC1B;","names":[]}
|
|
@@ -139,7 +139,7 @@ function validateContent(text, opts) {
|
|
|
139
139
|
}
|
|
140
140
|
return { valid: issues.length === 0, issues };
|
|
141
141
|
}
|
|
142
|
-
function scoreContent(original, cleaned, validation,
|
|
142
|
+
function scoreContent(original, cleaned, validation, _opts) {
|
|
143
143
|
let score = 100;
|
|
144
144
|
if (original.length > 0 && cleaned.length < original.length * 0.5) {
|
|
145
145
|
score -= 30;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/outputGuardrails.ts"],"sourcesContent":["/**\n * TITAN — Output Guardrails Pipeline\n *\n * Centralized post-processing for ALL LLM output before it reaches users.\n * Treats every LLM response as untrusted data — structurally validates,\n * strips chain-of-thought leaks, and scores quality before delivery.\n *\n * Inspired by OpenClaw's 6-stage output pipeline but adapted for Ollama\n * cloud models (glm-5.1, qwen3-coder, nemotron) that don't have structured\n * thinking blocks and leak chain-of-thought as plain text.\n *\n * Pipeline: EXTRACT → CLEAN → VALIDATE → SCORE\n *\n * Usage:\n * const result = applyOutputGuardrails(rawLLMOutput, { type: 'chat_response' });\n * if (result.passed) deliverToUser(result.content);\n * else retry();\n */\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'OutputGuardrails';\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type OutputContext =\n | 'chat_response'\n | 'facebook_post'\n | 'email'\n | 'sub_agent'\n | 'tool_result';\n\nexport interface GuardrailOptions {\n type: OutputContext;\n originalMessage?: string;\n model?: string;\n /** Custom structural requirements for validation stage */\n requirements?: {\n minLength?: number;\n maxLength?: number;\n mustContain?: string[];\n mustNotStartWith?: RegExp[];\n };\n}\n\nexport interface GuardrailResult {\n content: string;\n originalContent: string;\n score: number;\n passed: boolean;\n stages: {\n extracted: string;\n cleaned: string;\n validated: boolean;\n validationIssues: string[];\n };\n}\n\n// ── Stage 1: EXTRACT ─────────────────────────────────────────────\n// Strip thinking blocks, XML tags, internal markers.\n// Equivalent to OpenClaw's dropThinkingBlocks + stripBlockTags.\n\nfunction extractContent(raw: string): string {\n let text = raw;\n\n // Strip <think>...</think> blocks (Qwen, DeepSeek thinking models)\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/gi, '').trim();\n\n // Strip <final>...</final> wrapper tags but keep inner content\n text = text.replace(/<\\/?final>/gi, '').trim();\n\n // Strip markdown response headers the model sometimes adds\n text = text.replace(/^#+\\s*(?:Response|Answer|Reply|Output|Result)\\s*:?\\s*\\n/i, '').trim();\n\n // Strip XML-style tags that aren't part of actual content\n // (tool_call blocks, system blocks, internal markers)\n text = text.replace(/<\\/?(?:tool_call|system|internal|context|instruction)[^>]*>/gi, '').trim();\n\n // Strip triple-backtick wrapper if the entire content is wrapped\n // (model outputting `\\`\\`\\`\\nactual content\\n\\`\\`\\``)\n const codeBlockMatch = text.match(/^```\\w*\\n([\\s\\S]+?)\\n```$/);\n if (codeBlockMatch && !text.includes('\\n```\\n')) {\n text = codeBlockMatch[1].trim();\n }\n\n return text;\n}\n\n// ── Stage 2: CLEAN ───────────────────────────────────────────────\n// Remove meta-language, instruction echoes, narrator preamble.\n// Absorbs TITAN's existing stripNarratorPreamble + stripToolJson.\n\n/** Patterns that indicate the model is talking ABOUT the task instead of doing it */\nconst META_PREAMBLE_PATTERNS: RegExp[] = [\n // \"The user wants/asked/needs...\" — narrator describing the request\n /^(?:The user (?:wants|asked|needs|is asking|requested|said)[^.]*\\.\\s*)/i,\n // \"I should/need to/will...\" — model planning out loud\n /^(?:I (?:should|need to|will|can|could|would|might|must)\\s+\\w+[^.]*\\.\\s*)/i,\n // \"Let me think/brainstorm/analyze...\" — model narrating its process\n /^(?:Let me\\s+\\w+[^.]*\\.\\s*)/i,\n /^(?:Let's\\s+(?:brainstorm|think|see|try|start|figure|look|analyze)[^.]*\\.\\s*)/i,\n // \"Okay/OK/Alright/Well, let me...\" — filler + planning\n /^(?:(?:Okay|OK|Alright|Well|Hmm|Sure|Right),?\\s+(?:let me|let's|I'll|I should|I need|so)[^.]*\\.\\s*)/i,\n // \"Looking at the results...\" — narrating observation\n /^(?:Looking at\\s+[^.]*\\.\\s*)/i,\n // \"Based on my analysis/research, ...\" — unnecessary preamble (only strip up to the comma)\n /^(?:Based on\\s+(?:my|the|this)\\s+\\w+[^,]*,\\s*)/i,\n // \"Here's what I found:\" — meta-introduction.\n // `[^:{}]*` excludes braces so we don't accidentally eat into leaked\n // tool JSON like `Here is the answer. {\"name\": ...}` and strip the\n // real answer along with it (Hunt 2026-04-17).\n /^(?:Here(?:'s| is) (?:what|the|my)\\s+[^:{}\\n]*:\\s*)/i,\n // \"Actually, ...\" — model self-correcting out loud\n /^(?:Actually,?\\s+)/i,\n];\n\n/** Patterns that indicate the model is echoing instructions or examples back */\nconst INSTRUCTION_ECHO_PATTERNS: RegExp[] = [\n /\\b(?:under \\d+ (?:char|word|sentence))/i,\n /\\b(?:output only|write only|respond with only)\\b/i,\n /\\b(?:no explanation|no brainstorming|no numbered list|no planning)\\b/i,\n /\\b(?:in first person|confident.*tone|playful.*tone)\\b/i,\n /\\b(?:similar style|like the example|following the format)\\b/i,\n /\\b(?:include \\d+-?\\d* hashtag)/i,\n // Parenthetical instruction echo: \"(under 280 chars, 2-3 hashtags)\"\n /\\((?:under \\d+|must include|should be|include \\d+)/i,\n // Starts with parenthetical instruction description\n /^\\s*\\((?:under|must|should|include|use|keep|make)/i,\n // Example echoing — model copies the example instead of writing new content\n /^(?:Example\\s*(?:given|post|of|:))/i,\n /^(?:\\*?\\*?Reference\\s*(?:Example|Post)\\*?\\*?\\s*:)/i,\n /\\b(?:did you know I can)\\b.*\\byeah,?\\s+I'm kind of a big deal\\b/i,\n /\\b(?:Pro tip:.*autonomous mode.*independently)\\b/i, // Specific example echo\n];\n\nfunction cleanContent(text: string, type: OutputContext): string {\n if (type === 'tool_result') return text; // Don't clean tool results\n\n let cleaned = text;\n\n // Strip narrator preamble — try each pattern, take the first match\n for (const pattern of META_PREAMBLE_PATTERNS) {\n const match = cleaned.match(pattern);\n if (match) {\n const remainder = cleaned.slice(match[0].length).trim();\n // Only strip if there's meaningful content after the preamble\n if (remainder.length > 20) {\n logger.debug(COMPONENT, `[Clean] Stripped preamble: \"${match[0].slice(0, 60)}\"`);\n cleaned = remainder;\n break; // Only strip one preamble layer\n }\n }\n }\n\n // Strip instruction echoes embedded in the content\n for (const pattern of INSTRUCTION_ECHO_PATTERNS) {\n if (pattern.test(cleaned)) {\n // Don't strip — just flag. The score stage will deduct points.\n break;\n }\n }\n\n // Strip trailing planning (\"I should also...\", \"Next I'll...\")\n cleaned = cleaned.replace(/\\s*(?:I should also|Next I'll|I could also|Additionally, I)[^.]*\\.?\\s*$/i, '').trim();\n\n // Strip example/reference labels that the model prepended\n cleaned = cleaned.replace(/^\\*?\\*?(?:Reference|Example|Sample|Draft)\\s*(?:Example|Post|:)?\\*?\\*?\\s*:?\\s*/i, '').trim();\n // Strip wrapping quotes (model sometimes quotes the post)\n cleaned = cleaned.replace(/^[\"'\"\"]|[\"'\"\"]$/g, '').trim();\n\n // Strip tool JSON blocks that leaked into text\n cleaned = cleaned.replace(/```json\\s*\\{[\\s\\S]*?\\}\\s*```/g, '').trim();\n cleaned = cleaned.replace(/\\{\"(?:name|function|tool_call)\"[\\s\\S]*?\\}/g, '').trim();\n\n // Collapse multiple newlines\n cleaned = cleaned.replace(/\\n{3,}/g, '\\n\\n').trim();\n\n return cleaned;\n}\n\n// ── Stage 3: VALIDATE ────────────────────────────────────────────\n// Context-specific structural checks.\n\ninterface ValidationResult {\n valid: boolean;\n issues: string[];\n}\n\nfunction validateContent(text: string, opts: GuardrailOptions): ValidationResult {\n const issues: string[] = [];\n const req = opts.requirements || {};\n\n // Universal: must not be empty\n if (!text || text.trim().length === 0) {\n issues.push('empty');\n return { valid: false, issues };\n }\n\n // Custom length requirements\n if (req.minLength && text.length < req.minLength) {\n issues.push(`too_short:${text.length}<${req.minLength}`);\n }\n if (req.maxLength && text.length > req.maxLength) {\n issues.push(`too_long:${text.length}>${req.maxLength}`);\n }\n\n // Custom must-contain checks\n if (req.mustContain) {\n for (const pattern of req.mustContain) {\n if (!text.includes(pattern)) {\n issues.push(`missing:${pattern}`);\n }\n }\n }\n\n // Context-specific validation\n switch (opts.type) {\n case 'chat_response': {\n // Must not start with meta-language that slipped past cleaning\n const badStarts = /^\\s*(?:I'll\\s+(?:go with|brainstorm|think|start)|I should\\s|I could\\s|I would\\s|I need to\\s|Let me\\s|Let's\\s|The user\\s|The example\\s|Option \\d|Approach\\s*:|Topic\\s*:)/i;\n if (badStarts.test(text)) {\n issues.push('starts_with_meta');\n }\n break;\n }\n\n case 'facebook_post': {\n // Must have at least one hashtag\n if (!/#\\w+/.test(text)) {\n issues.push('no_hashtag');\n }\n // Must be 40-400 chars\n if (text.length < 40) issues.push('fb_too_short');\n if (text.length > 400) issues.push('fb_too_long');\n // Must not start with planning/brainstorming\n const fbBadStarts = /^\\s*(?:I'll\\s|I should\\s|I could\\s|Let me\\s|Let's\\s|The user\\s|Here(?:'s| is)\\s|(?:OK|Okay|Alright|Well|So|Hmm),?\\s|^\\d+\\.\\s|Should I|What if I|How about|Maybe I)/i;\n if (fbBadStarts.test(text)) {\n issues.push('fb_starts_with_meta');\n }\n // Must not start with a parenthetical — that's always an instruction echo\n if (/^\\s*\\(/.test(text)) {\n issues.push('fb_starts_with_paren');\n }\n // Must not be a numbered brainstorm list\n if (/^\\s*1\\.\\s+.{5,}\\n\\s*2\\.\\s+/.test(text)) {\n issues.push('fb_numbered_list');\n }\n // Must have real sentence structure — not just instructions wrapped in hashtags\n // Count actual words vs instruction words\n const instructionWords = /\\b(?:under|must|include|should|character|hashtag|tone|first person)\\b/gi;\n const matches = text.match(instructionWords) || [];\n if (matches.length >= 3) {\n issues.push('fb_instruction_heavy');\n }\n break;\n }\n\n case 'email':\n // Minimal validation — just ensure it's not empty\n break;\n\n case 'sub_agent':\n case 'tool_result':\n // Pass through — these don't need structural validation\n break;\n }\n\n return { valid: issues.length === 0, issues };\n}\n\n// ── Stage 4: SCORE ───────────────────────────────────────────────\n// Quality gate — score 0-100, pass/fail threshold.\n\nfunction scoreContent(\n original: string,\n cleaned: string,\n validation: ValidationResult,\n opts: GuardrailOptions,\n): number {\n let score = 100;\n\n // Deductions for cleaning that was needed\n if (original.length > 0 && cleaned.length < original.length * 0.5) {\n score -= 30; // More than half was stripped — likely garbage\n }\n\n // Deductions for instruction echoes\n for (const pattern of INSTRUCTION_ECHO_PATTERNS) {\n if (pattern.test(cleaned)) {\n score -= 10;\n break;\n }\n }\n\n // Deductions for validation issues\n for (const issue of validation.issues) {\n if (issue === 'empty') score -= 100;\n else if (issue.startsWith('starts_with_meta') || issue.startsWith('fb_starts_with_meta')) score -= 60;\n else if (issue === 'fb_starts_with_paren') score -= 80; // Instruction echo in parens\n else if (issue === 'fb_instruction_heavy') score -= 70; // Too many instruction words\n else if (issue === 'no_hashtag') score -= 60; // FB post MUST have hashtag\n else if (issue === 'fb_numbered_list') score -= 80; // Brainstorm lists are never valid posts\n else if (issue.startsWith('fb_too_short')) score -= 60; // Under 40 chars is never a valid FB post\n else if (issue.startsWith('too_short')) score -= 30;\n else if (issue.startsWith('too_long') || issue.startsWith('fb_too_long')) score -= 10;\n else if (issue.startsWith('missing:')) score -= 15;\n else score -= 10;\n }\n\n return Math.max(0, Math.min(100, score));\n}\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Apply the full output guardrails pipeline to raw LLM output.\n *\n * @param raw - The raw LLM response text\n * @param opts - Context and requirements for validation\n * @returns GuardrailResult with cleaned content, score, and pass/fail\n */\nexport function applyOutputGuardrails(raw: string, opts: GuardrailOptions): GuardrailResult {\n const original = raw || '';\n\n // Stage 1: Extract\n const extracted = extractContent(original);\n\n // Stage 2: Clean\n const cleaned = cleanContent(extracted, opts.type);\n\n // Stage 3: Validate\n const validation = validateContent(cleaned, opts);\n\n // Stage 4: Score\n const score = scoreContent(original, cleaned, validation, opts);\n const passed = score >= 50;\n\n // Truncate if over max length (after validation flagged it)\n let finalContent = cleaned;\n if (opts.requirements?.maxLength && finalContent.length > opts.requirements.maxLength) {\n finalContent = finalContent.slice(0, opts.requirements.maxLength - 3) + '...';\n }\n\n if (!passed) {\n logger.warn(COMPONENT, `[REJECTED] score=${score}/100 type=${opts.type} issues=[${validation.issues.join(',')}] content=\"${finalContent.slice(0, 120)}\"`);\n } else if (original !== finalContent) {\n logger.debug(COMPONENT, `[CLEANED] score=${score}/100 type=${opts.type} stripped=${original.length - finalContent.length} chars`);\n }\n\n return {\n content: finalContent,\n originalContent: original,\n score,\n passed,\n stages: {\n extracted,\n cleaned,\n validated: validation.valid,\n validationIssues: validation.issues,\n },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/agent/outputGuardrails.ts"],"sourcesContent":["/**\n * TITAN — Output Guardrails Pipeline\n *\n * Centralized post-processing for ALL LLM output before it reaches users.\n * Treats every LLM response as untrusted data — structurally validates,\n * strips chain-of-thought leaks, and scores quality before delivery.\n *\n * Inspired by OpenClaw's 6-stage output pipeline but adapted for Ollama\n * cloud models (glm-5.1, qwen3-coder, nemotron) that don't have structured\n * thinking blocks and leak chain-of-thought as plain text.\n *\n * Pipeline: EXTRACT → CLEAN → VALIDATE → SCORE\n *\n * Usage:\n * const result = applyOutputGuardrails(rawLLMOutput, { type: 'chat_response' });\n * if (result.passed) deliverToUser(result.content);\n * else retry();\n */\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'OutputGuardrails';\n\n// ── Types ────────────────────────────────────────────────────────\n\nexport type OutputContext =\n | 'chat_response'\n | 'facebook_post'\n | 'email'\n | 'sub_agent'\n | 'tool_result';\n\nexport interface GuardrailOptions {\n type: OutputContext;\n originalMessage?: string;\n model?: string;\n /** Custom structural requirements for validation stage */\n requirements?: {\n minLength?: number;\n maxLength?: number;\n mustContain?: string[];\n mustNotStartWith?: RegExp[];\n };\n}\n\nexport interface GuardrailResult {\n content: string;\n originalContent: string;\n score: number;\n passed: boolean;\n stages: {\n extracted: string;\n cleaned: string;\n validated: boolean;\n validationIssues: string[];\n };\n}\n\n// ── Stage 1: EXTRACT ─────────────────────────────────────────────\n// Strip thinking blocks, XML tags, internal markers.\n// Equivalent to OpenClaw's dropThinkingBlocks + stripBlockTags.\n\nfunction extractContent(raw: string): string {\n let text = raw;\n\n // Strip <think>...</think> blocks (Qwen, DeepSeek thinking models)\n text = text.replace(/<think>[\\s\\S]*?<\\/think>/gi, '').trim();\n\n // Strip <final>...</final> wrapper tags but keep inner content\n text = text.replace(/<\\/?final>/gi, '').trim();\n\n // Strip markdown response headers the model sometimes adds\n text = text.replace(/^#+\\s*(?:Response|Answer|Reply|Output|Result)\\s*:?\\s*\\n/i, '').trim();\n\n // Strip XML-style tags that aren't part of actual content\n // (tool_call blocks, system blocks, internal markers)\n text = text.replace(/<\\/?(?:tool_call|system|internal|context|instruction)[^>]*>/gi, '').trim();\n\n // Strip triple-backtick wrapper if the entire content is wrapped\n // (model outputting `\\`\\`\\`\\nactual content\\n\\`\\`\\``)\n const codeBlockMatch = text.match(/^```\\w*\\n([\\s\\S]+?)\\n```$/);\n if (codeBlockMatch && !text.includes('\\n```\\n')) {\n text = codeBlockMatch[1].trim();\n }\n\n return text;\n}\n\n// ── Stage 2: CLEAN ───────────────────────────────────────────────\n// Remove meta-language, instruction echoes, narrator preamble.\n// Absorbs TITAN's existing stripNarratorPreamble + stripToolJson.\n\n/** Patterns that indicate the model is talking ABOUT the task instead of doing it */\nconst META_PREAMBLE_PATTERNS: RegExp[] = [\n // \"The user wants/asked/needs...\" — narrator describing the request\n /^(?:The user (?:wants|asked|needs|is asking|requested|said)[^.]*\\.\\s*)/i,\n // \"I should/need to/will...\" — model planning out loud\n /^(?:I (?:should|need to|will|can|could|would|might|must)\\s+\\w+[^.]*\\.\\s*)/i,\n // \"Let me think/brainstorm/analyze...\" — model narrating its process\n /^(?:Let me\\s+\\w+[^.]*\\.\\s*)/i,\n /^(?:Let's\\s+(?:brainstorm|think|see|try|start|figure|look|analyze)[^.]*\\.\\s*)/i,\n // \"Okay/OK/Alright/Well, let me...\" — filler + planning\n /^(?:(?:Okay|OK|Alright|Well|Hmm|Sure|Right),?\\s+(?:let me|let's|I'll|I should|I need|so)[^.]*\\.\\s*)/i,\n // \"Looking at the results...\" — narrating observation\n /^(?:Looking at\\s+[^.]*\\.\\s*)/i,\n // \"Based on my analysis/research, ...\" — unnecessary preamble (only strip up to the comma)\n /^(?:Based on\\s+(?:my|the|this)\\s+\\w+[^,]*,\\s*)/i,\n // \"Here's what I found:\" — meta-introduction.\n // `[^:{}]*` excludes braces so we don't accidentally eat into leaked\n // tool JSON like `Here is the answer. {\"name\": ...}` and strip the\n // real answer along with it (Hunt 2026-04-17).\n /^(?:Here(?:'s| is) (?:what|the|my)\\s+[^:{}\\n]*:\\s*)/i,\n // \"Actually, ...\" — model self-correcting out loud\n /^(?:Actually,?\\s+)/i,\n];\n\n/** Patterns that indicate the model is echoing instructions or examples back */\nconst INSTRUCTION_ECHO_PATTERNS: RegExp[] = [\n /\\b(?:under \\d+ (?:char|word|sentence))/i,\n /\\b(?:output only|write only|respond with only)\\b/i,\n /\\b(?:no explanation|no brainstorming|no numbered list|no planning)\\b/i,\n /\\b(?:in first person|confident.*tone|playful.*tone)\\b/i,\n /\\b(?:similar style|like the example|following the format)\\b/i,\n /\\b(?:include \\d+-?\\d* hashtag)/i,\n // Parenthetical instruction echo: \"(under 280 chars, 2-3 hashtags)\"\n /\\((?:under \\d+|must include|should be|include \\d+)/i,\n // Starts with parenthetical instruction description\n /^\\s*\\((?:under|must|should|include|use|keep|make)/i,\n // Example echoing — model copies the example instead of writing new content\n /^(?:Example\\s*(?:given|post|of|:))/i,\n /^(?:\\*?\\*?Reference\\s*(?:Example|Post)\\*?\\*?\\s*:)/i,\n /\\b(?:did you know I can)\\b.*\\byeah,?\\s+I'm kind of a big deal\\b/i,\n /\\b(?:Pro tip:.*autonomous mode.*independently)\\b/i, // Specific example echo\n];\n\nfunction cleanContent(text: string, type: OutputContext): string {\n if (type === 'tool_result') return text; // Don't clean tool results\n\n let cleaned = text;\n\n // Strip narrator preamble — try each pattern, take the first match\n for (const pattern of META_PREAMBLE_PATTERNS) {\n const match = cleaned.match(pattern);\n if (match) {\n const remainder = cleaned.slice(match[0].length).trim();\n // Only strip if there's meaningful content after the preamble\n if (remainder.length > 20) {\n logger.debug(COMPONENT, `[Clean] Stripped preamble: \"${match[0].slice(0, 60)}\"`);\n cleaned = remainder;\n break; // Only strip one preamble layer\n }\n }\n }\n\n // Strip instruction echoes embedded in the content\n for (const pattern of INSTRUCTION_ECHO_PATTERNS) {\n if (pattern.test(cleaned)) {\n // Don't strip — just flag. The score stage will deduct points.\n break;\n }\n }\n\n // Strip trailing planning (\"I should also...\", \"Next I'll...\")\n cleaned = cleaned.replace(/\\s*(?:I should also|Next I'll|I could also|Additionally, I)[^.]*\\.?\\s*$/i, '').trim();\n\n // Strip example/reference labels that the model prepended\n cleaned = cleaned.replace(/^\\*?\\*?(?:Reference|Example|Sample|Draft)\\s*(?:Example|Post|:)?\\*?\\*?\\s*:?\\s*/i, '').trim();\n // Strip wrapping quotes (model sometimes quotes the post)\n cleaned = cleaned.replace(/^[\"'\"\"]|[\"'\"\"]$/g, '').trim();\n\n // Strip tool JSON blocks that leaked into text\n cleaned = cleaned.replace(/```json\\s*\\{[\\s\\S]*?\\}\\s*```/g, '').trim();\n cleaned = cleaned.replace(/\\{\"(?:name|function|tool_call)\"[\\s\\S]*?\\}/g, '').trim();\n\n // Collapse multiple newlines\n cleaned = cleaned.replace(/\\n{3,}/g, '\\n\\n').trim();\n\n return cleaned;\n}\n\n// ── Stage 3: VALIDATE ────────────────────────────────────────────\n// Context-specific structural checks.\n\ninterface ValidationResult {\n valid: boolean;\n issues: string[];\n}\n\nfunction validateContent(text: string, opts: GuardrailOptions): ValidationResult {\n const issues: string[] = [];\n const req = opts.requirements || {};\n\n // Universal: must not be empty\n if (!text || text.trim().length === 0) {\n issues.push('empty');\n return { valid: false, issues };\n }\n\n // Custom length requirements\n if (req.minLength && text.length < req.minLength) {\n issues.push(`too_short:${text.length}<${req.minLength}`);\n }\n if (req.maxLength && text.length > req.maxLength) {\n issues.push(`too_long:${text.length}>${req.maxLength}`);\n }\n\n // Custom must-contain checks\n if (req.mustContain) {\n for (const pattern of req.mustContain) {\n if (!text.includes(pattern)) {\n issues.push(`missing:${pattern}`);\n }\n }\n }\n\n // Context-specific validation\n switch (opts.type) {\n case 'chat_response': {\n // Must not start with meta-language that slipped past cleaning\n const badStarts = /^\\s*(?:I'll\\s+(?:go with|brainstorm|think|start)|I should\\s|I could\\s|I would\\s|I need to\\s|Let me\\s|Let's\\s|The user\\s|The example\\s|Option \\d|Approach\\s*:|Topic\\s*:)/i;\n if (badStarts.test(text)) {\n issues.push('starts_with_meta');\n }\n break;\n }\n\n case 'facebook_post': {\n // Must have at least one hashtag\n if (!/#\\w+/.test(text)) {\n issues.push('no_hashtag');\n }\n // Must be 40-400 chars\n if (text.length < 40) issues.push('fb_too_short');\n if (text.length > 400) issues.push('fb_too_long');\n // Must not start with planning/brainstorming\n const fbBadStarts = /^\\s*(?:I'll\\s|I should\\s|I could\\s|Let me\\s|Let's\\s|The user\\s|Here(?:'s| is)\\s|(?:OK|Okay|Alright|Well|So|Hmm),?\\s|^\\d+\\.\\s|Should I|What if I|How about|Maybe I)/i;\n if (fbBadStarts.test(text)) {\n issues.push('fb_starts_with_meta');\n }\n // Must not start with a parenthetical — that's always an instruction echo\n if (/^\\s*\\(/.test(text)) {\n issues.push('fb_starts_with_paren');\n }\n // Must not be a numbered brainstorm list\n if (/^\\s*1\\.\\s+.{5,}\\n\\s*2\\.\\s+/.test(text)) {\n issues.push('fb_numbered_list');\n }\n // Must have real sentence structure — not just instructions wrapped in hashtags\n // Count actual words vs instruction words\n const instructionWords = /\\b(?:under|must|include|should|character|hashtag|tone|first person)\\b/gi;\n const matches = text.match(instructionWords) || [];\n if (matches.length >= 3) {\n issues.push('fb_instruction_heavy');\n }\n break;\n }\n\n case 'email':\n // Minimal validation — just ensure it's not empty\n break;\n\n case 'sub_agent':\n case 'tool_result':\n // Pass through — these don't need structural validation\n break;\n }\n\n return { valid: issues.length === 0, issues };\n}\n\n// ── Stage 4: SCORE ───────────────────────────────────────────────\n// Quality gate — score 0-100, pass/fail threshold.\n\nfunction scoreContent(\n original: string,\n cleaned: string,\n validation: ValidationResult,\n _opts: GuardrailOptions,\n): number {\n let score = 100;\n\n // Deductions for cleaning that was needed\n if (original.length > 0 && cleaned.length < original.length * 0.5) {\n score -= 30; // More than half was stripped — likely garbage\n }\n\n // Deductions for instruction echoes\n for (const pattern of INSTRUCTION_ECHO_PATTERNS) {\n if (pattern.test(cleaned)) {\n score -= 10;\n break;\n }\n }\n\n // Deductions for validation issues\n for (const issue of validation.issues) {\n if (issue === 'empty') score -= 100;\n else if (issue.startsWith('starts_with_meta') || issue.startsWith('fb_starts_with_meta')) score -= 60;\n else if (issue === 'fb_starts_with_paren') score -= 80; // Instruction echo in parens\n else if (issue === 'fb_instruction_heavy') score -= 70; // Too many instruction words\n else if (issue === 'no_hashtag') score -= 60; // FB post MUST have hashtag\n else if (issue === 'fb_numbered_list') score -= 80; // Brainstorm lists are never valid posts\n else if (issue.startsWith('fb_too_short')) score -= 60; // Under 40 chars is never a valid FB post\n else if (issue.startsWith('too_short')) score -= 30;\n else if (issue.startsWith('too_long') || issue.startsWith('fb_too_long')) score -= 10;\n else if (issue.startsWith('missing:')) score -= 15;\n else score -= 10;\n }\n\n return Math.max(0, Math.min(100, score));\n}\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Apply the full output guardrails pipeline to raw LLM output.\n *\n * @param raw - The raw LLM response text\n * @param opts - Context and requirements for validation\n * @returns GuardrailResult with cleaned content, score, and pass/fail\n */\nexport function applyOutputGuardrails(raw: string, opts: GuardrailOptions): GuardrailResult {\n const original = raw || '';\n\n // Stage 1: Extract\n const extracted = extractContent(original);\n\n // Stage 2: Clean\n const cleaned = cleanContent(extracted, opts.type);\n\n // Stage 3: Validate\n const validation = validateContent(cleaned, opts);\n\n // Stage 4: Score\n const score = scoreContent(original, cleaned, validation, opts);\n const passed = score >= 50;\n\n // Truncate if over max length (after validation flagged it)\n let finalContent = cleaned;\n if (opts.requirements?.maxLength && finalContent.length > opts.requirements.maxLength) {\n finalContent = finalContent.slice(0, opts.requirements.maxLength - 3) + '...';\n }\n\n if (!passed) {\n logger.warn(COMPONENT, `[REJECTED] score=${score}/100 type=${opts.type} issues=[${validation.issues.join(',')}] content=\"${finalContent.slice(0, 120)}\"`);\n } else if (original !== finalContent) {\n logger.debug(COMPONENT, `[CLEANED] score=${score}/100 type=${opts.type} stripped=${original.length - finalContent.length} chars`);\n }\n\n return {\n content: finalContent,\n originalContent: original,\n score,\n passed,\n stages: {\n extracted,\n cleaned,\n validated: validation.valid,\n validationIssues: validation.issues,\n },\n };\n}\n"],"mappings":";AAkBA,OAAO,YAAY;AAEnB,MAAM,YAAY;AAyClB,SAAS,eAAe,KAAqB;AACzC,MAAI,OAAO;AAGX,SAAO,KAAK,QAAQ,8BAA8B,EAAE,EAAE,KAAK;AAG3D,SAAO,KAAK,QAAQ,gBAAgB,EAAE,EAAE,KAAK;AAG7C,SAAO,KAAK,QAAQ,4DAA4D,EAAE,EAAE,KAAK;AAIzF,SAAO,KAAK,QAAQ,iEAAiE,EAAE,EAAE,KAAK;AAI9F,QAAM,iBAAiB,KAAK,MAAM,2BAA2B;AAC7D,MAAI,kBAAkB,CAAC,KAAK,SAAS,SAAS,GAAG;AAC7C,WAAO,eAAe,CAAC,EAAE,KAAK;AAAA,EAClC;AAEA,SAAO;AACX;AAOA,MAAM,yBAAmC;AAAA;AAAA,EAErC;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA;AAAA,EAEA;AACJ;AAGA,MAAM,4BAAsC;AAAA,EACxC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA;AACJ;AAEA,SAAS,aAAa,MAAc,MAA6B;AAC7D,MAAI,SAAS,cAAe,QAAO;AAEnC,MAAI,UAAU;AAGd,aAAW,WAAW,wBAAwB;AAC1C,UAAM,QAAQ,QAAQ,MAAM,OAAO;AACnC,QAAI,OAAO;AACP,YAAM,YAAY,QAAQ,MAAM,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK;AAEtD,UAAI,UAAU,SAAS,IAAI;AACvB,eAAO,MAAM,WAAW,+BAA+B,MAAM,CAAC,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG;AAC/E,kBAAU;AACV;AAAA,MACJ;AAAA,IACJ;AAAA,EACJ;AAGA,aAAW,WAAW,2BAA2B;AAC7C,QAAI,QAAQ,KAAK,OAAO,GAAG;AAEvB;AAAA,IACJ;AAAA,EACJ;AAGA,YAAU,QAAQ,QAAQ,4EAA4E,EAAE,EAAE,KAAK;AAG/G,YAAU,QAAQ,QAAQ,kFAAkF,EAAE,EAAE,KAAK;AAErH,YAAU,QAAQ,QAAQ,oBAAoB,EAAE,EAAE,KAAK;AAGvD,YAAU,QAAQ,QAAQ,iCAAiC,EAAE,EAAE,KAAK;AACpE,YAAU,QAAQ,QAAQ,8CAA8C,EAAE,EAAE,KAAK;AAGjF,YAAU,QAAQ,QAAQ,WAAW,MAAM,EAAE,KAAK;AAElD,SAAO;AACX;AAUA,SAAS,gBAAgB,MAAc,MAA0C;AAC7E,QAAM,SAAmB,CAAC;AAC1B,QAAM,MAAM,KAAK,gBAAgB,CAAC;AAGlC,MAAI,CAAC,QAAQ,KAAK,KAAK,EAAE,WAAW,GAAG;AACnC,WAAO,KAAK,OAAO;AACnB,WAAO,EAAE,OAAO,OAAO,OAAO;AAAA,EAClC;AAGA,MAAI,IAAI,aAAa,KAAK,SAAS,IAAI,WAAW;AAC9C,WAAO,KAAK,aAAa,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE;AAAA,EAC3D;AACA,MAAI,IAAI,aAAa,KAAK,SAAS,IAAI,WAAW;AAC9C,WAAO,KAAK,YAAY,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE;AAAA,EAC1D;AAGA,MAAI,IAAI,aAAa;AACjB,eAAW,WAAW,IAAI,aAAa;AACnC,UAAI,CAAC,KAAK,SAAS,OAAO,GAAG;AACzB,eAAO,KAAK,WAAW,OAAO,EAAE;AAAA,MACpC;AAAA,IACJ;AAAA,EACJ;AAGA,UAAQ,KAAK,MAAM;AAAA,IACf,KAAK,iBAAiB;AAElB,YAAM,YAAY;AAClB,UAAI,UAAU,KAAK,IAAI,GAAG;AACtB,eAAO,KAAK,kBAAkB;AAAA,MAClC;AACA;AAAA,IACJ;AAAA,IAEA,KAAK,iBAAiB;AAElB,UAAI,CAAC,OAAO,KAAK,IAAI,GAAG;AACpB,eAAO,KAAK,YAAY;AAAA,MAC5B;AAEA,UAAI,KAAK,SAAS,GAAI,QAAO,KAAK,cAAc;AAChD,UAAI,KAAK,SAAS,IAAK,QAAO,KAAK,aAAa;AAEhD,YAAM,cAAc;AACpB,UAAI,YAAY,KAAK,IAAI,GAAG;AACxB,eAAO,KAAK,qBAAqB;AAAA,MACrC;AAEA,UAAI,SAAS,KAAK,IAAI,GAAG;AACrB,eAAO,KAAK,sBAAsB;AAAA,MACtC;AAEA,UAAI,6BAA6B,KAAK,IAAI,GAAG;AACzC,eAAO,KAAK,kBAAkB;AAAA,MAClC;AAGA,YAAM,mBAAmB;AACzB,YAAM,UAAU,KAAK,MAAM,gBAAgB,KAAK,CAAC;AACjD,UAAI,QAAQ,UAAU,GAAG;AACrB,eAAO,KAAK,sBAAsB;AAAA,MACtC;AACA;AAAA,IACJ;AAAA,IAEA,KAAK;AAED;AAAA,IAEJ,KAAK;AAAA,IACL,KAAK;AAED;AAAA,EACR;AAEA,SAAO,EAAE,OAAO,OAAO,WAAW,GAAG,OAAO;AAChD;AAKA,SAAS,aACL,UACA,SACA,YACA,OACM;AACN,MAAI,QAAQ;AAGZ,MAAI,SAAS,SAAS,KAAK,QAAQ,SAAS,SAAS,SAAS,KAAK;AAC/D,aAAS;AAAA,EACb;AAGA,aAAW,WAAW,2BAA2B;AAC7C,QAAI,QAAQ,KAAK,OAAO,GAAG;AACvB,eAAS;AACT;AAAA,IACJ;AAAA,EACJ;AAGA,aAAW,SAAS,WAAW,QAAQ;AACnC,QAAI,UAAU,QAAS,UAAS;AAAA,aACvB,MAAM,WAAW,kBAAkB,KAAK,MAAM,WAAW,qBAAqB,EAAG,UAAS;AAAA,aAC1F,UAAU,uBAAwB,UAAS;AAAA,aAC3C,UAAU,uBAAwB,UAAS;AAAA,aAC3C,UAAU,aAAc,UAAS;AAAA,aACjC,UAAU,mBAAoB,UAAS;AAAA,aACvC,MAAM,WAAW,cAAc,EAAG,UAAS;AAAA,aAC3C,MAAM,WAAW,WAAW,EAAG,UAAS;AAAA,aACxC,MAAM,WAAW,UAAU,KAAK,MAAM,WAAW,aAAa,EAAG,UAAS;AAAA,aAC1E,MAAM,WAAW,UAAU,EAAG,UAAS;AAAA,QAC3C,UAAS;AAAA,EAClB;AAEA,SAAO,KAAK,IAAI,GAAG,KAAK,IAAI,KAAK,KAAK,CAAC;AAC3C;AAWO,SAAS,sBAAsB,KAAa,MAAyC;AACxF,QAAM,WAAW,OAAO;AAGxB,QAAM,YAAY,eAAe,QAAQ;AAGzC,QAAM,UAAU,aAAa,WAAW,KAAK,IAAI;AAGjD,QAAM,aAAa,gBAAgB,SAAS,IAAI;AAGhD,QAAM,QAAQ,aAAa,UAAU,SAAS,YAAY,IAAI;AAC9D,QAAM,SAAS,SAAS;AAGxB,MAAI,eAAe;AACnB,MAAI,KAAK,cAAc,aAAa,aAAa,SAAS,KAAK,aAAa,WAAW;AACnF,mBAAe,aAAa,MAAM,GAAG,KAAK,aAAa,YAAY,CAAC,IAAI;AAAA,EAC5E;AAEA,MAAI,CAAC,QAAQ;AACT,WAAO,KAAK,WAAW,oBAAoB,KAAK,aAAa,KAAK,IAAI,YAAY,WAAW,OAAO,KAAK,GAAG,CAAC,cAAc,aAAa,MAAM,GAAG,GAAG,CAAC,GAAG;AAAA,EAC5J,WAAW,aAAa,cAAc;AAClC,WAAO,MAAM,WAAW,mBAAmB,KAAK,aAAa,KAAK,IAAI,aAAa,SAAS,SAAS,aAAa,MAAM,QAAQ;AAAA,EACpI;AAEA,SAAO;AAAA,IACH,SAAS;AAAA,IACT,iBAAiB;AAAA,IACjB;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,MACJ;AAAA,MACA;AAAA,MACA,WAAW,WAAW;AAAA,MACtB,kBAAkB,WAAW;AAAA,IACjC;AAAA,EACJ;AACJ;","names":[]}
|
package/dist/agent/shadowGit.js
CHANGED
|
@@ -135,7 +135,7 @@ function listCheckpoints(dirOrFile) {
|
|
|
135
135
|
}
|
|
136
136
|
return meta.checkpoints;
|
|
137
137
|
}
|
|
138
|
-
function diffCheckpoint(checkpointId,
|
|
138
|
+
function diffCheckpoint(checkpointId, _dirPath) {
|
|
139
139
|
if (!existsSync(CHECKPOINTS_BASE)) return "No checkpoints found.";
|
|
140
140
|
const repos = readdirSync(CHECKPOINTS_BASE);
|
|
141
141
|
for (const repo of repos) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/shadowGit.ts"],"sourcesContent":["/**\n * TITAN — Shadow Git Checkpoints\n *\n * Auto-snapshots files before write/edit/append operations using shadow git repos.\n * Enables point-in-time recovery of file state without touching the user's actual git.\n *\n * Shadow repos live at ~/.titan/file-checkpoints/{md5(dirPath)}/.\n * Inspired by Hermes checkpoint_manager.py.\n */\nimport { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';\nimport { join, resolve, dirname, relative, basename } from 'path';\nimport { homedir } from 'os';\nimport { createHash } from 'crypto';\nimport { execSync } from 'child_process';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'ShadowGit';\nconst CHECKPOINTS_BASE = join(homedir(), '.titan', 'file-checkpoints');\nconst MAX_DIR_ENTRIES = 50000;\nconst METADATA_FILE = 'titan-shadow-meta.json';\n\n// ── Types ─────────────────────────────────────────────────────────\nexport interface FileCheckpoint {\n id: string;\n timestamp: string;\n toolName: string;\n filePath: string;\n commitHash: string;\n repoPath: string;\n}\n\ninterface ShadowMeta {\n originalDir: string;\n createdAt: string;\n checkpoints: FileCheckpoint[];\n}\n\n// ── Helpers ───────────────────────────────────────────────────────\nfunction dirHash(dirPath: string): string {\n return createHash('md5').update(resolve(dirPath)).digest('hex').slice(0, 12);\n}\n\nfunction shadowRepoPath(dirPath: string): string {\n return join(CHECKPOINTS_BASE, dirHash(dirPath));\n}\n\nfunction execGit(cwd: string, cmd: string): string {\n try {\n return execSync(`git ${cmd}`, {\n cwd,\n stdio: 'pipe',\n timeout: 10000,\n env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },\n }).toString().trim();\n } catch (err) {\n throw new Error(`git ${cmd} failed: ${(err as Error).message}`);\n }\n}\n\nfunction loadMeta(repoPath: string): ShadowMeta {\n const metaPath = join(repoPath, METADATA_FILE);\n if (existsSync(metaPath)) {\n try {\n return JSON.parse(readFileSync(metaPath, 'utf-8'));\n } catch { /* fall through */ }\n }\n return { originalDir: '', createdAt: new Date().toISOString(), checkpoints: [] };\n}\n\nfunction saveMeta(repoPath: string, meta: ShadowMeta): void {\n writeFileSync(join(repoPath, METADATA_FILE), JSON.stringify(meta, null, 2), 'utf-8');\n}\n\n// ── Directory size check ──────────────────────────────────────────\n/**\n * Check if a directory has too many entries to safely snapshot.\n * Bails early once the count exceeds the threshold.\n */\nexport function shouldSkipDir(dirPath: string): boolean {\n try {\n const entries = readdirSync(dirPath);\n return entries.length > MAX_DIR_ENTRIES;\n } catch {\n return true; // Can't read → skip\n }\n}\n\n// ── Shadow Repo Management ────────────────────────────────────────\n/**\n * Initialize a shadow git repo for a directory.\n * Idempotent — returns the repo path if it already exists.\n */\nexport function initShadowRepo(dirPath: string): string {\n const repoPath = shadowRepoPath(dirPath);\n\n if (existsSync(join(repoPath, '.git'))) {\n return repoPath; // Already initialized\n }\n\n mkdirSync(repoPath, { recursive: true });\n execGit(repoPath, 'init');\n // Configure git for shadow repo\n execGit(repoPath, 'config user.name \"TITAN ShadowGit\"');\n execGit(repoPath, 'config user.email \"shadow@titan.local\"');\n\n // Save metadata\n const meta: ShadowMeta = {\n originalDir: resolve(dirPath),\n createdAt: new Date().toISOString(),\n checkpoints: [],\n };\n saveMeta(repoPath, meta);\n\n // Initial commit\n execGit(repoPath, 'add .');\n try {\n execGit(repoPath, 'commit -m \"Shadow repo initialized\" --allow-empty');\n } catch { /* empty commit if nothing to add */ }\n\n logger.info(COMPONENT, `Initialized shadow repo: ${dirPath} → ${repoPath}`);\n return repoPath;\n}\n\n// ── Snapshot Before Write ─────────────────────────────────────────\n/**\n * Snapshot a file before a write operation.\n * Fire-and-forget — errors are caught and logged.\n *\n * @returns The checkpoint record, or null if skipped.\n */\nexport async function snapshotBeforeWrite(\n toolName: string,\n filePath: string,\n): Promise<FileCheckpoint | null> {\n try {\n const absPath = resolve(filePath);\n const dirPath = dirname(absPath);\n\n // Skip if file doesn't exist yet (new file creation)\n if (!existsSync(absPath)) {\n logger.debug(COMPONENT, `Skipping snapshot for new file: ${absPath}`);\n return null;\n }\n\n // Skip large directories\n if (shouldSkipDir(dirPath)) {\n logger.debug(COMPONENT, `Skipping snapshot — directory too large: ${dirPath}`);\n return null;\n }\n\n // Initialize or get shadow repo\n const repoPath = initShadowRepo(dirPath);\n\n // Copy the file into the shadow repo\n const relPath = basename(absPath);\n const shadowFilePath = join(repoPath, relPath);\n copyFileSync(absPath, shadowFilePath);\n\n // Git add + commit\n execGit(repoPath, `add \"${relPath}\"`);\n\n const timestamp = new Date().toISOString();\n const commitMsg = `[${toolName}] pre-write snapshot: ${relPath} at ${timestamp}`;\n\n try {\n execGit(repoPath, `commit -m \"${commitMsg}\"`);\n } catch {\n // Nothing changed (file content identical to last snapshot)\n logger.debug(COMPONENT, `No changes to snapshot: ${relPath}`);\n return null;\n }\n\n // Get the commit hash\n const commitHash = execGit(repoPath, 'rev-parse HEAD');\n\n const checkpoint: FileCheckpoint = {\n id: commitHash.slice(0, 8),\n timestamp,\n toolName,\n filePath: absPath,\n commitHash,\n repoPath,\n };\n\n // Save to metadata\n const meta = loadMeta(repoPath);\n meta.checkpoints.push(checkpoint);\n // Keep last 100 checkpoints per repo\n if (meta.checkpoints.length > 100) {\n meta.checkpoints = meta.checkpoints.slice(-100);\n }\n saveMeta(repoPath, meta);\n\n logger.info(COMPONENT, `Snapshot: ${relPath} → ${commitHash.slice(0, 8)} (before ${toolName})`);\n return checkpoint;\n } catch (err) {\n logger.debug(COMPONENT, `Snapshot failed (non-fatal): ${(err as Error).message}`);\n return null;\n }\n}\n\n// ── Recovery Tools ────────────────────────────────────────────────\n/**\n * List checkpoints for a file or directory.\n */\nexport function listCheckpoints(dirOrFile: string): FileCheckpoint[] {\n const absPath = resolve(dirOrFile);\n let dirPath: string;\n\n try {\n const stat = statSync(absPath);\n dirPath = stat.isDirectory() ? absPath : dirname(absPath);\n } catch {\n dirPath = dirname(absPath);\n }\n\n const repoPath = shadowRepoPath(dirPath);\n if (!existsSync(join(repoPath, METADATA_FILE))) return [];\n\n const meta = loadMeta(repoPath);\n\n // If a specific file was given, filter to that file\n if (absPath !== dirPath) {\n return meta.checkpoints.filter(c => c.filePath === absPath);\n }\n\n return meta.checkpoints;\n}\n\n/**\n * Get the diff between a checkpoint and the current file.\n */\nexport function diffCheckpoint(checkpointId: string, dirPath?: string): string {\n // Search all shadow repos for this checkpoint\n if (!existsSync(CHECKPOINTS_BASE)) return 'No checkpoints found.';\n\n const repos = readdirSync(CHECKPOINTS_BASE);\n for (const repo of repos) {\n const repoPath = join(CHECKPOINTS_BASE, repo);\n if (!existsSync(join(repoPath, '.git'))) continue;\n\n const meta = loadMeta(repoPath);\n const checkpoint = meta.checkpoints.find(c => c.id === checkpointId || c.commitHash.startsWith(checkpointId));\n if (!checkpoint) continue;\n\n try {\n // Show what the file looked like at that checkpoint vs now\n const relPath = basename(checkpoint.filePath);\n const diff = execGit(repoPath, `diff ${checkpoint.commitHash} HEAD -- \"${relPath}\"`);\n return diff || 'No differences (file unchanged since checkpoint).';\n } catch (err) {\n return `Diff failed: ${(err as Error).message}`;\n }\n }\n\n return `Checkpoint \"${checkpointId}\" not found.`;\n}\n\n/**\n * Restore a file to its state at a specific checkpoint.\n */\nexport function restoreCheckpoint(checkpointId: string): string {\n if (!existsSync(CHECKPOINTS_BASE)) return 'No checkpoints found.';\n\n const repos = readdirSync(CHECKPOINTS_BASE);\n for (const repo of repos) {\n const repoPath = join(CHECKPOINTS_BASE, repo);\n if (!existsSync(join(repoPath, '.git'))) continue;\n\n const meta = loadMeta(repoPath);\n const checkpoint = meta.checkpoints.find(c => c.id === checkpointId || c.commitHash.startsWith(checkpointId));\n if (!checkpoint) continue;\n\n try {\n const relPath = basename(checkpoint.filePath);\n\n // Get file content at the checkpoint\n const content = execGit(repoPath, `show ${checkpoint.commitHash}:\"${relPath}\"`);\n\n // Write back to the original file\n writeFileSync(checkpoint.filePath, content, 'utf-8');\n\n logger.info(COMPONENT, `Restored: ${checkpoint.filePath} to checkpoint ${checkpointId}`);\n return `Restored ${checkpoint.filePath} to checkpoint ${checkpointId} (from ${checkpoint.timestamp}, before ${checkpoint.toolName}).`;\n } catch (err) {\n return `Restore failed: ${(err as Error).message}`;\n }\n }\n\n return `Checkpoint \"${checkpointId}\" not found.`;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/agent/shadowGit.ts"],"sourcesContent":["/**\n * TITAN — Shadow Git Checkpoints\n *\n * Auto-snapshots files before write/edit/append operations using shadow git repos.\n * Enables point-in-time recovery of file state without touching the user's actual git.\n *\n * Shadow repos live at ~/.titan/file-checkpoints/{md5(dirPath)}/.\n * Inspired by Hermes checkpoint_manager.py.\n */\nimport { existsSync, mkdirSync, copyFileSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';\nimport { join, resolve, dirname, relative, basename } from 'path';\nimport { homedir } from 'os';\nimport { createHash } from 'crypto';\nimport { execSync } from 'child_process';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'ShadowGit';\nconst CHECKPOINTS_BASE = join(homedir(), '.titan', 'file-checkpoints');\nconst MAX_DIR_ENTRIES = 50000;\nconst METADATA_FILE = 'titan-shadow-meta.json';\n\n// ── Types ─────────────────────────────────────────────────────────\nexport interface FileCheckpoint {\n id: string;\n timestamp: string;\n toolName: string;\n filePath: string;\n commitHash: string;\n repoPath: string;\n}\n\ninterface ShadowMeta {\n originalDir: string;\n createdAt: string;\n checkpoints: FileCheckpoint[];\n}\n\n// ── Helpers ───────────────────────────────────────────────────────\nfunction dirHash(dirPath: string): string {\n return createHash('md5').update(resolve(dirPath)).digest('hex').slice(0, 12);\n}\n\nfunction shadowRepoPath(dirPath: string): string {\n return join(CHECKPOINTS_BASE, dirHash(dirPath));\n}\n\nfunction execGit(cwd: string, cmd: string): string {\n try {\n return execSync(`git ${cmd}`, {\n cwd,\n stdio: 'pipe',\n timeout: 10000,\n env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },\n }).toString().trim();\n } catch (err) {\n throw new Error(`git ${cmd} failed: ${(err as Error).message}`);\n }\n}\n\nfunction loadMeta(repoPath: string): ShadowMeta {\n const metaPath = join(repoPath, METADATA_FILE);\n if (existsSync(metaPath)) {\n try {\n return JSON.parse(readFileSync(metaPath, 'utf-8'));\n } catch { /* fall through */ }\n }\n return { originalDir: '', createdAt: new Date().toISOString(), checkpoints: [] };\n}\n\nfunction saveMeta(repoPath: string, meta: ShadowMeta): void {\n writeFileSync(join(repoPath, METADATA_FILE), JSON.stringify(meta, null, 2), 'utf-8');\n}\n\n// ── Directory size check ──────────────────────────────────────────\n/**\n * Check if a directory has too many entries to safely snapshot.\n * Bails early once the count exceeds the threshold.\n */\nexport function shouldSkipDir(dirPath: string): boolean {\n try {\n const entries = readdirSync(dirPath);\n return entries.length > MAX_DIR_ENTRIES;\n } catch {\n return true; // Can't read → skip\n }\n}\n\n// ── Shadow Repo Management ────────────────────────────────────────\n/**\n * Initialize a shadow git repo for a directory.\n * Idempotent — returns the repo path if it already exists.\n */\nexport function initShadowRepo(dirPath: string): string {\n const repoPath = shadowRepoPath(dirPath);\n\n if (existsSync(join(repoPath, '.git'))) {\n return repoPath; // Already initialized\n }\n\n mkdirSync(repoPath, { recursive: true });\n execGit(repoPath, 'init');\n // Configure git for shadow repo\n execGit(repoPath, 'config user.name \"TITAN ShadowGit\"');\n execGit(repoPath, 'config user.email \"shadow@titan.local\"');\n\n // Save metadata\n const meta: ShadowMeta = {\n originalDir: resolve(dirPath),\n createdAt: new Date().toISOString(),\n checkpoints: [],\n };\n saveMeta(repoPath, meta);\n\n // Initial commit\n execGit(repoPath, 'add .');\n try {\n execGit(repoPath, 'commit -m \"Shadow repo initialized\" --allow-empty');\n } catch { /* empty commit if nothing to add */ }\n\n logger.info(COMPONENT, `Initialized shadow repo: ${dirPath} → ${repoPath}`);\n return repoPath;\n}\n\n// ── Snapshot Before Write ─────────────────────────────────────────\n/**\n * Snapshot a file before a write operation.\n * Fire-and-forget — errors are caught and logged.\n *\n * @returns The checkpoint record, or null if skipped.\n */\nexport async function snapshotBeforeWrite(\n toolName: string,\n filePath: string,\n): Promise<FileCheckpoint | null> {\n try {\n const absPath = resolve(filePath);\n const dirPath = dirname(absPath);\n\n // Skip if file doesn't exist yet (new file creation)\n if (!existsSync(absPath)) {\n logger.debug(COMPONENT, `Skipping snapshot for new file: ${absPath}`);\n return null;\n }\n\n // Skip large directories\n if (shouldSkipDir(dirPath)) {\n logger.debug(COMPONENT, `Skipping snapshot — directory too large: ${dirPath}`);\n return null;\n }\n\n // Initialize or get shadow repo\n const repoPath = initShadowRepo(dirPath);\n\n // Copy the file into the shadow repo\n const relPath = basename(absPath);\n const shadowFilePath = join(repoPath, relPath);\n copyFileSync(absPath, shadowFilePath);\n\n // Git add + commit\n execGit(repoPath, `add \"${relPath}\"`);\n\n const timestamp = new Date().toISOString();\n const commitMsg = `[${toolName}] pre-write snapshot: ${relPath} at ${timestamp}`;\n\n try {\n execGit(repoPath, `commit -m \"${commitMsg}\"`);\n } catch {\n // Nothing changed (file content identical to last snapshot)\n logger.debug(COMPONENT, `No changes to snapshot: ${relPath}`);\n return null;\n }\n\n // Get the commit hash\n const commitHash = execGit(repoPath, 'rev-parse HEAD');\n\n const checkpoint: FileCheckpoint = {\n id: commitHash.slice(0, 8),\n timestamp,\n toolName,\n filePath: absPath,\n commitHash,\n repoPath,\n };\n\n // Save to metadata\n const meta = loadMeta(repoPath);\n meta.checkpoints.push(checkpoint);\n // Keep last 100 checkpoints per repo\n if (meta.checkpoints.length > 100) {\n meta.checkpoints = meta.checkpoints.slice(-100);\n }\n saveMeta(repoPath, meta);\n\n logger.info(COMPONENT, `Snapshot: ${relPath} → ${commitHash.slice(0, 8)} (before ${toolName})`);\n return checkpoint;\n } catch (err) {\n logger.debug(COMPONENT, `Snapshot failed (non-fatal): ${(err as Error).message}`);\n return null;\n }\n}\n\n// ── Recovery Tools ────────────────────────────────────────────────\n/**\n * List checkpoints for a file or directory.\n */\nexport function listCheckpoints(dirOrFile: string): FileCheckpoint[] {\n const absPath = resolve(dirOrFile);\n let dirPath: string;\n\n try {\n const stat = statSync(absPath);\n dirPath = stat.isDirectory() ? absPath : dirname(absPath);\n } catch {\n dirPath = dirname(absPath);\n }\n\n const repoPath = shadowRepoPath(dirPath);\n if (!existsSync(join(repoPath, METADATA_FILE))) return [];\n\n const meta = loadMeta(repoPath);\n\n // If a specific file was given, filter to that file\n if (absPath !== dirPath) {\n return meta.checkpoints.filter(c => c.filePath === absPath);\n }\n\n return meta.checkpoints;\n}\n\n/**\n * Get the diff between a checkpoint and the current file.\n */\nexport function diffCheckpoint(checkpointId: string, _dirPath?: string): string {\n // Search all shadow repos for this checkpoint\n if (!existsSync(CHECKPOINTS_BASE)) return 'No checkpoints found.';\n\n const repos = readdirSync(CHECKPOINTS_BASE);\n for (const repo of repos) {\n const repoPath = join(CHECKPOINTS_BASE, repo);\n if (!existsSync(join(repoPath, '.git'))) continue;\n\n const meta = loadMeta(repoPath);\n const checkpoint = meta.checkpoints.find(c => c.id === checkpointId || c.commitHash.startsWith(checkpointId));\n if (!checkpoint) continue;\n\n try {\n // Show what the file looked like at that checkpoint vs now\n const relPath = basename(checkpoint.filePath);\n const diff = execGit(repoPath, `diff ${checkpoint.commitHash} HEAD -- \"${relPath}\"`);\n return diff || 'No differences (file unchanged since checkpoint).';\n } catch (err) {\n return `Diff failed: ${(err as Error).message}`;\n }\n }\n\n return `Checkpoint \"${checkpointId}\" not found.`;\n}\n\n/**\n * Restore a file to its state at a specific checkpoint.\n */\nexport function restoreCheckpoint(checkpointId: string): string {\n if (!existsSync(CHECKPOINTS_BASE)) return 'No checkpoints found.';\n\n const repos = readdirSync(CHECKPOINTS_BASE);\n for (const repo of repos) {\n const repoPath = join(CHECKPOINTS_BASE, repo);\n if (!existsSync(join(repoPath, '.git'))) continue;\n\n const meta = loadMeta(repoPath);\n const checkpoint = meta.checkpoints.find(c => c.id === checkpointId || c.commitHash.startsWith(checkpointId));\n if (!checkpoint) continue;\n\n try {\n const relPath = basename(checkpoint.filePath);\n\n // Get file content at the checkpoint\n const content = execGit(repoPath, `show ${checkpoint.commitHash}:\"${relPath}\"`);\n\n // Write back to the original file\n writeFileSync(checkpoint.filePath, content, 'utf-8');\n\n logger.info(COMPONENT, `Restored: ${checkpoint.filePath} to checkpoint ${checkpointId}`);\n return `Restored ${checkpoint.filePath} to checkpoint ${checkpointId} (from ${checkpoint.timestamp}, before ${checkpoint.toolName}).`;\n } catch (err) {\n return `Restore failed: ${(err as Error).message}`;\n }\n }\n\n return `Checkpoint \"${checkpointId}\" not found.`;\n}\n"],"mappings":";AASA,SAAS,YAAY,WAAW,cAAc,cAAc,eAAe,aAAa,gBAAgB;AACxG,SAAS,MAAM,SAAS,SAAmB,gBAAgB;AAC3D,SAAS,eAAe;AACxB,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB;AACzB,OAAO,YAAY;AAEnB,MAAM,YAAY;AAClB,MAAM,mBAAmB,KAAK,QAAQ,GAAG,UAAU,kBAAkB;AACrE,MAAM,kBAAkB;AACxB,MAAM,gBAAgB;AAmBtB,SAAS,QAAQ,SAAyB;AACtC,SAAO,WAAW,KAAK,EAAE,OAAO,QAAQ,OAAO,CAAC,EAAE,OAAO,KAAK,EAAE,MAAM,GAAG,EAAE;AAC/E;AAEA,SAAS,eAAe,SAAyB;AAC7C,SAAO,KAAK,kBAAkB,QAAQ,OAAO,CAAC;AAClD;AAEA,SAAS,QAAQ,KAAa,KAAqB;AAC/C,MAAI;AACA,WAAO,SAAS,OAAO,GAAG,IAAI;AAAA,MAC1B;AAAA,MACA,OAAO;AAAA,MACP,SAAS;AAAA,MACT,KAAK,EAAE,GAAG,QAAQ,KAAK,qBAAqB,IAAI;AAAA,IACpD,CAAC,EAAE,SAAS,EAAE,KAAK;AAAA,EACvB,SAAS,KAAK;AACV,UAAM,IAAI,MAAM,OAAO,GAAG,YAAa,IAAc,OAAO,EAAE;AAAA,EAClE;AACJ;AAEA,SAAS,SAAS,UAA8B;AAC5C,QAAM,WAAW,KAAK,UAAU,aAAa;AAC7C,MAAI,WAAW,QAAQ,GAAG;AACtB,QAAI;AACA,aAAO,KAAK,MAAM,aAAa,UAAU,OAAO,CAAC;AAAA,IACrD,QAAQ;AAAA,IAAqB;AAAA,EACjC;AACA,SAAO,EAAE,aAAa,IAAI,YAAW,oBAAI,KAAK,GAAE,YAAY,GAAG,aAAa,CAAC,EAAE;AACnF;AAEA,SAAS,SAAS,UAAkB,MAAwB;AACxD,gBAAc,KAAK,UAAU,aAAa,GAAG,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,OAAO;AACvF;AAOO,SAAS,cAAc,SAA0B;AACpD,MAAI;AACA,UAAM,UAAU,YAAY,OAAO;AACnC,WAAO,QAAQ,SAAS;AAAA,EAC5B,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAOO,SAAS,eAAe,SAAyB;AACpD,QAAM,WAAW,eAAe,OAAO;AAEvC,MAAI,WAAW,KAAK,UAAU,MAAM,CAAC,GAAG;AACpC,WAAO;AAAA,EACX;AAEA,YAAU,UAAU,EAAE,WAAW,KAAK,CAAC;AACvC,UAAQ,UAAU,MAAM;AAExB,UAAQ,UAAU,oCAAoC;AACtD,UAAQ,UAAU,wCAAwC;AAG1D,QAAM,OAAmB;AAAA,IACrB,aAAa,QAAQ,OAAO;AAAA,IAC5B,YAAW,oBAAI,KAAK,GAAE,YAAY;AAAA,IAClC,aAAa,CAAC;AAAA,EAClB;AACA,WAAS,UAAU,IAAI;AAGvB,UAAQ,UAAU,OAAO;AACzB,MAAI;AACA,YAAQ,UAAU,mDAAmD;AAAA,EACzE,QAAQ;AAAA,EAAuC;AAE/C,SAAO,KAAK,WAAW,4BAA4B,OAAO,WAAM,QAAQ,EAAE;AAC1E,SAAO;AACX;AASA,eAAsB,oBAClB,UACA,UAC8B;AAC9B,MAAI;AACA,UAAM,UAAU,QAAQ,QAAQ;AAChC,UAAM,UAAU,QAAQ,OAAO;AAG/B,QAAI,CAAC,WAAW,OAAO,GAAG;AACtB,aAAO,MAAM,WAAW,mCAAmC,OAAO,EAAE;AACpE,aAAO;AAAA,IACX;AAGA,QAAI,cAAc,OAAO,GAAG;AACxB,aAAO,MAAM,WAAW,iDAA4C,OAAO,EAAE;AAC7E,aAAO;AAAA,IACX;AAGA,UAAM,WAAW,eAAe,OAAO;AAGvC,UAAM,UAAU,SAAS,OAAO;AAChC,UAAM,iBAAiB,KAAK,UAAU,OAAO;AAC7C,iBAAa,SAAS,cAAc;AAGpC,YAAQ,UAAU,QAAQ,OAAO,GAAG;AAEpC,UAAM,aAAY,oBAAI,KAAK,GAAE,YAAY;AACzC,UAAM,YAAY,IAAI,QAAQ,yBAAyB,OAAO,OAAO,SAAS;AAE9E,QAAI;AACA,cAAQ,UAAU,cAAc,SAAS,GAAG;AAAA,IAChD,QAAQ;AAEJ,aAAO,MAAM,WAAW,2BAA2B,OAAO,EAAE;AAC5D,aAAO;AAAA,IACX;AAGA,UAAM,aAAa,QAAQ,UAAU,gBAAgB;AAErD,UAAM,aAA6B;AAAA,MAC/B,IAAI,WAAW,MAAM,GAAG,CAAC;AAAA,MACzB;AAAA,MACA;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA;AAAA,IACJ;AAGA,UAAM,OAAO,SAAS,QAAQ;AAC9B,SAAK,YAAY,KAAK,UAAU;AAEhC,QAAI,KAAK,YAAY,SAAS,KAAK;AAC/B,WAAK,cAAc,KAAK,YAAY,MAAM,IAAI;AAAA,IAClD;AACA,aAAS,UAAU,IAAI;AAEvB,WAAO,KAAK,WAAW,aAAa,OAAO,WAAM,WAAW,MAAM,GAAG,CAAC,CAAC,YAAY,QAAQ,GAAG;AAC9F,WAAO;AAAA,EACX,SAAS,KAAK;AACV,WAAO,MAAM,WAAW,gCAAiC,IAAc,OAAO,EAAE;AAChF,WAAO;AAAA,EACX;AACJ;AAMO,SAAS,gBAAgB,WAAqC;AACjE,QAAM,UAAU,QAAQ,SAAS;AACjC,MAAI;AAEJ,MAAI;AACA,UAAM,OAAO,SAAS,OAAO;AAC7B,cAAU,KAAK,YAAY,IAAI,UAAU,QAAQ,OAAO;AAAA,EAC5D,QAAQ;AACJ,cAAU,QAAQ,OAAO;AAAA,EAC7B;AAEA,QAAM,WAAW,eAAe,OAAO;AACvC,MAAI,CAAC,WAAW,KAAK,UAAU,aAAa,CAAC,EAAG,QAAO,CAAC;AAExD,QAAM,OAAO,SAAS,QAAQ;AAG9B,MAAI,YAAY,SAAS;AACrB,WAAO,KAAK,YAAY,OAAO,OAAK,EAAE,aAAa,OAAO;AAAA,EAC9D;AAEA,SAAO,KAAK;AAChB;AAKO,SAAS,eAAe,cAAsB,UAA2B;AAE5E,MAAI,CAAC,WAAW,gBAAgB,EAAG,QAAO;AAE1C,QAAM,QAAQ,YAAY,gBAAgB;AAC1C,aAAW,QAAQ,OAAO;AACtB,UAAM,WAAW,KAAK,kBAAkB,IAAI;AAC5C,QAAI,CAAC,WAAW,KAAK,UAAU,MAAM,CAAC,EAAG;AAEzC,UAAM,OAAO,SAAS,QAAQ;AAC9B,UAAM,aAAa,KAAK,YAAY,KAAK,OAAK,EAAE,OAAO,gBAAgB,EAAE,WAAW,WAAW,YAAY,CAAC;AAC5G,QAAI,CAAC,WAAY;AAEjB,QAAI;AAEA,YAAM,UAAU,SAAS,WAAW,QAAQ;AAC5C,YAAM,OAAO,QAAQ,UAAU,QAAQ,WAAW,UAAU,aAAa,OAAO,GAAG;AACnF,aAAO,QAAQ;AAAA,IACnB,SAAS,KAAK;AACV,aAAO,gBAAiB,IAAc,OAAO;AAAA,IACjD;AAAA,EACJ;AAEA,SAAO,eAAe,YAAY;AACtC;AAKO,SAAS,kBAAkB,cAA8B;AAC5D,MAAI,CAAC,WAAW,gBAAgB,EAAG,QAAO;AAE1C,QAAM,QAAQ,YAAY,gBAAgB;AAC1C,aAAW,QAAQ,OAAO;AACtB,UAAM,WAAW,KAAK,kBAAkB,IAAI;AAC5C,QAAI,CAAC,WAAW,KAAK,UAAU,MAAM,CAAC,EAAG;AAEzC,UAAM,OAAO,SAAS,QAAQ;AAC9B,UAAM,aAAa,KAAK,YAAY,KAAK,OAAK,EAAE,OAAO,gBAAgB,EAAE,WAAW,WAAW,YAAY,CAAC;AAC5G,QAAI,CAAC,WAAY;AAEjB,QAAI;AACA,YAAM,UAAU,SAAS,WAAW,QAAQ;AAG5C,YAAM,UAAU,QAAQ,UAAU,QAAQ,WAAW,UAAU,KAAK,OAAO,GAAG;AAG9E,oBAAc,WAAW,UAAU,SAAS,OAAO;AAEnD,aAAO,KAAK,WAAW,aAAa,WAAW,QAAQ,kBAAkB,YAAY,EAAE;AACvF,aAAO,YAAY,WAAW,QAAQ,kBAAkB,YAAY,UAAU,WAAW,SAAS,YAAY,WAAW,QAAQ;AAAA,IACrI,SAAS,KAAK;AACV,aAAO,mBAAoB,IAAc,OAAO;AAAA,IACpD;AAAA,EACJ;AAEA,SAAO,eAAe,YAAY;AACtC;","names":[]}
|
|
@@ -11,7 +11,7 @@ const TAIL_CHARS = 200;
|
|
|
11
11
|
const PROGRESS_INTERVAL = 4;
|
|
12
12
|
const MAX_PROGRESS_ENTRIES = 10;
|
|
13
13
|
const sessionProgress = /* @__PURE__ */ new Map();
|
|
14
|
-
async function compressToolResult(sessionId, toolName, toolCallId, result,
|
|
14
|
+
async function compressToolResult(sessionId, toolName, toolCallId, result, _round) {
|
|
15
15
|
persistResult(sessionId, toolCallId, toolName, result).catch((e) => logger.debug(COMPONENT, `Background result persistence failed: ${e.message}`));
|
|
16
16
|
const noCompressTools = /* @__PURE__ */ new Set([
|
|
17
17
|
"read_file",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/agent/trajectoryCompressor.ts"],"sourcesContent":["/**\n * TITAN — Trajectory Compressor\n * Compresses tool call/result pairs to preserve context window space.\n * Inspired by Hermes AI's trajectory compression pattern.\n *\n * After each tool round, long tool results are compressed to head+tail summaries.\n * A running progress summary is maintained and injected every N rounds.\n * Full results are persisted to disk for debugging.\n */\nimport { writeFile, mkdir } from 'fs/promises';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'TrajectoryCompressor';\nconst RESULTS_DIR = join(homedir(), '.titan', 'tool-results');\n\n// ── Config ───────────────────────────────────────────────────────\nconst MAX_RESULT_CHARS = 800; // Compress results longer than this\nconst HEAD_CHARS = 400; // Keep first N chars\nconst TAIL_CHARS = 200; // Keep last N chars\nconst PROGRESS_INTERVAL = 4; // Inject progress summary every N rounds\nconst MAX_PROGRESS_ENTRIES = 10; // Keep last N entries in progress summary\n\n// ── Per-session state ────────────────────────────────────────────\ninterface ToolStep {\n round: number;\n tool: string;\n success: boolean;\n summary: string; // One-line summary of what happened\n}\n\nconst sessionProgress = new Map<string, ToolStep[]>();\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Compress a tool result if it's too long.\n * Returns the compressed version (or original if short enough).\n * Persists the full result to disk.\n */\nexport async function compressToolResult(\n sessionId: string,\n toolName: string,\n toolCallId: string,\n result: string,\n round: number,\n): Promise<string> {\n // Persist full result to disk (fire-and-forget)\n persistResult(sessionId, toolCallId, toolName, result).catch(e => logger.debug(COMPONENT, `Background result persistence failed: ${(e as Error).message}`));\n\n // Never compress file content tools — the model needs the full text to\n // construct accurate edit_file targets. Compressing read_file to head+tail\n // causes edit_file to fail with \"target not found\" because the model can't\n // see the middle of the file. Same for edit_file results (confirmation text).\n const noCompressTools = new Set([\n 'read_file', 'edit_file', 'write_file', 'append_file', 'apply_patch',\n 'fb_read_feed', 'fb_get_insights', 'fb_read_comments', // Structured API data — compression destroys comment/engagement details\n 'graph_search', 'memory', // Knowledge retrieval — full context needed\n ]);\n if (noCompressTools.has(toolName)) return result;\n\n if (result.length <= MAX_RESULT_CHARS) return result;\n\n const head = result.slice(0, HEAD_CHARS);\n const tail = result.slice(-TAIL_CHARS);\n const omitted = result.length - HEAD_CHARS - TAIL_CHARS;\n\n const compressed = `${head}\\n\\n[... ${omitted} chars omitted — full result saved to disk ...]\\n\\n${tail}`;\n\n logger.info(COMPONENT, `Compressed ${toolName} result: ${result.length} → ${compressed.length} chars (saved ${omitted} chars)`);\n return compressed;\n}\n\n/**\n * Record a tool step for progress tracking.\n */\nexport function recordStep(\n sessionId: string,\n round: number,\n toolName: string,\n success: boolean,\n resultPreview: string,\n): void {\n if (!sessionProgress.has(sessionId)) sessionProgress.set(sessionId, []);\n const steps = sessionProgress.get(sessionId)!;\n\n // One-line summary\n const preview = resultPreview.replace(/\\n/g, ' ').slice(0, 100);\n const summary = success\n ? `${toolName}: ${preview}`\n : `${toolName}: FAILED — ${preview}`;\n\n steps.push({ round, tool: toolName, success, summary });\n\n // Keep bounded\n if (steps.length > MAX_PROGRESS_ENTRIES * 2) {\n sessionProgress.set(sessionId, steps.slice(-MAX_PROGRESS_ENTRIES));\n }\n}\n\n/**\n * Get a progress summary if it's time to inject one.\n * Returns null if not time yet.\n */\nexport function getProgressSummary(sessionId: string, round: number): string | null {\n if (round < PROGRESS_INTERVAL || round % PROGRESS_INTERVAL !== 0) return null;\n\n const steps = sessionProgress.get(sessionId);\n if (!steps || steps.length === 0) return null;\n\n const recent = steps.slice(-MAX_PROGRESS_ENTRIES);\n const successCount = recent.filter(s => s.success).length;\n const failCount = recent.length - successCount;\n\n const lines = recent.map((s, i) => `${i + 1}. ${s.summary}`);\n return [\n `[Progress Summary — Round ${round}, ${successCount} successes, ${failCount} failures]`,\n ...lines,\n 'Continue with the next step of the task.',\n ].join('\\n');\n}\n\n/**\n * Clear session progress (on session end).\n */\nexport function clearProgress(sessionId: string): void {\n sessionProgress.delete(sessionId);\n}\n\n// ── Tool Result Cache (deduplication) ────────────────────────────\n\ninterface CachedResult {\n result: string;\n cachedAt: number;\n}\n\nconst resultCache = new Map<string, CachedResult>();\nconst CACHE_TTL_MS = 60_000; // 60 seconds\n\nfunction cacheKey(toolName: string, args: string): string {\n return `${toolName}:${args}`;\n}\n\n/**\n * Check if a tool call result is cached (same tool + same args within TTL).\n * Only caches read-only tools to avoid skipping side effects.\n */\nexport function getCachedToolResult(toolName: string, args: string): string | null {\n // Only cache read-only tools\n const readOnlyTools = new Set(['read_file', 'list_dir', 'web_search', 'web_fetch', 'graph_search', 'graph_entities', 'system_info', 'weather']);\n if (!readOnlyTools.has(toolName)) return null;\n\n const key = cacheKey(toolName, args);\n const cached = resultCache.get(key);\n if (!cached) return null;\n\n if (Date.now() - cached.cachedAt > CACHE_TTL_MS) {\n resultCache.delete(key);\n return null;\n }\n\n logger.info(COMPONENT, `[Cache HIT] ${toolName} — returning cached result (${cached.result.length} chars)`);\n return cached.result;\n}\n\n/**\n * Cache a tool result for deduplication.\n */\nexport function cacheToolResult(toolName: string, args: string, result: string): void {\n const readOnlyTools = new Set(['read_file', 'list_dir', 'web_search', 'web_fetch', 'graph_search', 'graph_entities', 'system_info', 'weather']);\n if (!readOnlyTools.has(toolName)) return;\n\n const key = cacheKey(toolName, args);\n resultCache.set(key, { result, cachedAt: Date.now() });\n\n // Evict old entries\n if (resultCache.size > 100) {\n const now = Date.now();\n for (const [k, v] of resultCache) {\n if (now - v.cachedAt > CACHE_TTL_MS) resultCache.delete(k);\n }\n }\n}\n\n// ── Internal ─────────────────────────────────────────────────────\n\nasync function persistResult(sessionId: string, toolCallId: string, toolName: string, result: string): Promise<void> {\n try {\n const dir = join(RESULTS_DIR, sessionId.slice(0, 12));\n await mkdir(dir, { recursive: true });\n const filename = `${Date.now()}-${toolName}-${toolCallId.slice(0, 8)}.txt`;\n await writeFile(join(dir, filename), result, 'utf-8');\n } catch (err) {\n logger.debug(COMPONENT, `Failed to persist tool result: ${(err as Error).message}`);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/agent/trajectoryCompressor.ts"],"sourcesContent":["/**\n * TITAN — Trajectory Compressor\n * Compresses tool call/result pairs to preserve context window space.\n * Inspired by Hermes AI's trajectory compression pattern.\n *\n * After each tool round, long tool results are compressed to head+tail summaries.\n * A running progress summary is maintained and injected every N rounds.\n * Full results are persisted to disk for debugging.\n */\nimport { writeFile, mkdir } from 'fs/promises';\nimport { join } from 'path';\nimport { homedir } from 'os';\nimport logger from '../utils/logger.js';\n\nconst COMPONENT = 'TrajectoryCompressor';\nconst RESULTS_DIR = join(homedir(), '.titan', 'tool-results');\n\n// ── Config ───────────────────────────────────────────────────────\nconst MAX_RESULT_CHARS = 800; // Compress results longer than this\nconst HEAD_CHARS = 400; // Keep first N chars\nconst TAIL_CHARS = 200; // Keep last N chars\nconst PROGRESS_INTERVAL = 4; // Inject progress summary every N rounds\nconst MAX_PROGRESS_ENTRIES = 10; // Keep last N entries in progress summary\n\n// ── Per-session state ────────────────────────────────────────────\ninterface ToolStep {\n round: number;\n tool: string;\n success: boolean;\n summary: string; // One-line summary of what happened\n}\n\nconst sessionProgress = new Map<string, ToolStep[]>();\n\n// ── Public API ───────────────────────────────────────────────────\n\n/**\n * Compress a tool result if it's too long.\n * Returns the compressed version (or original if short enough).\n * Persists the full result to disk.\n */\nexport async function compressToolResult(\n sessionId: string,\n toolName: string,\n toolCallId: string,\n result: string,\n _round: number,\n): Promise<string> {\n // Persist full result to disk (fire-and-forget)\n persistResult(sessionId, toolCallId, toolName, result).catch(e => logger.debug(COMPONENT, `Background result persistence failed: ${(e as Error).message}`));\n\n // Never compress file content tools — the model needs the full text to\n // construct accurate edit_file targets. Compressing read_file to head+tail\n // causes edit_file to fail with \"target not found\" because the model can't\n // see the middle of the file. Same for edit_file results (confirmation text).\n const noCompressTools = new Set([\n 'read_file', 'edit_file', 'write_file', 'append_file', 'apply_patch',\n 'fb_read_feed', 'fb_get_insights', 'fb_read_comments', // Structured API data — compression destroys comment/engagement details\n 'graph_search', 'memory', // Knowledge retrieval — full context needed\n ]);\n if (noCompressTools.has(toolName)) return result;\n\n if (result.length <= MAX_RESULT_CHARS) return result;\n\n const head = result.slice(0, HEAD_CHARS);\n const tail = result.slice(-TAIL_CHARS);\n const omitted = result.length - HEAD_CHARS - TAIL_CHARS;\n\n const compressed = `${head}\\n\\n[... ${omitted} chars omitted — full result saved to disk ...]\\n\\n${tail}`;\n\n logger.info(COMPONENT, `Compressed ${toolName} result: ${result.length} → ${compressed.length} chars (saved ${omitted} chars)`);\n return compressed;\n}\n\n/**\n * Record a tool step for progress tracking.\n */\nexport function recordStep(\n sessionId: string,\n round: number,\n toolName: string,\n success: boolean,\n resultPreview: string,\n): void {\n if (!sessionProgress.has(sessionId)) sessionProgress.set(sessionId, []);\n const steps = sessionProgress.get(sessionId)!;\n\n // One-line summary\n const preview = resultPreview.replace(/\\n/g, ' ').slice(0, 100);\n const summary = success\n ? `${toolName}: ${preview}`\n : `${toolName}: FAILED — ${preview}`;\n\n steps.push({ round, tool: toolName, success, summary });\n\n // Keep bounded\n if (steps.length > MAX_PROGRESS_ENTRIES * 2) {\n sessionProgress.set(sessionId, steps.slice(-MAX_PROGRESS_ENTRIES));\n }\n}\n\n/**\n * Get a progress summary if it's time to inject one.\n * Returns null if not time yet.\n */\nexport function getProgressSummary(sessionId: string, round: number): string | null {\n if (round < PROGRESS_INTERVAL || round % PROGRESS_INTERVAL !== 0) return null;\n\n const steps = sessionProgress.get(sessionId);\n if (!steps || steps.length === 0) return null;\n\n const recent = steps.slice(-MAX_PROGRESS_ENTRIES);\n const successCount = recent.filter(s => s.success).length;\n const failCount = recent.length - successCount;\n\n const lines = recent.map((s, i) => `${i + 1}. ${s.summary}`);\n return [\n `[Progress Summary — Round ${round}, ${successCount} successes, ${failCount} failures]`,\n ...lines,\n 'Continue with the next step of the task.',\n ].join('\\n');\n}\n\n/**\n * Clear session progress (on session end).\n */\nexport function clearProgress(sessionId: string): void {\n sessionProgress.delete(sessionId);\n}\n\n// ── Tool Result Cache (deduplication) ────────────────────────────\n\ninterface CachedResult {\n result: string;\n cachedAt: number;\n}\n\nconst resultCache = new Map<string, CachedResult>();\nconst CACHE_TTL_MS = 60_000; // 60 seconds\n\nfunction cacheKey(toolName: string, args: string): string {\n return `${toolName}:${args}`;\n}\n\n/**\n * Check if a tool call result is cached (same tool + same args within TTL).\n * Only caches read-only tools to avoid skipping side effects.\n */\nexport function getCachedToolResult(toolName: string, args: string): string | null {\n // Only cache read-only tools\n const readOnlyTools = new Set(['read_file', 'list_dir', 'web_search', 'web_fetch', 'graph_search', 'graph_entities', 'system_info', 'weather']);\n if (!readOnlyTools.has(toolName)) return null;\n\n const key = cacheKey(toolName, args);\n const cached = resultCache.get(key);\n if (!cached) return null;\n\n if (Date.now() - cached.cachedAt > CACHE_TTL_MS) {\n resultCache.delete(key);\n return null;\n }\n\n logger.info(COMPONENT, `[Cache HIT] ${toolName} — returning cached result (${cached.result.length} chars)`);\n return cached.result;\n}\n\n/**\n * Cache a tool result for deduplication.\n */\nexport function cacheToolResult(toolName: string, args: string, result: string): void {\n const readOnlyTools = new Set(['read_file', 'list_dir', 'web_search', 'web_fetch', 'graph_search', 'graph_entities', 'system_info', 'weather']);\n if (!readOnlyTools.has(toolName)) return;\n\n const key = cacheKey(toolName, args);\n resultCache.set(key, { result, cachedAt: Date.now() });\n\n // Evict old entries\n if (resultCache.size > 100) {\n const now = Date.now();\n for (const [k, v] of resultCache) {\n if (now - v.cachedAt > CACHE_TTL_MS) resultCache.delete(k);\n }\n }\n}\n\n// ── Internal ─────────────────────────────────────────────────────\n\nasync function persistResult(sessionId: string, toolCallId: string, toolName: string, result: string): Promise<void> {\n try {\n const dir = join(RESULTS_DIR, sessionId.slice(0, 12));\n await mkdir(dir, { recursive: true });\n const filename = `${Date.now()}-${toolName}-${toolCallId.slice(0, 8)}.txt`;\n await writeFile(join(dir, filename), result, 'utf-8');\n } catch (err) {\n logger.debug(COMPONENT, `Failed to persist tool result: ${(err as Error).message}`);\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -14,7 +14,7 @@ function createOpenAICompatRouter() {
|
|
|
14
14
|
const aliases = config.agent.modelAliases || {};
|
|
15
15
|
const models = [
|
|
16
16
|
{ id: currentModel, object: "model", created: Math.floor(Date.now() / 1e3), owned_by: "titan" },
|
|
17
|
-
...Object.entries(aliases).filter(([, v]) => v && v !== currentModel).map(([
|
|
17
|
+
...Object.entries(aliases).filter(([, v]) => v && v !== currentModel).map(([_key, value]) => ({
|
|
18
18
|
id: value,
|
|
19
19
|
object: "model",
|
|
20
20
|
created: Math.floor(Date.now() / 1e3),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/gateway/openai-compat.ts"],"sourcesContent":["/**\n * TITAN — OpenAI API Compatibility Layer\n * Serves /v1/models, /v1/chat/completions, /v1/embeddings so any OpenAI-compatible\n * client (OpenWebUI, Continue.dev, LiteLLM, etc.) can connect to TITAN as a provider.\n */\nimport { Router, type Request, type Response } from 'express';\nimport { chat, chatStream } from '../providers/router.js';\nimport { loadConfig } from '../config/config.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { v4 as uuid } from 'uuid';\nimport logger from '../utils/logger.js';\nimport { setupSSEFlush } from '../utils/sseFlush.js';\n\nconst COMPONENT = 'OpenAI-Compat';\n\nexport function createOpenAICompatRouter(): Router {\n const router = Router();\n\n // GET /v1/models — list available models\n router.get('/models', (_req: Request, res: Response) => {\n const config = loadConfig();\n const currentModel = config.agent.model || 'unknown';\n const aliases = config.agent.modelAliases || {};\n\n const models = [\n { id: currentModel, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'titan' },\n ...Object.entries(aliases)\n .filter(([, v]) => v && v !== currentModel)\n .map(([key, value]) => ({\n id: value as string,\n object: 'model' as const,\n created: Math.floor(Date.now() / 1000),\n owned_by: 'titan',\n })),\n ];\n\n res.json({ object: 'list', data: models });\n });\n\n // POST /v1/chat/completions — chat completion (streaming and non-streaming)\n router.post('/chat/completions', async (req: Request, res: Response) => {\n const { model, messages, stream, temperature, max_tokens, top_p, stop } = req.body;\n\n if (!messages || !Array.isArray(messages)) {\n res.status(400).json({ error: { message: 'messages is required', type: 'invalid_request_error' } });\n return;\n }\n\n const config = loadConfig();\n const effectiveModel = model || config.agent.model;\n const chatId = `chatcmpl-${uuid().slice(0, 12)}`;\n\n logger.info(COMPONENT, `Chat completion: model=${effectiveModel}, messages=${messages.length}, stream=${!!stream}`);\n\n try {\n if (stream) {\n // SSE streaming response\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n });\n const sseWrite = setupSSEFlush(res);\n\n for await (const chunk of chatStream({\n model: effectiveModel,\n messages,\n temperature: temperature ?? config.agent.temperature,\n maxTokens: max_tokens ?? config.agent.maxTokens,\n })) {\n if (chunk.type === 'text' && chunk.content) {\n const data = {\n id: chatId,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: effectiveModel,\n choices: [{\n index: 0,\n delta: { content: chunk.content },\n finish_reason: null,\n }],\n };\n sseWrite(`data: ${JSON.stringify(data)}\\n\\n`);\n }\n }\n\n // Send final chunk\n sseWrite(`data: ${JSON.stringify({\n id: chatId,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: effectiveModel,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n })}\\n\\n`);\n sseWrite('data: [DONE]\\n\\n');\n res.end();\n } else {\n // Non-streaming response\n const response = await chat({\n model: effectiveModel,\n messages,\n temperature: temperature ?? config.agent.temperature,\n maxTokens: max_tokens ?? config.agent.maxTokens,\n });\n\n res.json({\n id: chatId,\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: response.model || effectiveModel,\n choices: [{\n index: 0,\n message: { role: 'assistant', content: response.content },\n finish_reason: response.finishReason || 'stop',\n }],\n usage: {\n prompt_tokens: response.usage?.promptTokens ?? 0,\n completion_tokens: response.usage?.completionTokens ?? 0,\n total_tokens: response.usage?.totalTokens ?? 0,\n },\n });\n }\n } catch (err) {\n logger.error(COMPONENT, `Chat completion error: ${(err as Error).message}`);\n res.status(500).json({\n error: { message: (err as Error).message, type: 'server_error' },\n });\n }\n });\n\n // POST /v1/embeddings — embedding generation (proxy to Ollama)\n router.post('/embeddings', async (req: Request, res: Response) => {\n const { model, input } = req.body;\n const config = loadConfig();\n const embeddingModel = model || (config.memory as Record<string, unknown>)?.embeddingModel || 'nomic-embed-text';\n\n try {\n // Proxy to Ollama embeddings endpoint\n const ollamaUrl = config.providers?.ollama?.baseUrl || 'http://localhost:11434';\n const texts = Array.isArray(input) ? input : [input];\n const embeddings = [];\n\n for (let i = 0; i < texts.length; i++) {\n const resp = await fetch(`${ollamaUrl}/api/embed`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ model: embeddingModel, input: texts[i] }),\n });\n\n if (!resp.ok) throw new Error(`Ollama embed failed: ${resp.status}`);\n const data = await resp.json() as { embeddings?: number[][] };\n embeddings.push({\n object: 'embedding',\n index: i,\n embedding: data.embeddings?.[0] || [],\n });\n }\n\n res.json({\n object: 'list',\n data: embeddings,\n model: embeddingModel,\n usage: { prompt_tokens: 0, total_tokens: 0 },\n });\n } catch (err) {\n res.status(500).json({\n error: { message: (err as Error).message, type: 'server_error' },\n });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/gateway/openai-compat.ts"],"sourcesContent":["/**\n * TITAN — OpenAI API Compatibility Layer\n * Serves /v1/models, /v1/chat/completions, /v1/embeddings so any OpenAI-compatible\n * client (OpenWebUI, Continue.dev, LiteLLM, etc.) can connect to TITAN as a provider.\n */\nimport { Router, type Request, type Response } from 'express';\nimport { chat, chatStream } from '../providers/router.js';\nimport { loadConfig } from '../config/config.js';\nimport { TITAN_VERSION } from '../utils/constants.js';\nimport { v4 as uuid } from 'uuid';\nimport logger from '../utils/logger.js';\nimport { setupSSEFlush } from '../utils/sseFlush.js';\n\nconst COMPONENT = 'OpenAI-Compat';\n\nexport function createOpenAICompatRouter(): Router {\n const router = Router();\n\n // GET /v1/models — list available models\n router.get('/models', (_req: Request, res: Response) => {\n const config = loadConfig();\n const currentModel = config.agent.model || 'unknown';\n const aliases = config.agent.modelAliases || {};\n\n const models = [\n { id: currentModel, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'titan' },\n ...Object.entries(aliases)\n .filter(([, v]) => v && v !== currentModel)\n .map(([_key, value]) => ({\n id: value as string,\n object: 'model' as const,\n created: Math.floor(Date.now() / 1000),\n owned_by: 'titan',\n })),\n ];\n\n res.json({ object: 'list', data: models });\n });\n\n // POST /v1/chat/completions — chat completion (streaming and non-streaming)\n router.post('/chat/completions', async (req: Request, res: Response) => {\n const { model, messages, stream, temperature, max_tokens, top_p, stop } = req.body;\n\n if (!messages || !Array.isArray(messages)) {\n res.status(400).json({ error: { message: 'messages is required', type: 'invalid_request_error' } });\n return;\n }\n\n const config = loadConfig();\n const effectiveModel = model || config.agent.model;\n const chatId = `chatcmpl-${uuid().slice(0, 12)}`;\n\n logger.info(COMPONENT, `Chat completion: model=${effectiveModel}, messages=${messages.length}, stream=${!!stream}`);\n\n try {\n if (stream) {\n // SSE streaming response\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n });\n const sseWrite = setupSSEFlush(res);\n\n for await (const chunk of chatStream({\n model: effectiveModel,\n messages,\n temperature: temperature ?? config.agent.temperature,\n maxTokens: max_tokens ?? config.agent.maxTokens,\n })) {\n if (chunk.type === 'text' && chunk.content) {\n const data = {\n id: chatId,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: effectiveModel,\n choices: [{\n index: 0,\n delta: { content: chunk.content },\n finish_reason: null,\n }],\n };\n sseWrite(`data: ${JSON.stringify(data)}\\n\\n`);\n }\n }\n\n // Send final chunk\n sseWrite(`data: ${JSON.stringify({\n id: chatId,\n object: 'chat.completion.chunk',\n created: Math.floor(Date.now() / 1000),\n model: effectiveModel,\n choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],\n })}\\n\\n`);\n sseWrite('data: [DONE]\\n\\n');\n res.end();\n } else {\n // Non-streaming response\n const response = await chat({\n model: effectiveModel,\n messages,\n temperature: temperature ?? config.agent.temperature,\n maxTokens: max_tokens ?? config.agent.maxTokens,\n });\n\n res.json({\n id: chatId,\n object: 'chat.completion',\n created: Math.floor(Date.now() / 1000),\n model: response.model || effectiveModel,\n choices: [{\n index: 0,\n message: { role: 'assistant', content: response.content },\n finish_reason: response.finishReason || 'stop',\n }],\n usage: {\n prompt_tokens: response.usage?.promptTokens ?? 0,\n completion_tokens: response.usage?.completionTokens ?? 0,\n total_tokens: response.usage?.totalTokens ?? 0,\n },\n });\n }\n } catch (err) {\n logger.error(COMPONENT, `Chat completion error: ${(err as Error).message}`);\n res.status(500).json({\n error: { message: (err as Error).message, type: 'server_error' },\n });\n }\n });\n\n // POST /v1/embeddings — embedding generation (proxy to Ollama)\n router.post('/embeddings', async (req: Request, res: Response) => {\n const { model, input } = req.body;\n const config = loadConfig();\n const embeddingModel = model || (config.memory as Record<string, unknown>)?.embeddingModel || 'nomic-embed-text';\n\n try {\n // Proxy to Ollama embeddings endpoint\n const ollamaUrl = config.providers?.ollama?.baseUrl || 'http://localhost:11434';\n const texts = Array.isArray(input) ? input : [input];\n const embeddings = [];\n\n for (let i = 0; i < texts.length; i++) {\n const resp = await fetch(`${ollamaUrl}/api/embed`, {\n method: 'POST',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify({ model: embeddingModel, input: texts[i] }),\n });\n\n if (!resp.ok) throw new Error(`Ollama embed failed: ${resp.status}`);\n const data = await resp.json() as { embeddings?: number[][] };\n embeddings.push({\n object: 'embedding',\n index: i,\n embedding: data.embeddings?.[0] || [],\n });\n }\n\n res.json({\n object: 'list',\n data: embeddings,\n model: embeddingModel,\n usage: { prompt_tokens: 0, total_tokens: 0 },\n });\n } catch (err) {\n res.status(500).json({\n error: { message: (err as Error).message, type: 'server_error' },\n });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/gateway/routes/paperclip.ts"],"sourcesContent":["/**\n * Gateway sub-router for /api/paperclip/* and /paperclip/* routes.\n *\n * Extracted from src/gateway/server.ts to decompose the gateway monolith\n * and make the Paperclip sidecar API surface independently testable.\n *\n * Routes:\n * GET /api/paperclip/status\n * POST /api/paperclip/start\n * POST /api/paperclip/stop\n * POST /api/paperclip/reset\n * ALL /api/paperclip/* → proxy to Paperclip API\n * ALL /paperclip/* → proxy to Paperclip web UI\n */\n\nimport {
|
|
1
|
+
{"version":3,"sources":["../../../src/gateway/routes/paperclip.ts"],"sourcesContent":["/**\n * Gateway sub-router for /api/paperclip/* and /paperclip/* routes.\n *\n * Extracted from src/gateway/server.ts to decompose the gateway monolith\n * and make the Paperclip sidecar API surface independently testable.\n *\n * Routes:\n * GET /api/paperclip/status\n * POST /api/paperclip/start\n * POST /api/paperclip/stop\n * POST /api/paperclip/reset\n * ALL /api/paperclip/* → proxy to Paperclip API\n * ALL /paperclip/* → proxy to Paperclip web UI\n */\n\nimport type { Request, Response } from 'express';\nimport { Router } from 'express';\nimport { startPaperclip, stopPaperclip, getPaperclipStatus } from '../../addons/paperclipSidecar.js';\nimport logger from '../../utils/logger.js';\nimport { titanEvents } from '../../agent/daemon.js';\n\nconst COMPONENT = 'Gateway:Paperclip';\nconst PAPERCLIP_PORT = 3100;\n\nexport function createPaperclipRouter(): Router {\n const router = Router();\n\n // ── Management routes ──\n\n router.get('/status', async (_req, res) => {\n try {\n res.json(await getPaperclipStatus());\n } catch (err) {\n res.status(500).json({ error: (err as Error).message });\n }\n });\n\n router.post('/start', async (_req, res) => {\n try {\n await startPaperclip({ enabled: true, port: PAPERCLIP_PORT, autoStart: true }, titanEvents);\n res.json({ ok: true });\n } catch (err) {\n logger.error(COMPONENT, `Paperclip start failed: ${(err as Error).message}`);\n res.status(500).json({ error: (err as Error).message });\n }\n });\n\n router.post('/stop', async (_req, res) => {\n try {\n await stopPaperclip();\n res.json({ ok: true });\n } catch (err) {\n logger.error(COMPONENT, `Paperclip stop failed: ${(err as Error).message}`);\n res.status(500).json({ error: (err as Error).message });\n }\n });\n\n router.post('/reset', async (_req, res) => {\n try {\n await stopPaperclip();\n await startPaperclip({ enabled: true, port: PAPERCLIP_PORT, autoStart: true }, titanEvents);\n res.json({ ok: true });\n } catch (err) {\n logger.error(COMPONENT, `Paperclip reset failed: ${(err as Error).message}`);\n res.status(500).json({ error: (err as Error).message });\n }\n });\n\n // ── API proxy (/api/paperclip/* → http://localhost:3100/api/*) ──\n\n router.all('/*', async (req: Request, res: Response) => {\n const targetPath = req.path.replace(/^\\/api\\/paperclip/, '/api');\n const query = req.url.includes('?') ? '?' + req.url.split('?')[1] : '';\n const targetUrl = `http://localhost:${PAPERCLIP_PORT}${targetPath}${query}`;\n\n try {\n const headers = new Headers();\n for (const [k, v] of Object.entries(req.headers)) {\n if (v && k.toLowerCase() !== 'host') headers.set(k, Array.isArray(v) ? v[0] : v);\n }\n\n const upstream = await fetch(targetUrl, {\n method: req.method,\n headers: headers as any,\n body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body),\n });\n\n res.status(upstream.status);\n upstream.headers.forEach((v, k) => res.setHeader(k, v));\n const body = await upstream.arrayBuffer();\n res.end(Buffer.from(body));\n } catch (err) {\n logger.error(COMPONENT, `Paperclip API proxy error: ${(err as Error).message}`);\n res.status(502).json({ error: 'Paperclip API proxy error', message: (err as Error).message });\n }\n });\n\n return router;\n}\n\n// ── Web UI proxy (/paperclip/* → http://localhost:3100/*) ──\n\nexport function createPaperclipUIRouter(): Router {\n const router = Router();\n\n router.all('/*', async (req: Request, res: Response) => {\n const targetPath = req.path.replace(/^\\/paperclip/, '') || '/';\n const query = req.url.includes('?') ? '?' + req.url.split('?')[1] : '';\n const targetUrl = `http://localhost:${PAPERCLIP_PORT}${targetPath}${query}`;\n\n try {\n const headers = new Headers();\n for (const [k, v] of Object.entries(req.headers)) {\n if (v && k.toLowerCase() !== 'host') headers.set(k, Array.isArray(v) ? v[0] : v);\n }\n\n const upstream = await fetch(targetUrl, {\n method: req.method,\n headers: headers as any,\n body: ['GET', 'HEAD'].includes(req.method) ? undefined : JSON.stringify(req.body),\n });\n\n res.status(upstream.status);\n upstream.headers.forEach((v, k) => res.setHeader(k, v));\n const body = await upstream.arrayBuffer();\n res.end(Buffer.from(body));\n } catch (err) {\n logger.error(COMPONENT, `Paperclip UI proxy error: ${(err as Error).message}`);\n res.status(502).json({ error: 'Paperclip UI proxy error', message: (err as Error).message });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -156,7 +156,7 @@ class ConfigErrorRepair {
|
|
|
156
156
|
canHandle(issue) {
|
|
157
157
|
return issue.category === "config_error" && this.config.enabledCategories.includes("config_error");
|
|
158
158
|
}
|
|
159
|
-
planActions(
|
|
159
|
+
planActions(_issue) {
|
|
160
160
|
return [
|
|
161
161
|
{ type: "fix_config", description: `Regenerate package-lock.json and node_modules`, command: "npm ci" }
|
|
162
162
|
];
|
|
@@ -189,7 +189,7 @@ class BuildFailureRepair {
|
|
|
189
189
|
canHandle(issue) {
|
|
190
190
|
return issue.category === "build_failure" && this.config.enabledCategories.includes("build_failure");
|
|
191
191
|
}
|
|
192
|
-
planActions(
|
|
192
|
+
planActions(_issue) {
|
|
193
193
|
return [
|
|
194
194
|
{ type: "fix_config", description: "Clean install dependencies", command: "rm -rf node_modules && npm install" },
|
|
195
195
|
{ type: "restart", description: "Clear Next.js build cache", command: "rm -rf .next" }
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/auto-heal/repair-strategies.ts"],"sourcesContent":["import { execSync } from 'child_process';\nimport type {\n DependencyIssue,\n RepairAction,\n RepairResult,\n AutoHealConfig} from './types.js';\nimport {\n DEFAULT_AUTO_HEAL_CONFIG,\n} from './types.js';\n\nexport class MissingPackageRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'missing_package' && this.config.enabledCategories.includes('missing_package');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n const pkg = issue.packageName;\n if (!pkg) return [];\n return [\n { type: 'install', description: `Install missing package ${pkg}`, command: `npm install ${pkg}` },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class BrokenImportRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'broken_import' && this.config.enabledCategories.includes('broken_import');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n const actions: RepairAction[] = [];\n if (issue.packageName) {\n actions.push({ type: 'reinstall', description: `Reinstall ${issue.packageName} to fix broken import`, command: `npm install ${issue.packageName}@latest` });\n }\n if (issue.importPath) {\n actions.push({ type: 'fix_import', description: `Clear node_modules cache for ${issue.importPath}` });\n }\n return actions;\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class VersionMismatchRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'version_mismatch' && this.config.enabledCategories.includes('version_mismatch');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n const pkg = issue.packageName;\n const ver = issue.requiredVersion;\n if (!pkg) return [];\n const target = ver ? `${pkg}@${ver}` : `${pkg}@latest`;\n return [\n { type: 'update', description: `Update ${pkg} to ${ver || 'latest'}`, command: `npm install ${target}` },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class OrphanModuleRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'orphan_module' && this.config.enabledCategories.includes('orphan_module');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n const pkg = issue.packageName;\n if (!pkg) return [];\n return [\n { type: 'remove', description: `Remove orphan package ${pkg}`, command: `npm uninstall ${pkg}` },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class ConfigErrorRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'config_error' && this.config.enabledCategories.includes('config_error');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n return [\n { type: 'fix_config', description: `Regenerate package-lock.json and node_modules`, command: 'npm ci' },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 180000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class BuildFailureRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'build_failure' && this.config.enabledCategories.includes('build_failure');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n return [\n { type: 'fix_config', description: 'Clean install dependencies', command: 'rm -rf node_modules && npm install' },\n { type: 'restart', description: 'Clear Next.js build cache', command: 'rm -rf .next' },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 300000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/auto-heal/repair-strategies.ts"],"sourcesContent":["import { execSync } from 'child_process';\nimport type {\n DependencyIssue,\n RepairAction,\n RepairResult,\n AutoHealConfig} from './types.js';\nimport {\n DEFAULT_AUTO_HEAL_CONFIG,\n} from './types.js';\n\nexport class MissingPackageRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'missing_package' && this.config.enabledCategories.includes('missing_package');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n const pkg = issue.packageName;\n if (!pkg) return [];\n return [\n { type: 'install', description: `Install missing package ${pkg}`, command: `npm install ${pkg}` },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class BrokenImportRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'broken_import' && this.config.enabledCategories.includes('broken_import');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n const actions: RepairAction[] = [];\n if (issue.packageName) {\n actions.push({ type: 'reinstall', description: `Reinstall ${issue.packageName} to fix broken import`, command: `npm install ${issue.packageName}@latest` });\n }\n if (issue.importPath) {\n actions.push({ type: 'fix_import', description: `Clear node_modules cache for ${issue.importPath}` });\n }\n return actions;\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class VersionMismatchRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'version_mismatch' && this.config.enabledCategories.includes('version_mismatch');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n const pkg = issue.packageName;\n const ver = issue.requiredVersion;\n if (!pkg) return [];\n const target = ver ? `${pkg}@${ver}` : `${pkg}@latest`;\n return [\n { type: 'update', description: `Update ${pkg} to ${ver || 'latest'}`, command: `npm install ${target}` },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class OrphanModuleRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'orphan_module' && this.config.enabledCategories.includes('orphan_module');\n }\n\n planActions(issue: DependencyIssue): RepairAction[] {\n const pkg = issue.packageName;\n if (!pkg) return [];\n return [\n { type: 'remove', description: `Remove orphan package ${pkg}`, command: `npm uninstall ${pkg}` },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 120000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class ConfigErrorRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'config_error' && this.config.enabledCategories.includes('config_error');\n }\n\n planActions(_issue: DependencyIssue): RepairAction[] {\n return [\n { type: 'fix_config', description: `Regenerate package-lock.json and node_modules`, command: 'npm ci' },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 180000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}\n\nexport class BuildFailureRepair {\n private config: AutoHealConfig;\n\n constructor(config: AutoHealConfig = DEFAULT_AUTO_HEAL_CONFIG) {\n this.config = config;\n }\n\n canHandle(issue: DependencyIssue): boolean {\n return issue.category === 'build_failure' && this.config.enabledCategories.includes('build_failure');\n }\n\n planActions(_issue: DependencyIssue): RepairAction[] {\n return [\n { type: 'fix_config', description: 'Clean install dependencies', command: 'rm -rf node_modules && npm install' },\n { type: 'restart', description: 'Clear Next.js build cache', command: 'rm -rf .next' },\n ];\n }\n\n async execute(issue: DependencyIssue): Promise<RepairResult> {\n const startedAt = Date.now();\n const actions = this.planActions(issue);\n let output = '';\n let status: RepairResult['status'] = 'succeeded';\n\n if (this.config.dryRun) {\n return { issueId: issue.id, status: 'skipped', actions, output: 'DRY RUN', attempts: 1, startedAt, completedAt: Date.now() };\n }\n\n for (const action of actions) {\n if (!action.command) continue;\n try {\n output += execSync(action.command, { encoding: 'utf-8', timeout: 300000, stdio: ['pipe', 'pipe', 'pipe'] });\n } catch (err: any) {\n status = 'failed';\n output += err.stdout || '' + (err.stderr || err.message);\n }\n }\n\n return { issueId: issue.id, status, actions, output, attempts: 1, startedAt, completedAt: Date.now() };\n }\n}"],"mappings":";AAAA,SAAS,gBAAgB;AAMzB;AAAA,EACE;AAAA,OACK;AAEA,MAAM,qBAAqB;AAAA,EACxB;AAAA,EAER,YAAY,SAAyB,0BAA0B;AAC7D,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,UAAU,OAAiC;AACzC,WAAO,MAAM,aAAa,qBAAqB,KAAK,OAAO,kBAAkB,SAAS,iBAAiB;AAAA,EACzG;AAAA,EAEA,YAAY,OAAwC;AAClD,UAAM,MAAM,MAAM;AAClB,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,WAAO;AAAA,MACL,EAAE,MAAM,WAAW,aAAa,2BAA2B,GAAG,IAAI,SAAS,eAAe,GAAG,GAAG;AAAA,IAClG;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,OAA+C;AAC3D,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,UAAU,KAAK,YAAY,KAAK;AACtC,QAAI,SAAS;AACb,QAAI,SAAiC;AAErC,QAAI,KAAK,OAAO,QAAQ;AACtB,aAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,IAC7H;AAEA,eAAW,UAAU,SAAS;AAC5B,UAAI,CAAC,OAAO,QAAS;AACrB,UAAI;AACF,kBAAU,SAAS,OAAO,SAAS,EAAE,UAAU,SAAS,SAAS,MAAQ,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAAA,MAC5G,SAAS,KAAU;AACjB,iBAAS;AACT,kBAAU,IAAI,UAAU,MAAM,IAAI,UAAU,IAAI;AAAA,MAClD;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,SAAS,QAAQ,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,EACvG;AACF;AAEO,MAAM,mBAAmB;AAAA,EACtB;AAAA,EAER,YAAY,SAAyB,0BAA0B;AAC7D,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,UAAU,OAAiC;AACzC,WAAO,MAAM,aAAa,mBAAmB,KAAK,OAAO,kBAAkB,SAAS,eAAe;AAAA,EACrG;AAAA,EAEA,YAAY,OAAwC;AAClD,UAAM,UAA0B,CAAC;AACjC,QAAI,MAAM,aAAa;AACrB,cAAQ,KAAK,EAAE,MAAM,aAAa,aAAa,aAAa,MAAM,WAAW,yBAAyB,SAAS,eAAe,MAAM,WAAW,UAAU,CAAC;AAAA,IAC5J;AACA,QAAI,MAAM,YAAY;AACpB,cAAQ,KAAK,EAAE,MAAM,cAAc,aAAa,gCAAgC,MAAM,UAAU,GAAG,CAAC;AAAA,IACtG;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,QAAQ,OAA+C;AAC3D,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,UAAU,KAAK,YAAY,KAAK;AACtC,QAAI,SAAS;AACb,QAAI,SAAiC;AAErC,QAAI,KAAK,OAAO,QAAQ;AACtB,aAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,IAC7H;AAEA,eAAW,UAAU,SAAS;AAC5B,UAAI,CAAC,OAAO,QAAS;AACrB,UAAI;AACF,kBAAU,SAAS,OAAO,SAAS,EAAE,UAAU,SAAS,SAAS,MAAQ,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAAA,MAC5G,SAAS,KAAU;AACjB,iBAAS;AACT,kBAAU,IAAI,UAAU,MAAM,IAAI,UAAU,IAAI;AAAA,MAClD;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,SAAS,QAAQ,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,EACvG;AACF;AAEO,MAAM,sBAAsB;AAAA,EACzB;AAAA,EAER,YAAY,SAAyB,0BAA0B;AAC7D,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,UAAU,OAAiC;AACzC,WAAO,MAAM,aAAa,sBAAsB,KAAK,OAAO,kBAAkB,SAAS,kBAAkB;AAAA,EAC3G;AAAA,EAEA,YAAY,OAAwC;AAClD,UAAM,MAAM,MAAM;AAClB,UAAM,MAAM,MAAM;AAClB,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,UAAM,SAAS,MAAM,GAAG,GAAG,IAAI,GAAG,KAAK,GAAG,GAAG;AAC7C,WAAO;AAAA,MACL,EAAE,MAAM,UAAU,aAAa,UAAU,GAAG,OAAO,OAAO,QAAQ,IAAI,SAAS,eAAe,MAAM,GAAG;AAAA,IACzG;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,OAA+C;AAC3D,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,UAAU,KAAK,YAAY,KAAK;AACtC,QAAI,SAAS;AACb,QAAI,SAAiC;AAErC,QAAI,KAAK,OAAO,QAAQ;AACtB,aAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,IAC7H;AAEA,eAAW,UAAU,SAAS;AAC5B,UAAI,CAAC,OAAO,QAAS;AACrB,UAAI;AACF,kBAAU,SAAS,OAAO,SAAS,EAAE,UAAU,SAAS,SAAS,MAAQ,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAAA,MAC5G,SAAS,KAAU;AACjB,iBAAS;AACT,kBAAU,IAAI,UAAU,MAAM,IAAI,UAAU,IAAI;AAAA,MAClD;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,SAAS,QAAQ,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,EACvG;AACF;AAEO,MAAM,mBAAmB;AAAA,EACtB;AAAA,EAER,YAAY,SAAyB,0BAA0B;AAC7D,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,UAAU,OAAiC;AACzC,WAAO,MAAM,aAAa,mBAAmB,KAAK,OAAO,kBAAkB,SAAS,eAAe;AAAA,EACrG;AAAA,EAEA,YAAY,OAAwC;AAClD,UAAM,MAAM,MAAM;AAClB,QAAI,CAAC,IAAK,QAAO,CAAC;AAClB,WAAO;AAAA,MACL,EAAE,MAAM,UAAU,aAAa,yBAAyB,GAAG,IAAI,SAAS,iBAAiB,GAAG,GAAG;AAAA,IACjG;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,OAA+C;AAC3D,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,UAAU,KAAK,YAAY,KAAK;AACtC,QAAI,SAAS;AACb,QAAI,SAAiC;AAErC,QAAI,KAAK,OAAO,QAAQ;AACtB,aAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,IAC7H;AAEA,eAAW,UAAU,SAAS;AAC5B,UAAI,CAAC,OAAO,QAAS;AACrB,UAAI;AACF,kBAAU,SAAS,OAAO,SAAS,EAAE,UAAU,SAAS,SAAS,MAAQ,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAAA,MAC5G,SAAS,KAAU;AACjB,iBAAS;AACT,kBAAU,IAAI,UAAU,MAAM,IAAI,UAAU,IAAI;AAAA,MAClD;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,SAAS,QAAQ,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,EACvG;AACF;AAEO,MAAM,kBAAkB;AAAA,EACrB;AAAA,EAER,YAAY,SAAyB,0BAA0B;AAC7D,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,UAAU,OAAiC;AACzC,WAAO,MAAM,aAAa,kBAAkB,KAAK,OAAO,kBAAkB,SAAS,cAAc;AAAA,EACnG;AAAA,EAEA,YAAY,QAAyC;AACnD,WAAO;AAAA,MACL,EAAE,MAAM,cAAc,aAAa,iDAAiD,SAAS,SAAS;AAAA,IACxG;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,OAA+C;AAC3D,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,UAAU,KAAK,YAAY,KAAK;AACtC,QAAI,SAAS;AACb,QAAI,SAAiC;AAErC,QAAI,KAAK,OAAO,QAAQ;AACtB,aAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,IAC7H;AAEA,eAAW,UAAU,SAAS;AAC5B,UAAI,CAAC,OAAO,QAAS;AACrB,UAAI;AACF,kBAAU,SAAS,OAAO,SAAS,EAAE,UAAU,SAAS,SAAS,MAAQ,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAAA,MAC5G,SAAS,KAAU;AACjB,iBAAS;AACT,kBAAU,IAAI,UAAU,MAAM,IAAI,UAAU,IAAI;AAAA,MAClD;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,SAAS,QAAQ,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,EACvG;AACF;AAEO,MAAM,mBAAmB;AAAA,EACtB;AAAA,EAER,YAAY,SAAyB,0BAA0B;AAC7D,SAAK,SAAS;AAAA,EAChB;AAAA,EAEA,UAAU,OAAiC;AACzC,WAAO,MAAM,aAAa,mBAAmB,KAAK,OAAO,kBAAkB,SAAS,eAAe;AAAA,EACrG;AAAA,EAEA,YAAY,QAAyC;AACnD,WAAO;AAAA,MACL,EAAE,MAAM,cAAc,aAAa,8BAA8B,SAAS,qCAAqC;AAAA,MAC/G,EAAE,MAAM,WAAW,aAAa,6BAA6B,SAAS,eAAe;AAAA,IACvF;AAAA,EACF;AAAA,EAEA,MAAM,QAAQ,OAA+C;AAC3D,UAAM,YAAY,KAAK,IAAI;AAC3B,UAAM,UAAU,KAAK,YAAY,KAAK;AACtC,QAAI,SAAS;AACb,QAAI,SAAiC;AAErC,QAAI,KAAK,OAAO,QAAQ;AACtB,aAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,WAAW,SAAS,QAAQ,WAAW,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,IAC7H;AAEA,eAAW,UAAU,SAAS;AAC5B,UAAI,CAAC,OAAO,QAAS;AACrB,UAAI;AACF,kBAAU,SAAS,OAAO,SAAS,EAAE,UAAU,SAAS,SAAS,KAAQ,OAAO,CAAC,QAAQ,QAAQ,MAAM,EAAE,CAAC;AAAA,MAC5G,SAAS,KAAU;AACjB,iBAAS;AACT,kBAAU,IAAI,UAAU,MAAM,IAAI,UAAU,IAAI;AAAA,MAClD;AAAA,IACF;AAEA,WAAO,EAAE,SAAS,MAAM,IAAI,QAAQ,SAAS,QAAQ,UAAU,GAAG,WAAW,aAAa,KAAK,IAAI,EAAE;AAAA,EACvG;AACF;","names":[]}
|
|
@@ -230,13 +230,33 @@ class AnthropicProvider extends LLMProvider {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
async listModels() {
|
|
233
|
-
|
|
233
|
+
const FALLBACK = [
|
|
234
234
|
"claude-opus-4-0",
|
|
235
235
|
"claude-sonnet-4-20250514",
|
|
236
236
|
"claude-haiku-4-20250414",
|
|
237
237
|
"claude-3-5-sonnet-20241022",
|
|
238
238
|
"claude-3-5-haiku-20241022"
|
|
239
239
|
];
|
|
240
|
+
if (!this.apiKey) return FALLBACK;
|
|
241
|
+
try {
|
|
242
|
+
const response = await fetch(`${this.baseUrl}/v1/models?limit=1000`, {
|
|
243
|
+
headers: {
|
|
244
|
+
"x-api-key": this.apiKey,
|
|
245
|
+
"anthropic-version": "2023-06-01"
|
|
246
|
+
},
|
|
247
|
+
signal: AbortSignal.timeout(5e3)
|
|
248
|
+
});
|
|
249
|
+
if (!response.ok) {
|
|
250
|
+
logger.debug(COMPONENT, `listModels: ${response.status} from /v1/models, using fallback`);
|
|
251
|
+
return FALLBACK;
|
|
252
|
+
}
|
|
253
|
+
const data = await response.json();
|
|
254
|
+
const ids = (data.data || []).map((m) => m.id).filter(Boolean);
|
|
255
|
+
return ids.length > 0 ? ids : FALLBACK;
|
|
256
|
+
} catch (err) {
|
|
257
|
+
logger.debug(COMPONENT, `listModels failed: ${err.message}, using fallback`);
|
|
258
|
+
return FALLBACK;
|
|
259
|
+
}
|
|
240
260
|
}
|
|
241
261
|
async healthCheck() {
|
|
242
262
|
try {
|