@evoclock/pi-agentic-driver 0.9.2 → 0.9.4
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/PROVENANCE.md +9 -0
- package/README.md +178 -73
- package/extensions/tasks.ts +570 -0
- package/janus/README.md +196 -0
- package/janus/janus/audit.ts +213 -0
- package/janus/janus/budget.ts +101 -0
- package/janus/janus/budget_breaker.ts +105 -0
- package/janus/janus/config.ts +332 -0
- package/janus/janus/errors.ts +90 -0
- package/janus/janus/evaluate.ts +230 -0
- package/janus/janus/keychain.ts +119 -0
- package/janus/janus/providers.ts +544 -0
- package/janus/janus/schema.ts +344 -0
- package/janus/janus/server.ts +267 -0
- package/janus/janus/single_flight.ts +99 -0
- package/janus/janus/verdict.ts +192 -0
- package/janus/package-lock.json +616 -0
- package/janus/package.json +29 -0
- package/janus/tests/accounting.test.ts +229 -0
- package/janus/tests/allowlist.test.ts +97 -0
- package/janus/tests/config.test.ts +151 -0
- package/janus/tests/errors.test.ts +147 -0
- package/janus/tests/evaluate_pipeline.test.ts +358 -0
- package/janus/tests/helpers.ts +217 -0
- package/janus/tests/providers.test.ts +292 -0
- package/janus/tests/redaction_budget.test.ts +227 -0
- package/janus/tests/schema.test.ts +356 -0
- package/janus/tests/server.test.ts +288 -0
- package/janus/tests/single_flight.test.ts +84 -0
- package/janus/tsconfig.json +19 -0
- package/janus/user.janus.plist +51 -0
- package/package.json +7 -2
- package/scripts/enforcement/router_schemas_pi.js +63 -13
- package/scripts/enforcement/session_tasks_core_pi.js +605 -0
- package/scripts/enforcement/session_tasks_validate_pi.js +235 -0
- package/skills/tasks/SKILL.md +110 -0
- package/templates/AGENTS.md +11 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
// Verdict enforcement (§6, review fix #8): the model's answers are checked
|
|
5
|
+
// against the enforced question contract before anything is returned to the
|
|
6
|
+
// caller. The answer shape is closed:
|
|
7
|
+
// - every requested question must have an answer (missing → fail);
|
|
8
|
+
// - the answer type must match the requested question type;
|
|
9
|
+
// - boolean probability must be a finite number in [0, 1];
|
|
10
|
+
// - choice must be a member of the requested choices;
|
|
11
|
+
// - score must be a finite number in [0, levels - 1];
|
|
12
|
+
// - probabilities maps (when present) must be finite numbers over the
|
|
13
|
+
// question's own vocabulary.
|
|
14
|
+
// ANY violation — missing, malformed, contradictory, wrong type, invalid
|
|
15
|
+
// probability, out-of-range score, out-of-set choice — fails the ENTIRE
|
|
16
|
+
// request with validation_error (atomic, fail-closed). Transport
|
|
17
|
+
// timeout/unavailability remains a no-verdict outcome (timeout /
|
|
18
|
+
// gateway_unavailable) and is classified upstream; enforcement itself never
|
|
19
|
+
// converts a provider shape problem into a no-verdict.
|
|
20
|
+
|
|
21
|
+
import type { EnforcedQuestion } from "./schema.js";
|
|
22
|
+
import { validationError } from "./errors.js";
|
|
23
|
+
|
|
24
|
+
/** One decimal, per §6. */
|
|
25
|
+
export function roundScore(value: number): number {
|
|
26
|
+
return Math.round(value * 10) / 10;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface EnforcedVerdict {
|
|
30
|
+
answers: Record<
|
|
31
|
+
string,
|
|
32
|
+
{ type: "boolean"; probability: number; passed: boolean } | {
|
|
33
|
+
type: "choice";
|
|
34
|
+
choice: string;
|
|
35
|
+
probabilities?: Record<string, number>;
|
|
36
|
+
} | {
|
|
37
|
+
type: "score";
|
|
38
|
+
score: number;
|
|
39
|
+
probabilities?: Record<string, number>;
|
|
40
|
+
}
|
|
41
|
+
>;
|
|
42
|
+
usage: {
|
|
43
|
+
inputTokens: number | undefined;
|
|
44
|
+
outputTokens: number | undefined;
|
|
45
|
+
totalTokens: number | undefined;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface RawAnswer {
|
|
50
|
+
type?: unknown;
|
|
51
|
+
probability?: unknown;
|
|
52
|
+
choice?: unknown;
|
|
53
|
+
score?: unknown;
|
|
54
|
+
probabilities?: unknown;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function enforceProbabilities(
|
|
58
|
+
question: EnforcedQuestion,
|
|
59
|
+
vocabulary: readonly string[] | undefined,
|
|
60
|
+
raw: unknown,
|
|
61
|
+
): Record<string, number> | undefined {
|
|
62
|
+
if (raw === undefined) return undefined;
|
|
63
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
64
|
+
throw validationError(
|
|
65
|
+
`question ${JSON.stringify(question.name)}: probabilities must be an object`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const collected: Record<string, number> = {};
|
|
69
|
+
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
|
|
70
|
+
if (vocabulary !== undefined && !vocabulary.includes(key)) {
|
|
71
|
+
throw validationError(
|
|
72
|
+
`question ${JSON.stringify(question.name)}: probabilities key ${JSON.stringify(key)} is outside the question's vocabulary`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
76
|
+
throw validationError(
|
|
77
|
+
`question ${JSON.stringify(question.name)}: probability for ${JSON.stringify(key)} is not a finite number`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
collected[key] = value;
|
|
81
|
+
}
|
|
82
|
+
return collected;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Enforce the provider result against the enforced questions.
|
|
87
|
+
* `rawAnswers` maps question name → provider answer.
|
|
88
|
+
*/
|
|
89
|
+
export function enforceAnswers(
|
|
90
|
+
questions: readonly EnforcedQuestion[],
|
|
91
|
+
rawAnswers: Record<string, unknown>,
|
|
92
|
+
usage: {
|
|
93
|
+
inputTokens: number | undefined;
|
|
94
|
+
outputTokens: number | undefined;
|
|
95
|
+
totalTokens: number | undefined;
|
|
96
|
+
},
|
|
97
|
+
): EnforcedVerdict {
|
|
98
|
+
const answers: EnforcedVerdict["answers"] = {};
|
|
99
|
+
|
|
100
|
+
for (const question of questions) {
|
|
101
|
+
const raw = rawAnswers[question.name];
|
|
102
|
+
if (raw === undefined || raw === null || typeof raw !== "object" || Array.isArray(raw)) {
|
|
103
|
+
// Atomic fail-closed: a missing or malformed answer fails the whole
|
|
104
|
+
// request with validation_error (review fix #8).
|
|
105
|
+
throw validationError(
|
|
106
|
+
`question ${JSON.stringify(question.name)}: no valid answer was returned`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
const answer = raw as RawAnswer;
|
|
110
|
+
|
|
111
|
+
if (question.kind === "boolean") {
|
|
112
|
+
if (answer.type !== undefined && answer.type !== "boolean") {
|
|
113
|
+
throw validationError(
|
|
114
|
+
`question ${JSON.stringify(question.name)}: expected a boolean answer, got type ${JSON.stringify(answer.type)}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const probability = answer.probability;
|
|
118
|
+
if (typeof probability !== "number" || !Number.isFinite(probability)) {
|
|
119
|
+
throw validationError(
|
|
120
|
+
`question ${JSON.stringify(question.name)}: boolean answer missing a numeric probability`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (probability < 0 || probability > 1) {
|
|
124
|
+
throw validationError(
|
|
125
|
+
`question ${JSON.stringify(question.name)}: probability ${probability} outside [0, 1]`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
answers[question.name] = {
|
|
129
|
+
type: "boolean",
|
|
130
|
+
probability,
|
|
131
|
+
// §6: comparison is probability >= threshold. "Model returned false"
|
|
132
|
+
// is a normal 200 verdict with passed=false.
|
|
133
|
+
passed: probability >= question.threshold,
|
|
134
|
+
};
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (question.kind === "choice") {
|
|
139
|
+
if (answer.type !== undefined && answer.type !== "choice") {
|
|
140
|
+
throw validationError(
|
|
141
|
+
`question ${JSON.stringify(question.name)}: expected a choice answer, got type ${JSON.stringify(answer.type)}`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const choice = answer.choice;
|
|
145
|
+
if (typeof choice !== "string") {
|
|
146
|
+
throw validationError(
|
|
147
|
+
`question ${JSON.stringify(question.name)}: choice answer missing the selected choice`,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
if (!question.choices.includes(choice)) {
|
|
151
|
+
// Atomic fail-closed: an answer outside the candidate set is a
|
|
152
|
+
// contradictory result, not a verdict.
|
|
153
|
+
throw validationError(
|
|
154
|
+
`question ${JSON.stringify(question.name)}: answer ${JSON.stringify(choice)} is not a member of the choices array`,
|
|
155
|
+
{ question: question.name, choice },
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const probabilities = enforceProbabilities(question, question.choices, answer.probabilities);
|
|
159
|
+
answers[question.name] =
|
|
160
|
+
probabilities === undefined
|
|
161
|
+
? { type: "choice", choice }
|
|
162
|
+
: { type: "choice", choice, probabilities };
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// score
|
|
167
|
+
if (answer.type !== undefined && answer.type !== "score") {
|
|
168
|
+
throw validationError(
|
|
169
|
+
`question ${JSON.stringify(question.name)}: expected a score answer, got type ${JSON.stringify(answer.type)}`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
const score = answer.score;
|
|
173
|
+
if (typeof score !== "number" || !Number.isFinite(score)) {
|
|
174
|
+
throw validationError(
|
|
175
|
+
`question ${JSON.stringify(question.name)}: score answer missing a numeric score`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
// Provider contract: fractional position in [0, levels - 1].
|
|
179
|
+
if (score < 0 || score > question.levels - 1) {
|
|
180
|
+
throw validationError(
|
|
181
|
+
`question ${JSON.stringify(question.name)}: score ${score} outside [0, ${question.levels - 1}]`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const probabilities = enforceProbabilities(question, undefined, answer.probabilities);
|
|
185
|
+
answers[question.name] =
|
|
186
|
+
probabilities === undefined
|
|
187
|
+
? { type: "score", score: roundScore(score) }
|
|
188
|
+
: { type: "score", score: roundScore(score), probabilities };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { answers, usage };
|
|
192
|
+
}
|