@agentguard-run/spend 0.7.0 → 0.9.0
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/CHANGELOG.md +14 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +17 -1
- package/dist/index.js.map +1 -1
- package/dist/outcomes/index.d.ts +6 -0
- package/dist/outcomes/index.d.ts.map +1 -0
- package/dist/outcomes/index.js +22 -0
- package/dist/outcomes/index.js.map +1 -0
- package/dist/outcomes/learning.d.ts +19 -0
- package/dist/outcomes/learning.d.ts.map +1 -0
- package/dist/outcomes/learning.js +104 -0
- package/dist/outcomes/learning.js.map +1 -0
- package/dist/outcomes/ledger.d.ts +42 -0
- package/dist/outcomes/ledger.d.ts.map +1 -0
- package/dist/outcomes/ledger.js +110 -0
- package/dist/outcomes/ledger.js.map +1 -0
- package/dist/outcomes/quality-gate.d.ts +12 -0
- package/dist/outcomes/quality-gate.d.ts.map +1 -0
- package/dist/outcomes/quality-gate.js +104 -0
- package/dist/outcomes/quality-gate.js.map +1 -0
- package/dist/outcomes/runtime.d.ts +21 -0
- package/dist/outcomes/runtime.d.ts.map +1 -0
- package/dist/outcomes/runtime.js +328 -0
- package/dist/outcomes/runtime.js.map +1 -0
- package/dist/outcomes/types.d.ts +156 -0
- package/dist/outcomes/types.d.ts.map +1 -0
- package/dist/outcomes/types.js +14 -0
- package/dist/outcomes/types.js.map +1 -0
- package/dist/router.d.ts +8 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +932 -229
- package/dist/router.js.map +1 -1
- package/dist/spend-guard.d.ts +4 -0
- package/dist/spend-guard.d.ts.map +1 -1
- package/dist/spend-guard.js +53 -0
- package/dist/spend-guard.js.map +1 -1
- package/dist/types.d.ts +3 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/verticals/catalogs.d.ts +62 -0
- package/dist/verticals/catalogs.d.ts.map +1 -0
- package/dist/verticals/catalogs.js +3936 -0
- package/dist/verticals/catalogs.js.map +1 -0
- package/dist/verticals/index.d.ts +2 -0
- package/dist/verticals/index.d.ts.map +1 -0
- package/dist/verticals/index.js +18 -0
- package/dist/verticals/index.js.map +1 -0
- package/package.json +14 -2
- package/src/outcomes/index.ts +5 -0
- package/src/outcomes/learning.ts +133 -0
- package/src/outcomes/ledger.ts +131 -0
- package/src/outcomes/quality-gate.ts +116 -0
- package/src/outcomes/runtime.ts +396 -0
- package/src/outcomes/types.ts +176 -0
- package/src/router.ts +938 -227
- package/src/verticals/catalogs.ts +3967 -0
- package/src/verticals/index.ts +1 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import { buildScopeKey } from '../policy';
|
|
3
|
+
import { computeCallCents, inferProvider } from '../cost-table';
|
|
4
|
+
import { recommend, type ModelRouteRecommendation } from '../router';
|
|
5
|
+
import type { SpendDecision, SignedDecisionLogEntry } from '../types';
|
|
6
|
+
import type { SpendGuard } from '../spend-guard';
|
|
7
|
+
import { OutcomeSpendLedger, type OutcomeSpendHold } from './ledger';
|
|
8
|
+
import { scoreOutcomeOutput, shouldRunReviewer, type OutcomeQualityGateResult } from './quality-gate';
|
|
9
|
+
import {
|
|
10
|
+
InMemoryOutcomeReceiptStore,
|
|
11
|
+
type OutcomeDecisionLink,
|
|
12
|
+
type OutcomeExecutionContext,
|
|
13
|
+
type OutcomeModelRequest,
|
|
14
|
+
type OutcomeModelResponse,
|
|
15
|
+
type OutcomeModelRunner,
|
|
16
|
+
type OutcomeReceiptStore,
|
|
17
|
+
type OutcomeRuntimeReceipt,
|
|
18
|
+
type OutcomeTemplate,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
export interface ExecuteOutcomeTaskOptions {
|
|
22
|
+
guard: SpendGuard;
|
|
23
|
+
template: OutcomeTemplate;
|
|
24
|
+
inputs: Record<string, unknown>;
|
|
25
|
+
runner: OutcomeModelRunner;
|
|
26
|
+
context: OutcomeExecutionContext;
|
|
27
|
+
ledger?: OutcomeSpendLedger;
|
|
28
|
+
receiptStore?: OutcomeReceiptStore;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ExecuteOutcomeTaskResult {
|
|
32
|
+
receipt: OutcomeRuntimeReceipt;
|
|
33
|
+
signed: SignedDecisionLogEntry | null;
|
|
34
|
+
outputText: string;
|
|
35
|
+
reviewerOutputText?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ModelRunResult {
|
|
39
|
+
response: OutcomeModelResponse;
|
|
40
|
+
decision: SpendDecision;
|
|
41
|
+
signed: SignedDecisionLogEntry | null;
|
|
42
|
+
actualInputTokens: number;
|
|
43
|
+
actualOutputTokens: number;
|
|
44
|
+
actualCents: number;
|
|
45
|
+
durationMs: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function executeOutcomeTask(options: ExecuteOutcomeTaskOptions): Promise<ExecuteOutcomeTaskResult> {
|
|
49
|
+
validateTemplate(options.template);
|
|
50
|
+
validateInputs(options.template, options.inputs);
|
|
51
|
+
const started = Date.now();
|
|
52
|
+
const receiptStore = options.receiptStore ?? new InMemoryOutcomeReceiptStore();
|
|
53
|
+
const ledger = options.ledger ?? new OutcomeSpendLedger(options.guard.getSpendStore());
|
|
54
|
+
const posture = options.context.posture ?? 'standard';
|
|
55
|
+
const budgetTier = options.context.budgetTier ?? 'solo';
|
|
56
|
+
const route = recommend({
|
|
57
|
+
vertical: options.template.vertical,
|
|
58
|
+
outcome: options.template.slug,
|
|
59
|
+
posture,
|
|
60
|
+
budgetTier,
|
|
61
|
+
imageCapable: options.context.imageCapable,
|
|
62
|
+
});
|
|
63
|
+
const blockedOrigins = [...new Set([...(options.template.blockedOrigins ?? []), ...route.blockedOrigins])];
|
|
64
|
+
const hold = await ledger.hold({
|
|
65
|
+
scope: options.context.scope,
|
|
66
|
+
window: options.template.spendCaps.window ?? 'per_call',
|
|
67
|
+
amountCents: options.template.spendCaps.maxCostCents,
|
|
68
|
+
limitCents: options.template.spendCaps.periodCapCents ?? options.template.spendCaps.maxCostCents,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const links: OutcomeDecisionLink[] = [];
|
|
72
|
+
let status: OutcomeRuntimeReceipt['status'] = 'failed';
|
|
73
|
+
let drafter: ModelRunResult | null = null;
|
|
74
|
+
let reviewer: ModelRunResult | null = null;
|
|
75
|
+
let fallback: OutcomeRuntimeReceipt['fallback'] = null;
|
|
76
|
+
let outputText = '';
|
|
77
|
+
let reviewerOutputText: string | undefined;
|
|
78
|
+
let gate: OutcomeQualityGateResult = { score: 0, riskScore: 0, passed: false, issues: ['not_run'], triggerFired: [] };
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
drafter = await runGuardedModel({
|
|
82
|
+
guard: options.guard,
|
|
83
|
+
template: options.template,
|
|
84
|
+
context: options.context,
|
|
85
|
+
route,
|
|
86
|
+
role: 'drafter',
|
|
87
|
+
model: route.drafter,
|
|
88
|
+
inputs: options.inputs,
|
|
89
|
+
runner: options.runner,
|
|
90
|
+
});
|
|
91
|
+
links.push(linkFor('drafter', drafter));
|
|
92
|
+
outputText = drafter.response.text;
|
|
93
|
+
gate = scoreOutcomeOutput(outputText, options.template, drafter.response.qualityScore);
|
|
94
|
+
|
|
95
|
+
if (shouldRunReviewer(gate) && options.context.reviewerCascadeAuto === true) {
|
|
96
|
+
const reviewerModel = antiCorrelatedReviewer(route, drafter.decision.modelResolved);
|
|
97
|
+
reviewer = await runGuardedModel({
|
|
98
|
+
guard: options.guard,
|
|
99
|
+
template: options.template,
|
|
100
|
+
context: options.context,
|
|
101
|
+
route,
|
|
102
|
+
role: 'reviewer',
|
|
103
|
+
model: reviewerModel,
|
|
104
|
+
inputs: { outcome_metadata_only: true, trigger_fired: gate.triggerFired.join(',') },
|
|
105
|
+
runner: options.runner,
|
|
106
|
+
priorOutputText: outputText,
|
|
107
|
+
});
|
|
108
|
+
links.push(linkFor('reviewer', reviewer));
|
|
109
|
+
reviewerOutputText = reviewer.response.text;
|
|
110
|
+
status = 'completed';
|
|
111
|
+
} else {
|
|
112
|
+
status = gate.passed ? 'completed' : 'quality_failed';
|
|
113
|
+
}
|
|
114
|
+
} catch (err) {
|
|
115
|
+
if (!drafter) {
|
|
116
|
+
const fallbackModel = antiCorrelatedFallback(route, route.drafter);
|
|
117
|
+
if (fallbackModel) {
|
|
118
|
+
fallback = { model: fallbackModel, reason: errorReason(err) };
|
|
119
|
+
try {
|
|
120
|
+
drafter = await runGuardedModel({
|
|
121
|
+
guard: options.guard,
|
|
122
|
+
template: options.template,
|
|
123
|
+
context: options.context,
|
|
124
|
+
route,
|
|
125
|
+
role: 'fallback',
|
|
126
|
+
model: fallbackModel,
|
|
127
|
+
inputs: options.inputs,
|
|
128
|
+
runner: options.runner,
|
|
129
|
+
});
|
|
130
|
+
fallback.decisionId = drafter.decision.decisionId;
|
|
131
|
+
links.push(linkFor('fallback', drafter));
|
|
132
|
+
outputText = drafter.response.text;
|
|
133
|
+
gate = scoreOutcomeOutput(outputText, options.template, drafter.response.qualityScore);
|
|
134
|
+
status = gate.passed ? 'completed' : 'quality_failed';
|
|
135
|
+
} catch {
|
|
136
|
+
status = 'failed';
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
status = 'failed';
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
status = 'failed';
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const totalCostCents = links.reduce((sum, item) => sum + item.actualCents, 0);
|
|
147
|
+
if (status === 'completed') await ledger.settle(hold, totalCostCents);
|
|
148
|
+
else await ledger.refund(hold);
|
|
149
|
+
|
|
150
|
+
const receipt = buildReceipt({
|
|
151
|
+
template: options.template,
|
|
152
|
+
context: options.context,
|
|
153
|
+
hold,
|
|
154
|
+
route,
|
|
155
|
+
blockedOrigins,
|
|
156
|
+
status,
|
|
157
|
+
drafter,
|
|
158
|
+
reviewer,
|
|
159
|
+
fallback,
|
|
160
|
+
links,
|
|
161
|
+
gate,
|
|
162
|
+
totalCostCents,
|
|
163
|
+
durationMs: Date.now() - started,
|
|
164
|
+
posture,
|
|
165
|
+
budgetTier,
|
|
166
|
+
});
|
|
167
|
+
await receiptStore.append(receipt);
|
|
168
|
+
const signedReceipt = await options.guard.recordOutcomeReceipt(receiptToMetadata(receipt));
|
|
169
|
+
return { receipt, signed: signedReceipt.signed, outputText, reviewerOutputText };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function runGuardedModel(args: {
|
|
173
|
+
guard: SpendGuard;
|
|
174
|
+
template: OutcomeTemplate;
|
|
175
|
+
context: OutcomeExecutionContext;
|
|
176
|
+
route: ModelRouteRecommendation;
|
|
177
|
+
role: 'drafter' | 'reviewer' | 'fallback';
|
|
178
|
+
model: string;
|
|
179
|
+
inputs: Record<string, unknown>;
|
|
180
|
+
runner: OutcomeModelRunner;
|
|
181
|
+
priorOutputText?: string;
|
|
182
|
+
}): Promise<ModelRunResult> {
|
|
183
|
+
const request = buildModelRequest(args);
|
|
184
|
+
const inputTokens = args.guard.estimateTokens(request.systemPrompt + '\n' + request.userPrompt);
|
|
185
|
+
const outputTokens = estimatedOutputTokens(args.template);
|
|
186
|
+
const { decision, signed } = await args.guard.decide({
|
|
187
|
+
provider: inferProvider(args.model),
|
|
188
|
+
model: args.model,
|
|
189
|
+
inputTokens,
|
|
190
|
+
outputTokens,
|
|
191
|
+
scope: args.context.scope,
|
|
192
|
+
capabilityClaim: args.template.capability,
|
|
193
|
+
label: args.template.slug + ':' + args.role,
|
|
194
|
+
});
|
|
195
|
+
if (decision.action === 'block') throw new Error('Outcome model call blocked by policy');
|
|
196
|
+
|
|
197
|
+
const started = Date.now();
|
|
198
|
+
let response: OutcomeModelResponse;
|
|
199
|
+
try {
|
|
200
|
+
response = await args.runner.run({ ...request, model: decision.modelResolved });
|
|
201
|
+
} catch (err) {
|
|
202
|
+
await args.guard.settleStreamUsage(decision.decisionId, 0, 0, { partial: true, reason: errorReason(err) });
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
const actualInputTokens = validToken(response.inputTokens) ? response.inputTokens : inputTokens;
|
|
206
|
+
const actualOutputTokens = validToken(response.outputTokens) ? response.outputTokens : args.guard.estimateTokens(response.text);
|
|
207
|
+
const computed = computeCallCents(decision.modelResolved, actualInputTokens, actualOutputTokens);
|
|
208
|
+
const actualCents = validCents(response.costCents) ? response.costCents : computed ?? decision.projectedCents;
|
|
209
|
+
await args.guard.settleStreamUsage(decision.decisionId, actualInputTokens, actualOutputTokens, { partial: false });
|
|
210
|
+
return { response, decision, signed, actualInputTokens, actualOutputTokens, actualCents, durationMs: Date.now() - started };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function buildModelRequest(args: {
|
|
214
|
+
template: OutcomeTemplate;
|
|
215
|
+
context: OutcomeExecutionContext;
|
|
216
|
+
route: ModelRouteRecommendation;
|
|
217
|
+
role: 'drafter' | 'reviewer' | 'fallback';
|
|
218
|
+
model: string;
|
|
219
|
+
inputs: Record<string, unknown>;
|
|
220
|
+
priorOutputText?: string;
|
|
221
|
+
}): OutcomeModelRequest {
|
|
222
|
+
const contextLines = (args.template.contextInjection ?? []).map((item) => {
|
|
223
|
+
const value = args.context.context?.[item.field] ?? item.example ?? '';
|
|
224
|
+
return item.label + ': ' + value;
|
|
225
|
+
});
|
|
226
|
+
const inputLines = Object.entries(args.inputs).map(([key, value]) => key + ': ' + JSON.stringify(value));
|
|
227
|
+
const userPrompt = [
|
|
228
|
+
'Outcome: ' + args.template.slug,
|
|
229
|
+
'Role: ' + args.role,
|
|
230
|
+
...contextLines,
|
|
231
|
+
...inputLines,
|
|
232
|
+
args.priorOutputText ? 'Prior drafter output:\n' + args.priorOutputText : '',
|
|
233
|
+
'Return format: ' + args.template.outputFormat,
|
|
234
|
+
].filter(Boolean).join('\n');
|
|
235
|
+
return {
|
|
236
|
+
role: args.role,
|
|
237
|
+
model: args.model,
|
|
238
|
+
vertical: String(args.template.vertical),
|
|
239
|
+
outcome: args.template.slug,
|
|
240
|
+
promptVersion: args.template.promptVersion,
|
|
241
|
+
systemPrompt: args.template.systemPrompt + (args.template.disclaimer ? '\n' + args.template.disclaimer : ''),
|
|
242
|
+
userPrompt,
|
|
243
|
+
outputFormat: args.template.outputFormat,
|
|
244
|
+
metadata: {
|
|
245
|
+
vertical: String(args.template.vertical),
|
|
246
|
+
outcome: args.template.slug,
|
|
247
|
+
promptVersion: args.template.promptVersion,
|
|
248
|
+
need: args.route.need,
|
|
249
|
+
role: args.role,
|
|
250
|
+
effort: effortForRole(args.route, args.role),
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function effortForRole(route: ModelRouteRecommendation, role: 'drafter' | 'reviewer' | 'fallback'): string {
|
|
256
|
+
return role === 'reviewer' ? route.reviewerEffort : route.drafterEffort;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function buildReceipt(args: {
|
|
260
|
+
template: OutcomeTemplate;
|
|
261
|
+
context: OutcomeExecutionContext;
|
|
262
|
+
hold: OutcomeSpendHold;
|
|
263
|
+
route: ModelRouteRecommendation;
|
|
264
|
+
blockedOrigins: string[];
|
|
265
|
+
status: OutcomeRuntimeReceipt['status'];
|
|
266
|
+
drafter: ModelRunResult | null;
|
|
267
|
+
reviewer: ModelRunResult | null;
|
|
268
|
+
fallback: OutcomeRuntimeReceipt['fallback'];
|
|
269
|
+
links: OutcomeDecisionLink[];
|
|
270
|
+
gate: OutcomeQualityGateResult;
|
|
271
|
+
totalCostCents: number;
|
|
272
|
+
durationMs: number;
|
|
273
|
+
posture: string;
|
|
274
|
+
budgetTier: string;
|
|
275
|
+
}): OutcomeRuntimeReceipt {
|
|
276
|
+
return {
|
|
277
|
+
receiptId: 'ag_outcome_' + randomUUID(),
|
|
278
|
+
timestamp: new Date().toISOString(),
|
|
279
|
+
vertical: String(args.template.vertical),
|
|
280
|
+
outcome: args.template.slug,
|
|
281
|
+
promptVersion: args.template.promptVersion,
|
|
282
|
+
customerId: args.context.customerId,
|
|
283
|
+
scopeKey: buildScopeKey(args.context.scope),
|
|
284
|
+
status: args.status,
|
|
285
|
+
drafter: modelSummary(args.drafter),
|
|
286
|
+
reviewer: args.reviewer ? { ...modelSummary(args.reviewer), verdict: args.reviewer.response.verdict ?? 'reviewed' } : null,
|
|
287
|
+
fallback: args.fallback,
|
|
288
|
+
triggerFired: args.gate.triggerFired,
|
|
289
|
+
qualityScore: args.gate.score,
|
|
290
|
+
pass: args.status === 'completed',
|
|
291
|
+
totalCostCents: args.totalCostCents,
|
|
292
|
+
durationMs: args.durationMs,
|
|
293
|
+
decisionLinks: args.links,
|
|
294
|
+
metadata: {
|
|
295
|
+
need: args.route.need,
|
|
296
|
+
capability: args.template.capability,
|
|
297
|
+
posture: args.posture,
|
|
298
|
+
budgetTier: args.budgetTier,
|
|
299
|
+
blockedOrigins: args.blockedOrigins,
|
|
300
|
+
effort: args.route.effort,
|
|
301
|
+
drafterEffort: args.route.drafterEffort,
|
|
302
|
+
reviewerEffort: args.route.reviewerEffort,
|
|
303
|
+
provenance: provenanceSummary(args.drafter, args.reviewer),
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function modelSummary(run: ModelRunResult | null): OutcomeRuntimeReceipt['drafter'] {
|
|
309
|
+
if (!run) return { model: 'none', costCents: 0, inputTokens: 0, outputTokens: 0 };
|
|
310
|
+
return {
|
|
311
|
+
model: run.decision.modelResolved,
|
|
312
|
+
decisionId: run.decision.decisionId,
|
|
313
|
+
costCents: run.actualCents,
|
|
314
|
+
inputTokens: run.actualInputTokens,
|
|
315
|
+
outputTokens: run.actualOutputTokens,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function provenanceSummary(drafter: ModelRunResult | null, reviewer: ModelRunResult | null): Record<string, string | number | boolean> {
|
|
320
|
+
const out: Record<string, string | number | boolean> = {};
|
|
321
|
+
if (drafter?.response.durationMs) out.drafterDurationMs = drafter.response.durationMs;
|
|
322
|
+
if (reviewer?.response.durationMs) out.reviewerDurationMs = reviewer.response.durationMs;
|
|
323
|
+
return out;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function receiptToMetadata(receipt: OutcomeRuntimeReceipt): Record<string, unknown> {
|
|
327
|
+
return JSON.parse(JSON.stringify(receipt)) as Record<string, unknown>;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function linkFor(role: 'drafter' | 'reviewer' | 'fallback', run: ModelRunResult): OutcomeDecisionLink {
|
|
331
|
+
return {
|
|
332
|
+
role,
|
|
333
|
+
decisionId: run.decision.decisionId,
|
|
334
|
+
entryHash: run.signed?.entryHash,
|
|
335
|
+
modelRequested: run.decision.modelRequested,
|
|
336
|
+
modelResolved: run.decision.modelResolved,
|
|
337
|
+
projectedCents: run.decision.projectedCents,
|
|
338
|
+
actualCents: run.actualCents,
|
|
339
|
+
inputTokens: run.actualInputTokens,
|
|
340
|
+
outputTokens: run.actualOutputTokens,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function antiCorrelatedReviewer(route: ModelRouteRecommendation, drafterModel: string): string {
|
|
345
|
+
if (familyOf(route.reviewer) !== familyOf(drafterModel)) return route.reviewer;
|
|
346
|
+
return antiCorrelatedFallback(route, drafterModel) ?? route.reviewer;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function antiCorrelatedFallback(route: ModelRouteRecommendation, failedModel: string): string | null {
|
|
350
|
+
const failedFamily = familyOf(failedModel);
|
|
351
|
+
return route.allowedModels.find((model) => model !== failedModel && familyOf(model) !== failedFamily && computeCallCents(model, 1, 1) !== null) ?? null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function familyOf(model: string): string {
|
|
355
|
+
const id = model.toLowerCase();
|
|
356
|
+
if (id.includes('opus')) return 'opus';
|
|
357
|
+
if (id.includes('sonnet')) return 'sonnet';
|
|
358
|
+
if (id.includes('haiku')) return 'haiku';
|
|
359
|
+
if (id.includes('gpt-oss')) return 'gpt-oss';
|
|
360
|
+
if (id.includes('gpt-5')) return 'gpt-5';
|
|
361
|
+
if (id.includes('gpt-4')) return 'gpt-4';
|
|
362
|
+
if (id.includes('llama')) return 'llama';
|
|
363
|
+
if (id.includes('mistral')) return 'mistral';
|
|
364
|
+
if (id.includes('nova')) return 'nova';
|
|
365
|
+
return id.split('/')[0] || id.split('.')[0] || 'unknown';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function estimatedOutputTokens(template: OutcomeTemplate): number {
|
|
369
|
+
return Math.max(128, Math.min(4096, Math.ceil(template.spendCaps.maxCostCents * 64)));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function validateTemplate(template: OutcomeTemplate): void {
|
|
373
|
+
if (!template.slug || !template.vertical) throw new Error('Outcome template must include slug and vertical');
|
|
374
|
+
if (!Number.isSafeInteger(template.spendCaps.maxCostCents) || template.spendCaps.maxCostCents < 0) {
|
|
375
|
+
throw new Error('Outcome spend cap must be a non-negative safe integer');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function validateInputs(template: OutcomeTemplate, inputs: Record<string, unknown>): void {
|
|
380
|
+
for (const input of template.inputs) {
|
|
381
|
+
if (input.required && !(input.name in inputs)) throw new Error('Missing required outcome input: ' + input.name);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function validToken(value: unknown): value is number {
|
|
386
|
+
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function validCents(value: unknown): value is number {
|
|
390
|
+
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function errorReason(err: unknown): string {
|
|
394
|
+
if (err instanceof Error && err.message) return err.message.slice(0, 120);
|
|
395
|
+
return 'model_error';
|
|
396
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { CapabilityTier, SpendScope, SpendWindow } from '../types';
|
|
2
|
+
|
|
3
|
+
export type OutcomeVertical = 'law' | 'accounting' | 'insurance' | 'realestate' | 'ecommerce' | string;
|
|
4
|
+
export type OutcomeRole = 'drafter' | 'reviewer' | 'fallback';
|
|
5
|
+
export type OutcomeStatus = 'completed' | 'blocked' | 'failed' | 'quality_failed';
|
|
6
|
+
export type OutcomeEffort = 'low' | 'medium' | 'high' | 'xhigh';
|
|
7
|
+
export type OutcomeGuardrailTier = 'grounded_cite_or_refuse' | 'reviewer_required' | 'human_of_record';
|
|
8
|
+
export type OutcomeTemplateStatus = 'active' | 'specced';
|
|
9
|
+
|
|
10
|
+
export interface OutcomeInputDefinition {
|
|
11
|
+
name: string;
|
|
12
|
+
type: 'string' | 'number' | 'boolean' | 'json' | 'date';
|
|
13
|
+
required?: boolean;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface OutcomeSpendCaps {
|
|
18
|
+
maxCostCents: number;
|
|
19
|
+
window?: SpendWindow;
|
|
20
|
+
periodCapCents?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OutcomeQualityGateRubric {
|
|
24
|
+
name: string;
|
|
25
|
+
weight?: number;
|
|
26
|
+
passThreshold?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface OutcomeQualityGateConfig {
|
|
30
|
+
passThreshold?: number;
|
|
31
|
+
reviewerTriggerScore?: number;
|
|
32
|
+
riskKeywords?: string[];
|
|
33
|
+
rubrics?: OutcomeQualityGateRubric[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface OutcomeContextInjection {
|
|
37
|
+
field: string;
|
|
38
|
+
label: string;
|
|
39
|
+
required?: boolean;
|
|
40
|
+
example?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface OutcomeTemplate {
|
|
44
|
+
slug: string;
|
|
45
|
+
vertical: OutcomeVertical;
|
|
46
|
+
systemPrompt: string;
|
|
47
|
+
inputs: OutcomeInputDefinition[];
|
|
48
|
+
outputFormat: string;
|
|
49
|
+
need: string;
|
|
50
|
+
capability: CapabilityTier;
|
|
51
|
+
spendCaps: OutcomeSpendCaps;
|
|
52
|
+
promptVersion: string;
|
|
53
|
+
effort?: OutcomeEffort;
|
|
54
|
+
guardrailTier?: OutcomeGuardrailTier;
|
|
55
|
+
status?: OutcomeTemplateStatus;
|
|
56
|
+
contextInjection?: OutcomeContextInjection[];
|
|
57
|
+
blockedOrigins?: string[];
|
|
58
|
+
premiumAgent?: string;
|
|
59
|
+
disclaimer?: string;
|
|
60
|
+
qualityGate?: OutcomeQualityGateConfig;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface OutcomeExecutionContext {
|
|
64
|
+
scope: SpendScope;
|
|
65
|
+
customerId?: string;
|
|
66
|
+
posture?: string;
|
|
67
|
+
budgetTier?: string;
|
|
68
|
+
imageCapable?: boolean;
|
|
69
|
+
context?: Record<string, string>;
|
|
70
|
+
reviewerCascadeAuto?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface OutcomeModelRequest {
|
|
74
|
+
role: OutcomeRole;
|
|
75
|
+
model: string;
|
|
76
|
+
vertical: string;
|
|
77
|
+
outcome: string;
|
|
78
|
+
promptVersion: string;
|
|
79
|
+
systemPrompt: string;
|
|
80
|
+
userPrompt: string;
|
|
81
|
+
outputFormat: string;
|
|
82
|
+
metadata: Record<string, string | number | boolean>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface OutcomeModelResponse {
|
|
86
|
+
text: string;
|
|
87
|
+
inputTokens?: number;
|
|
88
|
+
outputTokens?: number;
|
|
89
|
+
costCents?: number;
|
|
90
|
+
qualityScore?: number;
|
|
91
|
+
confidence?: number;
|
|
92
|
+
verdict?: string;
|
|
93
|
+
durationMs?: number;
|
|
94
|
+
provenance?: Record<string, string | number | boolean>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface OutcomeModelRunner {
|
|
98
|
+
run(request: OutcomeModelRequest): Promise<OutcomeModelResponse>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface OutcomeDecisionLink {
|
|
102
|
+
role: OutcomeRole;
|
|
103
|
+
decisionId: string;
|
|
104
|
+
entryHash?: string;
|
|
105
|
+
modelRequested: string;
|
|
106
|
+
modelResolved: string;
|
|
107
|
+
projectedCents: number;
|
|
108
|
+
actualCents: number;
|
|
109
|
+
inputTokens: number;
|
|
110
|
+
outputTokens: number;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface OutcomeRuntimeReceipt {
|
|
114
|
+
receiptId: string;
|
|
115
|
+
timestamp: string;
|
|
116
|
+
vertical: string;
|
|
117
|
+
outcome: string;
|
|
118
|
+
promptVersion: string;
|
|
119
|
+
customerId?: string;
|
|
120
|
+
scopeKey: string;
|
|
121
|
+
status: OutcomeStatus;
|
|
122
|
+
drafter: {
|
|
123
|
+
model: string;
|
|
124
|
+
decisionId?: string;
|
|
125
|
+
costCents: number;
|
|
126
|
+
inputTokens: number;
|
|
127
|
+
outputTokens: number;
|
|
128
|
+
};
|
|
129
|
+
reviewer: {
|
|
130
|
+
model: string;
|
|
131
|
+
decisionId?: string;
|
|
132
|
+
costCents: number;
|
|
133
|
+
inputTokens: number;
|
|
134
|
+
outputTokens: number;
|
|
135
|
+
verdict: string;
|
|
136
|
+
} | null;
|
|
137
|
+
fallback: {
|
|
138
|
+
model: string;
|
|
139
|
+
decisionId?: string;
|
|
140
|
+
reason: string;
|
|
141
|
+
} | null;
|
|
142
|
+
triggerFired: string[];
|
|
143
|
+
qualityScore: number;
|
|
144
|
+
pass: boolean;
|
|
145
|
+
totalCostCents: number;
|
|
146
|
+
durationMs: number;
|
|
147
|
+
decisionLinks: OutcomeDecisionLink[];
|
|
148
|
+
metadata: {
|
|
149
|
+
need: string;
|
|
150
|
+
capability: CapabilityTier;
|
|
151
|
+
posture: string;
|
|
152
|
+
budgetTier: string;
|
|
153
|
+
blockedOrigins: string[];
|
|
154
|
+
effort: OutcomeEffort;
|
|
155
|
+
drafterEffort: OutcomeEffort;
|
|
156
|
+
reviewerEffort: OutcomeEffort;
|
|
157
|
+
provenance: Record<string, string | number | boolean>;
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface OutcomeReceiptStore {
|
|
162
|
+
append(receipt: OutcomeRuntimeReceipt): Promise<void>;
|
|
163
|
+
readAll(): Promise<OutcomeRuntimeReceipt[]>;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export class InMemoryOutcomeReceiptStore implements OutcomeReceiptStore {
|
|
167
|
+
private readonly receipts: OutcomeRuntimeReceipt[] = [];
|
|
168
|
+
|
|
169
|
+
async append(receipt: OutcomeRuntimeReceipt): Promise<void> {
|
|
170
|
+
this.receipts.push(receipt);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async readAll(): Promise<OutcomeRuntimeReceipt[]> {
|
|
174
|
+
return [...this.receipts];
|
|
175
|
+
}
|
|
176
|
+
}
|