@actuarial-ts/agents 0.15.0 → 0.16.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 +35 -1
- package/dist/advisor.d.ts +1 -0
- package/dist/advisor.d.ts.map +1 -1
- package/dist/advisor.js +2 -0
- package/dist/advisor.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -1
- package/dist/evals.d.ts +1 -1
- package/dist/evals.js +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/judgment.d.ts +1 -1
- package/dist/judgment.js +1 -1
- package/dist/review.d.ts +729 -0
- package/dist/review.d.ts.map +1 -0
- package/dist/review.js +674 -0
- package/dist/review.js.map +1 -0
- package/dist/tools.d.ts +2 -2
- package/dist/tools.js +1 -1
- package/package.json +6 -6
- package/src/advisor.ts +4 -0
- package/src/errors.ts +18 -0
- package/src/evals.ts +1 -1
- package/src/index.ts +1 -0
- package/src/judgment.ts +1 -1
- package/src/remote.ts +1 -1
- package/src/review.ts +1116 -0
- package/src/tools.ts +2 -2
package/src/review.ts
ADDED
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trusted agent boundary for a complete analytical review.
|
|
3
|
+
*
|
|
4
|
+
* The model can select only opaque identifiers from a host-owned catalog. It
|
|
5
|
+
* cannot author a plan, formula, source location, financial definition, input
|
|
6
|
+
* payload or tenant id. The host resolves those identifiers, performs the
|
|
7
|
+
* calculation, and returns a current-process VerifiedReviewSnapshot together
|
|
8
|
+
* with an exact scope receipt. This module validates that receipt before
|
|
9
|
+
* projecting review evidence back to the model.
|
|
10
|
+
*
|
|
11
|
+
* Manual priors, factor overrides, selections, waivers and adopted ranges are
|
|
12
|
+
* separate judgment gates. They reuse createJudgmentChain so every adopted
|
|
13
|
+
* decision suspends, requires a rationale, reads actor identity from trusted
|
|
14
|
+
* request context and enters the compliance ledger.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
canonicalJson,
|
|
19
|
+
fnv1a64,
|
|
20
|
+
isDiagnosticToken,
|
|
21
|
+
type AnalysisReference,
|
|
22
|
+
type JsonValue,
|
|
23
|
+
} from "@actuarial-ts/core";
|
|
24
|
+
import { analysisReferenceSchema } from "@actuarial-ts/data";
|
|
25
|
+
import {
|
|
26
|
+
assertVerifiedReviewSnapshot,
|
|
27
|
+
reviewEvidenceLinkSchema,
|
|
28
|
+
type ReviewSnapshotReadiness,
|
|
29
|
+
type VerifiedReviewSnapshot,
|
|
30
|
+
} from "@actuarial-ts/compliance";
|
|
31
|
+
import { z } from "zod";
|
|
32
|
+
import { AgentsError } from "./errors.js";
|
|
33
|
+
import {
|
|
34
|
+
createJudgmentChain,
|
|
35
|
+
type CreateJudgmentChainOptions,
|
|
36
|
+
type JudgmentApplication,
|
|
37
|
+
type JudgmentChainWorkflow,
|
|
38
|
+
type JudgmentGateContext,
|
|
39
|
+
} from "./judgment.js";
|
|
40
|
+
import {
|
|
41
|
+
defineActuarialTool,
|
|
42
|
+
tenantOf,
|
|
43
|
+
type DefinedActuarialTool,
|
|
44
|
+
type ToolEnvelopeFailure,
|
|
45
|
+
} from "./tools.js";
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
// Public limits and schemas
|
|
49
|
+
|
|
50
|
+
export const REVIEW_AGENT_LIMITS = Object.freeze({
|
|
51
|
+
maximumPresets: 64,
|
|
52
|
+
maximumMethods: 64,
|
|
53
|
+
maximumAssumptions: 256,
|
|
54
|
+
maximumOptions: 128,
|
|
55
|
+
maximumJudgments: 128,
|
|
56
|
+
maximumCandidatesPerJudgment: 128,
|
|
57
|
+
maximumEligibilityItems: 640,
|
|
58
|
+
maximumReasonCodes: 64,
|
|
59
|
+
maximumAffectedCoordinates: 1_000,
|
|
60
|
+
maximumRemedies: 64,
|
|
61
|
+
maximumTextCharacters: 20_000,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const token = z
|
|
65
|
+
.string()
|
|
66
|
+
.max(REVIEW_AGENT_LIMITS.maximumTextCharacters)
|
|
67
|
+
.refine(isDiagnosticToken);
|
|
68
|
+
const tag = z.string().regex(/^fnv1a64-jcs-v1:[0-9a-f]{16}$/);
|
|
69
|
+
const text = z
|
|
70
|
+
.string()
|
|
71
|
+
.max(REVIEW_AGENT_LIMITS.maximumTextCharacters)
|
|
72
|
+
.refine((value) => value.trim().length > 0, "Expected nonblank text");
|
|
73
|
+
const uniqueTokens = (maximum: number, minimum = 0) =>
|
|
74
|
+
z
|
|
75
|
+
.array(token)
|
|
76
|
+
.min(minimum)
|
|
77
|
+
.max(maximum)
|
|
78
|
+
.refine((values) => new Set(values).size === values.length, {
|
|
79
|
+
message: "Identifiers must be distinct",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export const reviewAgentDiscoveryInputSchema = z
|
|
83
|
+
.object({ runPresetId: token })
|
|
84
|
+
.strict();
|
|
85
|
+
|
|
86
|
+
export const reviewAgentExecutionInputSchema = z
|
|
87
|
+
.object({
|
|
88
|
+
runPresetId: token,
|
|
89
|
+
planId: token,
|
|
90
|
+
inputId: token,
|
|
91
|
+
methodIds: uniqueTokens(REVIEW_AGENT_LIMITS.maximumMethods, 1),
|
|
92
|
+
assumptionIds: uniqueTokens(REVIEW_AGENT_LIMITS.maximumAssumptions),
|
|
93
|
+
optionIds: uniqueTokens(REVIEW_AGENT_LIMITS.maximumOptions),
|
|
94
|
+
})
|
|
95
|
+
.strict();
|
|
96
|
+
|
|
97
|
+
export type ReviewAgentDiscoveryInput = z.output<
|
|
98
|
+
typeof reviewAgentDiscoveryInputSchema
|
|
99
|
+
>;
|
|
100
|
+
export type ReviewAgentExecutionInput = z.output<
|
|
101
|
+
typeof reviewAgentExecutionInputSchema
|
|
102
|
+
>;
|
|
103
|
+
|
|
104
|
+
export const REVIEW_AGENT_ELIGIBILITY_STATUSES = [
|
|
105
|
+
"supported",
|
|
106
|
+
"supported-with-warnings",
|
|
107
|
+
"requires-input",
|
|
108
|
+
"unsupported",
|
|
109
|
+
] as const;
|
|
110
|
+
export type ReviewAgentEligibilityStatus =
|
|
111
|
+
(typeof REVIEW_AGENT_ELIGIBILITY_STATUSES)[number];
|
|
112
|
+
export const REVIEW_AGENT_SUBJECT_KINDS = [
|
|
113
|
+
"plan",
|
|
114
|
+
"input",
|
|
115
|
+
"method",
|
|
116
|
+
"assumption",
|
|
117
|
+
"option",
|
|
118
|
+
] as const;
|
|
119
|
+
export type ReviewAgentSubjectKind =
|
|
120
|
+
(typeof REVIEW_AGENT_SUBJECT_KINDS)[number];
|
|
121
|
+
|
|
122
|
+
export interface ReviewAgentEligibility {
|
|
123
|
+
readonly kind: ReviewAgentSubjectKind;
|
|
124
|
+
readonly id: string;
|
|
125
|
+
readonly status: ReviewAgentEligibilityStatus;
|
|
126
|
+
readonly reasonCodes: readonly string[];
|
|
127
|
+
readonly affectedCoordinates: readonly string[];
|
|
128
|
+
readonly remedies: readonly string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const reviewAgentEligibilitySchema: z.ZodType<ReviewAgentEligibility> = z
|
|
132
|
+
.object({
|
|
133
|
+
kind: z.enum(REVIEW_AGENT_SUBJECT_KINDS),
|
|
134
|
+
id: token,
|
|
135
|
+
status: z.enum(REVIEW_AGENT_ELIGIBILITY_STATUSES),
|
|
136
|
+
reasonCodes: uniqueTokens(REVIEW_AGENT_LIMITS.maximumReasonCodes),
|
|
137
|
+
affectedCoordinates: z
|
|
138
|
+
.array(text)
|
|
139
|
+
.max(REVIEW_AGENT_LIMITS.maximumAffectedCoordinates),
|
|
140
|
+
remedies: z.array(text).max(REVIEW_AGENT_LIMITS.maximumRemedies),
|
|
141
|
+
})
|
|
142
|
+
.strict();
|
|
143
|
+
|
|
144
|
+
export const REVIEW_AGENT_JUDGMENT_KINDS = [
|
|
145
|
+
"manual-prior",
|
|
146
|
+
"factor-override",
|
|
147
|
+
"manual-selection",
|
|
148
|
+
"requirement-waiver",
|
|
149
|
+
"adopted-range",
|
|
150
|
+
] as const;
|
|
151
|
+
export type ReviewAgentJudgmentKind =
|
|
152
|
+
(typeof REVIEW_AGENT_JUDGMENT_KINDS)[number];
|
|
153
|
+
|
|
154
|
+
export interface ReviewAgentCatalogItem {
|
|
155
|
+
readonly id: string;
|
|
156
|
+
readonly label: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface ReviewAgentMethod extends ReviewAgentCatalogItem {
|
|
160
|
+
/** Exact node and operation which must occur in the returned review plan. */
|
|
161
|
+
readonly nodeId: string;
|
|
162
|
+
readonly operation: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface ReviewAgentAssumption extends ReviewAgentCatalogItem {
|
|
166
|
+
readonly reference: AnalysisReference;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ReviewAgentJudgmentDescriptor {
|
|
170
|
+
readonly id: string;
|
|
171
|
+
readonly kind: ReviewAgentJudgmentKind;
|
|
172
|
+
readonly targetId: string;
|
|
173
|
+
readonly candidateIds: readonly string[];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface ReviewAgentPresetInspectionInput {
|
|
177
|
+
readonly tenantId: string;
|
|
178
|
+
readonly runPresetId: string;
|
|
179
|
+
readonly presetVersion: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export interface ReviewAgentPresetExecutionInput
|
|
183
|
+
extends ReviewAgentPresetInspectionInput {
|
|
184
|
+
readonly requestIdentity: string;
|
|
185
|
+
readonly planId: string;
|
|
186
|
+
readonly inputId: string;
|
|
187
|
+
readonly methodIds: readonly string[];
|
|
188
|
+
readonly assumptionIds: readonly string[];
|
|
189
|
+
readonly optionIds: readonly string[];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface ReviewAgentExecutionScope
|
|
193
|
+
extends ReviewAgentPresetExecutionInput {}
|
|
194
|
+
|
|
195
|
+
export interface ReviewAgentExecutionReceipt {
|
|
196
|
+
readonly scope: ReviewAgentExecutionScope;
|
|
197
|
+
/** Must retain its current-process authenticity brand. Copies are refused. */
|
|
198
|
+
readonly snapshot: VerifiedReviewSnapshot;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface ReviewAgentRunPreset {
|
|
202
|
+
readonly id: string;
|
|
203
|
+
readonly version: string;
|
|
204
|
+
readonly label: string;
|
|
205
|
+
readonly plan: {
|
|
206
|
+
readonly id: string;
|
|
207
|
+
readonly reference: AnalysisReference;
|
|
208
|
+
};
|
|
209
|
+
readonly input: {
|
|
210
|
+
readonly id: string;
|
|
211
|
+
/** Exact input/scope reference which must occur in the verified run. */
|
|
212
|
+
readonly reference: AnalysisReference;
|
|
213
|
+
};
|
|
214
|
+
readonly profile: { readonly id: string; readonly version: string };
|
|
215
|
+
readonly methods: readonly ReviewAgentMethod[];
|
|
216
|
+
readonly assumptions: readonly ReviewAgentAssumption[];
|
|
217
|
+
readonly options: readonly ReviewAgentCatalogItem[];
|
|
218
|
+
readonly judgments: readonly ReviewAgentJudgmentDescriptor[];
|
|
219
|
+
/** Returns one contextual eligibility item for every exposed catalog item. */
|
|
220
|
+
readonly inspect: (
|
|
221
|
+
input: ReviewAgentPresetInspectionInput,
|
|
222
|
+
) => Promise<readonly ReviewAgentEligibility[]>;
|
|
223
|
+
readonly execute: (
|
|
224
|
+
input: ReviewAgentPresetExecutionInput,
|
|
225
|
+
) => Promise<ReviewAgentExecutionReceipt>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Result contracts
|
|
230
|
+
|
|
231
|
+
const failureSchema = z
|
|
232
|
+
.object({
|
|
233
|
+
success: z.literal(false),
|
|
234
|
+
error: z.object({ code: z.string(), message: z.string() }).strict(),
|
|
235
|
+
})
|
|
236
|
+
.strict();
|
|
237
|
+
const catalogItemSchema = z.object({ id: token, label: text }).strict();
|
|
238
|
+
const methodSchema = catalogItemSchema
|
|
239
|
+
.extend({ nodeId: token, operation: token })
|
|
240
|
+
.strict();
|
|
241
|
+
const judgmentDescriptorSchema = z
|
|
242
|
+
.object({
|
|
243
|
+
id: token,
|
|
244
|
+
kind: z.enum(REVIEW_AGENT_JUDGMENT_KINDS),
|
|
245
|
+
targetId: token,
|
|
246
|
+
candidateIds: uniqueTokens(
|
|
247
|
+
REVIEW_AGENT_LIMITS.maximumCandidatesPerJudgment,
|
|
248
|
+
),
|
|
249
|
+
})
|
|
250
|
+
.strict();
|
|
251
|
+
|
|
252
|
+
export const reviewAgentDiscoveryResultSchema = z.union([
|
|
253
|
+
z
|
|
254
|
+
.object({
|
|
255
|
+
success: z.literal(true),
|
|
256
|
+
data: z
|
|
257
|
+
.object({
|
|
258
|
+
runPresetId: token,
|
|
259
|
+
presetVersion: token,
|
|
260
|
+
label: text,
|
|
261
|
+
planId: token,
|
|
262
|
+
inputId: token,
|
|
263
|
+
profile: z.object({ id: token, version: token }).strict(),
|
|
264
|
+
methods: z.array(methodSchema).max(REVIEW_AGENT_LIMITS.maximumMethods),
|
|
265
|
+
assumptions: z
|
|
266
|
+
.array(catalogItemSchema)
|
|
267
|
+
.max(REVIEW_AGENT_LIMITS.maximumAssumptions),
|
|
268
|
+
options: z
|
|
269
|
+
.array(catalogItemSchema)
|
|
270
|
+
.max(REVIEW_AGENT_LIMITS.maximumOptions),
|
|
271
|
+
judgments: z
|
|
272
|
+
.array(judgmentDescriptorSchema)
|
|
273
|
+
.max(REVIEW_AGENT_LIMITS.maximumJudgments),
|
|
274
|
+
eligibility: z
|
|
275
|
+
.array(reviewAgentEligibilitySchema)
|
|
276
|
+
.max(REVIEW_AGENT_LIMITS.maximumEligibilityItems),
|
|
277
|
+
})
|
|
278
|
+
.strict(),
|
|
279
|
+
})
|
|
280
|
+
.strict(),
|
|
281
|
+
failureSchema,
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
const readinessCheckSchema = z
|
|
285
|
+
.object({
|
|
286
|
+
id: text,
|
|
287
|
+
status: z.enum(["satisfied", "unresolved", "waived", "not-applicable"]),
|
|
288
|
+
message: text,
|
|
289
|
+
node: text.nullable(),
|
|
290
|
+
coordinates: z.array(text).max(1_000),
|
|
291
|
+
evidence: z.array(reviewEvidenceLinkSchema).max(1_024).optional(),
|
|
292
|
+
facet: z.enum(["validation", "selection", "review"]).optional(),
|
|
293
|
+
})
|
|
294
|
+
.strict();
|
|
295
|
+
const readinessFacetsSchema = z
|
|
296
|
+
.object({
|
|
297
|
+
calculation: z.enum(["satisfied", "unresolved"]),
|
|
298
|
+
validation: z.enum(["satisfied", "unresolved", "not-applicable"]),
|
|
299
|
+
selection: z.enum(["satisfied", "unresolved", "not-applicable"]),
|
|
300
|
+
review: z.enum(["satisfied", "unresolved", "not-applicable"]),
|
|
301
|
+
externalRelease: z.literal("not-reported"),
|
|
302
|
+
})
|
|
303
|
+
.strict();
|
|
304
|
+
const readinessSchema: z.ZodType<ReviewSnapshotReadiness> = z
|
|
305
|
+
.object({
|
|
306
|
+
status: z.enum(["requirements-satisfied", "unresolved"]),
|
|
307
|
+
checks: z.array(readinessCheckSchema),
|
|
308
|
+
assurance: z.enum([
|
|
309
|
+
"declared-minimum-requirements-only",
|
|
310
|
+
"caller-defined-versioned-profile",
|
|
311
|
+
]),
|
|
312
|
+
stage: z
|
|
313
|
+
.enum(["exploratory", "calculated", "validated", "selected", "published"])
|
|
314
|
+
.optional(),
|
|
315
|
+
facets: readinessFacetsSchema.optional(),
|
|
316
|
+
})
|
|
317
|
+
.strict();
|
|
318
|
+
|
|
319
|
+
export const reviewAgentExecutionResultSchema = z.union([
|
|
320
|
+
z
|
|
321
|
+
.object({
|
|
322
|
+
success: z.literal(true),
|
|
323
|
+
data: z
|
|
324
|
+
.object({
|
|
325
|
+
runPresetId: token,
|
|
326
|
+
presetVersion: token,
|
|
327
|
+
requestIdentity: tag,
|
|
328
|
+
planId: token,
|
|
329
|
+
inputId: token,
|
|
330
|
+
methodIds: z.array(token).max(REVIEW_AGENT_LIMITS.maximumMethods),
|
|
331
|
+
assumptionIds: z
|
|
332
|
+
.array(token)
|
|
333
|
+
.max(REVIEW_AGENT_LIMITS.maximumAssumptions),
|
|
334
|
+
optionIds: z.array(token).max(REVIEW_AGENT_LIMITS.maximumOptions),
|
|
335
|
+
snapshot: z
|
|
336
|
+
.object({
|
|
337
|
+
id: text,
|
|
338
|
+
version: text,
|
|
339
|
+
reference: analysisReferenceSchema,
|
|
340
|
+
profile: z.object({ id: text, version: text }).strict(),
|
|
341
|
+
readiness: readinessSchema,
|
|
342
|
+
limitations: z.array(
|
|
343
|
+
z
|
|
344
|
+
.object({
|
|
345
|
+
id: text,
|
|
346
|
+
disposition: z.enum(["unresolved", "accepted"]),
|
|
347
|
+
})
|
|
348
|
+
.strict(),
|
|
349
|
+
),
|
|
350
|
+
findingCodes: z.array(text),
|
|
351
|
+
assurance: z
|
|
352
|
+
.object({
|
|
353
|
+
frozen: z.literal(true),
|
|
354
|
+
review: z.literal("not-attested"),
|
|
355
|
+
publication: z.literal("not-reported"),
|
|
356
|
+
})
|
|
357
|
+
.strict(),
|
|
358
|
+
})
|
|
359
|
+
.strict(),
|
|
360
|
+
})
|
|
361
|
+
.strict(),
|
|
362
|
+
})
|
|
363
|
+
.strict(),
|
|
364
|
+
failureSchema,
|
|
365
|
+
]);
|
|
366
|
+
|
|
367
|
+
export type ReviewAgentDiscoverySuccess = Extract<
|
|
368
|
+
z.output<typeof reviewAgentDiscoveryResultSchema>,
|
|
369
|
+
{ success: true }
|
|
370
|
+
>;
|
|
371
|
+
export type ReviewAgentExecutionSuccess = Extract<
|
|
372
|
+
z.output<typeof reviewAgentExecutionResultSchema>,
|
|
373
|
+
{ success: true }
|
|
374
|
+
>;
|
|
375
|
+
|
|
376
|
+
// ---------------------------------------------------------------------------
|
|
377
|
+
// Catalog validation
|
|
378
|
+
|
|
379
|
+
interface OwnedPreset {
|
|
380
|
+
readonly id: string;
|
|
381
|
+
readonly version: string;
|
|
382
|
+
readonly label: string;
|
|
383
|
+
readonly plan: { readonly id: string; readonly reference: AnalysisReference };
|
|
384
|
+
readonly input: { readonly id: string; readonly reference: AnalysisReference };
|
|
385
|
+
readonly profile: { readonly id: string; readonly version: string };
|
|
386
|
+
readonly methods: readonly ReviewAgentMethod[];
|
|
387
|
+
readonly assumptions: readonly ReviewAgentAssumption[];
|
|
388
|
+
readonly options: readonly ReviewAgentCatalogItem[];
|
|
389
|
+
readonly judgments: readonly ReviewAgentJudgmentDescriptor[];
|
|
390
|
+
readonly inspect: ReviewAgentRunPreset["inspect"];
|
|
391
|
+
readonly execute: ReviewAgentRunPreset["execute"];
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const presetMetadataSchema = z
|
|
395
|
+
.object({
|
|
396
|
+
id: token,
|
|
397
|
+
version: token,
|
|
398
|
+
label: text,
|
|
399
|
+
plan: z.object({ id: token, reference: analysisReferenceSchema }).strict(),
|
|
400
|
+
input: z.object({ id: token, reference: analysisReferenceSchema }).strict(),
|
|
401
|
+
profile: z.object({ id: token, version: token }).strict(),
|
|
402
|
+
methods: z
|
|
403
|
+
.array(methodSchema)
|
|
404
|
+
.min(1)
|
|
405
|
+
.max(REVIEW_AGENT_LIMITS.maximumMethods),
|
|
406
|
+
assumptions: z
|
|
407
|
+
.array(catalogItemSchema.extend({ reference: analysisReferenceSchema }).strict())
|
|
408
|
+
.max(REVIEW_AGENT_LIMITS.maximumAssumptions),
|
|
409
|
+
options: z
|
|
410
|
+
.array(catalogItemSchema)
|
|
411
|
+
.max(REVIEW_AGENT_LIMITS.maximumOptions),
|
|
412
|
+
judgments: z
|
|
413
|
+
.array(judgmentDescriptorSchema)
|
|
414
|
+
.max(REVIEW_AGENT_LIMITS.maximumJudgments),
|
|
415
|
+
})
|
|
416
|
+
.strict();
|
|
417
|
+
|
|
418
|
+
function distinct(values: readonly string[], message: string): void {
|
|
419
|
+
if (new Set(values).size !== values.length)
|
|
420
|
+
throw new AgentsError("BAD_REVIEW_CATALOG", message);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function ownPreset(value: ReviewAgentRunPreset): OwnedPreset {
|
|
424
|
+
if (typeof value?.inspect !== "function" || typeof value?.execute !== "function")
|
|
425
|
+
throw new AgentsError(
|
|
426
|
+
"BAD_REVIEW_CATALOG",
|
|
427
|
+
"Every review preset requires host inspect and execute functions",
|
|
428
|
+
);
|
|
429
|
+
const parsed = presetMetadataSchema.safeParse({
|
|
430
|
+
id: value.id,
|
|
431
|
+
version: value.version,
|
|
432
|
+
label: value.label,
|
|
433
|
+
plan: value.plan,
|
|
434
|
+
input: value.input,
|
|
435
|
+
profile: value.profile,
|
|
436
|
+
methods: value.methods,
|
|
437
|
+
assumptions: value.assumptions,
|
|
438
|
+
options: value.options,
|
|
439
|
+
judgments: value.judgments,
|
|
440
|
+
});
|
|
441
|
+
if (!parsed.success)
|
|
442
|
+
throw new AgentsError(
|
|
443
|
+
"BAD_REVIEW_CATALOG",
|
|
444
|
+
`Invalid review preset: ${parsed.error.issues.map((issue) => issue.message).join("; ")}`,
|
|
445
|
+
);
|
|
446
|
+
const metadata = parsed.data;
|
|
447
|
+
distinct(metadata.methods.map((item) => item.id), `Preset ${metadata.id} has duplicate method IDs`);
|
|
448
|
+
distinct(metadata.methods.map((item) => item.nodeId), `Preset ${metadata.id} maps two methods to one node`);
|
|
449
|
+
distinct(metadata.assumptions.map((item) => item.id), `Preset ${metadata.id} has duplicate assumption IDs`);
|
|
450
|
+
distinct(
|
|
451
|
+
metadata.assumptions.map((item) => canonicalJson(item.reference)),
|
|
452
|
+
`Preset ${metadata.id} maps more than one assumption ID to the same reference`,
|
|
453
|
+
);
|
|
454
|
+
distinct(metadata.options.map((item) => item.id), `Preset ${metadata.id} has duplicate option IDs`);
|
|
455
|
+
distinct(metadata.judgments.map((item) => item.id), `Preset ${metadata.id} has duplicate judgment IDs`);
|
|
456
|
+
for (const assumption of metadata.assumptions)
|
|
457
|
+
if (assumption.reference.kind !== "assumption")
|
|
458
|
+
throw new AgentsError(
|
|
459
|
+
"BAD_REVIEW_CATALOG",
|
|
460
|
+
`Preset ${metadata.id} assumption ${assumption.id} must carry an assumption reference`,
|
|
461
|
+
);
|
|
462
|
+
if (metadata.plan.reference.kind !== "result")
|
|
463
|
+
throw new AgentsError(
|
|
464
|
+
"BAD_REVIEW_CATALOG",
|
|
465
|
+
`Preset ${metadata.id} plan reference must be a result reference`,
|
|
466
|
+
);
|
|
467
|
+
return Object.freeze({
|
|
468
|
+
...metadata,
|
|
469
|
+
methods: Object.freeze(metadata.methods.map((item) => Object.freeze(item))),
|
|
470
|
+
assumptions: Object.freeze(
|
|
471
|
+
metadata.assumptions.map((item) => Object.freeze(item)),
|
|
472
|
+
),
|
|
473
|
+
options: Object.freeze(metadata.options.map((item) => Object.freeze(item))),
|
|
474
|
+
judgments: Object.freeze(
|
|
475
|
+
metadata.judgments.map((item) => Object.freeze(item)),
|
|
476
|
+
),
|
|
477
|
+
inspect: value.inspect,
|
|
478
|
+
execute: value.execute,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function catalogOf(values: readonly ReviewAgentRunPreset[]): Map<string, OwnedPreset> {
|
|
483
|
+
if (!Array.isArray(values) || values.length === 0 || values.length > REVIEW_AGENT_LIMITS.maximumPresets)
|
|
484
|
+
throw new AgentsError(
|
|
485
|
+
"BAD_REVIEW_CATALOG",
|
|
486
|
+
"Review tools require a nonempty bounded preset catalog",
|
|
487
|
+
);
|
|
488
|
+
const result = new Map<string, OwnedPreset>();
|
|
489
|
+
for (const raw of values) {
|
|
490
|
+
const preset = ownPreset(raw);
|
|
491
|
+
if (result.has(preset.id))
|
|
492
|
+
throw new AgentsError(
|
|
493
|
+
"BAD_REVIEW_CATALOG",
|
|
494
|
+
`Duplicate review preset ${preset.id}`,
|
|
495
|
+
);
|
|
496
|
+
result.set(preset.id, preset);
|
|
497
|
+
}
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function subjectKey(kind: ReviewAgentSubjectKind, id: string): string {
|
|
502
|
+
return `${kind}\u0000${id}`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function expectedSubjects(preset: OwnedPreset): Set<string> {
|
|
506
|
+
return new Set([
|
|
507
|
+
subjectKey("plan", preset.plan.id),
|
|
508
|
+
subjectKey("input", preset.input.id),
|
|
509
|
+
...preset.methods.map((item) => subjectKey("method", item.id)),
|
|
510
|
+
...preset.assumptions.map((item) => subjectKey("assumption", item.id)),
|
|
511
|
+
...preset.options.map((item) => subjectKey("option", item.id)),
|
|
512
|
+
]);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
async function inspectPreset(
|
|
516
|
+
preset: OwnedPreset,
|
|
517
|
+
tenantId: string,
|
|
518
|
+
): Promise<readonly ReviewAgentEligibility[]> {
|
|
519
|
+
const parsed = z
|
|
520
|
+
.array(reviewAgentEligibilitySchema)
|
|
521
|
+
.max(REVIEW_AGENT_LIMITS.maximumEligibilityItems)
|
|
522
|
+
.safeParse(
|
|
523
|
+
await preset.inspect({
|
|
524
|
+
tenantId,
|
|
525
|
+
runPresetId: preset.id,
|
|
526
|
+
presetVersion: preset.version,
|
|
527
|
+
}),
|
|
528
|
+
);
|
|
529
|
+
if (!parsed.success)
|
|
530
|
+
throw new AgentsError(
|
|
531
|
+
"REVIEW_ELIGIBILITY_MISMATCH",
|
|
532
|
+
"Review preset returned malformed contextual eligibility",
|
|
533
|
+
);
|
|
534
|
+
const expected = expectedSubjects(preset);
|
|
535
|
+
const actual = parsed.data.map((item) => subjectKey(item.kind, item.id));
|
|
536
|
+
if (
|
|
537
|
+
new Set(actual).size !== actual.length ||
|
|
538
|
+
actual.length !== expected.size ||
|
|
539
|
+
actual.some((key) => !expected.has(key))
|
|
540
|
+
)
|
|
541
|
+
throw new AgentsError(
|
|
542
|
+
"REVIEW_ELIGIBILITY_MISMATCH",
|
|
543
|
+
"Review preset must return exactly one eligibility outcome for every exposed item",
|
|
544
|
+
);
|
|
545
|
+
const kindOrder = new Map(
|
|
546
|
+
REVIEW_AGENT_SUBJECT_KINDS.map((kind, index) => [kind, index]),
|
|
547
|
+
);
|
|
548
|
+
return [...parsed.data].sort(
|
|
549
|
+
(left, right) =>
|
|
550
|
+
kindOrder.get(left.kind)! - kindOrder.get(right.kind)! ||
|
|
551
|
+
left.id.localeCompare(right.id),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function sorted(values: readonly string[]): string[] {
|
|
556
|
+
return [...values].sort();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function exactMembers(left: readonly string[], right: readonly string[]): boolean {
|
|
560
|
+
return canonicalJson(sorted(left)) === canonicalJson(sorted(right));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function exactReference(left: AnalysisReference, right: AnalysisReference): boolean {
|
|
564
|
+
return canonicalJson(left) === canonicalJson(right);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function requestIdentity(input: ReviewAgentExecutionInput): string {
|
|
568
|
+
return `fnv1a64-jcs-v1:${fnv1a64(
|
|
569
|
+
canonicalJson({
|
|
570
|
+
...input,
|
|
571
|
+
methodIds: sorted(input.methodIds),
|
|
572
|
+
assumptionIds: sorted(input.assumptionIds),
|
|
573
|
+
optionIds: sorted(input.optionIds),
|
|
574
|
+
}),
|
|
575
|
+
)}`;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function selectedEligibility(
|
|
579
|
+
eligibility: readonly ReviewAgentEligibility[],
|
|
580
|
+
input: ReviewAgentExecutionInput,
|
|
581
|
+
): readonly ReviewAgentEligibility[] {
|
|
582
|
+
const selected = new Set([
|
|
583
|
+
subjectKey("plan", input.planId),
|
|
584
|
+
subjectKey("input", input.inputId),
|
|
585
|
+
...input.methodIds.map((id) => subjectKey("method", id)),
|
|
586
|
+
...input.assumptionIds.map((id) => subjectKey("assumption", id)),
|
|
587
|
+
...input.optionIds.map((id) => subjectKey("option", id)),
|
|
588
|
+
]);
|
|
589
|
+
return eligibility.filter((item) => selected.has(subjectKey(item.kind, item.id)));
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function assertAllowedRequest(
|
|
593
|
+
preset: OwnedPreset,
|
|
594
|
+
input: ReviewAgentExecutionInput,
|
|
595
|
+
): void {
|
|
596
|
+
if (input.planId !== preset.plan.id || input.inputId !== preset.input.id)
|
|
597
|
+
throw new AgentsError(
|
|
598
|
+
"REVIEW_REQUEST_MISMATCH",
|
|
599
|
+
"The requested plan or input does not belong to the selected preset",
|
|
600
|
+
);
|
|
601
|
+
const allowedMethods = new Set(preset.methods.map((item) => item.id));
|
|
602
|
+
const allowedAssumptions = new Set(preset.assumptions.map((item) => item.id));
|
|
603
|
+
const allowedOptions = new Set(preset.options.map((item) => item.id));
|
|
604
|
+
if (input.methodIds.some((id) => !allowedMethods.has(id)))
|
|
605
|
+
throw new AgentsError(
|
|
606
|
+
"UNAPPROVED_REVIEW_METHOD",
|
|
607
|
+
"One or more requested methods are not approved by the selected preset",
|
|
608
|
+
);
|
|
609
|
+
if (input.assumptionIds.some((id) => !allowedAssumptions.has(id)))
|
|
610
|
+
throw new AgentsError(
|
|
611
|
+
"UNAPPROVED_REVIEW_ASSUMPTION",
|
|
612
|
+
"One or more requested assumptions are not approved by the selected preset",
|
|
613
|
+
);
|
|
614
|
+
if (input.optionIds.some((id) => !allowedOptions.has(id)))
|
|
615
|
+
throw new AgentsError(
|
|
616
|
+
"UNAPPROVED_REVIEW_OPTION",
|
|
617
|
+
"One or more requested options are not approved by the selected preset",
|
|
618
|
+
);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function assertReceipt(
|
|
622
|
+
preset: OwnedPreset,
|
|
623
|
+
request: ReviewAgentPresetExecutionInput,
|
|
624
|
+
receipt: ReviewAgentExecutionReceipt,
|
|
625
|
+
): VerifiedReviewSnapshot {
|
|
626
|
+
if (receipt === null || typeof receipt !== "object" || Array.isArray(receipt))
|
|
627
|
+
throw new AgentsError(
|
|
628
|
+
"REVIEW_EXECUTION_MISMATCH",
|
|
629
|
+
"Review executor returned a malformed receipt",
|
|
630
|
+
);
|
|
631
|
+
const scopeField = Object.getOwnPropertyDescriptor(receipt, "scope");
|
|
632
|
+
const snapshotField = Object.getOwnPropertyDescriptor(receipt, "snapshot");
|
|
633
|
+
if (
|
|
634
|
+
![Object.prototype, null].includes(Object.getPrototypeOf(receipt)) ||
|
|
635
|
+
Reflect.ownKeys(receipt).some(
|
|
636
|
+
(key) => typeof key !== "string" || !["scope", "snapshot"].includes(key),
|
|
637
|
+
) ||
|
|
638
|
+
!scopeField?.enumerable ||
|
|
639
|
+
!("value" in scopeField) ||
|
|
640
|
+
!snapshotField?.enumerable ||
|
|
641
|
+
!("value" in snapshotField)
|
|
642
|
+
)
|
|
643
|
+
throw new AgentsError(
|
|
644
|
+
"REVIEW_EXECUTION_MISMATCH",
|
|
645
|
+
"Review executor receipt must contain only scope and snapshot data fields",
|
|
646
|
+
);
|
|
647
|
+
const scope = z
|
|
648
|
+
.object({
|
|
649
|
+
tenantId: token,
|
|
650
|
+
runPresetId: token,
|
|
651
|
+
presetVersion: token,
|
|
652
|
+
requestIdentity: tag,
|
|
653
|
+
planId: token,
|
|
654
|
+
inputId: token,
|
|
655
|
+
methodIds: uniqueTokens(REVIEW_AGENT_LIMITS.maximumMethods, 1),
|
|
656
|
+
assumptionIds: uniqueTokens(REVIEW_AGENT_LIMITS.maximumAssumptions),
|
|
657
|
+
optionIds: uniqueTokens(REVIEW_AGENT_LIMITS.maximumOptions),
|
|
658
|
+
})
|
|
659
|
+
.strict()
|
|
660
|
+
.safeParse(scopeField.value);
|
|
661
|
+
if (!scope.success || canonicalJson(scope.data) !== canonicalJson(request))
|
|
662
|
+
throw new AgentsError(
|
|
663
|
+
"REVIEW_EXECUTION_MISMATCH",
|
|
664
|
+
"Review executor result is bound to another tenant, request, definition or preset",
|
|
665
|
+
);
|
|
666
|
+
try {
|
|
667
|
+
assertVerifiedReviewSnapshot(snapshotField.value);
|
|
668
|
+
} catch {
|
|
669
|
+
throw new AgentsError(
|
|
670
|
+
"REVIEW_EXECUTION_MISMATCH",
|
|
671
|
+
"Review executor must return an authentic current-process verified snapshot",
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
const snapshot = snapshotField.value as VerifiedReviewSnapshot;
|
|
675
|
+
if (
|
|
676
|
+
snapshot.profile.id !== preset.profile.id ||
|
|
677
|
+
snapshot.profile.version !== preset.profile.version ||
|
|
678
|
+
!exactReference(
|
|
679
|
+
snapshot.run.document.analysisReview.run.plan.reference,
|
|
680
|
+
preset.plan.reference,
|
|
681
|
+
)
|
|
682
|
+
)
|
|
683
|
+
throw new AgentsError(
|
|
684
|
+
"REVIEW_EXECUTION_MISMATCH",
|
|
685
|
+
"Verified snapshot belongs to another review plan or profile",
|
|
686
|
+
);
|
|
687
|
+
const indexedReferences = snapshot.run.document.analysisReview.referenceIndex.definitions.map(
|
|
688
|
+
(entry) => entry.reference,
|
|
689
|
+
);
|
|
690
|
+
if (!indexedReferences.some((reference) => exactReference(reference, preset.input.reference)))
|
|
691
|
+
throw new AgentsError(
|
|
692
|
+
"REVIEW_EXECUTION_MISMATCH",
|
|
693
|
+
"Verified snapshot does not contain the exact approved input reference",
|
|
694
|
+
);
|
|
695
|
+
const methods = request.methodIds.map(
|
|
696
|
+
(id) => preset.methods.find((item) => item.id === id)!,
|
|
697
|
+
);
|
|
698
|
+
const planNodes = snapshot.run.document.analysisReview.run.plan.plan.nodes;
|
|
699
|
+
if (
|
|
700
|
+
methods.some(
|
|
701
|
+
(method) =>
|
|
702
|
+
!planNodes.some(
|
|
703
|
+
(node) =>
|
|
704
|
+
node.id === method.nodeId && node.operation === method.operation,
|
|
705
|
+
),
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
throw new AgentsError(
|
|
709
|
+
"REVIEW_EXECUTION_MISMATCH",
|
|
710
|
+
"Verified snapshot is missing an exact approved method node",
|
|
711
|
+
);
|
|
712
|
+
const requestedAssumptions = request.assumptionIds.map(
|
|
713
|
+
(id) => preset.assumptions.find((item) => item.id === id)!.reference,
|
|
714
|
+
);
|
|
715
|
+
if (
|
|
716
|
+
!exactMembers(
|
|
717
|
+
snapshot.profile.assumptions.map((reference) => canonicalJson(reference)),
|
|
718
|
+
requestedAssumptions.map((reference) => canonicalJson(reference)),
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
throw new AgentsError(
|
|
722
|
+
"REVIEW_EXECUTION_MISMATCH",
|
|
723
|
+
"Verified snapshot assumptions do not exactly match the approved request",
|
|
724
|
+
);
|
|
725
|
+
return snapshot;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
// Discovery and execution tools
|
|
730
|
+
|
|
731
|
+
export interface CreateReviewAgentToolsInput {
|
|
732
|
+
readonly runPresets: readonly ReviewAgentRunPreset[];
|
|
733
|
+
readonly discoveryId?: string;
|
|
734
|
+
readonly executionId?: string;
|
|
735
|
+
readonly tenantContextKey?: string;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
export interface ReviewAgentTools {
|
|
739
|
+
readonly discover: DefinedActuarialTool<
|
|
740
|
+
z.input<typeof reviewAgentDiscoveryInputSchema>,
|
|
741
|
+
z.output<typeof reviewAgentDiscoveryResultSchema>
|
|
742
|
+
>;
|
|
743
|
+
readonly execute: DefinedActuarialTool<
|
|
744
|
+
z.input<typeof reviewAgentExecutionInputSchema>,
|
|
745
|
+
z.output<typeof reviewAgentExecutionResultSchema>
|
|
746
|
+
>;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/** Builds the paired read-only tools over one immutable host catalog. */
|
|
750
|
+
export function createReviewAgentTools(
|
|
751
|
+
input: CreateReviewAgentToolsInput,
|
|
752
|
+
): ReviewAgentTools {
|
|
753
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
754
|
+
throw new AgentsError(
|
|
755
|
+
"BAD_REVIEW_CATALOG",
|
|
756
|
+
"Review tool options must be an object",
|
|
757
|
+
);
|
|
758
|
+
for (const [name, value] of [
|
|
759
|
+
["discoveryId", input.discoveryId],
|
|
760
|
+
["executionId", input.executionId],
|
|
761
|
+
["tenantContextKey", input.tenantContextKey],
|
|
762
|
+
] as const)
|
|
763
|
+
if (value !== undefined && !isDiagnosticToken(value))
|
|
764
|
+
throw new AgentsError(
|
|
765
|
+
"BAD_REVIEW_CATALOG",
|
|
766
|
+
`Review tool ${name} must be a nonempty identifier when supplied`,
|
|
767
|
+
);
|
|
768
|
+
if (
|
|
769
|
+
input.discoveryId !== undefined &&
|
|
770
|
+
input.executionId !== undefined &&
|
|
771
|
+
input.discoveryId === input.executionId
|
|
772
|
+
)
|
|
773
|
+
throw new AgentsError(
|
|
774
|
+
"BAD_REVIEW_CATALOG",
|
|
775
|
+
"Review discovery and execution tool IDs must differ",
|
|
776
|
+
);
|
|
777
|
+
const catalog = catalogOf(input.runPresets);
|
|
778
|
+
const tenantKey = input.tenantContextKey ?? "projectId";
|
|
779
|
+
|
|
780
|
+
const discover = defineActuarialTool({
|
|
781
|
+
id: input.discoveryId ?? "discover-approved-review",
|
|
782
|
+
description:
|
|
783
|
+
"Discover one host-approved analytical review and its contextual eligibility",
|
|
784
|
+
kind: "read",
|
|
785
|
+
tenant: "required",
|
|
786
|
+
tenantKey,
|
|
787
|
+
inputSchema: reviewAgentDiscoveryInputSchema,
|
|
788
|
+
outputSchema: reviewAgentDiscoveryResultSchema,
|
|
789
|
+
execute: async (request, tenantId) => {
|
|
790
|
+
const preset = catalog.get(request.runPresetId);
|
|
791
|
+
if (!preset)
|
|
792
|
+
throw new AgentsError(
|
|
793
|
+
"UNKNOWN_REVIEW_PRESET",
|
|
794
|
+
`Unknown review preset ${request.runPresetId}`,
|
|
795
|
+
);
|
|
796
|
+
const eligibility = await inspectPreset(preset, tenantId);
|
|
797
|
+
return {
|
|
798
|
+
success: true as const,
|
|
799
|
+
data: {
|
|
800
|
+
runPresetId: preset.id,
|
|
801
|
+
presetVersion: preset.version,
|
|
802
|
+
label: preset.label,
|
|
803
|
+
planId: preset.plan.id,
|
|
804
|
+
inputId: preset.input.id,
|
|
805
|
+
profile: preset.profile,
|
|
806
|
+
methods: preset.methods.map((item) => ({ ...item })),
|
|
807
|
+
assumptions: preset.assumptions.map(({ id, label }) => ({ id, label })),
|
|
808
|
+
options: preset.options.map((item) => ({ ...item })),
|
|
809
|
+
judgments: preset.judgments.map((item) => ({
|
|
810
|
+
...item,
|
|
811
|
+
candidateIds: [...item.candidateIds],
|
|
812
|
+
})),
|
|
813
|
+
eligibility: [...eligibility],
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
|
|
819
|
+
const execute = defineActuarialTool({
|
|
820
|
+
id: input.executionId ?? "run-approved-review",
|
|
821
|
+
description:
|
|
822
|
+
"Run a host-approved analytical review and return verified readiness evidence",
|
|
823
|
+
kind: "read",
|
|
824
|
+
tenant: "required",
|
|
825
|
+
tenantKey,
|
|
826
|
+
inputSchema: reviewAgentExecutionInputSchema,
|
|
827
|
+
outputSchema: reviewAgentExecutionResultSchema,
|
|
828
|
+
execute: async (raw, tenantId) => {
|
|
829
|
+
const preset = catalog.get(raw.runPresetId);
|
|
830
|
+
if (!preset)
|
|
831
|
+
throw new AgentsError(
|
|
832
|
+
"UNKNOWN_REVIEW_PRESET",
|
|
833
|
+
`Unknown review preset ${raw.runPresetId}`,
|
|
834
|
+
);
|
|
835
|
+
assertAllowedRequest(preset, raw);
|
|
836
|
+
const eligibility = await inspectPreset(preset, tenantId);
|
|
837
|
+
const unavailable = selectedEligibility(eligibility, raw).filter(
|
|
838
|
+
(item) => item.status === "requires-input" || item.status === "unsupported",
|
|
839
|
+
);
|
|
840
|
+
if (unavailable.length > 0)
|
|
841
|
+
throw new AgentsError(
|
|
842
|
+
"REVIEW_NOT_ELIGIBLE",
|
|
843
|
+
`Approved review cannot run because these selected items are ineligible: ${unavailable
|
|
844
|
+
.map((item) => `${item.kind}:${item.id}`)
|
|
845
|
+
.join(", ")}`,
|
|
846
|
+
);
|
|
847
|
+
const normalized: ReviewAgentExecutionInput = {
|
|
848
|
+
...raw,
|
|
849
|
+
methodIds: sorted(raw.methodIds),
|
|
850
|
+
assumptionIds: sorted(raw.assumptionIds),
|
|
851
|
+
optionIds: sorted(raw.optionIds),
|
|
852
|
+
};
|
|
853
|
+
const executionInput: ReviewAgentPresetExecutionInput = {
|
|
854
|
+
tenantId,
|
|
855
|
+
runPresetId: preset.id,
|
|
856
|
+
presetVersion: preset.version,
|
|
857
|
+
requestIdentity: requestIdentity(normalized),
|
|
858
|
+
planId: normalized.planId,
|
|
859
|
+
inputId: normalized.inputId,
|
|
860
|
+
methodIds: normalized.methodIds,
|
|
861
|
+
assumptionIds: normalized.assumptionIds,
|
|
862
|
+
optionIds: normalized.optionIds,
|
|
863
|
+
};
|
|
864
|
+
const receipt = await preset.execute(executionInput);
|
|
865
|
+
const snapshot = assertReceipt(preset, executionInput, receipt);
|
|
866
|
+
return {
|
|
867
|
+
success: true as const,
|
|
868
|
+
data: {
|
|
869
|
+
runPresetId: preset.id,
|
|
870
|
+
presetVersion: preset.version,
|
|
871
|
+
requestIdentity: executionInput.requestIdentity,
|
|
872
|
+
planId: preset.plan.id,
|
|
873
|
+
inputId: preset.input.id,
|
|
874
|
+
methodIds: [...executionInput.methodIds],
|
|
875
|
+
assumptionIds: [...executionInput.assumptionIds],
|
|
876
|
+
optionIds: [...executionInput.optionIds],
|
|
877
|
+
snapshot: {
|
|
878
|
+
id: snapshot.id,
|
|
879
|
+
version: snapshot.version,
|
|
880
|
+
reference: snapshot.reference,
|
|
881
|
+
profile: { id: snapshot.profile.id, version: snapshot.profile.version },
|
|
882
|
+
readiness: snapshot.readiness,
|
|
883
|
+
limitations: snapshot.limitations.map(({ id, disposition }) => ({
|
|
884
|
+
id,
|
|
885
|
+
disposition,
|
|
886
|
+
})),
|
|
887
|
+
findingCodes: [...new Set(snapshot.findings.map((item) => item.code))].sort(),
|
|
888
|
+
assurance: snapshot.assurance,
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
};
|
|
892
|
+
},
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
return Object.freeze({ discover, execute });
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ---------------------------------------------------------------------------
|
|
899
|
+
// Human judgment wrapper
|
|
900
|
+
|
|
901
|
+
export interface ReviewAgentJudgmentDecision {
|
|
902
|
+
readonly decision: "adopt" | "decline";
|
|
903
|
+
readonly candidateId: string | null;
|
|
904
|
+
readonly rationale: string;
|
|
905
|
+
readonly actor?: "actuary" | "agent";
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export interface ReviewAgentJudgmentContext {
|
|
909
|
+
readonly tenantId: string;
|
|
910
|
+
readonly presetId: string;
|
|
911
|
+
readonly presetVersion: string;
|
|
912
|
+
readonly gateId: string;
|
|
913
|
+
readonly kind: ReviewAgentJudgmentKind;
|
|
914
|
+
readonly targetId: string;
|
|
915
|
+
readonly decisions: JudgmentGateContext["decisions"];
|
|
916
|
+
readonly trail: JudgmentGateContext["trail"];
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
export interface ReviewAgentJudgmentGateSpec {
|
|
920
|
+
readonly id: string;
|
|
921
|
+
readonly stage: string;
|
|
922
|
+
readonly presetId: string;
|
|
923
|
+
readonly presetVersion: string;
|
|
924
|
+
readonly kind: ReviewAgentJudgmentKind;
|
|
925
|
+
readonly targetId: string;
|
|
926
|
+
readonly allowedCandidateIds: readonly string[];
|
|
927
|
+
readonly gatherEvidence: (
|
|
928
|
+
context: ReviewAgentJudgmentContext,
|
|
929
|
+
) =>
|
|
930
|
+
| Promise<{ readonly recommendation: string; readonly evidence: unknown }>
|
|
931
|
+
| { readonly recommendation: string; readonly evidence: unknown };
|
|
932
|
+
readonly applyDecision: (
|
|
933
|
+
context: ReviewAgentJudgmentContext,
|
|
934
|
+
decision: ReviewAgentJudgmentDecision,
|
|
935
|
+
) => Promise<JudgmentApplication>;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export interface CreateReviewAgentJudgmentChainInput {
|
|
939
|
+
readonly id: string;
|
|
940
|
+
readonly gates: readonly ReviewAgentJudgmentGateSpec[];
|
|
941
|
+
readonly now: () => string;
|
|
942
|
+
readonly tenantContextKey?: string;
|
|
943
|
+
readonly onComplete?: CreateJudgmentChainOptions["onComplete"];
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const judgmentDecisionSchema = z
|
|
947
|
+
.object({
|
|
948
|
+
decision: z.enum(["adopt", "decline"]),
|
|
949
|
+
candidateId: token.nullable(),
|
|
950
|
+
rationale: z.string(),
|
|
951
|
+
actor: z.enum(["actuary", "agent"]).optional(),
|
|
952
|
+
})
|
|
953
|
+
.strict();
|
|
954
|
+
|
|
955
|
+
function judgmentContext(
|
|
956
|
+
gate: ReviewAgentJudgmentGateSpec,
|
|
957
|
+
context: JudgmentGateContext,
|
|
958
|
+
tenantKey: string,
|
|
959
|
+
): ReviewAgentJudgmentContext {
|
|
960
|
+
return {
|
|
961
|
+
tenantId: tenantOf({ requestContext: context.requestContext }, tenantKey),
|
|
962
|
+
presetId: gate.presetId,
|
|
963
|
+
presetVersion: gate.presetVersion,
|
|
964
|
+
gateId: gate.id,
|
|
965
|
+
kind: gate.kind,
|
|
966
|
+
targetId: gate.targetId,
|
|
967
|
+
decisions: context.decisions,
|
|
968
|
+
trail: context.trail,
|
|
969
|
+
};
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Creates a resumable review-judgment workflow. The fixed schema admits only
|
|
974
|
+
* an allowlisted candidate ID and a documented adopt/decline decision. The
|
|
975
|
+
* wrapper always records the review judgment in the ledger, even when the
|
|
976
|
+
* host application has no additional assumption entries to append.
|
|
977
|
+
*/
|
|
978
|
+
export function createReviewAgentJudgmentChain(
|
|
979
|
+
input: CreateReviewAgentJudgmentChainInput,
|
|
980
|
+
): JudgmentChainWorkflow {
|
|
981
|
+
if (input === null || typeof input !== "object" || Array.isArray(input))
|
|
982
|
+
throw new AgentsError(
|
|
983
|
+
"BAD_REVIEW_JUDGMENT_CATALOG",
|
|
984
|
+
"Review judgment options must be an object",
|
|
985
|
+
);
|
|
986
|
+
const tenantKey = input.tenantContextKey ?? "projectId";
|
|
987
|
+
if (
|
|
988
|
+
!isDiagnosticToken(input.id) ||
|
|
989
|
+
!isDiagnosticToken(tenantKey) ||
|
|
990
|
+
typeof input.now !== "function"
|
|
991
|
+
)
|
|
992
|
+
throw new AgentsError(
|
|
993
|
+
"BAD_REVIEW_JUDGMENT_CATALOG",
|
|
994
|
+
"Review judgment chain requires an ID and host clock",
|
|
995
|
+
);
|
|
996
|
+
if (!Array.isArray(input.gates) || input.gates.length === 0 || input.gates.length > REVIEW_AGENT_LIMITS.maximumJudgments)
|
|
997
|
+
throw new AgentsError(
|
|
998
|
+
"BAD_REVIEW_JUDGMENT_CATALOG",
|
|
999
|
+
"Review judgment chain requires a nonempty bounded gate list",
|
|
1000
|
+
);
|
|
1001
|
+
if (new Set(input.gates.map((gate) => gate?.id)).size !== input.gates.length)
|
|
1002
|
+
throw new AgentsError(
|
|
1003
|
+
"BAD_REVIEW_JUDGMENT_CATALOG",
|
|
1004
|
+
"Review judgment gate IDs must be distinct",
|
|
1005
|
+
);
|
|
1006
|
+
const gates = input.gates.map((gate) => {
|
|
1007
|
+
const parsed = z
|
|
1008
|
+
.object({
|
|
1009
|
+
id: token,
|
|
1010
|
+
stage: text,
|
|
1011
|
+
presetId: token,
|
|
1012
|
+
presetVersion: token,
|
|
1013
|
+
kind: z.enum(REVIEW_AGENT_JUDGMENT_KINDS),
|
|
1014
|
+
targetId: token,
|
|
1015
|
+
allowedCandidateIds: uniqueTokens(
|
|
1016
|
+
REVIEW_AGENT_LIMITS.maximumCandidatesPerJudgment,
|
|
1017
|
+
),
|
|
1018
|
+
})
|
|
1019
|
+
.strict()
|
|
1020
|
+
.safeParse({
|
|
1021
|
+
id: gate.id,
|
|
1022
|
+
stage: gate.stage,
|
|
1023
|
+
presetId: gate.presetId,
|
|
1024
|
+
presetVersion: gate.presetVersion,
|
|
1025
|
+
kind: gate.kind,
|
|
1026
|
+
targetId: gate.targetId,
|
|
1027
|
+
allowedCandidateIds: gate.allowedCandidateIds,
|
|
1028
|
+
});
|
|
1029
|
+
if (
|
|
1030
|
+
!parsed.success ||
|
|
1031
|
+
typeof gate.gatherEvidence !== "function" ||
|
|
1032
|
+
typeof gate.applyDecision !== "function"
|
|
1033
|
+
)
|
|
1034
|
+
throw new AgentsError(
|
|
1035
|
+
"BAD_REVIEW_JUDGMENT_CATALOG",
|
|
1036
|
+
"Review judgment gates require valid metadata and host callbacks",
|
|
1037
|
+
);
|
|
1038
|
+
const owned = parsed.data;
|
|
1039
|
+
return {
|
|
1040
|
+
id: owned.id,
|
|
1041
|
+
stage: owned.stage,
|
|
1042
|
+
resumeSchema: judgmentDecisionSchema,
|
|
1043
|
+
gatherEvidence: async (context: JudgmentGateContext) =>
|
|
1044
|
+
gate.gatherEvidence(judgmentContext(gate, context, tenantKey)),
|
|
1045
|
+
applyDecision: async (
|
|
1046
|
+
context: JudgmentGateContext,
|
|
1047
|
+
decision: ReviewAgentJudgmentDecision,
|
|
1048
|
+
): Promise<JudgmentApplication> => {
|
|
1049
|
+
const adopting = decision.decision === "adopt";
|
|
1050
|
+
if (
|
|
1051
|
+
(adopting &&
|
|
1052
|
+
(decision.candidateId === null ||
|
|
1053
|
+
!owned.allowedCandidateIds.includes(decision.candidateId))) ||
|
|
1054
|
+
(!adopting && decision.candidateId !== null)
|
|
1055
|
+
)
|
|
1056
|
+
throw new AgentsError(
|
|
1057
|
+
"UNAPPROVED_REVIEW_JUDGMENT",
|
|
1058
|
+
"Review judgment must adopt one approved candidate or decline without a candidate",
|
|
1059
|
+
);
|
|
1060
|
+
const trusted = judgmentContext(gate, context, tenantKey);
|
|
1061
|
+
const application = await gate.applyDecision(trusted, decision);
|
|
1062
|
+
const auditValue: JsonValue = {
|
|
1063
|
+
presetId: owned.presetId,
|
|
1064
|
+
presetVersion: owned.presetVersion,
|
|
1065
|
+
kind: owned.kind,
|
|
1066
|
+
targetId: owned.targetId,
|
|
1067
|
+
decision: decision.decision,
|
|
1068
|
+
candidateId: decision.candidateId,
|
|
1069
|
+
};
|
|
1070
|
+
return {
|
|
1071
|
+
summary:
|
|
1072
|
+
application.summary ??
|
|
1073
|
+
(adopting
|
|
1074
|
+
? `adopted ${decision.candidateId}`
|
|
1075
|
+
: `declined ${owned.targetId}`),
|
|
1076
|
+
ledgerEntries: [
|
|
1077
|
+
{
|
|
1078
|
+
field: `review.judgment.${owned.kind}.${owned.targetId}`,
|
|
1079
|
+
value: auditValue,
|
|
1080
|
+
source: `review-agent-gate:${owned.id}`,
|
|
1081
|
+
},
|
|
1082
|
+
...(application.ledgerEntries ?? []),
|
|
1083
|
+
],
|
|
1084
|
+
};
|
|
1085
|
+
},
|
|
1086
|
+
};
|
|
1087
|
+
});
|
|
1088
|
+
return createJudgmentChain({
|
|
1089
|
+
id: input.id,
|
|
1090
|
+
gates,
|
|
1091
|
+
now: input.now,
|
|
1092
|
+
onComplete: input.onComplete,
|
|
1093
|
+
requestContextSchema: z.object({ [tenantKey]: z.string().min(1) }),
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
/** Ready-to-run offline eval cases for the default review tool IDs. */
|
|
1098
|
+
export const REVIEW_AGENT_TOOL_SELECTION_CASES = Object.freeze([
|
|
1099
|
+
{
|
|
1100
|
+
id: "discover-approved-review",
|
|
1101
|
+
prompt:
|
|
1102
|
+
"Show which approved review methods and assumptions are eligible before calculating anything.",
|
|
1103
|
+
expectTools: ["discover-approved-review"],
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
id: "run-approved-review",
|
|
1107
|
+
prompt:
|
|
1108
|
+
"Run the approved review I selected and return its verified readiness evidence.",
|
|
1109
|
+
expectTools: ["run-approved-review"],
|
|
1110
|
+
},
|
|
1111
|
+
] as const);
|
|
1112
|
+
|
|
1113
|
+
export type ReviewAgentToolResult =
|
|
1114
|
+
| ReviewAgentDiscoverySuccess
|
|
1115
|
+
| ReviewAgentExecutionSuccess
|
|
1116
|
+
| ToolEnvelopeFailure;
|