@actuarial-ts/agents 0.6.0 → 0.7.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/src/tools.ts CHANGED
@@ -18,7 +18,10 @@
18
18
  */
19
19
 
20
20
  import { createTool, type Tool } from "@mastra/core/tools";
21
- import { toStandardSchema, type StandardSchemaWithJSON } from "@mastra/core/schema";
21
+ import {
22
+ toStandardSchema,
23
+ type StandardSchemaWithJSON,
24
+ } from "@mastra/core/schema";
22
25
  import type { z } from "zod";
23
26
  import { AgentsError } from "./errors.js";
24
27
 
@@ -33,21 +36,32 @@ export type ToolEnvelopeFailure = {
33
36
 
34
37
  const TOOL_INPUT_INVALID: ToolEnvelopeFailure = Object.freeze({
35
38
  success: false,
36
- error: Object.freeze({ code: "TOOL_INPUT_INVALID", message: "Tool input failed schema validation" }),
39
+ error: Object.freeze({
40
+ code: "TOOL_INPUT_INVALID",
41
+ message: "Tool input failed schema validation",
42
+ }),
37
43
  });
38
44
  const TOOL_OUTPUT_INVALID: ToolEnvelopeFailure = Object.freeze({
39
45
  success: false,
40
- error: Object.freeze({ code: "TOOL_OUTPUT_INVALID", message: "Tool output failed schema validation" }),
46
+ error: Object.freeze({
47
+ code: "TOOL_OUTPUT_INVALID",
48
+ message: "Tool output failed schema validation",
49
+ }),
41
50
  });
42
51
 
43
52
  function readonlyFailure(code: string, message: string): ToolEnvelopeFailure {
44
- return Object.freeze({ success: false, error: Object.freeze({ code, message }) });
53
+ return Object.freeze({
54
+ success: false,
55
+ error: Object.freeze({ code, message }),
56
+ });
45
57
  }
46
58
 
47
59
  function deepFreezeResult<T>(value: T, seen = new WeakSet<object>()): T {
48
- if (value === null || typeof value !== "object" || seen.has(value)) return value;
60
+ if (value === null || typeof value !== "object" || seen.has(value))
61
+ return value;
49
62
  seen.add(value);
50
- for (const child of Object.values(value as Record<string, unknown>)) deepFreezeResult(child, seen);
63
+ for (const child of Object.values(value as Record<string, unknown>))
64
+ deepFreezeResult(child, seen);
51
65
  return Object.freeze(value);
52
66
  }
53
67
 
@@ -57,7 +71,10 @@ function deepFreezeResult<T>(value: T, seen = new WeakSet<object>()): T {
57
71
  * AgentsError, ComplianceError) keep their code; everything else gets
58
72
  * fallbackCode.
59
73
  */
60
- export function envelopeFailure(err: unknown, fallbackCode = "TOOL_ERROR"): ToolEnvelopeFailure {
74
+ export function envelopeFailure(
75
+ err: unknown,
76
+ fallbackCode = "TOOL_ERROR",
77
+ ): ToolEnvelopeFailure {
61
78
  let code = fallbackCode;
62
79
  let message = "Unknown error";
63
80
  try {
@@ -102,7 +119,10 @@ export interface TenantToolContext {
102
119
  * a defineActuarialTool execute the wrapper converts that throw into the
103
120
  * failure envelope, so the model sees a recoverable error, never a crash.
104
121
  */
105
- export function tenantOf(context: TenantToolContext | undefined, key = "projectId"): string {
122
+ export function tenantOf(
123
+ context: TenantToolContext | undefined,
124
+ key = "projectId",
125
+ ): string {
106
126
  return resolveTenant(context, { source: "request-context", key });
107
127
  }
108
128
 
@@ -121,7 +141,9 @@ export interface McpContextLike {
121
141
  * the transport-by-transport story). Returns undefined when no auth info is
122
142
  * present — the caller decides that is fatal.
123
143
  */
124
- export function resolveMcpAuthInfo(context: McpContextLike | undefined): McpAuthInfoLike | undefined {
144
+ export function resolveMcpAuthInfo(
145
+ context: McpContextLike | undefined,
146
+ ): McpAuthInfoLike | undefined {
125
147
  if (!context) return undefined;
126
148
 
127
149
  // Primary: the streamable-HTTP call path passes the transport extra at
@@ -139,7 +161,9 @@ export function resolveMcpAuthInfo(context: McpContextLike | undefined): McpAuth
139
161
 
140
162
  // Lock-tested @mastra/mcp 1.17.3: createProxiedRequestContext copies each
141
163
  // extra key onto the RequestContext verbatim, so authInfo is top-level.
142
- const topLevelAuthInfo = requestContext.get("authInfo") as McpAuthInfoLike | undefined;
164
+ const topLevelAuthInfo = requestContext.get("authInfo") as
165
+ | McpAuthInfoLike
166
+ | undefined;
143
167
  if (topLevelAuthInfo) return topLevelAuthInfo;
144
168
  }
145
169
  return undefined;
@@ -196,7 +220,9 @@ export function resolveTenant(
196
220
  const TENANT_KEY_PATTERN = /^(project|tenant)[_-]?id$/i;
197
221
 
198
222
  /** Top-level shape keys of a zod object schema, or null when not an object schema. */
199
- export function zodObjectShape(schema: unknown): Record<string, unknown> | null {
223
+ export function zodObjectShape(
224
+ schema: unknown,
225
+ ): Record<string, unknown> | null {
200
226
  if (typeof schema !== "object" || schema === null) return null;
201
227
  const def = (schema as { _def?: { typeName?: unknown } })._def;
202
228
  if (def?.typeName !== "ZodObject") return null;
@@ -302,7 +328,15 @@ function assertNoTenantKeys(
302
328
  }
303
329
  stack.add(schema);
304
330
  try {
305
- lintSchemaNode(schema, toolId, path, allowUninspected, usedAllowances, stack, depth);
331
+ lintSchemaNode(
332
+ schema,
333
+ toolId,
334
+ path,
335
+ allowUninspected,
336
+ usedAllowances,
337
+ stack,
338
+ depth,
339
+ );
306
340
  } finally {
307
341
  stack.delete(schema);
308
342
  }
@@ -343,7 +377,15 @@ function lintSchemaNode(
343
377
  `Tool "${toolId}" declares input key "${path}.${key}": tenant ids travel only via the server-set RequestContext (read them with tenantOf), never through the model-facing input schema`,
344
378
  );
345
379
  }
346
- assertNoTenantKeys(value, toolId, `${path}.${key}`, allowUninspected, usedAllowances, stack, depth + 1);
380
+ assertNoTenantKeys(
381
+ value,
382
+ toolId,
383
+ `${path}.${key}`,
384
+ allowUninspected,
385
+ usedAllowances,
386
+ stack,
387
+ depth + 1,
388
+ );
347
389
  }
348
390
  return;
349
391
  }
@@ -369,13 +411,21 @@ function lintSchemaNode(
369
411
  throw new AgentsError(
370
412
  "BAD_INPUT_SCHEMA",
371
413
  `Tool "${toolId}": the ${typeName} at "${path}" admits values the tenant lint cannot ` +
372
- 'inspect. Either declare the shape with typed keys, or — if this input is validated ' +
373
- 'downstream (parseDocument etc.) — name the exact path in `allowUninspected` so the ' +
414
+ "inspect. Either declare the shape with typed keys, or — if this input is validated " +
415
+ "downstream (parseDocument etc.) — name the exact path in `allowUninspected` so the " +
374
416
  "exception is deliberate and greppable",
375
417
  );
376
418
  }
377
419
  if (typeName === "ZodArray") {
378
- assertNoTenantKeys(def.type, toolId, `${path}[]`, allowUninspected, usedAllowances, stack, depth + 1);
420
+ assertNoTenantKeys(
421
+ def.type,
422
+ toolId,
423
+ `${path}[]`,
424
+ allowUninspected,
425
+ usedAllowances,
426
+ stack,
427
+ depth + 1,
428
+ );
379
429
  return;
380
430
  }
381
431
  if (
@@ -385,49 +435,146 @@ function lintSchemaNode(
385
435
  typeName === "ZodCatch" ||
386
436
  typeName === "ZodReadonly"
387
437
  ) {
388
- assertNoTenantKeys(def.innerType, toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
438
+ assertNoTenantKeys(
439
+ def.innerType,
440
+ toolId,
441
+ path,
442
+ allowUninspected,
443
+ usedAllowances,
444
+ stack,
445
+ depth + 1,
446
+ );
389
447
  return;
390
448
  }
391
449
  if (typeName === "ZodPromise" || typeName === "ZodBranded") {
392
- assertNoTenantKeys(def.type, toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
450
+ assertNoTenantKeys(
451
+ def.type,
452
+ toolId,
453
+ path,
454
+ allowUninspected,
455
+ usedAllowances,
456
+ stack,
457
+ depth + 1,
458
+ );
393
459
  return;
394
460
  }
395
461
  if (typeName === "ZodEffects") {
396
- assertNoTenantKeys(def.schema, toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
462
+ assertNoTenantKeys(
463
+ def.schema,
464
+ toolId,
465
+ path,
466
+ allowUninspected,
467
+ usedAllowances,
468
+ stack,
469
+ depth + 1,
470
+ );
397
471
  return;
398
472
  }
399
473
  if (typeName === "ZodUnion" || typeName === "ZodDiscriminatedUnion") {
400
- for (const opt of def.options ?? []) assertNoTenantKeys(opt, toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
474
+ for (const opt of def.options ?? [])
475
+ assertNoTenantKeys(
476
+ opt,
477
+ toolId,
478
+ path,
479
+ allowUninspected,
480
+ usedAllowances,
481
+ stack,
482
+ depth + 1,
483
+ );
401
484
  return;
402
485
  }
403
486
  if (typeName === "ZodTuple") {
404
487
  for (const [index, item] of (def.items ?? []).entries()) {
405
- assertNoTenantKeys(item, toolId, `${path}[${index}]`, allowUninspected, usedAllowances, stack, depth + 1);
488
+ assertNoTenantKeys(
489
+ item,
490
+ toolId,
491
+ `${path}[${index}]`,
492
+ allowUninspected,
493
+ usedAllowances,
494
+ stack,
495
+ depth + 1,
496
+ );
406
497
  }
407
498
  if (def.rest !== undefined && def.rest !== null) {
408
- assertNoTenantKeys(def.rest, toolId, `${path}[rest]`, allowUninspected, usedAllowances, stack, depth + 1);
499
+ assertNoTenantKeys(
500
+ def.rest,
501
+ toolId,
502
+ `${path}[rest]`,
503
+ allowUninspected,
504
+ usedAllowances,
505
+ stack,
506
+ depth + 1,
507
+ );
409
508
  }
410
509
  return;
411
510
  }
412
511
  if (typeName === "ZodIntersection") {
413
- assertNoTenantKeys(def.left, toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
414
- assertNoTenantKeys(def.right, toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
512
+ assertNoTenantKeys(
513
+ def.left,
514
+ toolId,
515
+ path,
516
+ allowUninspected,
517
+ usedAllowances,
518
+ stack,
519
+ depth + 1,
520
+ );
521
+ assertNoTenantKeys(
522
+ def.right,
523
+ toolId,
524
+ path,
525
+ allowUninspected,
526
+ usedAllowances,
527
+ stack,
528
+ depth + 1,
529
+ );
415
530
  return;
416
531
  }
417
532
  if (typeName === "ZodPipeline") {
418
- assertNoTenantKeys(def.in, toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
419
- assertNoTenantKeys(def.out, toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
533
+ assertNoTenantKeys(
534
+ def.in,
535
+ toolId,
536
+ path,
537
+ allowUninspected,
538
+ usedAllowances,
539
+ stack,
540
+ depth + 1,
541
+ );
542
+ assertNoTenantKeys(
543
+ def.out,
544
+ toolId,
545
+ path,
546
+ allowUninspected,
547
+ usedAllowances,
548
+ stack,
549
+ depth + 1,
550
+ );
420
551
  return;
421
552
  }
422
553
  if (typeName === "ZodSet") {
423
- assertNoTenantKeys(def.valueType, toolId, `${path}[]`, allowUninspected, usedAllowances, stack, depth + 1);
554
+ assertNoTenantKeys(
555
+ def.valueType,
556
+ toolId,
557
+ `${path}[]`,
558
+ allowUninspected,
559
+ usedAllowances,
560
+ stack,
561
+ depth + 1,
562
+ );
424
563
  return;
425
564
  }
426
565
  if (typeName === "ZodLazy") {
427
566
  // Resolve once. A self-referential lazy resolves to a node already on the
428
567
  // recursion stack and is caught by the cycle guard in assertNoTenantKeys;
429
568
  // a generative lazy (fresh node per resolution) hits the frame budget.
430
- assertNoTenantKeys(def.getter?.(), toolId, path, allowUninspected, usedAllowances, stack, depth + 1);
569
+ assertNoTenantKeys(
570
+ def.getter?.(),
571
+ toolId,
572
+ path,
573
+ allowUninspected,
574
+ usedAllowances,
575
+ stack,
576
+ depth + 1,
577
+ );
431
578
  return;
432
579
  }
433
580
 
@@ -481,7 +628,11 @@ interface DefineActuarialToolCommon<TShape extends z.ZodRawShape, TResult> {
481
628
  * Optional observable-result schema. It must admit the complete success /
482
629
  * failure union because validation errors are ordinary tool results.
483
630
  */
484
- outputSchema?: z.ZodType<TResult | ToolEnvelopeFailure, z.ZodTypeDef, unknown>;
631
+ outputSchema?: z.ZodType<
632
+ TResult | ToolEnvelopeFailure,
633
+ z.ZodTypeDef,
634
+ unknown
635
+ >;
485
636
  }
486
637
 
487
638
  /**
@@ -541,7 +692,9 @@ export type DefinedActuarialTool<TInput, TOutput> = Omit<
541
692
  execute: (input: TInput, context: ActuarialToolContext) => Promise<TOutput>;
542
693
  };
543
694
 
544
- function metadataBridge(schema: z.ZodTypeAny): StandardSchemaWithJSON<unknown, unknown> {
695
+ function metadataBridge(
696
+ schema: z.ZodTypeAny,
697
+ ): StandardSchemaWithJSON<unknown, unknown> {
545
698
  const real = toStandardSchema(schema);
546
699
  return {
547
700
  "~standard": {
@@ -559,7 +712,12 @@ function metadataBridge(schema: z.ZodTypeAny): StandardSchemaWithJSON<unknown, u
559
712
  function isFailure(value: unknown): value is ToolEnvelopeFailure {
560
713
  if (value === null || typeof value !== "object") return false;
561
714
  const candidate = value as { success?: unknown; error?: unknown };
562
- if (candidate.success !== false || candidate.error === null || typeof candidate.error !== "object") return false;
715
+ if (
716
+ candidate.success !== false ||
717
+ candidate.error === null ||
718
+ typeof candidate.error !== "object"
719
+ )
720
+ return false;
563
721
  const error = candidate.error as { code?: unknown; message?: unknown };
564
722
  return typeof error.code === "string" && typeof error.message === "string";
565
723
  }
@@ -578,7 +736,10 @@ function sameJson(left: unknown, right: unknown): boolean {
578
736
  */
579
737
  export function defineActuarialTool<TShape extends z.ZodRawShape, TResult>(
580
738
  options: DefineActuarialToolOptions<TShape, TResult>,
581
- ): DefinedActuarialTool<z.input<z.ZodObject<TShape>>, TResult | ToolEnvelopeFailure> {
739
+ ): DefinedActuarialTool<
740
+ z.input<z.ZodObject<TShape>>,
741
+ TResult | ToolEnvelopeFailure
742
+ > {
582
743
  // FAIL CLOSED: a schema the seam cannot inspect is not definable, and the
583
744
  // tenant-key lint recurses through every container the model could reach.
584
745
  const shape = zodObjectShape(options.inputSchema);
@@ -590,7 +751,13 @@ export function defineActuarialTool<TShape extends z.ZodRawShape, TResult>(
590
751
  }
591
752
  const allowUninspected = new Set(options.allowUninspected ?? []);
592
753
  const usedAllowances = new Set<string>();
593
- assertNoTenantKeys(options.inputSchema, options.id, "input", allowUninspected, usedAllowances);
754
+ assertNoTenantKeys(
755
+ options.inputSchema,
756
+ options.id,
757
+ "input",
758
+ allowUninspected,
759
+ usedAllowances,
760
+ );
594
761
  for (const declared of allowUninspected) {
595
762
  if (!usedAllowances.has(declared)) {
596
763
  throw new AgentsError(
@@ -632,7 +799,9 @@ export function defineActuarialTool<TShape extends z.ZodRawShape, TResult>(
632
799
  id: options.id,
633
800
  description: options.description,
634
801
  inputSchema: metadataBridge(options.inputSchema),
635
- ...(options.outputSchema === undefined ? {} : { outputSchema: metadataBridge(options.outputSchema) }),
802
+ ...(options.outputSchema === undefined
803
+ ? {}
804
+ : { outputSchema: metadataBridge(options.outputSchema) }),
636
805
  });
637
806
 
638
807
  const execute = async (
@@ -672,14 +841,18 @@ export function defineActuarialTool<TShape extends z.ZodRawShape, TResult>(
672
841
  return TOOL_OUTPUT_INVALID;
673
842
  }
674
843
  if (!parsedOutput.success) return TOOL_OUTPUT_INVALID;
675
- if (isFailure(rawOutput) && !sameJson(parsedOutput.data, rawOutput)) return TOOL_OUTPUT_INVALID;
844
+ if (isFailure(rawOutput) && !sameJson(parsedOutput.data, rawOutput))
845
+ return TOOL_OUTPUT_INVALID;
676
846
  return deepFreezeResult(parsedOutput.data);
677
847
  };
678
848
 
679
849
  // The metadata bridges deliberately erase domain inference on the inherited
680
850
  // Mastra surface. This is the single convergence assertion: the adapter
681
851
  // above is the only executor and has the exact public input/output contract.
682
- const defined = Object.assign(tool, { execute, kind: options.kind }) as DefinedActuarialTool<
852
+ const defined = Object.assign(tool, {
853
+ execute,
854
+ kind: options.kind,
855
+ }) as DefinedActuarialTool<
683
856
  z.input<z.ZodObject<TShape>>,
684
857
  TResult | ToolEnvelopeFailure
685
858
  >;
@@ -710,10 +883,10 @@ export interface ActuarialToolRegistry<T extends RegistrableActuarialTool> {
710
883
  export function toolRegistry<T extends RegistrableActuarialTool>(
711
884
  tools: readonly T[],
712
885
  ): ActuarialToolRegistry<T> {
713
- const record: Record<string, T> = {};
886
+ const record = Object.create(null) as Record<string, T>;
714
887
  const actionToolIds = new Set<string>();
715
888
  for (const tool of tools) {
716
- if (record[tool.id]) {
889
+ if (Object.prototype.hasOwnProperty.call(record, tool.id)) {
717
890
  throw new AgentsError(
718
891
  "DUPLICATE_TOOL_ID",
719
892
  `Two tools share the id "${tool.id}"; tool ids must be unique within a registry`,