pi-system-one 1.1.4 → 1.2.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/tool.ts CHANGED
@@ -10,130 +10,297 @@ import { Type } from "typebox";
10
10
  import { renderSystemOneResult } from "./render.ts";
11
11
 
12
12
  /**
13
- * JSON value accepted by System One.
13
+ * Public tool parameters (the LLM-facing boundary schema).
14
14
  *
15
- * Arrays/objects use Type.Any() recursively because tool arguments already
16
- * cross a JSON boundary. Keeping the schema non-recursive also tends to be
17
- * easier for tool-calling models to consume.
15
+ * The LLM-facing tool contract is intentionally more permissive than the
16
+ * System One domain model. The schema answers only "can I safely accept
17
+ * and inspect this?" — every field is optional, extra keys are allowed,
18
+ * and `questions` accepts any JSON so near-miss shapes (singular
19
+ * `question`, arrays, unwrapped singles, alias spellings like `Choice`,
20
+ * `rating`, or `Yes/No`) survive Pi's pre-execution validation on every
21
+ * Pi version, including older ones without `prepareArguments`.
22
+ * Normalization (`prepareSystemOneArgs`) repairs representation only;
23
+ * semantic validation in `execute()` (via `assertAnswerableQuestions`)
24
+ * establishes the canonical System One contract — `{type: "choice" |
25
+ * "noul" | "score", instructions, criteria}` per question — and rejects
26
+ * what cannot be repaired with an actionable error.
18
27
  */
19
- const JsonValue = Type.Union(
20
- [
21
- Type.String(),
22
- Type.Number(),
23
- Type.Boolean(),
24
- Type.Null(),
25
- Type.Array(Type.Any()),
26
- Type.Record(Type.String(), Type.Any()),
27
- ],
28
- {
29
- description:
30
- "A JSON value: string, number, boolean, null, object, or array.",
31
- },
32
- );
33
-
34
- const ChoiceQ = Type.Object(
28
+ export const systemOneParams = Type.Object(
35
29
  {
36
- type: Type.Literal("choice", {
37
- description: "Select exactly one outcome from a bounded set.",
38
- }),
30
+ state: Type.Optional(
31
+ Type.Any({
32
+ description:
33
+ "Shared state or evidence that all questions should be evaluated against. Usually a string or object. Aliases: context, evidence.",
34
+ }),
35
+ ),
39
36
 
40
- instructions: JsonValue,
37
+ context: Type.Optional(Type.Any()),
38
+ evidence: Type.Optional(Type.Any()),
41
39
 
42
- criteria: Type.Record(Type.String(), JsonValue, {
43
- minProperties: 1,
44
- description:
45
- "Available choices keyed by choice label. Each value describes that choice; use null when the label is self-explanatory. The keys are the choices; do not provide a separate options field.",
46
- }),
40
+ questions: Type.Optional(
41
+ Type.Any({
42
+ description:
43
+ 'One or more named decision questions. "choice" selects one outcome from a bounded set (criteria is an object keyed by label); "noul" estimates a yes/no likelihood (criteria usually omitted); "score" scores against an ordered rubric (criteria is an array of at least two levels). Accepts a name-keyed object, an array of questions, or a single unwrapped question. Aliases: question, queries; near-miss type spellings are normalized.',
44
+ }),
45
+ ),
46
+ question: Type.Optional(Type.Any()),
47
+ queries: Type.Optional(Type.Any()),
47
48
  },
48
49
  {
49
- additionalProperties: false,
50
+ additionalProperties: true,
50
51
  description:
51
- "A closed-set categorical decision. The keys of criteria are the available choices.",
52
+ 'Evaluate bounded decision questions against shared state. Example: {"state": "...", "questions": {"q1": {"type": "choice", "instructions": "...", "criteria": {"a": null, "b": null}}}}.',
52
53
  },
53
54
  );
54
55
 
55
- const NoulCriteria = Type.Object(
56
- {
57
- true: Type.Optional(JsonValue),
58
- false: Type.Optional(JsonValue),
59
- },
60
- {
61
- additionalProperties: false,
62
- minProperties: 1,
63
- description: "Optional descriptions of the true/yes and false/no outcomes.",
64
- },
65
- );
56
+ export type SystemOneParams = Static<typeof systemOneParams>;
66
57
 
67
- const NoulQ = Type.Object(
68
- {
69
- type: Type.Literal("noul", {
70
- description: "Estimate the likelihood of a yes/true outcome.",
71
- }),
58
+ /**
59
+ * Near-miss spellings low-quality models emit for the `type` discriminator.
60
+ * Lookup is case-insensitive; unlisted values pass through untouched so
61
+ * validation still reports a clear error for genuinely unknown kinds.
62
+ *
63
+ * Every entry here must be SAFE: a representation-only difference whose
64
+ * meaning is unambiguous. Deliberately NOT aliased (rejected instead):
65
+ * - "probability" / "likelihood": describe an output statistic, not a task.
66
+ * They could mean a noul question, a choice with probabilities, or a
67
+ * free-floating number. Guessing noul would silently convert the wrong
68
+ * task into a valid but semantically incorrect request — a believable
69
+ * wrong answer is worse than a validation error the model can retry.
70
+ * - "ranking": ordering items relative to each other is not scoring each
71
+ * against a rubric. Mapping it to score would corrupt the task.
72
+ */
73
+ const QUESTION_TYPE_ALIASES = new Map<string, string>([
74
+ ["choice", "choice"],
75
+ ["choose", "choice"],
76
+ ["multiple_choice", "choice"],
77
+ ["categorical", "choice"],
78
+ ["select", "choice"],
79
+ ["classify", "choice"],
80
+ ["classification", "choice"],
81
+ ["noul", "noul"],
82
+ ["boolean", "noul"],
83
+ ["bool", "noul"],
84
+ ["yes_no", "noul"],
85
+ ["yesno", "noul"],
86
+ ["score", "score"],
87
+ ["rating", "score"],
88
+ ["rate", "score"],
89
+ ["scale", "score"],
90
+ ["rubric", "score"],
91
+ ["grade", "score"],
92
+ ]);
72
93
 
73
- instructions: JsonValue,
94
+ /** Field aliases models use instead of `criteria`. First hit wins. */
95
+ const CRITERIA_ALIASES = [
96
+ "criteria",
97
+ "options",
98
+ "choices",
99
+ "levels",
100
+ "rubric",
101
+ "labels",
102
+ "candidates",
103
+ "outcomes",
104
+ ];
74
105
 
75
- criteria: Type.Optional(NoulCriteria),
76
- },
77
- {
78
- additionalProperties: false,
79
- description:
80
- "A probabilistic yes/no judgment. Omit criteria unless the true or false outcomes need clarification.",
81
- },
82
- );
106
+ /** Field aliases models use instead of `instructions`. First hit wins. */
107
+ const INSTRUCTIONS_ALIASES = [
108
+ "instructions",
109
+ "instruction",
110
+ "prompt",
111
+ "question",
112
+ "text",
113
+ "query",
114
+ "task",
115
+ ];
83
116
 
84
- const ScoreQ = Type.Object(
85
- {
86
- type: Type.Literal("score", {
87
- description: "Score the state against an ordered rubric.",
88
- }),
117
+ /**
118
+ * Question-level keys that are never part of the schema (naming metadata
119
+ * from array-form questions, or hallucinated documentation fields).
120
+ * Dropped so they don't trip `additionalProperties: false`.
121
+ */
122
+ const QUESTION_JUNK_KEYS = new Set([
123
+ "description",
124
+ "title",
125
+ "detail",
126
+ "details",
127
+ "example",
128
+ "examples",
129
+ "id",
130
+ "key",
131
+ "name",
132
+ "label",
133
+ ]);
89
134
 
90
- instructions: JsonValue,
135
+ function isRecord(value: unknown): value is Record<string, unknown> {
136
+ return typeof value === "object" && value !== null && !Array.isArray(value);
137
+ }
91
138
 
92
- criteria: Type.Array(JsonValue, {
93
- minItems: 2,
94
- description:
95
- "Ordered score levels from lowest to highest. Each array item defines one level of the rubric.",
96
- }),
97
- },
98
- {
99
- additionalProperties: false,
100
- description:
101
- "An ordered rubric decision. Criteria are ordered from lowest to highest.",
102
- },
103
- );
139
+ /**
140
+ * Assign an externally controlled key. Plain `obj[key] = value` with
141
+ * `key === "__proto__"` would reparent the object instead of storing an
142
+ * entry; defineProperty always creates an ordinary own data property.
143
+ */
144
+ function setOwn(
145
+ obj: Record<string, unknown>,
146
+ key: string,
147
+ value: unknown,
148
+ ): void {
149
+ Object.defineProperty(obj, key, {
150
+ value,
151
+ enumerable: true,
152
+ writable: true,
153
+ configurable: true,
154
+ });
155
+ }
104
156
 
105
- const Question = Type.Union([ChoiceQ, NoulQ, ScoreQ]);
157
+ function normalizeQuestionType(raw: unknown): unknown {
158
+ if (typeof raw !== "string") return raw;
159
+ const key = raw
160
+ .trim()
161
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
162
+ .toLowerCase()
163
+ .replace(/[-\s/.]+/g, "_");
164
+ // Map lookup (not a plain object) so inherited names like "__proto__"
165
+ // or "constructor" can never accidentally resolve to an alias.
166
+ return QUESTION_TYPE_ALIASES.get(key) ?? raw;
167
+ }
106
168
 
107
- export const systemOneParams = Type.Object(
108
- {
109
- state: Type.Union(
110
- [
111
- Type.String(),
112
- Type.Number(),
113
- Type.Boolean(),
114
- Type.Null(),
115
- Type.Record(Type.String(), Type.Any()),
116
- Type.Array(Type.Any()),
117
- ],
118
- {
119
- description:
120
- "Shared state or evidence that all questions should be evaluated against.",
121
- },
122
- ),
169
+ function pickAlias(
170
+ input: Record<string, unknown>,
171
+ aliases: readonly string[],
172
+ ): unknown {
173
+ for (const alias of aliases) {
174
+ if (input[alias] !== undefined) return input[alias];
175
+ }
176
+ return undefined;
177
+ }
123
178
 
124
- questions: Type.Record(Type.String(), Question, {
125
- minProperties: 1,
126
- description:
127
- "One or more named decision questions. Questions sharing the same state should be sent together.",
128
- }),
129
- },
130
- {
131
- additionalProperties: false,
132
- description: "Evaluate bounded decision questions against shared state.",
133
- },
134
- );
179
+ /**
180
+ * Coerce one question into the flat schema shape: canonical `type`,
181
+ * `instructions` / `criteria` resolved through aliases, array-form choice
182
+ * criteria (`["a", "b"]`) folded to `{"a": null, "b": null}`, and
183
+ * naming/junk keys stripped.
184
+ */
185
+ function normalizeQuestion(input: unknown): unknown {
186
+ if (!isRecord(input)) return input;
187
+ const out: Record<string, unknown> = {};
188
+ if (Object.hasOwn(input, "type"))
189
+ out.type = normalizeQuestionType(input.type);
190
+ const instructions = pickAlias(input, INSTRUCTIONS_ALIASES);
191
+ if (instructions !== undefined) out.instructions = instructions;
192
+ else if (Object.hasOwn(input, "instructions"))
193
+ out.instructions = input.instructions;
194
+ let criteria = pickAlias(input, CRITERIA_ALIASES);
195
+ // `null` is this schema's idiom for "no content"; on noul it reads as
196
+ // "omitted" rather than a degenerate value worth a backend round trip.
197
+ if (out.type === "noul" && criteria === null) criteria = undefined;
198
+ if (
199
+ out.type === "choice" &&
200
+ Array.isArray(criteria) &&
201
+ criteria.every((item) => typeof item === "string")
202
+ ) {
203
+ const folded: Record<string, unknown> = {};
204
+ for (const item of criteria as string[]) setOwn(folded, item, null);
205
+ criteria = folded;
206
+ }
207
+ if (criteria !== undefined) out.criteria = criteria;
208
+ // Preserve any other non-junk keys so genuinely new fields still surface
209
+ // in validation instead of being silently dropped.
210
+ for (const [key, value] of Object.entries(input)) {
211
+ if (
212
+ key === "type" ||
213
+ INSTRUCTIONS_ALIASES.includes(key) ||
214
+ CRITERIA_ALIASES.includes(key) ||
215
+ QUESTION_JUNK_KEYS.has(key)
216
+ ) {
217
+ continue;
218
+ }
219
+ setOwn(out, key, value);
220
+ }
221
+ return out;
222
+ }
135
223
 
136
- export type SystemOneParams = Static<typeof systemOneParams>;
224
+ function normalizeQuestions(input: unknown): unknown {
225
+ if (Array.isArray(input)) {
226
+ const record: Record<string, unknown> = {};
227
+ const used = new Set<string>();
228
+ // Duplicate names would silently overwrite; suffix instead so every
229
+ // requested question reaches the backend.
230
+ const claimName = (base: string): string => {
231
+ let candidate = base;
232
+ for (let n = 2; used.has(candidate); n += 1) candidate = `${base}_${n}`;
233
+ used.add(candidate);
234
+ return candidate;
235
+ };
236
+ input.forEach((entry, index) => {
237
+ if (isRecord(entry)) {
238
+ const { name, id, key, ...rest } = entry;
239
+ const rawName = name ?? id ?? key;
240
+ const base =
241
+ typeof rawName === "string" && rawName.trim() !== ""
242
+ ? rawName
243
+ : `q${index + 1}`;
244
+ setOwn(record, claimName(base), normalizeQuestion(rest));
245
+ } else {
246
+ setOwn(record, claimName(`q${index + 1}`), entry);
247
+ }
248
+ });
249
+ return record;
250
+ }
251
+ if (isRecord(input)) {
252
+ // Single question sent without a wrapping name (`questions: {type, ...}`)
253
+ // instead of (`questions: {q1: {type, ...}}`). Unwrap on a known kind so
254
+ // a hallucinated extra field still reports against the question (not as
255
+ // phantom questions named "type"/"instructions"); fall back to the
256
+ // vocabulary check for unknown types so those report precisely too. A
257
+ // record that merely contains a question *named* "type" has an object
258
+ // there, never a string, so it is never misread.
259
+ if (typeof input.type === "string") {
260
+ const kind = normalizeQuestionType(input.type);
261
+ const knownKind =
262
+ kind === "choice" || kind === "noul" || kind === "score";
263
+ const keys = Object.keys(input);
264
+ const allQuestionKeys = keys.every(
265
+ (key) =>
266
+ key === "type" ||
267
+ INSTRUCTIONS_ALIASES.includes(key) ||
268
+ CRITERIA_ALIASES.includes(key) ||
269
+ QUESTION_JUNK_KEYS.has(key),
270
+ );
271
+ if (knownKind || allQuestionKeys) return { q: normalizeQuestion(input) };
272
+ }
273
+ const record: Record<string, unknown> = {};
274
+ for (const [name, question] of Object.entries(input)) {
275
+ setOwn(record, name, normalizeQuestion(question));
276
+ }
277
+ return record;
278
+ }
279
+ return input;
280
+ }
281
+
282
+ /**
283
+ * Compatibility shim for sloppy tool arguments. Runs before schema
284
+ * validation (wired as `prepareArguments`) and is idempotent, so `execute`
285
+ * re-applies it defensively for direct calls that bypass validation.
286
+ */
287
+ export function prepareSystemOneArgs(args: unknown): SystemOneParams {
288
+ if (!isRecord(args)) return args as SystemOneParams;
289
+ const out: Record<string, unknown> = {};
290
+ if (args.state !== undefined) {
291
+ out.state = args.state;
292
+ } else if (args.context !== undefined) {
293
+ out.state = args.context;
294
+ } else if (args.evidence !== undefined) {
295
+ out.state = args.evidence;
296
+ }
297
+ const questions =
298
+ args.questions ?? args.question ?? args.queries ?? undefined;
299
+ if (questions !== undefined) out.questions = normalizeQuestions(questions);
300
+ // Drop unknown top-level keys; a missing `state` / `questions` still
301
+ // produces a clear error from semantic validation in execute().
302
+ return out as SystemOneParams;
303
+ }
137
304
 
138
305
  export interface SystemOneDetails {
139
306
  answers: Record<string, unknown>;
@@ -149,6 +316,84 @@ export interface SystemOneDetails {
149
316
  };
150
317
  }
151
318
 
319
+ /**
320
+ * Runtime semantic validation: "is this actually a valid System One
321
+ * request?" The public schema lets repairable shapes through; this gate
322
+ * enforces the canonical contract the provider requires. Limits here are
323
+ * the ones the tool itself guarantees (verified against
324
+ * system-one-core, which enforces no option/level counts — the backend
325
+ * is the final arbiter for anything stricter). Every error states what
326
+ * was wrong, which question it was in, and what valid shape looks like.
327
+ */
328
+ const KNOWN_QUESTION_KEYS = new Set(["type", "instructions", "criteria"]);
329
+
330
+ function assertAnswerableQuestions(questions: unknown): void {
331
+ if (questions === undefined) {
332
+ throw new Error(
333
+ 'system_one: questions is required. Provide at least one named question, e.g. {"q1": {"type": "choice", "instructions": "...", "criteria": {"a": null}}}.',
334
+ );
335
+ }
336
+ // The semantic gate is authoritative: anything that is not a question
337
+ // map after normalization is rejected here, never sent to the provider.
338
+ if (!isRecord(questions)) {
339
+ throw new Error(
340
+ 'system_one: "questions" must resolve to a question map after normalization, e.g. {"q1": {"type": "choice", "instructions": "...", "criteria": {"a": null}}}.',
341
+ );
342
+ }
343
+ if (Object.keys(questions).length === 0) {
344
+ throw new Error(
345
+ "system_one: at least one question is required in questions.",
346
+ );
347
+ }
348
+ for (const [name, value] of Object.entries(questions)) {
349
+ if (!isRecord(value)) {
350
+ throw new Error(
351
+ `system_one: question "${name}" must be an object with "type", "instructions", and "criteria".`,
352
+ );
353
+ }
354
+ for (const key of Object.keys(value)) {
355
+ if (!KNOWN_QUESTION_KEYS.has(key)) {
356
+ throw new Error(
357
+ `system_one: question "${name}" has unknown field "${key}"; use only "type", "instructions", and "criteria".`,
358
+ );
359
+ }
360
+ }
361
+ if (value.instructions === undefined || value.instructions === null) {
362
+ throw new Error(
363
+ `system_one: question "${name}" needs "instructions" describing what to evaluate, e.g. {"type": "${typeof value.type === "string" ? value.type : "choice"}", "instructions": "Which option fits best?"}.`,
364
+ );
365
+ }
366
+ if (value.type === "choice") {
367
+ if (
368
+ !isRecord(value.criteria) ||
369
+ Object.keys(value.criteria).length === 0
370
+ ) {
371
+ throw new Error(
372
+ `system_one: choice question "${name}" needs criteria as an object keyed by choice label with at least one choice, e.g. {"a": null, "b": null}.`,
373
+ );
374
+ }
375
+ } else if (value.type === "score") {
376
+ if (!Array.isArray(value.criteria) || value.criteria.length < 2) {
377
+ throw new Error(
378
+ `system_one: score question "${name}" needs criteria as an ordered array of at least two levels from lowest to highest, e.g. ["low", "high"].`,
379
+ );
380
+ }
381
+ } else if (value.type === "noul") {
382
+ if (value.criteria !== undefined && !isRecord(value.criteria)) {
383
+ throw new Error(
384
+ `system_one: noul question "${name}" needs criteria omitted or as an object describing the true/false outcomes, e.g. {"true": "...", "false": "..."}.`,
385
+ );
386
+ }
387
+ } else {
388
+ const got =
389
+ typeof value.type === "string" ? `"${value.type}"` : "missing";
390
+ throw new Error(
391
+ `system_one: question "${name}" has unknown type ${got}; use "choice", "noul", or "score".`,
392
+ );
393
+ }
394
+ }
395
+ }
396
+
152
397
  export function buildSystemOneTool(deps: {
153
398
  provider: SystemOneProvider;
154
399
  }): ToolDefinition<typeof systemOneParams, SystemOneDetails> {
@@ -161,37 +406,63 @@ export function buildSystemOneTool(deps: {
161
406
  label: "System One",
162
407
 
163
408
  description:
164
- "Evaluate bounded choice, yes/no likelihood, and ordered score questions against shared state. " +
165
- "Use this when the possible outcomes or scoring rubric are known and structured probabilistic results are useful. " +
166
- "Do not use it for open-ended text generation. " +
167
- "Questions that share the same state can be evaluated together in one call.",
409
+ "Judge bounded decisions over explicitly supplied state with a dedicated judge model — calibrated probabilities conditional on that state (confidence for choice/score only; noul has no confidence field, its probability is the uncertainty measure). " +
410
+ "Call system_one instead of answering directly when the request is a pick from named options, a yes/no likelihood, or a rubric score over state already in context — the judge's state-conditional probabilities beat guesses from priors. " +
411
+ "The judge only weighs the state you supply: it cannot recall facts or browse, so retrieve evidence first and never ask it to recall facts. " +
412
+ "Do not use it for open-ended text generation or factual lookup. " +
413
+ "Questions that share the same state can be evaluated together in one call. " +
414
+ 'Each question is {"type": "choice" | "noul" | "score", "instructions": "...", "criteria": ...}: ' +
415
+ 'choice criteria is an object keyed by choice label ({"a": null, "b": null}), ' +
416
+ "noul usually omits criteria, " +
417
+ "score criteria is an ordered array of at least two levels.",
168
418
 
169
419
  parameters: systemOneParams,
170
420
 
171
421
  promptSnippet:
172
- "Evaluate bounded choices, yes/no likelihoods, and ordered scores from shared state",
422
+ "Use system_one for bounded choice, yes/no, and ordered-scale judgments over supplied evidence",
173
423
 
174
424
  promptGuidelines: [
175
- "Use system_one for closed-set classification, probabilistic yes/no judgments, or ordered rubric scoring.",
176
- "Do not use system_one for open-ended text generation or when there is no bounded answer space or rubric.",
177
- "Preserve user-specified choice labels, criteria, and score levels instead of inventing replacements.",
178
- "For choice questions, the keys of criteria are the available choices; never add a separate options field.",
179
- "For self-explanatory choice labels, prefer null criteria values instead of inventing definitions that could bias the decision.",
180
- "For noul questions, omit criteria unless the true/yes or false/no outcomes need clarification.",
181
- "For score questions, criteria must contain at least two ordered levels from lowest to highest.",
182
- "Batch independent questions into one system_one call when they share the same state.",
425
+ "Before answering a bounded judgment over supplied or retrieved evidence, call system_one instead of making the judgment yourself. A bounded judgment is: choosing among explicit alternatives, making a yes/no judgment, or rating something on an ordered scale.",
426
+ "Pick the system_one question type mechanically: unordered alternatives (frontend, backend, platform) -> choice; yes/no -> noul; ordered scale, rating, severity, risk, or grade -> score. Never use choice for an ordered scale merely because its levels have names: very low / low / moderate / high / very high is score, not choice.",
427
+ "Retrieve missing factual evidence into state first; never ask system_one to recall facts or browse. Use system_one for bounded judgments over available evidence, not as a replacement for factual lookup.",
428
+ "Batch independent system_one questions sharing the same state into one call instead of one call per question.",
429
+ "For system_one choice questions, the keys of criteria are the available choices — preserve user-specified labels and use null values when labels are self-explanatory; never add a separate options field.",
183
430
  ],
184
431
 
432
+ prepareArguments: (args) => prepareSystemOneArgs(args),
433
+
185
434
  async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
186
435
  /*
187
436
  * The LLM-facing TypeBox schema is intentionally separate from the
188
437
  * provider protocol types. Cast only at this boundary instead of
189
438
  * spreading `any` throughout the implementation.
439
+ *
440
+ * `prepareArguments` above is an optimization for newer Pi only: it
441
+ * runs before schema validation there, but older Pi versions ignore
442
+ * it and validate the raw model output against the permissive
443
+ * schema instead. Correctness never depends on it — re-normalize
444
+ * here so every path (new Pi, old Pi, direct calls) converges on
445
+ * the same canonical shape before semantic validation.
190
446
  */
447
+ const normalized = prepareSystemOneArgs(params);
448
+ // prepareSystemOneArgs passes non-objects through untouched; direct
449
+ // calls bypassing Pi validation must get an actionable error, not a
450
+ // TypeError from property access on null.
451
+ if (!isRecord(normalized)) {
452
+ throw new Error(
453
+ 'system_one: request must be an object with "state" and "questions".',
454
+ );
455
+ }
456
+ if (normalized.state === undefined) {
457
+ throw new Error(
458
+ 'system_one: state is required. Put the material under judgment in "state" (string or object), e.g. {"state": "<evidence>", "questions": {...}}.',
459
+ );
460
+ }
461
+ assertAnswerableQuestions(normalized.questions);
191
462
  const response = await systemOne.evaluate(
192
463
  {
193
- state: params.state as SystemOneState,
194
- questions: params.questions as QuestionMap,
464
+ state: normalized.state as SystemOneState,
465
+ questions: normalized.questions as QuestionMap,
195
466
  },
196
467
  { signal },
197
468
  );