@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/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: { ultimate: number | null; unpaid: number | null; standardError: number | null };
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: { safeParse(input: unknown): { success: boolean; data?: T; error?: z.ZodError } },
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 doc.result.engine.name === stamp.name && doc.result.engine.version === stamp.version;
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
- (profile.alignment as Record<string, EngineAlignment | undefined>)[engineName] ?? null;
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({ origin: row.origin, metric: "standardError", deviation: row.standardError });
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) maxSe = Math.max(maxSe ?? 0, totals.standardError);
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 && maxSe !== null && maxSe > tolerance.standardError;
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 = engineMatches(a, body.engines.b) && engineMatches(b, body.engines.a);
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
- "engine b and vice versa; pass them in the report's a/b order"
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
- `a=${body.engines.a.name}@${body.engines.a.version} vs ` +
371
- `b=${body.engines.b.name}@${body.engines.b.version}, got ` +
372
- `a=${a.result.engine.name}@${a.result.engine.version} and ` +
373
- `b=${b.result.engine.name}@${b.result.engine.version}`,
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 ?? b.result.engine.conventionProfile ?? null;
379
- const profile = claimedProfileId !== null ? (CONVENTION_PROFILES[claimedProfileId] ?? null) : null;
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
- : ((profile.alignment as Record<string, EngineAlignment | undefined>)[
392
- doc.result.engine.name
393
- ] ?? null),
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
- "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.",
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(DIVERGENCE_EVIDENCE_CONTEXT_KEY);
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 { success: true as const, evidence: evidence as DivergenceEvidence };
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(options: CreateDivergenceExplainerOptions): Agent {
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([{ role: "user", content: prompt }], {
636
- structuredOutput: { schema: divergenceHypothesisSchema },
637
- requestContext,
638
- maxSteps: options.maxSteps ?? 4,
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
- ) => Promise<{ recommendation: string; evidence: unknown }> | { recommendation: string; evidence: unknown };
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: (ctx: JudgmentGateContext, decision: TDecision) => Promise<JudgmentApplication>;
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.custom<AssumptionLedger>(
228
- (value) =>
229
- typeof value === "object" &&
230
- value !== null &&
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 ? raw.decisions : {},
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 { rationale: _rationale, actor: _actor, ...rest } = decision as Record<string, unknown>;
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" ? 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<string, typeof chainInputSchema, typeof chainResultSchema>
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(options: CreateJudgmentChainOptions): JudgmentChainWorkflow {
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("BAD_GATE", `Judgment chain "${id}" needs at least one gate`);
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("BAD_GATE", `Duplicate gate id "${gate.id}" in chain "${id}"`);
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 = { skipped: true, reason: skipReason };
372
+ const skip: JudgmentSkipRecord = {
373
+ skipped: true,
374
+ reason: skipReason,
375
+ };
350
376
  return {
351
377
  trail: [
352
378
  ...state.trail,
353
- { stage: gate.stage, decision: "skipped", rationale: skipReason, skipped: true },
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)?.rationale;
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 ? rawIdentity : undefined;
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 ? { previousValue: entry.previousValue } : {}),
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,