@actuarial-ts/agents 0.6.0 → 0.6.1
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/README.md +7 -5
- package/dist/diagnostics.d.ts +32 -27
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +604 -41
- package/dist/diagnostics.js.map +1 -1
- package/dist/divergence.d.ts.map +1 -1
- package/dist/divergence.js +30 -10
- package/dist/divergence.js.map +1 -1
- package/dist/judgment.d.ts +75 -2
- package/dist/judgment.d.ts.map +1 -1
- package/dist/judgment.js +28 -10
- package/dist/judgment.js.map +1 -1
- package/dist/remote.d.ts +5 -5
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +27 -11
- package/dist/tools.js.map +1 -1
- package/package.json +7 -6
- package/src/diagnostics.ts +819 -57
- package/src/divergence.ts +73 -29
- package/src/judgment.ts +56 -20
- package/src/tools.ts +211 -38
package/src/divergence.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
|
|
44
44
|
import { Agent } from "@mastra/core/agent";
|
|
45
45
|
import { RequestContext } from "@mastra/core/request-context";
|
|
46
|
-
import { ReservingError } from "@actuarial-ts/core";
|
|
46
|
+
import { ReservingError, hasDiagnosticOwn } from "@actuarial-ts/core";
|
|
47
47
|
import {
|
|
48
48
|
CONVENTION_PROFILES,
|
|
49
49
|
crosscheckReportDocSchema,
|
|
@@ -107,7 +107,11 @@ export interface DeviationSignature {
|
|
|
107
107
|
standardErrorExceedsTolerance: boolean;
|
|
108
108
|
/** central | standard-error | mixed: which metric family breaches tolerance. */
|
|
109
109
|
concentration: "central" | "standard-error" | "mixed" | "none";
|
|
110
|
-
totals: {
|
|
110
|
+
totals: {
|
|
111
|
+
ultimate: number | null;
|
|
112
|
+
unpaid: number | null;
|
|
113
|
+
standardError: number | null;
|
|
114
|
+
};
|
|
111
115
|
/** The largest per-origin deviations, ranked descending (top 5). */
|
|
112
116
|
worstOrigins: {
|
|
113
117
|
origin: string;
|
|
@@ -155,7 +159,13 @@ function sameValue(x: unknown, y: unknown): boolean {
|
|
|
155
159
|
function validateDoc<T>(
|
|
156
160
|
label: string,
|
|
157
161
|
doc: unknown,
|
|
158
|
-
schema: {
|
|
162
|
+
schema: {
|
|
163
|
+
safeParse(input: unknown): {
|
|
164
|
+
success: boolean;
|
|
165
|
+
data?: T;
|
|
166
|
+
error?: z.ZodError;
|
|
167
|
+
};
|
|
168
|
+
},
|
|
159
169
|
): T {
|
|
160
170
|
const parsed = schema.safeParse(doc);
|
|
161
171
|
if (!parsed.success) {
|
|
@@ -173,7 +183,10 @@ function engineMatches(
|
|
|
173
183
|
doc: MethodResultDoc,
|
|
174
184
|
stamp: { name: string; version: string },
|
|
175
185
|
): boolean {
|
|
176
|
-
return
|
|
186
|
+
return (
|
|
187
|
+
doc.result.engine.name === stamp.name &&
|
|
188
|
+
doc.result.engine.version === stamp.version
|
|
189
|
+
);
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
const STATUS_RANK: Record<AlignmentFinding["status"], number> = {
|
|
@@ -208,8 +221,13 @@ function alignmentFindingsFor(
|
|
|
208
221
|
},
|
|
209
222
|
];
|
|
210
223
|
}
|
|
211
|
-
const alignment
|
|
212
|
-
|
|
224
|
+
const alignments = profile.alignment as Record<
|
|
225
|
+
string,
|
|
226
|
+
EngineAlignment | undefined
|
|
227
|
+
>;
|
|
228
|
+
const alignment = hasDiagnosticOwn(alignments, engineName)
|
|
229
|
+
? (alignments[engineName] ?? null)
|
|
230
|
+
: null;
|
|
213
231
|
if (alignment === null) {
|
|
214
232
|
return [
|
|
215
233
|
{
|
|
@@ -293,7 +311,11 @@ function deviationSignatureOf(report: CrosscheckReportDoc): DeviationSignature {
|
|
|
293
311
|
}
|
|
294
312
|
if (row.standardError !== null) {
|
|
295
313
|
maxSe = Math.max(maxSe ?? 0, row.standardError);
|
|
296
|
-
ranked.push({
|
|
314
|
+
ranked.push({
|
|
315
|
+
origin: row.origin,
|
|
316
|
+
metric: "standardError",
|
|
317
|
+
deviation: row.standardError,
|
|
318
|
+
});
|
|
297
319
|
}
|
|
298
320
|
}
|
|
299
321
|
const totals = body.deviations.totals;
|
|
@@ -301,11 +323,14 @@ function deviationSignatureOf(report: CrosscheckReportDoc): DeviationSignature {
|
|
|
301
323
|
const deviation = totals[metric];
|
|
302
324
|
if (deviation !== null) maxCentral = Math.max(maxCentral, deviation);
|
|
303
325
|
}
|
|
304
|
-
if (totals.standardError !== null)
|
|
326
|
+
if (totals.standardError !== null)
|
|
327
|
+
maxSe = Math.max(maxSe ?? 0, totals.standardError);
|
|
305
328
|
|
|
306
329
|
const centralExceeds = maxCentral > tolerance.central;
|
|
307
330
|
const seExceeds =
|
|
308
|
-
tolerance.standardError !== null &&
|
|
331
|
+
tolerance.standardError !== null &&
|
|
332
|
+
maxSe !== null &&
|
|
333
|
+
maxSe > tolerance.standardError;
|
|
309
334
|
const concentration: DeviationSignature["concentration"] =
|
|
310
335
|
centralExceeds && seExceeds
|
|
311
336
|
? "mixed"
|
|
@@ -360,23 +385,30 @@ export function assembleDivergenceEvidence(
|
|
|
360
385
|
);
|
|
361
386
|
}
|
|
362
387
|
if (!engineMatches(a, body.engines.a) || !engineMatches(b, body.engines.b)) {
|
|
363
|
-
const swapped =
|
|
388
|
+
const swapped =
|
|
389
|
+
engineMatches(a, body.engines.b) && engineMatches(b, body.engines.a);
|
|
364
390
|
throw new AgentsError(
|
|
365
391
|
"DIVERGENCE_INPUT_MISMATCH",
|
|
366
392
|
swapped
|
|
367
393
|
? "The supplied result docs are SWAPPED relative to the report: doc a matches the report's " +
|
|
368
|
-
|
|
394
|
+
"engine b and vice versa; pass them in the report's a/b order"
|
|
369
395
|
: `The supplied result docs do not match the report's engine stamps: report compared ` +
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
396
|
+
`a=${body.engines.a.name}@${body.engines.a.version} vs ` +
|
|
397
|
+
`b=${body.engines.b.name}@${body.engines.b.version}, got ` +
|
|
398
|
+
`a=${a.result.engine.name}@${a.result.engine.version} and ` +
|
|
399
|
+
`b=${b.result.engine.name}@${b.result.engine.version}`,
|
|
374
400
|
);
|
|
375
401
|
}
|
|
376
402
|
|
|
377
403
|
const claimedProfileId =
|
|
378
|
-
a.result.engine.conventionProfile ??
|
|
379
|
-
|
|
404
|
+
a.result.engine.conventionProfile ??
|
|
405
|
+
b.result.engine.conventionProfile ??
|
|
406
|
+
null;
|
|
407
|
+
const profile =
|
|
408
|
+
claimedProfileId !== null &&
|
|
409
|
+
hasDiagnosticOwn(CONVENTION_PROFILES, claimedProfileId)
|
|
410
|
+
? CONVENTION_PROFILES[claimedProfileId]!
|
|
411
|
+
: null;
|
|
380
412
|
|
|
381
413
|
const engineEvidenceOf = (doc: MethodResultDoc): EngineParameterEvidence => ({
|
|
382
414
|
name: doc.result.engine.name,
|
|
@@ -388,9 +420,11 @@ export function assembleDivergenceEvidence(
|
|
|
388
420
|
profileAlignment:
|
|
389
421
|
profile === null
|
|
390
422
|
? null
|
|
391
|
-
: (
|
|
392
|
-
|
|
393
|
-
|
|
423
|
+
: hasDiagnosticOwn(profile.alignment, doc.result.engine.name)
|
|
424
|
+
? ((profile.alignment as Record<string, EngineAlignment | undefined>)[
|
|
425
|
+
doc.result.engine.name
|
|
426
|
+
] ?? null)
|
|
427
|
+
: null,
|
|
394
428
|
});
|
|
395
429
|
|
|
396
430
|
const findings = [
|
|
@@ -449,7 +483,7 @@ export const DIVERGENCE_EVIDENCE_CONTEXT_KEY = "divergenceEvidence";
|
|
|
449
483
|
* backtick characters (house gotcha).
|
|
450
484
|
*/
|
|
451
485
|
export const DIVERGENCE_EXPLAINER_INSTRUCTIONS = [
|
|
452
|
-
|
|
486
|
+
'You are a cross-engine divergence diagnostician inside an actuarial reserving toolchain. A deterministic referee compared the same computation run by two independent engines and returned the verdict "disagree". Your job is to produce a structured HYPOTHESIS about the root cause - you never re-litigate the verdict, and you never change any state.',
|
|
453
487
|
"## Working rules",
|
|
454
488
|
[
|
|
455
489
|
"1. Every claim you make must come from the supplied divergence evidence (in the user message, and available again via the get_divergence_evidence tool). Never invent parameters, deviations, or profile requirements.",
|
|
@@ -521,7 +555,9 @@ export function createDivergenceEvidenceTool() {
|
|
|
521
555
|
// evidence is scoped by the host before this tool can see it.
|
|
522
556
|
tenant: "none",
|
|
523
557
|
execute: async (_input, _tenant, context) => {
|
|
524
|
-
const evidence = context.requestContext?.get(
|
|
558
|
+
const evidence = context.requestContext?.get(
|
|
559
|
+
DIVERGENCE_EVIDENCE_CONTEXT_KEY,
|
|
560
|
+
);
|
|
525
561
|
if (evidence === undefined) {
|
|
526
562
|
throw new AgentsError(
|
|
527
563
|
"NO_DIVERGENCE_EVIDENCE",
|
|
@@ -529,7 +565,10 @@ export function createDivergenceEvidenceTool() {
|
|
|
529
565
|
"drive this agent through explainDivergence, which assembles and injects it",
|
|
530
566
|
);
|
|
531
567
|
}
|
|
532
|
-
return {
|
|
568
|
+
return {
|
|
569
|
+
success: true as const,
|
|
570
|
+
evidence: evidence as DivergenceEvidence,
|
|
571
|
+
};
|
|
533
572
|
},
|
|
534
573
|
});
|
|
535
574
|
}
|
|
@@ -556,7 +595,9 @@ export interface CreateDivergenceExplainerOptions {
|
|
|
556
595
|
* which assembles the evidence, injects it into the request context, and
|
|
557
596
|
* runs the one structured-output generate call.
|
|
558
597
|
*/
|
|
559
|
-
export function createDivergenceExplainer(
|
|
598
|
+
export function createDivergenceExplainer(
|
|
599
|
+
options: CreateDivergenceExplainerOptions,
|
|
600
|
+
): Agent {
|
|
560
601
|
return new Agent({
|
|
561
602
|
id: options.id ?? "divergence-explainer",
|
|
562
603
|
name: options.name ?? "Divergence Explainer",
|
|
@@ -632,11 +673,14 @@ export async function explainDivergence(
|
|
|
632
673
|
const requestContext = options.requestContext ?? new RequestContext();
|
|
633
674
|
requestContext.set(DIVERGENCE_EVIDENCE_CONTEXT_KEY, evidence);
|
|
634
675
|
const prompt = assembleDivergencePrompt(evidence);
|
|
635
|
-
const result = await options.explainer.generate(
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
676
|
+
const result = await options.explainer.generate(
|
|
677
|
+
[{ role: "user", content: prompt }],
|
|
678
|
+
{
|
|
679
|
+
structuredOutput: { schema: divergenceHypothesisSchema },
|
|
680
|
+
requestContext,
|
|
681
|
+
maxSteps: options.maxSteps ?? 4,
|
|
682
|
+
},
|
|
683
|
+
);
|
|
640
684
|
const hypothesis = divergenceHypothesisSchema.parse(result.object);
|
|
641
685
|
return { hypothesis, evidence, prompt };
|
|
642
686
|
}
|
package/src/judgment.ts
CHANGED
|
@@ -150,9 +150,14 @@ export interface JudgmentGateSpec<TDecision = unknown> {
|
|
|
150
150
|
/** Gathers the evidence and recommendation the gate suspends with. */
|
|
151
151
|
gatherEvidence: (
|
|
152
152
|
ctx: JudgmentGateContext,
|
|
153
|
-
) =>
|
|
153
|
+
) =>
|
|
154
|
+
| Promise<{ recommendation: string; evidence: unknown }>
|
|
155
|
+
| { recommendation: string; evidence: unknown };
|
|
154
156
|
/** Applies the human decision through the host's service layer. */
|
|
155
|
-
applyDecision: (
|
|
157
|
+
applyDecision: (
|
|
158
|
+
ctx: JudgmentGateContext,
|
|
159
|
+
decision: TDecision,
|
|
160
|
+
) => Promise<JudgmentApplication>;
|
|
156
161
|
}
|
|
157
162
|
|
|
158
163
|
/** The completed chain's output: the audit trail and the fused compliance ledger. */
|
|
@@ -224,12 +229,10 @@ const suspendSchema = z.object({
|
|
|
224
229
|
const chainInputSchema = z.object({});
|
|
225
230
|
const chainResultSchema = z.object({
|
|
226
231
|
trail: z.array(trailEntrySchema),
|
|
227
|
-
ledger: z
|
|
228
|
-
(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
Array.isArray((value as { entries?: unknown }).entries),
|
|
232
|
-
),
|
|
232
|
+
ledger: z
|
|
233
|
+
.object({ entries: z.array(ledgerEntrySchema) })
|
|
234
|
+
.strict()
|
|
235
|
+
.transform((value) => value as AssumptionLedger),
|
|
233
236
|
});
|
|
234
237
|
|
|
235
238
|
interface ChainState {
|
|
@@ -249,7 +252,9 @@ function normalizeState(input: unknown): ChainState {
|
|
|
249
252
|
trail: Array.isArray(raw.trail) ? raw.trail : [],
|
|
250
253
|
ledgerEntries: Array.isArray(raw.ledgerEntries) ? raw.ledgerEntries : [],
|
|
251
254
|
decisions:
|
|
252
|
-
typeof raw.decisions === "object" && raw.decisions !== null
|
|
255
|
+
typeof raw.decisions === "object" && raw.decisions !== null
|
|
256
|
+
? raw.decisions
|
|
257
|
+
: {},
|
|
253
258
|
};
|
|
254
259
|
}
|
|
255
260
|
|
|
@@ -258,7 +263,11 @@ function describeDecision(decision: unknown): string {
|
|
|
258
263
|
if (typeof decision === "object" && decision !== null) {
|
|
259
264
|
const named = (decision as { decision?: unknown }).decision;
|
|
260
265
|
if (typeof named === "string" && named.length > 0) return named;
|
|
261
|
-
const {
|
|
266
|
+
const {
|
|
267
|
+
rationale: _rationale,
|
|
268
|
+
actor: _actor,
|
|
269
|
+
...rest
|
|
270
|
+
} = decision as Record<string, unknown>;
|
|
262
271
|
if (Object.keys(rest).length > 0) return JSON.stringify(rest);
|
|
263
272
|
}
|
|
264
273
|
return "decided";
|
|
@@ -266,7 +275,9 @@ function describeDecision(decision: unknown): string {
|
|
|
266
275
|
|
|
267
276
|
function actorOf(decision: unknown): AssumptionActor {
|
|
268
277
|
const actor = (decision as { actor?: unknown } | null | undefined)?.actor;
|
|
269
|
-
return actor === "agent" || actor === "default" || actor === "actuary"
|
|
278
|
+
return actor === "agent" || actor === "default" || actor === "actuary"
|
|
279
|
+
? actor
|
|
280
|
+
: "actuary";
|
|
270
281
|
}
|
|
271
282
|
|
|
272
283
|
// ---------------------------------------------------------------------------
|
|
@@ -283,7 +294,11 @@ const COMPLETE_STEP_ID = "complete";
|
|
|
283
294
|
* at the bottom of createJudgmentChain.)
|
|
284
295
|
*/
|
|
285
296
|
export type JudgmentChainWorkflow = ReturnType<
|
|
286
|
-
typeof createWorkflow<
|
|
297
|
+
typeof createWorkflow<
|
|
298
|
+
string,
|
|
299
|
+
typeof chainInputSchema,
|
|
300
|
+
typeof chainResultSchema
|
|
301
|
+
>
|
|
287
302
|
>;
|
|
288
303
|
|
|
289
304
|
/**
|
|
@@ -301,11 +316,16 @@ export type JudgmentChainWorkflow = ReturnType<
|
|
|
301
316
|
* promise never settles. Assign it synchronously and register it on your
|
|
302
317
|
* Mastra instance.
|
|
303
318
|
*/
|
|
304
|
-
export function createJudgmentChain(
|
|
319
|
+
export function createJudgmentChain(
|
|
320
|
+
options: CreateJudgmentChainOptions,
|
|
321
|
+
): JudgmentChainWorkflow {
|
|
305
322
|
const { id, gates, now, onComplete, requestContextSchema } = options;
|
|
306
323
|
|
|
307
324
|
if (gates.length === 0) {
|
|
308
|
-
throw new AgentsError(
|
|
325
|
+
throw new AgentsError(
|
|
326
|
+
"BAD_GATE",
|
|
327
|
+
`Judgment chain "${id}" needs at least one gate`,
|
|
328
|
+
);
|
|
309
329
|
}
|
|
310
330
|
const seen = new Set<string>();
|
|
311
331
|
for (const gate of gates) {
|
|
@@ -316,7 +336,10 @@ export function createJudgmentChain(options: CreateJudgmentChainOptions): Judgme
|
|
|
316
336
|
);
|
|
317
337
|
}
|
|
318
338
|
if (seen.has(gate.id)) {
|
|
319
|
-
throw new AgentsError(
|
|
339
|
+
throw new AgentsError(
|
|
340
|
+
"BAD_GATE",
|
|
341
|
+
`Duplicate gate id "${gate.id}" in chain "${id}"`,
|
|
342
|
+
);
|
|
320
343
|
}
|
|
321
344
|
seen.add(gate.id);
|
|
322
345
|
const shape = zodObjectShape(gate.resumeSchema);
|
|
@@ -346,11 +369,19 @@ export function createJudgmentChain(options: CreateJudgmentChainOptions): Judgme
|
|
|
346
369
|
|
|
347
370
|
const skipReason = gate.skipWhen?.(ctx) ?? null;
|
|
348
371
|
if (skipReason !== null) {
|
|
349
|
-
const skip: JudgmentSkipRecord = {
|
|
372
|
+
const skip: JudgmentSkipRecord = {
|
|
373
|
+
skipped: true,
|
|
374
|
+
reason: skipReason,
|
|
375
|
+
};
|
|
350
376
|
return {
|
|
351
377
|
trail: [
|
|
352
378
|
...state.trail,
|
|
353
|
-
{
|
|
379
|
+
{
|
|
380
|
+
stage: gate.stage,
|
|
381
|
+
decision: "skipped",
|
|
382
|
+
rationale: skipReason,
|
|
383
|
+
skipped: true,
|
|
384
|
+
},
|
|
354
385
|
],
|
|
355
386
|
ledgerEntries: state.ledgerEntries,
|
|
356
387
|
decisions: { ...state.decisions, [gate.id]: skip },
|
|
@@ -364,7 +395,8 @@ export function createJudgmentChain(options: CreateJudgmentChainOptions): Judgme
|
|
|
364
395
|
}
|
|
365
396
|
|
|
366
397
|
const decision = resumeData;
|
|
367
|
-
const rationale = (decision as { rationale?: unknown } | null)
|
|
398
|
+
const rationale = (decision as { rationale?: unknown } | null)
|
|
399
|
+
?.rationale;
|
|
368
400
|
if (typeof rationale !== "string" || rationale.trim() === "") {
|
|
369
401
|
throw new AgentsError(
|
|
370
402
|
"MISSING_RATIONALE",
|
|
@@ -376,7 +408,9 @@ export function createJudgmentChain(options: CreateJudgmentChainOptions): Judgme
|
|
|
376
408
|
// the payload's job is the decision and its coarse classification.
|
|
377
409
|
const rawIdentity = requestContext?.get(ACTOR_IDENTITY_CONTEXT_KEY);
|
|
378
410
|
const actorIdentity =
|
|
379
|
-
typeof rawIdentity === "string" && rawIdentity.length > 0
|
|
411
|
+
typeof rawIdentity === "string" && rawIdentity.length > 0
|
|
412
|
+
? rawIdentity
|
|
413
|
+
: undefined;
|
|
380
414
|
|
|
381
415
|
const applied = await gate.applyDecision(ctx, decision);
|
|
382
416
|
|
|
@@ -388,7 +422,9 @@ export function createJudgmentChain(options: CreateJudgmentChainOptions): Judgme
|
|
|
388
422
|
ledger = recordAssumption(ledger, {
|
|
389
423
|
field: entry.field,
|
|
390
424
|
value: entry.value,
|
|
391
|
-
...(entry.previousValue !== undefined
|
|
425
|
+
...(entry.previousValue !== undefined
|
|
426
|
+
? { previousValue: entry.previousValue }
|
|
427
|
+
: {}),
|
|
392
428
|
...(entry.source !== undefined ? { source: entry.source } : {}),
|
|
393
429
|
timestamp: entry.timestamp ?? now(),
|
|
394
430
|
actor: entry.actor ?? actor,
|