/** * AgentGuard(TM) Spend: Advisor conversation state. * * Patent notice: Protected by U.S. patent-pending technology * (App. Nos. 63/983,615; 63/983,621; 63/983,843; 63/984,626; * 64/071,781; 64/071,789). */ import * as fs from 'fs'; import * as path from 'path'; import type { CapabilityTier, SpendCap, SpendPolicy } from '../types'; import { applyPostureCapability, normalizePosture, postureProfile, suggestPostureForVertical, type GovernancePosture } from './posture'; export type AdvisorQuestionId = 'building' | 'posture' | 'scale' | 'tasks' | 'budget' | 'confirm'; export interface AdvisorQuestion { id: AdvisorQuestionId; prompt: string; } export interface AdvisorAnswers { building?: string; scale?: string; tasks?: string; budget?: string; confirm?: string; language?: 'ts' | 'py'; posture?: GovernancePosture; } export interface AdvisorBusinessProfile { vertical: string; tenantId: string; teamSize: number; monthlyVolume: number; tasks: string[]; monthlyBudgetCents: number; requiredCapability: CapabilityTier; primaryModel: string; fallbackModel: string; hostedOssModel: string; offlineModel: string; baaCoveredModel: string; perCallCapCents: number; perDayCapCents: number; perMonthCapCents: number; scopeLabel: string; language: 'ts' | 'py'; posture: GovernancePosture; } export interface ProjectedSavingsRow { label: string; beforeCents: number; afterCents: number; savingsCents: number; } export interface ProjectedSavings { rows: ProjectedSavingsRow[]; monthlyBeforeCents: number; monthlyAfterCents: number; monthlySavingsCents: number; savingsPercent: number; } export const ADVISOR_QUESTIONS: AdvisorQuestion[] = [ { id: 'building', prompt: 'What are you building or running? Include your business type and the agent workflow.' }, { id: 'posture', prompt: 'Confirm or change governance posture: Velocity / Standard / Compliance.' }, { id: 'scale', prompt: 'How big is the team and roughly how many AI calls, tickets, orders, encounters, matters, or jobs happen per month?' }, { id: 'tasks', prompt: 'What are the top 3 AI tasks this agent will perform?' }, { id: 'budget', prompt: 'What monthly AI budget or per-task ceiling should this stay under?' }, { id: 'confirm', prompt: 'Confirm the setup or list any refinements before I write policy.yaml and quickstart code.' }, ]; const CHINESE_ORIGIN_MODEL_PATTERNS = [/kimi/i, /qwen/i, /deepseek/i, /(^|\/)yi([-/]|$)/i, /01-ai/i]; const HOSTED_OSS_DEFAULT = 'baseten/llama-4'; const HOSTED_OSS_VELOCITY = 'baseten/llama-4-13b'; const OFFLINE_DEFAULT = 'offline/llama-4-m4-ultra'; const BAA_COVERED_DEFAULT = 'anthropic.claude-sonnet-4-v1:0'; export function isChineseOriginModel(model: string): boolean { return CHINESE_ORIGIN_MODEL_PATTERNS.some((pattern) => pattern.test(model)); } export function enforceAdvisorModelPolicy(model: string, posture: GovernancePosture, replacement = 'anthropic/claude-haiku-4-5'): string { if (posture === 'compliance' && isChineseOriginModel(model)) { console.warn(`AgentGuard Advisor blocked ${model} for compliance posture and selected ${replacement}.`); return replacement; } return model; } function defaultModelsFor(vertical: string, hint: { primary: string; fallback: string }, posture: GovernancePosture): { primary: string; fallback: string; hostedOss: string; offline: string; baaCovered: string } { if (posture === 'velocity') { return { primary: HOSTED_OSS_VELOCITY, fallback: 'anthropic/claude-haiku-4-5', hostedOss: HOSTED_OSS_VELOCITY, offline: OFFLINE_DEFAULT, baaCovered: BAA_COVERED_DEFAULT }; } if (posture === 'compliance') { const regulatedPrimary = vertical === 'accounting' ? 'openai/gpt-5' : vertical === 'real-estate' ? 'google/gemini-3.1-pro-preview' : 'anthropic/claude-opus-4-7'; return { primary: regulatedPrimary, fallback: 'anthropic/claude-sonnet-4-6', hostedOss: HOSTED_OSS_DEFAULT, offline: OFFLINE_DEFAULT, baaCovered: BAA_COVERED_DEFAULT }; } return { primary: hint.primary, fallback: HOSTED_OSS_DEFAULT, hostedOss: HOSTED_OSS_DEFAULT, offline: OFFLINE_DEFAULT, baaCovered: BAA_COVERED_DEFAULT }; } const VERTICAL_HINTS: Array<{ vertical: string; needles: string[]; scopeLabel: string; capability: CapabilityTier; primary: string; fallback: string; perCall: number }> = [ { vertical: 'law-firm', needles: ['law', 'legal', 'attorney', 'paralegal', 'contract', 'matter', 'discovery'], scopeLabel: 'matter', capability: 'data_write', primary: 'anthropic/claude-sonnet-4-6', fallback: 'anthropic/claude-haiku-4-5', perCall: 300 }, { vertical: 'insurance', needles: ['insurance', 'carrier', 'producer', 'agency', 'policy comparison', 'quote letter', 'certificate of insurance', 'renewal', 'e&o'], scopeLabel: 'policy', capability: 'data_write', primary: 'anthropic/claude-sonnet-4-6', fallback: 'baseten/llama-4', perCall: 150 }, { vertical: 'healthcare', needles: ['health', 'medical', 'dental', 'clinic', 'patient', 'chart', 'hipaa', 'encounter'], scopeLabel: 'encounter', capability: 'data_write', primary: 'anthropic.claude-sonnet-4-v1:0', fallback: 'amazon.nova-lite-v1:0', perCall: 300 }, { vertical: 'ecommerce', needles: ['shopify', 'commerce', 'store', 'order', 'refund', 'chargeback', 'fraud', 'returns'], scopeLabel: 'order', capability: 'payment_initiate', primary: 'openai/gpt-5-mini', fallback: 'openai/gpt-4o-mini', perCall: 25 }, { vertical: 'accounting', needles: ['accounting', 'bookkeeping', 'tax', 'ledger', 'receipt', 'close', 'audit', 'sox'], scopeLabel: 'engagement', capability: 'data_write', primary: 'anthropic/claude-sonnet-4-6', fallback: 'anthropic/claude-haiku-4-5', perCall: 150 }, { vertical: 'software', needles: ['software', 'code', 'developer', 'github', 'repo', 'pull request', 'pr'], scopeLabel: 'repo', capability: 'read_only', primary: 'google/gemini-3-flash-preview', fallback: 'openai/gpt-4o-mini', perCall: 10 }, { vertical: 'real-estate', needles: ['real estate', 'broker', 'listing', 'mortgage', 'tenant', 'lease', 'property'], scopeLabel: 'transaction', capability: 'data_write', primary: 'openai/gpt-5-mini', fallback: 'openai/gpt-4o-mini', perCall: 50 }, { vertical: 'marketing', needles: ['marketing', 'agency', 'content', 'campaign', 'brand'], scopeLabel: 'campaign', capability: 'data_write', primary: 'openai/gpt-5-mini', fallback: 'openai/gpt-4o-mini', perCall: 25 }, { vertical: 'local-services', needles: ['salon', 'gym', 'restaurant', 'landscaping', 'construction', 'photography', 'fitness', 'pet'], scopeLabel: 'job', capability: 'data_write', primary: 'openai/gpt-5-mini', fallback: 'openai/gpt-4o-mini', perCall: 25 }, ]; export class AdvisorConversation { private answers: AdvisorAnswers = {}; private index = 0; constructor(initial?: AdvisorAnswers) { if (initial) this.answers = { ...initial }; this.index = firstMissingIndex(this.answers); } currentQuestion(): AdvisorQuestion | null { const question = ADVISOR_QUESTIONS[this.index] ?? null; if (question?.id !== 'posture') return question; const vertical = detectVertical(this.answers.building ?? ''); const suggested = suggestPostureForVertical(vertical); return { id: 'posture', prompt: `Based on ${vertical}, we suggest ${suggested} governance posture. Confirm or change: Velocity / Standard / Compliance.`, }; } answer(value: string): void { const question = this.currentQuestion(); if (!question) return; if (question.id === 'posture') { this.answers.posture = normalizePosture(value) ?? suggestedPostureForText(this.answers.building ?? ''); return; } this.answers[question.id] = value.trim(); } next(): AdvisorQuestion | null { this.index = Math.min(this.index + 1, ADVISOR_QUESTIONS.length); return this.currentQuestion(); } back(): AdvisorQuestion | null { this.index = Math.max(0, this.index - 1); return this.currentQuestion(); } isComplete(): boolean { return ADVISOR_QUESTIONS.every((question) => Boolean(this.answers[question.id]?.trim())); } snapshot(): AdvisorAnswers { return { ...this.answers }; } setPosture(posture: GovernancePosture): void { this.answers.posture = posture; } profile(cwd = process.cwd()): AdvisorBusinessProfile { return buildBusinessProfile(this.answers, cwd); } } export function detectVertical(text: string): string { const joined = text.toLowerCase(); return (VERTICAL_HINTS.find((candidate) => candidate.needles.some((needle) => joined.includes(needle))) ?? VERTICAL_HINTS[0]!).vertical; } export function suggestedPostureForText(text: string): GovernancePosture { return suggestPostureForVertical(detectVertical(text)); } export function buildBusinessProfile(answers: AdvisorAnswers, cwd = process.cwd()): AdvisorBusinessProfile { const joined = `${answers.building ?? ''} ${answers.scale ?? ''} ${answers.tasks ?? ''}`.toLowerCase(); const hint = VERTICAL_HINTS.find((candidate) => candidate.needles.some((needle) => joined.includes(needle))) ?? VERTICAL_HINTS[0]!; const posture = answers.posture ?? suggestPostureForVertical(hint.vertical); const scale = parseScale(answers.scale ?? ''); const budgetCents = parseBudgetCents(answers.budget ?? '', Math.max(4900, Math.ceil(scale.monthlyVolume * hint.perCall * 0.35))); const tasks = parseTasks(answers.tasks ?? answers.building ?? hint.vertical); const perMonthCapCents = Math.max(1000, budgetCents); const perDayCapCents = Math.max(hint.perCall * 10, Math.ceil(perMonthCapCents / 20)); const models = defaultModelsFor(hint.vertical, hint, posture); return { vertical: hint.vertical, tenantId: tenantIdFromBusiness(answers.building ?? hint.vertical), teamSize: scale.teamSize, monthlyVolume: scale.monthlyVolume, tasks, monthlyBudgetCents: perMonthCapCents, requiredCapability: capabilityFor(joined, hint.capability, posture), primaryModel: enforceAdvisorModelPolicy(models.primary, posture), fallbackModel: enforceAdvisorModelPolicy(models.fallback, posture), hostedOssModel: enforceAdvisorModelPolicy(models.hostedOss, posture), offlineModel: models.offline, baaCoveredModel: enforceAdvisorModelPolicy(models.baaCovered, posture), perCallCapCents: hint.perCall, perDayCapCents, perMonthCapCents, scopeLabel: hint.scopeLabel, language: answers.language ?? detectProjectLanguage(cwd), posture, }; } export function buildPolicyFromProfile(profile: AdvisorBusinessProfile): SpendPolicy { const posture = postureProfile(profile.posture); const conservativeBlock = profile.posture === 'compliance' && profile.requiredCapability !== 'read_only'; const perCallAmount = profile.posture === 'velocity' ? Math.max(1, Math.ceil(profile.perCallCapCents * 0.6)) : profile.perCallCapCents; const caps: SpendCap[] = [ conservativeBlock ? { amountCents: perCallAmount, window: 'per_call', action: 'block', reason: 'Per-call regulated capability budget reached', } : { amountCents: perCallAmount, window: 'per_call', action: 'downgrade', downgradeTo: profile.fallbackModel, reason: 'Per-call budget reached, route to fallback model', }, { amountCents: profile.perDayCapCents, window: 'per_day', action: 'block', reason: 'Daily budget reached', }, { amountCents: profile.perMonthCapCents, window: 'per_month', action: 'block', reason: 'Monthly budget reached', }, ]; return { id: `advisor-${profile.vertical}-v1`, name: `Advisor generated ${profile.vertical} policy`, scope: { tenantId: profile.tenantId }, caps, mode: posture.defaultMode, requiredCapability: profile.requiredCapability, version: 1, effectiveFrom: new Date().toISOString(), }; } export function projectedSavings(profile: AdvisorBusinessProfile): ProjectedSavings { const heavyPerCall = Math.max(profile.perCallCapCents * 4, 100); const routedPerCall = Math.max(1, Math.ceil(profile.perCallCapCents * 0.45)); const monthlyBeforeCents = profile.monthlyVolume * heavyPerCall; const monthlyAfterCents = Math.min(profile.perMonthCapCents, profile.monthlyVolume * routedPerCall); const monthlySavingsCents = Math.max(0, monthlyBeforeCents - monthlyAfterCents); return { rows: [ { label: 'All traffic on premium model', beforeCents: monthlyBeforeCents, afterCents: 0, savingsCents: 0 }, { label: 'Task routed with AgentGuard caps', beforeCents: 0, afterCents: monthlyAfterCents, savingsCents: monthlySavingsCents }, ], monthlyBeforeCents, monthlyAfterCents, monthlySavingsCents, savingsPercent: monthlyBeforeCents > 0 ? Math.round((monthlySavingsCents / monthlyBeforeCents) * 100) : 0, }; } export function formatCents(cents: number): string { return `$${(cents / 100).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; } export function detectProjectLanguage(cwd: string): 'ts' | 'py' { try { if (fs.existsSync(path.join(cwd, 'pyproject.toml')) || fs.existsSync(path.join(cwd, 'requirements.txt'))) return 'py'; if (fs.existsSync(path.join(cwd, 'package.json'))) return 'ts'; } catch { return 'ts'; } return 'ts'; } function firstMissingIndex(answers: AdvisorAnswers): number { const index = ADVISOR_QUESTIONS.findIndex((question) => !answers[question.id]?.trim()); return index === -1 ? ADVISOR_QUESTIONS.length : index; } function parseScale(value: string): { teamSize: number; monthlyVolume: number } { const numbers = (value.match(/\d[\d,]*/g) ?? []).map((part) => Number(part.replace(/,/g, ''))).filter(Number.isFinite); const teamSize = Math.max(1, numbers[0] ?? 3); const monthlyVolume = Math.max(25, numbers[1] ?? numbers[0] ?? 500); return { teamSize, monthlyVolume }; } function parseBudgetCents(value: string, fallback: number): number { const money = value.match(/\$?\s*(\d[\d,]*(?:\.\d{1,2})?)/); if (!money) return fallback; const amount = Number(money[1]!.replace(/,/g, '')); if (!Number.isFinite(amount) || amount <= 0) return fallback; return Math.round(amount * 100); } function parseTasks(value: string): string[] { const parts = value.split(/,|\n|;| and /i).map((part) => part.trim()).filter(Boolean); return parts.length > 0 ? parts.slice(0, 3) : ['review requests', 'route models', 'write audit receipts']; } function tenantIdFromBusiness(value: string): string { const cleaned = value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40); return cleaned || 'local-business'; } function capabilityFor(text: string, fallback: CapabilityTier, posture: GovernancePosture): CapabilityTier { return applyPostureCapability(posture, text, fallback); }