import { randomUUID } from 'crypto'; declare const fetch: (input: string, init?: { method?: string; headers?: Record; body?: string }) => Promise<{ ok: boolean; status: number; json(): Promise }>; import { computeChainHash, validateReceiptChain } from './chain-validator'; import { AgentGuardBudgetCapError, AgentGuardChainCorruptError, AgentGuardDurationCapError, AgentGuardWorkflowStateError } from './errors'; import { buildReceipt, roundUsd } from './receipt'; import type { CheckpointReceipt, ReceiptV2, WorkflowConfig, WorkflowState, WorkflowStatus } from './types'; interface WorkflowCreateResponse { workflow_id: string; created_at: string; state?: WorkflowState; total_spend_usd?: number; outcome_count?: number; last_receipt_id?: string | null; last_checkpoint_id?: string | null; last_chain_hash?: string | null; receipts?: ReceiptV2[]; } export class WorkflowContext { public readonly workflow_id: string; public readonly user_id: string; public readonly config: WorkflowConfig; public readonly created_at: Date; private _total_spend_usd = 0; private _outcome_count = 0; private _last_receipt_id: string | null = null; private _last_checkpoint_id: string | null = null; private _last_chain_hash: string | null = null; private _state: WorkflowState = 'active'; private readonly _receipts: ReceiptV2[] = []; private constructor(args: { workflow_id: string; user_id: string; config: WorkflowConfig; created_at: Date; state?: WorkflowState; total_spend_usd?: number; outcome_count?: number; last_receipt_id?: string | null; last_checkpoint_id?: string | null; last_chain_hash?: string | null; receipts?: ReceiptV2[]; }) { this.workflow_id = args.workflow_id; this.user_id = args.user_id; this.config = args.config; this.created_at = args.created_at; this._state = args.state ?? 'active'; this._total_spend_usd = args.total_spend_usd ?? 0; this._outcome_count = args.outcome_count ?? 0; this._last_receipt_id = args.last_receipt_id ?? null; this._last_checkpoint_id = args.last_checkpoint_id ?? null; this._last_chain_hash = args.last_chain_hash ?? null; this._receipts.push(...(args.receipts ?? [])); } static async create(config: WorkflowConfig): Promise { validateConfig(config); const userId = String(config.metadata?.user_id || config.user_id || 'local-user'); if (config.resume_if_exists) { const existing = await tryResume(config, userId); if (existing) return existing; } const apiBase = resolveApiBase(config); if (apiBase) { const body = { name: config.name, budget_cap_usd: config.budget_cap_usd, duration_cap_hours: config.duration_cap_hours, checkpoint_every_outcomes: config.checkpoint_every_outcomes ?? 25, parent_outcomes: config.parent_outcomes, metadata: config.metadata, }; const created = await postJson(config, `${apiBase}/api/workflows/create`, body); return new WorkflowContext({ workflow_id: created.workflow_id, user_id: userId, config, created_at: new Date(created.created_at), state: created.state, total_spend_usd: created.total_spend_usd, outcome_count: created.outcome_count, last_receipt_id: created.last_receipt_id, last_checkpoint_id: created.last_checkpoint_id, last_chain_hash: created.last_chain_hash, receipts: created.receipts, }); } return new WorkflowContext({ workflow_id: `ag_w_${randomUUID().replace(/-/g, '').slice(0, 16)}`, user_id: userId, config, created_at: new Date(), }); } static fromCheckpoint(config: WorkflowConfig, existing: WorkflowCreateResponse, userId = 'local-user'): WorkflowContext { return new WorkflowContext({ workflow_id: existing.workflow_id, user_id: userId, config, created_at: new Date(existing.created_at), state: existing.state ?? 'active', total_spend_usd: existing.total_spend_usd, outcome_count: existing.outcome_count, last_receipt_id: existing.last_receipt_id, last_checkpoint_id: existing.last_checkpoint_id, last_chain_hash: existing.last_chain_hash, receipts: existing.receipts, }); } async outcome(name: string, fn: () => Promise): Promise { this.assertActive(); this.assertOutcomeAllowed(name); this.assertBudgetRemaining(); this.assertDurationRemaining(); const result = await fn(); const cost = this.extractOutcomeCost(result); const parentReceiptId = this._last_receipt_id; const chainHash = this._receipts.length ? await computeChainHash(this._receipts[this._receipts.length - 1]!) : null; const receipt = await buildReceipt({ workflow_id: this.workflow_id, outcome_name: name, user_id: this.user_id, cost_usd: cost, parent_receipt_id: parentReceiptId, chain_validation_hash: chainHash, workflow_checkpoint_idx: null, workflow_total_spend_usd_to_date: this._total_spend_usd + cost, reviewer_cascade: extractReviewerCascade(result), signingKeys: this.config.signingKeys, }); this._receipts.push(receipt); this._total_spend_usd = roundUsd(this._total_spend_usd + cost); this._outcome_count += 1; this._last_receipt_id = receipt.receipt_id; this._last_chain_hash = await computeChainHash(receipt); await this.postReceipt(receipt); if (this._outcome_count % (this.config.checkpoint_every_outcomes ?? 25) === 0) { await this.writeCheckpoint(); } await this.checkPostCallCaps(); return result; } async checkpoint(label?: string): Promise { return this.writeCheckpoint(label); } async cancel(reason: string): Promise { this._state = 'cancelled'; await this.writeFinalReceipt('cancelled', reason); } async finalize(): Promise { if (this._state === 'active') { this._state = 'completed'; await this.writeFinalReceipt('completed', 'workflow completed'); } } status(): WorkflowStatus { const elapsed = Date.now() - this.created_at.getTime(); const capMs = this.config.duration_cap_hours * 3_600_000; return { workflow_id: this.workflow_id, state: this._state, total_spend_usd: this._total_spend_usd, budget_cap_usd: this.config.budget_cap_usd, budget_remaining_usd: roundUsd(Math.max(0, this.config.budget_cap_usd - this._total_spend_usd)), budget_pct_used: this.config.budget_cap_usd === 0 ? 1 : this._total_spend_usd / this.config.budget_cap_usd, outcome_count: this._outcome_count, last_receipt_id: this._last_receipt_id, last_checkpoint_id: this._last_checkpoint_id, last_chain_hash: this._last_chain_hash, duration_elapsed_ms: elapsed, duration_cap_ms: capMs, duration_remaining_ms: Math.max(0, capMs - elapsed), }; } receipts(): ReceiptV2[] { return [...this._receipts]; } private async writeCheckpoint(label?: string): Promise { const chainHash = this._receipts.length ? await computeChainHash(this._receipts[this._receipts.length - 1]!) : null; const checkpoint = await buildReceipt({ workflow_id: this.workflow_id, outcome_name: 'CHECKPOINT', user_id: this.user_id, cost_usd: 0, parent_receipt_id: this._last_receipt_id, chain_validation_hash: chainHash, workflow_checkpoint_idx: this._outcome_count, workflow_total_spend_usd_to_date: this._total_spend_usd, is_checkpoint: true, checkpoint_label: label ?? null, receipt_type: 'checkpoint', signingKeys: this.config.signingKeys, }) as CheckpointReceipt; this._receipts.push(checkpoint); this._last_checkpoint_id = checkpoint.receipt_id; this._last_receipt_id = checkpoint.receipt_id; this._last_chain_hash = await computeChainHash(checkpoint); await this.postReceipt(checkpoint); return checkpoint; } private async writeFinalReceipt(type: 'cancelled' | 'completed' | 'cap_hit', reason: string): Promise { const chainHash = this._receipts.length ? await computeChainHash(this._receipts[this._receipts.length - 1]!) : null; const receipt = await buildReceipt({ workflow_id: this.workflow_id, outcome_name: type === 'completed' ? 'WORKFLOW_COMPLETED' : 'WORKFLOW_STOPPED', user_id: this.user_id, cost_usd: 0, parent_receipt_id: this._last_receipt_id, chain_validation_hash: chainHash, workflow_checkpoint_idx: this._outcome_count, workflow_total_spend_usd_to_date: this._total_spend_usd, receipt_type: type === 'cancelled' ? 'cancel' : type, cancelled_reason: reason, signingKeys: this.config.signingKeys, }); this._receipts.push(receipt); this._last_receipt_id = receipt.receipt_id; this._last_chain_hash = await computeChainHash(receipt); await this.postReceipt(receipt); } private assertActive(): void { if (this._state === 'budget_capped') { throw new AgentGuardBudgetCapError( `Workflow ${this.workflow_id} exceeded budget cap of $${this.config.budget_cap_usd}. Total spend to date: $${this._total_spend_usd}.`, { workflow_id: this.workflow_id, total_spend_usd: this._total_spend_usd }, ); } if (this._state === 'duration_capped') { throw new AgentGuardDurationCapError(`Workflow ${this.workflow_id} exceeded duration cap.`, { workflow_id: this.workflow_id, elapsed_ms: Date.now() - this.created_at.getTime(), }); } if (this._state !== 'active') { throw new AgentGuardWorkflowStateError(`Workflow ${this.workflow_id} is ${this._state}.`, { workflow_id: this.workflow_id, state: this._state, }); } } private assertOutcomeAllowed(name: string): void { if (this.config.parent_outcomes && !this.config.parent_outcomes.includes(name)) { throw new AgentGuardWorkflowStateError(`Outcome ${name} is not allowed in workflow ${this.workflow_id}.`, { workflow_id: this.workflow_id, state: this._state, }); } } private assertBudgetRemaining(): void { if (this._total_spend_usd >= this.config.budget_cap_usd) { this._state = 'budget_capped'; throw new AgentGuardBudgetCapError( `Workflow ${this.workflow_id} exceeded budget cap of $${this.config.budget_cap_usd}. Total spend to date: $${this._total_spend_usd}.`, { workflow_id: this.workflow_id, total_spend_usd: this._total_spend_usd }, ); } } private assertDurationRemaining(): void { const elapsed = Date.now() - this.created_at.getTime(); const cap = this.config.duration_cap_hours * 3_600_000; if (elapsed >= cap) { this._state = 'duration_capped'; throw new AgentGuardDurationCapError(`Workflow ${this.workflow_id} exceeded duration cap.`, { workflow_id: this.workflow_id, elapsed_ms: elapsed, }); } } private async checkPostCallCaps(): Promise { if (this._total_spend_usd >= this.config.budget_cap_usd) { this._state = 'budget_capped'; await this.writeFinalReceipt('cap_hit', 'budget cap reached'); } const elapsed = Date.now() - this.created_at.getTime(); if (elapsed >= this.config.duration_cap_hours * 3_600_000) { this._state = 'duration_capped'; await this.writeFinalReceipt('cap_hit', 'duration cap reached'); } } private extractOutcomeCost(result: unknown): number { if (isRecord(result)) { if (typeof result.cost_usd === 'number') return result.cost_usd; const receipt = result.receipt; if (isRecord(receipt) && typeof receipt.cost_usd === 'number') return receipt.cost_usd; } const configured = this.config.metadata?.default_outcome_cost_usd; return typeof configured === 'number' ? configured : 0; } private async postReceipt(receipt: ReceiptV2): Promise { const apiBase = resolveApiBase(this.config); if (!apiBase) return; await postJson(this.config, `${apiBase}/api/workflows/${this.workflow_id}/receipt`, { receipt }); } } export async function workflow(config: WorkflowConfig, fn: (ctx: WorkflowContext) => Promise): Promise { const ctx = await WorkflowContext.create(config); try { return await fn(ctx); } finally { await ctx.finalize(); } } async function tryResume(config: WorkflowConfig, userId: string): Promise { const apiBase = resolveApiBase(config); if (!apiBase) return null; const headers = authHeaders(config); const existing = await getJson( config, `${apiBase}/api/workflows/lookup?name=${encodeURIComponent(config.name)}`, headers, ); if (!existing || ['cancelled', 'completed', 'budget_capped', 'duration_capped'].includes(existing.state ?? '')) { return null; } const chain = await getJson<{ receipts: ReceiptV2[] }>( config, `${apiBase}/api/workflows/${existing.workflow_id}/chain`, headers, ); const validation = await validateReceiptChain(chain.receipts); if (!validation.ok) { throw new AgentGuardChainCorruptError(`Cannot resume workflow ${existing.workflow_id}: chain validation failed.`, { workflow_id: existing.workflow_id, broken_at: validation.broken_at, reason: validation.reason, }); } return WorkflowContext.fromCheckpoint(config, { ...existing, receipts: chain.receipts }, userId); } function resolveApiBase(config: WorkflowConfig): string | null { const value = config.api_base_url || process.env.AGENTGUARD_WORKFLOW_API_BASE || ''; return value ? value.replace(/\/$/, '') : null; } function authHeaders(config: WorkflowConfig): Record { const license = config.license_key || process.env.AGENTGUARD_LICENSE_KEY || ''; return { 'content-type': 'application/json', ...(license ? { authorization: `Bearer ${license}` } : {}), }; } async function postJson(config: WorkflowConfig, url: string, body: unknown): Promise { const headers = authHeaders(config); if (config.post_json) return config.post_json(url, body, headers) as Promise; const response = await fetch(url, { method: 'POST', headers, body: JSON.stringify(body) }); if (!response.ok) throw new Error(`Workflow API request failed: ${response.status}`); return response.json() as Promise; } async function getJson(config: WorkflowConfig, url: string, headers: Record): Promise { if (config.get_json) return config.get_json(url, headers) as Promise; const response = await fetch(url, { headers }); if (response.status === 404) return null as T; if (!response.ok) throw new Error(`Workflow API request failed: ${response.status}`); return response.json() as Promise; } function extractReviewerCascade(result: unknown): ReceiptV2['reviewer_cascade'] { if (!isRecord(result)) return null; const value = result.reviewer_cascade; if (!isRecord(value)) return null; return { triggered: value.triggered === true, drafter_model: typeof value.drafter_model === 'string' ? value.drafter_model : null, reviewer_model: typeof value.reviewer_model === 'string' ? value.reviewer_model : null, drafter_output_hash: typeof value.drafter_output_hash === 'string' ? value.drafter_output_hash : null, reviewer_verdict: value.reviewer_verdict === 'approve' || value.reviewer_verdict === 'block' || value.reviewer_verdict === 'revise' ? value.reviewer_verdict : null, reviewer_reasoning_hash: typeof value.reviewer_reasoning_hash === 'string' ? value.reviewer_reasoning_hash : null, review_started_at: typeof value.review_started_at === 'string' ? value.review_started_at : null, review_completed_at: typeof value.review_completed_at === 'string' ? value.review_completed_at : null, }; } function validateConfig(config: WorkflowConfig): void { if (!config.name || !config.name.trim()) throw new Error('Workflow name is required'); if (!Number.isFinite(config.budget_cap_usd) || config.budget_cap_usd <= 0) { throw new Error('Workflow budget_cap_usd must be greater than zero'); } if (!Number.isFinite(config.duration_cap_hours) || config.duration_cap_hours <= 0) { throw new Error('Workflow duration_cap_hours must be greater than zero'); } } function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null; }