@actuarial-ts/agents 0.4.0 → 0.6.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/README.md +31 -129
- package/dist/diagnostics.d.ts +64 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +92 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/divergence.d.ts +4 -3
- package/dist/divergence.d.ts.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +7 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +2 -2
- package/dist/promotion.d.ts.map +1 -1
- package/dist/promotion.js +2 -0
- package/dist/promotion.js.map +1 -1
- package/dist/remote.d.ts +12 -14
- package/dist/remote.d.ts.map +1 -1
- package/dist/tools.d.ts +26 -12
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +113 -20
- package/dist/tools.js.map +1 -1
- package/package.json +10 -10
- package/src/diagnostics.ts +71 -0
- package/src/errors.ts +7 -0
- package/src/index.ts +1 -0
- package/src/mcp.ts +2 -2
- package/src/promotion.ts +2 -0
- package/src/tools.ts +153 -29
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { assertCompiledDiagnosticDefinition, type CompiledDiagnosticDefinition, type DiagnosticDeepReadonly } from "@actuarial-ts/core";
|
|
2
|
+
import { assertVerifiedDiagnosticRunProvenance, type VerifiedDiagnosticRunProvenance } from "@actuarial-ts/compliance";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { AgentsError } from "./errors.js";
|
|
5
|
+
import { defineActuarialTool, type DefinedActuarialTool, type ToolEnvelopeFailure } from "./tools.js";
|
|
6
|
+
|
|
7
|
+
export const diagnosticAgentToolInputSchema = z.object({
|
|
8
|
+
runPresetId:z.string().min(1),
|
|
9
|
+
instanceIds:z.array(z.string().min(1)).min(1),
|
|
10
|
+
view:z.enum(["emergence","triangles","latest-diagonal"]),
|
|
11
|
+
}).strict();
|
|
12
|
+
export type DiagnosticAgentToolInput=z.input<typeof diagnosticAgentToolInputSchema>;
|
|
13
|
+
|
|
14
|
+
export interface DiagnosticAgentRunPreset {
|
|
15
|
+
readonly id:string;
|
|
16
|
+
readonly definitionIntegrity:string;
|
|
17
|
+
readonly allowedInstanceIds:readonly string[];
|
|
18
|
+
readonly execute:(input:{readonly tenant:string;readonly instanceIds:readonly string[]})=>Promise<VerifiedDiagnosticRunProvenance>;
|
|
19
|
+
}
|
|
20
|
+
export interface CreateDiagnosticSelectionToolInput { readonly definition:CompiledDiagnosticDefinition;readonly presets:readonly DiagnosticAgentRunPreset[];readonly id?:string;readonly description?:string;readonly tenantContextKey?:string }
|
|
21
|
+
export type DiagnosticAgentDisplayProjection=
|
|
22
|
+
| {readonly view:"emergence";readonly value:VerifiedDiagnosticRunProvenance["result"]["emergence"]}
|
|
23
|
+
| {readonly view:"triangles";readonly value:VerifiedDiagnosticRunProvenance["result"]["triangles"]}
|
|
24
|
+
| {readonly view:"latest-diagonal";readonly value:VerifiedDiagnosticRunProvenance["result"]["latestDiagonal"]};
|
|
25
|
+
export type DiagnosticAgentDisplayPoint=
|
|
26
|
+
| VerifiedDiagnosticRunProvenance["result"]["emergence"][number]
|
|
27
|
+
| VerifiedDiagnosticRunProvenance["result"]["triangles"][number]
|
|
28
|
+
| VerifiedDiagnosticRunProvenance["result"]["latestDiagonal"][number];
|
|
29
|
+
export interface DiagnosticAgentToolSuccess {
|
|
30
|
+
readonly success:true;readonly runPresetId:string;readonly instanceIds:readonly string[];
|
|
31
|
+
readonly definitionIntegrity:string;readonly formulaFingerprints:Readonly<Record<string,string>>;
|
|
32
|
+
readonly calculationFingerprints:Readonly<Record<string,string>>;readonly runFingerprint:string;
|
|
33
|
+
readonly resultFingerprint:string;readonly runResultFingerprint:string;
|
|
34
|
+
readonly review:VerifiedDiagnosticRunProvenance["review"];readonly display:DiagnosticAgentDisplayProjection;
|
|
35
|
+
}
|
|
36
|
+
export type DiagnosticAgentToolResult=DiagnosticDeepReadonly<DiagnosticAgentToolSuccess>|ToolEnvelopeFailure;
|
|
37
|
+
|
|
38
|
+
const toolFailureSchema=z.object({success:z.literal(false),error:z.object({code:z.string(),message:z.string()}).strict()}).strict();
|
|
39
|
+
const displaySchema=z.discriminatedUnion("view",[
|
|
40
|
+
z.object({view:z.literal("emergence"),value:z.custom<Extract<DiagnosticAgentDisplayProjection,{readonly view:"emergence"}>["value"]>()}).strict(),
|
|
41
|
+
z.object({view:z.literal("triangles"),value:z.custom<Extract<DiagnosticAgentDisplayProjection,{readonly view:"triangles"}>["value"]>()}).strict(),
|
|
42
|
+
z.object({view:z.literal("latest-diagonal"),value:z.custom<Extract<DiagnosticAgentDisplayProjection,{readonly view:"latest-diagonal"}>["value"]>()}).strict(),
|
|
43
|
+
]);
|
|
44
|
+
const toolSuccessSchema=z.object({
|
|
45
|
+
success:z.literal(true),runPresetId:z.string(),instanceIds:z.array(z.string()),definitionIntegrity:z.string(),
|
|
46
|
+
formulaFingerprints:z.record(z.string()),calculationFingerprints:z.record(z.string()),runFingerprint:z.string(),
|
|
47
|
+
resultFingerprint:z.string(),runResultFingerprint:z.string(),review:z.custom<VerifiedDiagnosticRunProvenance["review"]>(),
|
|
48
|
+
display:displaySchema,
|
|
49
|
+
}).strict();
|
|
50
|
+
/** Strict model-visible output schema, including the wrapper's failure branch. */
|
|
51
|
+
export const diagnosticAgentToolResultSchema:z.ZodType<DiagnosticAgentToolResult>=z.union([toolSuccessSchema,toolFailureSchema]);
|
|
52
|
+
export type DiagnosticSelectionTool=DefinedActuarialTool<DiagnosticAgentToolInput,DiagnosticAgentToolResult>;
|
|
53
|
+
|
|
54
|
+
function token(value:string,label:string):void{if(value.length===0||/^[\t-\r ]|[\t-\r ]$/.test(value)||value.includes("\0"))throw new AgentsError("BAD_DIAGNOSTIC_CATALOG",`${label} must be a nonempty token`)}
|
|
55
|
+
function sortUniqueRequested(values:readonly string[]):string[]{const result=[...new Set(values)];result.sort((a,b)=>a<b?-1:a>b?1:0);return result}
|
|
56
|
+
|
|
57
|
+
export function createDiagnosticSelectionTool(input:CreateDiagnosticSelectionToolInput):DiagnosticSelectionTool{
|
|
58
|
+
try{assertCompiledDiagnosticDefinition(input.definition)}catch{throw new AgentsError("BAD_DIAGNOSTIC_CATALOG","definition must be an authentic compiled diagnostic definition")}
|
|
59
|
+
const id=input.id??"run_diagnostic_selection";const description=input.description??"Run a host-approved diagnostic preset for selected registered metric instances.";const tenantKey=input.tenantContextKey??"projectId";
|
|
60
|
+
token(id,"tool id");token(tenantKey,"tenant context key");if(description.trim().length===0)throw new AgentsError("BAD_DIAGNOSTIC_CATALOG","description must be nonblank");if(input.presets.length===0)throw new AgentsError("BAD_DIAGNOSTIC_CATALOG","at least one approved diagnostic preset is required");
|
|
61
|
+
const known=new Set(input.definition.definition.instances.map((item)=>item.id));const catalog=new Map<string,{definitionIntegrity:string;allowedInstanceIds:readonly string[];execute:DiagnosticAgentRunPreset["execute"]}>();
|
|
62
|
+
for(const preset of input.presets){token(preset.id,"preset id");if(catalog.has(preset.id))throw new AgentsError("BAD_DIAGNOSTIC_CATALOG",`duplicate preset ${preset.id}`);if(preset.definitionIntegrity!==input.definition.definitionIntegrity)throw new AgentsError("BAD_DIAGNOSTIC_CATALOG",`preset ${preset.id} targets another definition`);if(typeof preset.execute!=="function")throw new AgentsError("BAD_DIAGNOSTIC_CATALOG",`preset ${preset.id} has no executor`);const seen=new Set<string>();for(const instanceId of preset.allowedInstanceIds){token(instanceId,"allowed instance id");if(seen.has(instanceId))throw new AgentsError("BAD_DIAGNOSTIC_CATALOG",`preset ${preset.id} repeats ${instanceId}`);if(!known.has(instanceId))throw new AgentsError("BAD_DIAGNOSTIC_CATALOG",`preset ${preset.id} references unknown instance ${instanceId}`);seen.add(instanceId)}if(seen.size===0)throw new AgentsError("BAD_DIAGNOSTIC_CATALOG",`preset ${preset.id} has no allowed instances`);catalog.set(preset.id,Object.freeze({definitionIntegrity:preset.definitionIntegrity,allowedInstanceIds:Object.freeze([...seen].sort()),execute:preset.execute}))}
|
|
63
|
+
return defineActuarialTool({id,description,kind:"read",tenant:"required",tenantKey,inputSchema:diagnosticAgentToolInputSchema,outputSchema:diagnosticAgentToolResultSchema,execute:async(raw,tenant):Promise<DiagnosticAgentToolSuccess>=>{
|
|
64
|
+
const preset=catalog.get(raw.runPresetId);if(!preset)throw new AgentsError("UNKNOWN_DIAGNOSTIC_PRESET",`Unknown diagnostic preset ${raw.runPresetId}`);const selected=sortUniqueRequested(raw.instanceIds);if(selected.some((item)=>!preset.allowedInstanceIds.includes(item)))throw new AgentsError("UNAPPROVED_DIAGNOSTIC_INSTANCE","One or more diagnostic instances are not approved by the selected preset");
|
|
65
|
+
const provenance=await preset.execute({tenant,instanceIds:selected});try{assertVerifiedDiagnosticRunProvenance(provenance)}catch{throw new AgentsError("DIAGNOSTIC_RUN_MISMATCH","Preset executor returned unauthenticated diagnostic provenance")}
|
|
66
|
+
const filter=provenance.manifest.preparation.filter;if(provenance.definitionIdentities.definition!==input.definition.definitionIntegrity||provenance.manifest.runPresetId!==raw.runPresetId||!filter||JSON.stringify(filter.instanceIds??[])!==JSON.stringify(selected))throw new AgentsError("DIAGNOSTIC_RUN_MISMATCH","Verified run does not match the selected definition, preset, and exact instance set");
|
|
67
|
+
const instances=input.definition.definition.instances.filter((item)=>selected.includes(item.id));const formulaIds=[...new Set(instances.map((item)=>item.formulaId))].sort();const formulaFingerprints=Object.fromEntries(formulaIds.map((formulaId)=>[formulaId,provenance.definitionIdentities.formulaById[formulaId]!]));const calculationFingerprints=Object.fromEntries(selected.map((instanceId)=>[instanceId,provenance.definitionIdentities.calculationByInstanceId[instanceId]!]));
|
|
68
|
+
const display=raw.view==="emergence"?{view:"emergence" as const,value:provenance.result.emergence}:raw.view==="triangles"?{view:"triangles" as const,value:provenance.result.triangles}:{view:"latest-diagonal" as const,value:provenance.result.latestDiagonal};
|
|
69
|
+
return {success:true,runPresetId:raw.runPresetId,instanceIds:selected,definitionIntegrity:provenance.definitionIdentities.definition,formulaFingerprints,calculationFingerprints,runFingerprint:provenance.runFingerprint,resultFingerprint:provenance.resultFingerprint,runResultFingerprint:provenance.runResultFingerprint,review:provenance.review,display};
|
|
70
|
+
}});
|
|
71
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -43,6 +43,13 @@ export const AGENTS_ERROR_CODES = [
|
|
|
43
43
|
"REMOTE_RESULT_INVALID",
|
|
44
44
|
/** The boot self-test found an MCP-exposed probe tool that did NOT fail closed without tenant context; the MCP tenant seam is not wired up and the server must abort startup. */
|
|
45
45
|
"MCP_SELF_TEST_FAILED",
|
|
46
|
+
"BAD_DIAGNOSTIC_CATALOG",
|
|
47
|
+
"UNKNOWN_DIAGNOSTIC_PRESET",
|
|
48
|
+
"UNAPPROVED_DIAGNOSTIC_INSTANCE",
|
|
49
|
+
"DIAGNOSTIC_RUN_MISMATCH",
|
|
50
|
+
"BAD_OUTPUT_SCHEMA",
|
|
51
|
+
"TOOL_INPUT_INVALID",
|
|
52
|
+
"TOOL_OUTPUT_INVALID",
|
|
46
53
|
] as const;
|
|
47
54
|
|
|
48
55
|
export type AgentsErrorCode = (typeof AGENTS_ERROR_CODES)[number];
|
package/src/index.ts
CHANGED
package/src/mcp.ts
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* startup MUST abort.
|
|
23
23
|
*
|
|
24
24
|
* ---------------------------------------------------------------------------
|
|
25
|
-
* VERIFIED against the
|
|
25
|
+
* VERIFIED against the lock-tested @mastra/mcp 1.17.3 (house rule — types and
|
|
26
26
|
* compiled source, not memory):
|
|
27
27
|
*
|
|
28
28
|
* - MCPServer.executeTool(toolId, args, executionContext?: { messages?,
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* tool context TWO ways: directly at `context.mcp.extra`, and — via
|
|
41
41
|
* createProxiedRequestContext — as INDIVIDUAL keys set on a fresh
|
|
42
42
|
* RequestContext (`context.requestContext.get("authInfo")`). NOTE the
|
|
43
|
-
* surprise: the
|
|
43
|
+
* surprise: the lock-tested 1.17.3 sets each extra key on the RequestContext
|
|
44
44
|
* verbatim, so the tenant lives at `requestContext.get("authInfo")`, NOT
|
|
45
45
|
* under a single `"mcp.extra"` key as the research draft documented. This
|
|
46
46
|
* helper tries all three shapes so it is correct against both the installed
|
package/src/promotion.ts
CHANGED
|
@@ -408,6 +408,7 @@ function structuralCheck(
|
|
|
408
408
|
description,
|
|
409
409
|
status: findings.length > 0 ? "warning" : "pass",
|
|
410
410
|
details: capDetails(findings),
|
|
411
|
+
findings: findings.map((message) => ({ code: id, message, context: {} })),
|
|
411
412
|
};
|
|
412
413
|
}
|
|
413
414
|
|
|
@@ -472,6 +473,7 @@ function notEvaluatedReview(reason: string): DataReviewReport {
|
|
|
472
473
|
description: "ASOP 23-oriented triangle review",
|
|
473
474
|
status: "not-evaluated",
|
|
474
475
|
details: [`not evaluated: ${reason}`],
|
|
476
|
+
findings: [],
|
|
475
477
|
},
|
|
476
478
|
],
|
|
477
479
|
summary: { pass: 0, warning: 0, fail: 0, notEvaluated: 1 },
|
package/src/tools.ts
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* keep their code; everything else gets the fallback.
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { createTool } from "@mastra/core/tools";
|
|
20
|
+
import { createTool, type Tool } from "@mastra/core/tools";
|
|
21
|
+
import { toStandardSchema, type StandardSchemaWithJSON } from "@mastra/core/schema";
|
|
21
22
|
import type { z } from "zod";
|
|
22
23
|
import { AgentsError } from "./errors.js";
|
|
23
24
|
|
|
@@ -26,10 +27,30 @@ import { AgentsError } from "./errors.js";
|
|
|
26
27
|
|
|
27
28
|
/** The uniform tool-failure shape: agents branch on success, hosts log code. */
|
|
28
29
|
export type ToolEnvelopeFailure = {
|
|
29
|
-
success: false;
|
|
30
|
-
error: { code: string; message: string };
|
|
30
|
+
readonly success: false;
|
|
31
|
+
readonly error: { readonly code: string; readonly message: string };
|
|
31
32
|
};
|
|
32
33
|
|
|
34
|
+
const TOOL_INPUT_INVALID: ToolEnvelopeFailure = Object.freeze({
|
|
35
|
+
success: false,
|
|
36
|
+
error: Object.freeze({ code: "TOOL_INPUT_INVALID", message: "Tool input failed schema validation" }),
|
|
37
|
+
});
|
|
38
|
+
const TOOL_OUTPUT_INVALID: ToolEnvelopeFailure = Object.freeze({
|
|
39
|
+
success: false,
|
|
40
|
+
error: Object.freeze({ code: "TOOL_OUTPUT_INVALID", message: "Tool output failed schema validation" }),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function readonlyFailure(code: string, message: string): ToolEnvelopeFailure {
|
|
44
|
+
return Object.freeze({ success: false, error: Object.freeze({ code, message }) });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function deepFreezeResult<T>(value: T, seen = new WeakSet<object>()): T {
|
|
48
|
+
if (value === null || typeof value !== "object" || seen.has(value)) return value;
|
|
49
|
+
seen.add(value);
|
|
50
|
+
for (const child of Object.values(value as Record<string, unknown>)) deepFreezeResult(child, seen);
|
|
51
|
+
return Object.freeze(value);
|
|
52
|
+
}
|
|
53
|
+
|
|
33
54
|
/**
|
|
34
55
|
* Converts anything thrown by a tool into the failure envelope. Never throws.
|
|
35
56
|
* Error-like values with a non-empty string "code" property (HttpError,
|
|
@@ -60,7 +81,7 @@ export function envelopeFailure(err: unknown, fallbackCode = "TOOL_ERROR"): Tool
|
|
|
60
81
|
} catch {
|
|
61
82
|
// A hostile getter must not break the envelope; keep the fallbacks.
|
|
62
83
|
}
|
|
63
|
-
return
|
|
84
|
+
return readonlyFailure(code, message);
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
// ---------------------------------------------------------------------------
|
|
@@ -116,7 +137,7 @@ export function resolveMcpAuthInfo(context: McpContextLike | undefined): McpAuth
|
|
|
116
137
|
| undefined;
|
|
117
138
|
if (proxiedExtra?.authInfo) return proxiedExtra.authInfo;
|
|
118
139
|
|
|
119
|
-
//
|
|
140
|
+
// Lock-tested @mastra/mcp 1.17.3: createProxiedRequestContext copies each
|
|
120
141
|
// extra key onto the RequestContext verbatim, so authInfo is top-level.
|
|
121
142
|
const topLevelAuthInfo = requestContext.get("authInfo") as McpAuthInfoLike | undefined;
|
|
122
143
|
if (topLevelAuthInfo) return topLevelAuthInfo;
|
|
@@ -433,7 +454,7 @@ export type ActuarialToolKind = "read" | "action";
|
|
|
433
454
|
*/
|
|
434
455
|
export type ActuarialToolContext = TenantToolContext;
|
|
435
456
|
|
|
436
|
-
interface DefineActuarialToolCommon<TShape extends z.ZodRawShape> {
|
|
457
|
+
interface DefineActuarialToolCommon<TShape extends z.ZodRawShape, TResult> {
|
|
437
458
|
/**
|
|
438
459
|
* Exact schema paths (the lint's dot notation, rooted at "input") where an
|
|
439
460
|
* uninspectable type — z.unknown(), z.any(), z.map() — is INTENTIONAL
|
|
@@ -456,6 +477,11 @@ interface DefineActuarialToolCommon<TShape extends z.ZodRawShape> {
|
|
|
456
477
|
* AgentsError("TENANT_IN_SCHEMA") at definition time if it does.
|
|
457
478
|
*/
|
|
458
479
|
inputSchema: z.ZodObject<TShape>;
|
|
480
|
+
/**
|
|
481
|
+
* Optional observable-result schema. It must admit the complete success /
|
|
482
|
+
* failure union because validation errors are ordinary tool results.
|
|
483
|
+
*/
|
|
484
|
+
outputSchema?: z.ZodType<TResult | ToolEnvelopeFailure, z.ZodTypeDef, unknown>;
|
|
459
485
|
}
|
|
460
486
|
|
|
461
487
|
/**
|
|
@@ -474,7 +500,7 @@ interface DefineActuarialToolCommon<TShape extends z.ZodRawShape> {
|
|
|
474
500
|
* reviewable at the definition site.
|
|
475
501
|
*/
|
|
476
502
|
export type DefineActuarialToolOptions<TShape extends z.ZodRawShape, TResult> =
|
|
477
|
-
| (DefineActuarialToolCommon<TShape> & {
|
|
503
|
+
| (DefineActuarialToolCommon<TShape, TResult> & {
|
|
478
504
|
tenant: "required";
|
|
479
505
|
/** Trusted source for the tenant id. Default "request-context". */
|
|
480
506
|
tenantSource?: TenantSource;
|
|
@@ -487,27 +513,72 @@ export type DefineActuarialToolOptions<TShape extends z.ZodRawShape, TResult> =
|
|
|
487
513
|
* envelope, never an exception.
|
|
488
514
|
*/
|
|
489
515
|
execute: (
|
|
490
|
-
input: z.
|
|
516
|
+
input: z.output<z.ZodObject<TShape>>,
|
|
491
517
|
tenant: string,
|
|
492
518
|
context: ActuarialToolContext,
|
|
493
519
|
) => Promise<TResult>;
|
|
494
520
|
})
|
|
495
|
-
| (DefineActuarialToolCommon<TShape> & {
|
|
521
|
+
| (DefineActuarialToolCommon<TShape, TResult> & {
|
|
496
522
|
tenant: "none";
|
|
497
523
|
execute: (
|
|
498
|
-
input: z.
|
|
524
|
+
input: z.output<z.ZodObject<TShape>>,
|
|
499
525
|
tenant: null,
|
|
500
526
|
context: ActuarialToolContext,
|
|
501
527
|
) => Promise<TResult>;
|
|
502
528
|
});
|
|
503
529
|
|
|
530
|
+
/**
|
|
531
|
+
* A Mastra-compatible tool whose direct execute boundary is fully owned by
|
|
532
|
+
* this SDK. Mastra metadata stays intentionally unknown: the real domain
|
|
533
|
+
* schemas are retained privately so framework validation cannot run their
|
|
534
|
+
* transforms a second time.
|
|
535
|
+
*/
|
|
536
|
+
export type DefinedActuarialTool<TInput, TOutput> = Omit<
|
|
537
|
+
Tool<unknown, unknown>,
|
|
538
|
+
"execute"
|
|
539
|
+
> & {
|
|
540
|
+
readonly kind: ActuarialToolKind;
|
|
541
|
+
execute: (input: TInput, context: ActuarialToolContext) => Promise<TOutput>;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
function metadataBridge(schema: z.ZodTypeAny): StandardSchemaWithJSON<unknown, unknown> {
|
|
545
|
+
const real = toStandardSchema(schema);
|
|
546
|
+
return {
|
|
547
|
+
"~standard": {
|
|
548
|
+
version: 1,
|
|
549
|
+
vendor: "actuarial-ts-metadata-bridge",
|
|
550
|
+
validate: (value: unknown) => ({ value }),
|
|
551
|
+
jsonSchema: {
|
|
552
|
+
input: (options) => real["~standard"].jsonSchema.input(options),
|
|
553
|
+
output: (options) => real["~standard"].jsonSchema.output(options),
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function isFailure(value: unknown): value is ToolEnvelopeFailure {
|
|
560
|
+
if (value === null || typeof value !== "object") return false;
|
|
561
|
+
const candidate = value as { success?: unknown; error?: unknown };
|
|
562
|
+
if (candidate.success !== false || candidate.error === null || typeof candidate.error !== "object") return false;
|
|
563
|
+
const error = candidate.error as { code?: unknown; message?: unknown };
|
|
564
|
+
return typeof error.code === "string" && typeof error.message === "string";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function sameJson(left: unknown, right: unknown): boolean {
|
|
568
|
+
try {
|
|
569
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
570
|
+
} catch {
|
|
571
|
+
return false;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
504
575
|
/**
|
|
505
576
|
* Wraps Mastra's createTool with the envelope + tenant-seam guarantees and
|
|
506
577
|
* tags the result with its kind for toolRegistry classification.
|
|
507
578
|
*/
|
|
508
579
|
export function defineActuarialTool<TShape extends z.ZodRawShape, TResult>(
|
|
509
580
|
options: DefineActuarialToolOptions<TShape, TResult>,
|
|
510
|
-
) {
|
|
581
|
+
): DefinedActuarialTool<z.input<z.ZodObject<TShape>>, TResult | ToolEnvelopeFailure> {
|
|
511
582
|
// FAIL CLOSED: a schema the seam cannot inspect is not definable, and the
|
|
512
583
|
// tenant-key lint recurses through every container the model could reach.
|
|
513
584
|
const shape = zodObjectShape(options.inputSchema);
|
|
@@ -538,28 +609,81 @@ export function defineActuarialTool<TShape extends z.ZodRawShape, TResult>(
|
|
|
538
609
|
"relationship to the tenant seam explicitly",
|
|
539
610
|
);
|
|
540
611
|
}
|
|
612
|
+
|
|
613
|
+
if (options.outputSchema !== undefined) {
|
|
614
|
+
let probe;
|
|
615
|
+
try {
|
|
616
|
+
probe = options.outputSchema.safeParse(TOOL_OUTPUT_INVALID);
|
|
617
|
+
} catch {
|
|
618
|
+
throw new AgentsError(
|
|
619
|
+
"BAD_OUTPUT_SCHEMA",
|
|
620
|
+
`Tool "${options.id}": outputSchema threw while validating the required failure envelope`,
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
if (!probe.success || !sameJson(probe.data, TOOL_OUTPUT_INVALID)) {
|
|
624
|
+
throw new AgentsError(
|
|
625
|
+
"BAD_OUTPUT_SCHEMA",
|
|
626
|
+
`Tool "${options.id}": outputSchema must preserve the complete tool failure envelope`,
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
541
631
|
const tool = createTool({
|
|
542
632
|
id: options.id,
|
|
543
633
|
description: options.description,
|
|
544
|
-
inputSchema: options.inputSchema,
|
|
545
|
-
|
|
546
|
-
try {
|
|
547
|
-
if (options.tenant === "required") {
|
|
548
|
-
// Resolve BEFORE the body runs: an unauthenticated call fails closed
|
|
549
|
-
// here, and execute never sees it.
|
|
550
|
-
const tenant = resolveTenant(context as ActuarialToolContext, {
|
|
551
|
-
source: options.tenantSource,
|
|
552
|
-
key: options.tenantKey,
|
|
553
|
-
});
|
|
554
|
-
return await options.execute(input, tenant, context as ActuarialToolContext);
|
|
555
|
-
}
|
|
556
|
-
return await options.execute(input, null, context as ActuarialToolContext);
|
|
557
|
-
} catch (err) {
|
|
558
|
-
return envelopeFailure(err);
|
|
559
|
-
}
|
|
560
|
-
},
|
|
634
|
+
inputSchema: metadataBridge(options.inputSchema),
|
|
635
|
+
...(options.outputSchema === undefined ? {} : { outputSchema: metadataBridge(options.outputSchema) }),
|
|
561
636
|
});
|
|
562
|
-
|
|
637
|
+
|
|
638
|
+
const execute = async (
|
|
639
|
+
rawInput: z.input<z.ZodObject<TShape>>,
|
|
640
|
+
context: ActuarialToolContext,
|
|
641
|
+
): Promise<TResult | ToolEnvelopeFailure> => {
|
|
642
|
+
let parsedInput;
|
|
643
|
+
try {
|
|
644
|
+
parsedInput = options.inputSchema.safeParse(rawInput);
|
|
645
|
+
} catch {
|
|
646
|
+
return TOOL_INPUT_INVALID;
|
|
647
|
+
}
|
|
648
|
+
if (!parsedInput.success) return TOOL_INPUT_INVALID;
|
|
649
|
+
|
|
650
|
+
let rawOutput: TResult | ToolEnvelopeFailure;
|
|
651
|
+
try {
|
|
652
|
+
if (options.tenant === "required") {
|
|
653
|
+
const tenant = resolveTenant(context, {
|
|
654
|
+
source: options.tenantSource,
|
|
655
|
+
key: options.tenantKey,
|
|
656
|
+
});
|
|
657
|
+
rawOutput = await options.execute(parsedInput.data, tenant, context);
|
|
658
|
+
} else {
|
|
659
|
+
rawOutput = await options.execute(parsedInput.data, null, context);
|
|
660
|
+
}
|
|
661
|
+
} catch (err) {
|
|
662
|
+
rawOutput = envelopeFailure(err);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
if (options.outputSchema === undefined) {
|
|
666
|
+
return rawOutput === undefined ? TOOL_OUTPUT_INVALID : rawOutput;
|
|
667
|
+
}
|
|
668
|
+
let parsedOutput;
|
|
669
|
+
try {
|
|
670
|
+
parsedOutput = options.outputSchema.safeParse(rawOutput);
|
|
671
|
+
} catch {
|
|
672
|
+
return TOOL_OUTPUT_INVALID;
|
|
673
|
+
}
|
|
674
|
+
if (!parsedOutput.success) return TOOL_OUTPUT_INVALID;
|
|
675
|
+
if (isFailure(rawOutput) && !sameJson(parsedOutput.data, rawOutput)) return TOOL_OUTPUT_INVALID;
|
|
676
|
+
return deepFreezeResult(parsedOutput.data);
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// The metadata bridges deliberately erase domain inference on the inherited
|
|
680
|
+
// Mastra surface. This is the single convergence assertion: the adapter
|
|
681
|
+
// 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<
|
|
683
|
+
z.input<z.ZodObject<TShape>>,
|
|
684
|
+
TResult | ToolEnvelopeFailure
|
|
685
|
+
>;
|
|
686
|
+
return defined;
|
|
563
687
|
}
|
|
564
688
|
|
|
565
689
|
// ---------------------------------------------------------------------------
|