pi-system-one 1.1.4 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +100 -3
- package/demo.gif +0 -0
- package/package.json +7 -2
- package/prompts/judge.md +29 -0
- package/skills/system-one/SKILL.md +91 -0
- package/skills/system-one/references/use-cases.md +171 -0
- package/src/extension.ts +30 -0
- package/src/tool.ts +517 -118
- package/tests/extension.test.ts +123 -0
- package/tests/packaging.test.ts +62 -0
- package/tests/tool.test.ts +1414 -1
package/src/tool.ts
CHANGED
|
@@ -10,130 +10,425 @@ import { Type } from "typebox";
|
|
|
10
10
|
import { renderSystemOneResult } from "./render.ts";
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
*
|
|
13
|
+
* Public tool parameters (the LLM-facing boundary schema).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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
|
|
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
|
-
|
|
37
|
-
|
|
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
|
-
|
|
37
|
+
context: Type.Optional(Type.Any()),
|
|
38
|
+
evidence: Type.Optional(Type.Any()),
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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:
|
|
50
|
+
additionalProperties: true,
|
|
50
51
|
description:
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
135
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
136
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
137
|
+
}
|
|
91
138
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
179
|
+
/**
|
|
180
|
+
* Copy-on-write canonicality check for one question. Returns true when
|
|
181
|
+
* running the builder below would produce a key-for-key, value-for-value
|
|
182
|
+
* identical record — in which case the caller reuses the input by
|
|
183
|
+
* reference instead of allocating a copy. Any doubt returns false (the
|
|
184
|
+
* builder then produces the correct output); false negatives only cost an
|
|
185
|
+
* allocation, never correctness.
|
|
186
|
+
*
|
|
187
|
+
* Deliberately allocation-free itself (own-key probes, no entries/keys
|
|
188
|
+
* arrays) so the defensive second normalization in execute() stays cheap.
|
|
189
|
+
*/
|
|
190
|
+
function isCanonicalQuestion(input: Record<string, unknown>): boolean {
|
|
191
|
+
const hasType = Object.hasOwn(input, "type");
|
|
192
|
+
// Probe only: an exact canonical spelling (alias-map value identical to
|
|
193
|
+
// the key) needs no work. Near-miss spellings report non-canonical
|
|
194
|
+
// WITHOUT running full normalization here — the builder below runs it
|
|
195
|
+
// exactly once. Non-string types pass (the builder copies them verbatim),
|
|
196
|
+
// so doubt always errs toward a wasted rebuild, never a wrong share.
|
|
197
|
+
if (
|
|
198
|
+
hasType &&
|
|
199
|
+
typeof input.type === "string" &&
|
|
200
|
+
QUESTION_TYPE_ALIASES.get(input.type) !== input.type
|
|
201
|
+
) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
// Strings reaching here are already canonical, so the raw type doubles
|
|
205
|
+
// as the normalized one for the criteria decisions below.
|
|
206
|
+
const normalizedType = hasType ? input.type : undefined;
|
|
207
|
+
const instructions = pickAlias(input, INSTRUCTIONS_ALIASES);
|
|
208
|
+
// NOTE: no `else` for an own defined "instructions" slot — "instructions"
|
|
209
|
+
// is the first alias, so pickAlias would have returned it above; reaching
|
|
210
|
+
// here with one means it is undefined and the builder omits it too.
|
|
211
|
+
if (instructions !== undefined) {
|
|
212
|
+
// The builder emits own "instructions" with this value; canonical only
|
|
213
|
+
// if it already sits there and no sibling alias key lingers (dropped).
|
|
214
|
+
if (
|
|
215
|
+
!Object.hasOwn(input, "instructions") ||
|
|
216
|
+
input.instructions !== instructions
|
|
217
|
+
) {
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
for (const alias of INSTRUCTIONS_ALIASES) {
|
|
222
|
+
if (alias !== "instructions" && Object.hasOwn(input, alias)) return false;
|
|
223
|
+
}
|
|
224
|
+
let criteria = pickAlias(input, CRITERIA_ALIASES);
|
|
225
|
+
if (normalizedType === "noul" && criteria === null) {
|
|
226
|
+
// The builder drops it; canonical only if no own alias key is dropped.
|
|
227
|
+
for (const alias of CRITERIA_ALIASES) {
|
|
228
|
+
if (Object.hasOwn(input, alias)) return false;
|
|
229
|
+
}
|
|
230
|
+
criteria = undefined;
|
|
231
|
+
}
|
|
232
|
+
if (
|
|
233
|
+
normalizedType === "choice" &&
|
|
234
|
+
Array.isArray(criteria) &&
|
|
235
|
+
(criteria as unknown[]).every((item) => typeof item === "string")
|
|
236
|
+
) {
|
|
237
|
+
return false; // The builder folds string arrays to an object.
|
|
238
|
+
}
|
|
239
|
+
if (criteria !== undefined) {
|
|
240
|
+
if (!Object.hasOwn(input, "criteria") || input.criteria !== criteria) {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
} else if (Object.hasOwn(input, "criteria")) {
|
|
244
|
+
return false; // Own undefined criteria is dropped by the builder.
|
|
245
|
+
}
|
|
246
|
+
for (const alias of CRITERIA_ALIASES) {
|
|
247
|
+
if (alias !== "criteria" && Object.hasOwn(input, alias)) return false;
|
|
248
|
+
}
|
|
249
|
+
for (const key of QUESTION_JUNK_KEYS) {
|
|
250
|
+
if (Object.hasOwn(input, key)) return false;
|
|
251
|
+
}
|
|
252
|
+
// Any other own key is preserved by reference, so it cannot differ.
|
|
253
|
+
return true;
|
|
254
|
+
}
|
|
135
255
|
|
|
136
|
-
|
|
256
|
+
/**
|
|
257
|
+
* Coerce one question into the flat schema shape: canonical `type`,
|
|
258
|
+
* `instructions` / `criteria` resolved through aliases, array-form choice
|
|
259
|
+
* criteria (`["a", "b"]`) folded to `{"a": null, "b": null}`, and
|
|
260
|
+
* naming/junk keys stripped.
|
|
261
|
+
*
|
|
262
|
+
* Pure, idempotent, non-mutating, with structural sharing: canonical input
|
|
263
|
+
* is returned by reference; only repaired questions allocate.
|
|
264
|
+
*/
|
|
265
|
+
function normalizeQuestion(input: unknown): unknown {
|
|
266
|
+
if (!isRecord(input)) return input;
|
|
267
|
+
if (isCanonicalQuestion(input)) return input;
|
|
268
|
+
const out: Record<string, unknown> = {};
|
|
269
|
+
if (Object.hasOwn(input, "type"))
|
|
270
|
+
out.type = normalizeQuestionType(input.type);
|
|
271
|
+
const instructions = pickAlias(input, INSTRUCTIONS_ALIASES);
|
|
272
|
+
if (instructions !== undefined) out.instructions = instructions;
|
|
273
|
+
else if (Object.hasOwn(input, "instructions"))
|
|
274
|
+
out.instructions = input.instructions;
|
|
275
|
+
let criteria = pickAlias(input, CRITERIA_ALIASES);
|
|
276
|
+
// `null` is this schema's idiom for "no content"; on noul it reads as
|
|
277
|
+
// "omitted" rather than a degenerate value worth a backend round trip.
|
|
278
|
+
if (out.type === "noul" && criteria === null) criteria = undefined;
|
|
279
|
+
if (
|
|
280
|
+
out.type === "choice" &&
|
|
281
|
+
Array.isArray(criteria) &&
|
|
282
|
+
criteria.every((item) => typeof item === "string")
|
|
283
|
+
) {
|
|
284
|
+
const folded: Record<string, unknown> = {};
|
|
285
|
+
for (const item of criteria as string[]) setOwn(folded, item, null);
|
|
286
|
+
criteria = folded;
|
|
287
|
+
}
|
|
288
|
+
if (criteria !== undefined) out.criteria = criteria;
|
|
289
|
+
// Preserve any other non-junk keys so genuinely new fields still surface
|
|
290
|
+
// in validation instead of being silently dropped.
|
|
291
|
+
for (const [key, value] of Object.entries(input)) {
|
|
292
|
+
if (
|
|
293
|
+
key === "type" ||
|
|
294
|
+
INSTRUCTIONS_ALIASES.includes(key) ||
|
|
295
|
+
CRITERIA_ALIASES.includes(key) ||
|
|
296
|
+
QUESTION_JUNK_KEYS.has(key)
|
|
297
|
+
) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
setOwn(out, key, value);
|
|
301
|
+
}
|
|
302
|
+
return out;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function normalizeQuestions(input: unknown): unknown {
|
|
306
|
+
if (Array.isArray(input)) {
|
|
307
|
+
const record: Record<string, unknown> = {};
|
|
308
|
+
const used = new Set<string>();
|
|
309
|
+
// Duplicate names would silently overwrite; suffix instead so every
|
|
310
|
+
// requested question reaches the backend.
|
|
311
|
+
const claimName = (base: string): string => {
|
|
312
|
+
let candidate = base;
|
|
313
|
+
for (let n = 2; used.has(candidate); n += 1) candidate = `${base}_${n}`;
|
|
314
|
+
used.add(candidate);
|
|
315
|
+
return candidate;
|
|
316
|
+
};
|
|
317
|
+
input.forEach((entry, index) => {
|
|
318
|
+
if (isRecord(entry)) {
|
|
319
|
+
// No intermediate `{...rest}` copy: normalizeQuestion() already
|
|
320
|
+
// drops name/id/key as junk, so passing the entry through repairs
|
|
321
|
+
// naming metadata and aliases in a single construction pass.
|
|
322
|
+
const rawName = entry.name ?? entry.id ?? entry.key;
|
|
323
|
+
const base =
|
|
324
|
+
typeof rawName === "string" && rawName.trim() !== ""
|
|
325
|
+
? rawName
|
|
326
|
+
: `q${index + 1}`;
|
|
327
|
+
setOwn(record, claimName(base), normalizeQuestion(entry));
|
|
328
|
+
} else {
|
|
329
|
+
setOwn(record, claimName(`q${index + 1}`), entry);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
return record;
|
|
333
|
+
}
|
|
334
|
+
if (isRecord(input)) {
|
|
335
|
+
// Single question sent without a wrapping name (`questions: {type, ...}`)
|
|
336
|
+
// instead of (`questions: {q1: {type, ...}}`). Unwrap on a known kind so
|
|
337
|
+
// a hallucinated extra field still reports against the question (not as
|
|
338
|
+
// phantom questions named "type"/"instructions"); fall back to the
|
|
339
|
+
// vocabulary check for unknown types so those report precisely too. A
|
|
340
|
+
// record that merely contains a question *named* "type" has an object
|
|
341
|
+
// there, never a string, so it is never misread.
|
|
342
|
+
if (typeof input.type === "string") {
|
|
343
|
+
const kind = normalizeQuestionType(input.type);
|
|
344
|
+
const knownKind =
|
|
345
|
+
kind === "choice" || kind === "noul" || kind === "score";
|
|
346
|
+
const keys = Object.keys(input);
|
|
347
|
+
const allQuestionKeys = keys.every(
|
|
348
|
+
(key) =>
|
|
349
|
+
key === "type" ||
|
|
350
|
+
INSTRUCTIONS_ALIASES.includes(key) ||
|
|
351
|
+
CRITERIA_ALIASES.includes(key) ||
|
|
352
|
+
QUESTION_JUNK_KEYS.has(key),
|
|
353
|
+
);
|
|
354
|
+
if (knownKind || allQuestionKeys) return { q: normalizeQuestion(input) };
|
|
355
|
+
}
|
|
356
|
+
// Copy-on-write: share the map (and each canonical question) when
|
|
357
|
+
// nothing needs repair; otherwise rebuild with repaired questions and
|
|
358
|
+
// unaffected siblings shared by reference. The scan probes
|
|
359
|
+
// canonicality only — it must never build (and discard) repairs.
|
|
360
|
+
let dirty = false;
|
|
361
|
+
for (const name in input) {
|
|
362
|
+
if (!Object.hasOwn(input, name)) continue;
|
|
363
|
+
const question = input[name];
|
|
364
|
+
if (!isRecord(question) || !isCanonicalQuestion(question)) {
|
|
365
|
+
dirty = true;
|
|
366
|
+
break;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!dirty) return input;
|
|
370
|
+
const record: Record<string, unknown> = {};
|
|
371
|
+
for (const name in input) {
|
|
372
|
+
if (!Object.hasOwn(input, name)) continue;
|
|
373
|
+
setOwn(record, name, normalizeQuestion(input[name]));
|
|
374
|
+
}
|
|
375
|
+
return record;
|
|
376
|
+
}
|
|
377
|
+
return input;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Compatibility shim for sloppy tool arguments. Runs before schema
|
|
382
|
+
* validation (wired as `prepareArguments`) and is idempotent, so `execute`
|
|
383
|
+
* re-applies it defensively for direct calls that bypass validation.
|
|
384
|
+
*/
|
|
385
|
+
export function prepareSystemOneArgs(args: unknown): SystemOneParams {
|
|
386
|
+
if (!isRecord(args)) return args as SystemOneParams;
|
|
387
|
+
const questions =
|
|
388
|
+
args.questions ?? args.question ?? args.queries ?? undefined;
|
|
389
|
+
const normalizedQuestions =
|
|
390
|
+
questions !== undefined ? normalizeQuestions(questions) : undefined;
|
|
391
|
+
// Copy-on-write fast path: canonical state/questions slots, no alias keys
|
|
392
|
+
// to rename, no unknown keys to drop — return the input by reference so
|
|
393
|
+
// the defensive re-normalization in execute() allocates ~nothing.
|
|
394
|
+
const questionsCanonical =
|
|
395
|
+
normalizedQuestions === questions &&
|
|
396
|
+
(questions === undefined
|
|
397
|
+
? !Object.hasOwn(args, "questions") &&
|
|
398
|
+
!Object.hasOwn(args, "question") &&
|
|
399
|
+
!Object.hasOwn(args, "queries")
|
|
400
|
+
: questions === args.questions &&
|
|
401
|
+
!Object.hasOwn(args, "question") &&
|
|
402
|
+
!Object.hasOwn(args, "queries"));
|
|
403
|
+
if (
|
|
404
|
+
questionsCanonical &&
|
|
405
|
+
Object.hasOwn(args, "state") &&
|
|
406
|
+
args.state !== undefined &&
|
|
407
|
+
!Object.hasOwn(args, "context") &&
|
|
408
|
+
!Object.hasOwn(args, "evidence")
|
|
409
|
+
) {
|
|
410
|
+
let knownKeysOnly = true;
|
|
411
|
+
for (const key in args) {
|
|
412
|
+
if (Object.hasOwn(args, key) && key !== "state" && key !== "questions") {
|
|
413
|
+
knownKeysOnly = false;
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
if (knownKeysOnly) return args as SystemOneParams;
|
|
418
|
+
}
|
|
419
|
+
const out: Record<string, unknown> = {};
|
|
420
|
+
if (args.state !== undefined) {
|
|
421
|
+
out.state = args.state;
|
|
422
|
+
} else if (args.context !== undefined) {
|
|
423
|
+
out.state = args.context;
|
|
424
|
+
} else if (args.evidence !== undefined) {
|
|
425
|
+
out.state = args.evidence;
|
|
426
|
+
}
|
|
427
|
+
if (normalizedQuestions !== undefined) out.questions = normalizedQuestions;
|
|
428
|
+
// Drop unknown top-level keys; a missing `state` / `questions` still
|
|
429
|
+
// produces a clear error from semantic validation in execute().
|
|
430
|
+
return out as SystemOneParams;
|
|
431
|
+
}
|
|
137
432
|
|
|
138
433
|
export interface SystemOneDetails {
|
|
139
434
|
answers: Record<string, unknown>;
|
|
@@ -149,6 +444,84 @@ export interface SystemOneDetails {
|
|
|
149
444
|
};
|
|
150
445
|
}
|
|
151
446
|
|
|
447
|
+
/**
|
|
448
|
+
* Runtime semantic validation: "is this actually a valid System One
|
|
449
|
+
* request?" The public schema lets repairable shapes through; this gate
|
|
450
|
+
* enforces the canonical contract the provider requires. Limits here are
|
|
451
|
+
* the ones the tool itself guarantees (verified against
|
|
452
|
+
* system-one-core, which enforces no option/level counts — the backend
|
|
453
|
+
* is the final arbiter for anything stricter). Every error states what
|
|
454
|
+
* was wrong, which question it was in, and what valid shape looks like.
|
|
455
|
+
*/
|
|
456
|
+
const KNOWN_QUESTION_KEYS = new Set(["type", "instructions", "criteria"]);
|
|
457
|
+
|
|
458
|
+
function assertAnswerableQuestions(questions: unknown): void {
|
|
459
|
+
if (questions === undefined) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
'system_one: questions is required. Provide at least one named question, e.g. {"q1": {"type": "choice", "instructions": "...", "criteria": {"a": null}}}.',
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
// The semantic gate is authoritative: anything that is not a question
|
|
465
|
+
// map after normalization is rejected here, never sent to the provider.
|
|
466
|
+
if (!isRecord(questions)) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
'system_one: "questions" must resolve to a question map after normalization, e.g. {"q1": {"type": "choice", "instructions": "...", "criteria": {"a": null}}}.',
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
if (Object.keys(questions).length === 0) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
"system_one: at least one question is required in questions.",
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
for (const [name, value] of Object.entries(questions)) {
|
|
477
|
+
if (!isRecord(value)) {
|
|
478
|
+
throw new Error(
|
|
479
|
+
`system_one: question "${name}" must be an object with "type", "instructions", and "criteria".`,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
for (const key of Object.keys(value)) {
|
|
483
|
+
if (!KNOWN_QUESTION_KEYS.has(key)) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`system_one: question "${name}" has unknown field "${key}"; use only "type", "instructions", and "criteria".`,
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (value.instructions === undefined || value.instructions === null) {
|
|
490
|
+
throw new Error(
|
|
491
|
+
`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?"}.`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
if (value.type === "choice") {
|
|
495
|
+
if (
|
|
496
|
+
!isRecord(value.criteria) ||
|
|
497
|
+
Object.keys(value.criteria).length === 0
|
|
498
|
+
) {
|
|
499
|
+
throw new Error(
|
|
500
|
+
`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}.`,
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
} else if (value.type === "score") {
|
|
504
|
+
if (!Array.isArray(value.criteria) || value.criteria.length < 2) {
|
|
505
|
+
throw new Error(
|
|
506
|
+
`system_one: score question "${name}" needs criteria as an ordered array of at least two levels from lowest to highest, e.g. ["low", "high"].`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
} else if (value.type === "noul") {
|
|
510
|
+
if (value.criteria !== undefined && !isRecord(value.criteria)) {
|
|
511
|
+
throw new Error(
|
|
512
|
+
`system_one: noul question "${name}" needs criteria omitted or as an object describing the true/false outcomes, e.g. {"true": "...", "false": "..."}.`,
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
} else {
|
|
516
|
+
const got =
|
|
517
|
+
typeof value.type === "string" ? `"${value.type}"` : "missing";
|
|
518
|
+
throw new Error(
|
|
519
|
+
`system_one: question "${name}" has unknown type ${got}; use "choice", "noul", or "score".`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
152
525
|
export function buildSystemOneTool(deps: {
|
|
153
526
|
provider: SystemOneProvider;
|
|
154
527
|
}): ToolDefinition<typeof systemOneParams, SystemOneDetails> {
|
|
@@ -161,37 +534,63 @@ export function buildSystemOneTool(deps: {
|
|
|
161
534
|
label: "System One",
|
|
162
535
|
|
|
163
536
|
description:
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
"
|
|
167
|
-
"
|
|
537
|
+
"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). " +
|
|
538
|
+
"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. " +
|
|
539
|
+
"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. " +
|
|
540
|
+
"Do not use it for open-ended text generation or factual lookup. " +
|
|
541
|
+
"Questions that share the same state can be evaluated together in one call. " +
|
|
542
|
+
'Each question is {"type": "choice" | "noul" | "score", "instructions": "...", "criteria": ...}: ' +
|
|
543
|
+
'choice criteria is an object keyed by choice label ({"a": null, "b": null}), ' +
|
|
544
|
+
"noul usually omits criteria, " +
|
|
545
|
+
"score criteria is an ordered array of at least two levels.",
|
|
168
546
|
|
|
169
547
|
parameters: systemOneParams,
|
|
170
548
|
|
|
171
549
|
promptSnippet:
|
|
172
|
-
"
|
|
550
|
+
"Use system_one for bounded choice, yes/no, and ordered-scale judgments over supplied evidence",
|
|
173
551
|
|
|
174
552
|
promptGuidelines: [
|
|
175
|
-
"
|
|
176
|
-
"
|
|
177
|
-
"
|
|
178
|
-
"
|
|
179
|
-
"For
|
|
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.",
|
|
553
|
+
"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.",
|
|
554
|
+
"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.",
|
|
555
|
+
"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.",
|
|
556
|
+
"Batch independent system_one questions sharing the same state into one call instead of one call per question.",
|
|
557
|
+
"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
558
|
],
|
|
184
559
|
|
|
560
|
+
prepareArguments: (args) => prepareSystemOneArgs(args),
|
|
561
|
+
|
|
185
562
|
async execute(_toolCallId, params, signal, _onUpdate, _ctx) {
|
|
186
563
|
/*
|
|
187
564
|
* The LLM-facing TypeBox schema is intentionally separate from the
|
|
188
565
|
* provider protocol types. Cast only at this boundary instead of
|
|
189
566
|
* spreading `any` throughout the implementation.
|
|
567
|
+
*
|
|
568
|
+
* `prepareArguments` above is an optimization for newer Pi only: it
|
|
569
|
+
* runs before schema validation there, but older Pi versions ignore
|
|
570
|
+
* it and validate the raw model output against the permissive
|
|
571
|
+
* schema instead. Correctness never depends on it — re-normalize
|
|
572
|
+
* here so every path (new Pi, old Pi, direct calls) converges on
|
|
573
|
+
* the same canonical shape before semantic validation.
|
|
190
574
|
*/
|
|
575
|
+
const normalized = prepareSystemOneArgs(params);
|
|
576
|
+
// prepareSystemOneArgs passes non-objects through untouched; direct
|
|
577
|
+
// calls bypassing Pi validation must get an actionable error, not a
|
|
578
|
+
// TypeError from property access on null.
|
|
579
|
+
if (!isRecord(normalized)) {
|
|
580
|
+
throw new Error(
|
|
581
|
+
'system_one: request must be an object with "state" and "questions".',
|
|
582
|
+
);
|
|
583
|
+
}
|
|
584
|
+
if (normalized.state === undefined) {
|
|
585
|
+
throw new Error(
|
|
586
|
+
'system_one: state is required. Put the material under judgment in "state" (string or object), e.g. {"state": "<evidence>", "questions": {...}}.',
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
assertAnswerableQuestions(normalized.questions);
|
|
191
590
|
const response = await systemOne.evaluate(
|
|
192
591
|
{
|
|
193
|
-
state:
|
|
194
|
-
questions:
|
|
592
|
+
state: normalized.state as SystemOneState,
|
|
593
|
+
questions: normalized.questions as QuestionMap,
|
|
195
594
|
},
|
|
196
595
|
{ signal },
|
|
197
596
|
);
|