/** * AgentGuard(TM) Spend: Advisor output writers. * * Files are written locally under ~/.agentguard by default. Conversation logs * stay in ~/.agentguard/advisor-sessions and are never uploaded by this SDK. * * 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 os from 'os'; import * as path from 'path'; import type { SpendPolicy } from '../types'; import { buildPolicyFromProfile, formatCents, projectedSavings, type AdvisorBusinessProfile, type ProjectedSavings, } from './conversation'; import { postureProfile } from './posture'; export interface AdvisorOutputOptions { home?: string; now?: Date; language?: 'ts' | 'py'; overwrite?: boolean; } export interface AdvisorOutputs { policy: SpendPolicy; policyPath: string; quickstartPath: string; sessionDir: string; savings: ProjectedSavings; policyYaml: string; quickstartCode: string; savingsTable: string; } export interface AdvisorSessionLogger { path: string; append: (event: string, payload?: Record) => void; } export function agentguardHome(): string { return process.env.AGENTGUARD_HOME || path.join(os.homedir(), '.agentguard'); } export function advisorSessionDir(home = agentguardHome()): string { return path.join(home, 'advisor-sessions'); } export function createAdvisorSessionLogger(home = agentguardHome(), now = new Date()): AdvisorSessionLogger { const dir = advisorSessionDir(home); fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); const stamp = now.toISOString().replace(/[:.]/g, '-'); const file = path.join(dir, `${stamp}.jsonl`); fs.closeSync(fs.openSync(file, 'a', 0o600)); return { path: file, append(event: string, payload: Record = {}) { const line = JSON.stringify({ ts: new Date().toISOString(), event, ...payload }) + '\n'; fs.appendFileSync(file, line, { encoding: 'utf8', mode: 0o600 }); }, }; } export function writeAdvisorOutputs(profile: AdvisorBusinessProfile, options: AdvisorOutputOptions = {}): AdvisorOutputs { const home = options.home ?? agentguardHome(); const language = options.language ?? profile.language; const sessionDir = advisorSessionDir(home); fs.mkdirSync(home, { recursive: true, mode: 0o700 }); fs.mkdirSync(sessionDir, { recursive: true, mode: 0o700 }); const policy = buildPolicyFromProfile(profile); const policyPath = path.join(home, 'policy.yaml'); const quickstartPath = path.join(home, language === 'py' ? 'quickstart.py' : 'quickstart.ts'); const savings = projectedSavings(profile); const policyYaml = renderPolicyYaml(policy, profile, options.now ?? new Date()); const quickstartCode = language === 'py' ? renderQuickstartPy(policy, profile) : renderQuickstartTs(policy, profile); const savingsTable = renderSavingsTable(savings); if (!options.overwrite) { assertWritableTarget(policyPath); assertWritableTarget(quickstartPath); } atomicWrite(policyPath, policyYaml, 0o600); atomicWrite(quickstartPath, quickstartCode, 0o600); return { policy, policyPath, quickstartPath, sessionDir, savings, policyYaml, quickstartCode, savingsTable }; } export function renderPolicyYaml(policy: SpendPolicy, profile: AdvisorBusinessProfile, now = new Date()): string { const caps = policy.caps.map((cap) => { const lines = [ ` # WHY: ${whyForWindow(cap.window)}`, ` - amountCents: ${cap.amountCents}`, ` window: ${cap.window}`, ` action: ${cap.action}`, ]; if (cap.downgradeTo) lines.push(` downgradeTo: ${cap.downgradeTo}`); if (cap.reason) lines.push(` reason: ${quoteYaml(cap.reason)}`); return lines.join('\n'); }).join('\n'); const tasks = profile.tasks.map((task) => ` - ${quoteYaml(task)}`).join('\n'); const posture = postureProfile(profile.posture); const canary = posture.canaryPercent === undefined ? '' : ` canaryPercent: ${posture.canaryPercent}\n`; return `# AgentGuard Spend policy generated by agentguard advisor # Generated at: ${now.toISOString()} # Scope key: ${profile.scopeLabel} id: ${policy.id} name: ${quoteYaml(policy.name)} version: ${policy.version} effectiveFrom: ${quoteYaml(policy.effectiveFrom)} mode: ${policy.mode} requiredCapability: ${policy.requiredCapability ?? 'read_only'} scope: tenantId: ${quoteYaml(policy.scope.tenantId)} models: cloudFrontierPrimary: ${profile.primaryModel} hostedOssFallback: ${profile.hostedOssModel} offlineOption: ${profile.offlineModel} baaCoveredOption: ${profile.baaCoveredModel} primary: ${profile.primaryModel} fallback: ${profile.fallbackModel} governancePosture: posture: ${profile.posture} auditRetentionDays: ${posture.auditRetentionDays} approvalGates: ${posture.approvalGates} downgradeStyle: ${posture.downgradeStyle} ${canary}reviewerCascade: default: ${profile.posture === 'compliance' ? 'always' : 'risk_triggered'} triageClassifier: local-3b tasks: ${tasks} caps: ${caps} systemInstructions: | ${indent(systemInstructions(profile), 2)} `; } export function renderQuickstartTs(policy: SpendPolicy, profile: AdvisorBusinessProfile): string { return `import OpenAI from 'openai';\nimport { withSpendGuard, type SpendPolicy } from '@agentguard-run/spend';\n\nconst policy: SpendPolicy = ${JSON.stringify(policy, null, 2)};\n\nconst openrouter = new OpenAI({\n baseURL: 'https://openrouter.ai/api/v1',\n apiKey: process.env.OPENROUTER_API_KEY,\n});\n\nexport const guardedClient = withSpendGuard(openrouter, {\n policy,\n scope: { tenantId: '${escapeTs(profile.tenantId)}', agentId: '${escapeTs(profile.vertical)}' },\n capabilityClaim: '${policy.requiredCapability ?? 'read_only'}',\n});\n\nexport async function runGuardedTask(prompt: string) {\n return guardedClient.chat.completions.create({\n model: '${escapeTs(profile.primaryModel)}',\n messages: [{ role: 'user', content: prompt }],\n });\n}\n`; } export function renderQuickstartPy(policy: SpendPolicy, profile: AdvisorBusinessProfile): string { const caps = policy.caps.map((cap) => { const args = [`amountCents=${cap.amountCents}`, `window='${cap.window}'`, `action='${cap.action}'`]; if (cap.downgradeTo) args.push(`downgradeTo='${escapePy(cap.downgradeTo)}'`); return ` SpendCap(${args.join(', ')}),`; }).join('\n'); return `from openai import OpenAI\nfrom agentguard_spend import with_spend_guard\nfrom agentguard_spend.types import SpendPolicy, SpendCap\n\npolicy = SpendPolicy(\n id='${escapePy(policy.id)}',\n name='${escapePy(policy.name)}',\n scope={'tenantId': '${escapePy(profile.tenantId)}'},\n caps=[\n${caps}\n ],\n mode='${policy.mode}',\n requiredCapability='${policy.requiredCapability ?? 'read_only'}',\n version=${policy.version},\n effectiveFrom='${escapePy(policy.effectiveFrom)}',\n)\n\nopenrouter = OpenAI(base_url='https://openrouter.ai/api/v1')\nguarded_client = with_spend_guard(\n openrouter,\n policy=policy,\n scope={'tenantId': '${escapePy(profile.tenantId)}', 'agentId': '${escapePy(profile.vertical)}'},\n capability_claim='${policy.requiredCapability ?? 'read_only'}',\n)\n`; } export function renderSavingsTable(savings: ProjectedSavings): string { const rows = [ 'Projected monthly savings', `Before AgentGuard: ${formatCents(savings.monthlyBeforeCents)}`, `After AgentGuard: ${formatCents(savings.monthlyAfterCents)}`, `Savings: ${formatCents(savings.monthlySavingsCents)} (${savings.savingsPercent}%)`, ]; return rows.join('\n'); } function atomicWrite(file: string, content: string, mode: number): void { const temp = `${file}.${process.pid}.${Date.now()}.tmp`; fs.writeFileSync(temp, content, { encoding: 'utf8', mode }); fs.renameSync(temp, file); fs.chmodSync(file, mode); } function assertWritableTarget(file: string): void { if (!fs.existsSync(file)) return; fs.accessSync(file, fs.constants.W_OK); } function whyForWindow(window: string): string { if (window === 'per_call') return 'Bounds one agent action and routes overflow to the fallback model.'; if (window === 'per_day') return 'Catches runaway loops and unexpected daily volume.'; if (window === 'per_month') return 'Keeps the finance view predictable for the billing cycle.'; return 'Limits spend inside the selected time window.'; } function systemInstructions(profile: AdvisorBusinessProfile): string { const posture = postureProfile(profile.posture); return `Run ${profile.vertical} tasks with ${profile.requiredCapability} capability under ${posture.label} governance posture. Prefer ${profile.primaryModel} for high-value work, ${profile.hostedOssModel} for hosted OSS fallback, ${profile.offlineModel} for offline runs, and ${profile.baaCoveredModel} when a BAA-covered provider is required. Keep evidence pointers in outputs and escalate anything outside the configured capability tier.`; } function quoteYaml(value: string): string { return JSON.stringify(value); } function indent(value: string, spaces: number): string { const prefix = ' '.repeat(spaces); return value.split('\n').map((line) => prefix + line).join('\n'); } function escapeTs(value: string): string { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); } function escapePy(value: string): string { return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); }