@unson/brainbase-mcp 0.3.1 → 0.4.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 +154 -450
- package/contracts/judgment-dag/digest.json +32 -0
- package/contracts/judgment-dag/fixture.json +21 -0
- package/contracts/judgment-dag/schema.json +80 -0
- package/contracts/judgment-dag/source-lock.json +25 -0
- package/contracts/public-message-candidate.schema.json +143 -0
- package/dist/cli.js +29 -6
- package/dist/judgment-autonomy.d.ts +62 -0
- package/dist/judgment-autonomy.js +301 -0
- package/dist/judgment-dag-core.d.ts +113 -0
- package/dist/judgment-dag-core.js +439 -0
- package/dist/judgment-dag-runner.d.ts +70 -0
- package/dist/judgment-dag-runner.js +325 -0
- package/dist/judgment-dag.d.ts +10 -0
- package/dist/judgment-dag.js +8 -0
- package/dist/judgment-host.d.ts +21 -0
- package/dist/judgment-host.js +180 -0
- package/docs/architecture/judgment-dag-core.md +313 -0
- package/docs/management/judgment-dag-milestones.md +143 -0
- package/package.json +30 -4
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { JudgmentDAGValidationError, validateJudgmentDAG } from './judgment-dag-core.js';
|
|
2
|
+
export class JudgmentDAGExecutionError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
node_id;
|
|
5
|
+
node_type;
|
|
6
|
+
runner_type;
|
|
7
|
+
failure_kind;
|
|
8
|
+
cause;
|
|
9
|
+
constructor(code, message, details = {}) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.name = 'JudgmentDAGExecutionError';
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.node_id = details.node_id;
|
|
14
|
+
this.node_type = details.node_type;
|
|
15
|
+
this.runner_type = details.runner_type;
|
|
16
|
+
this.failure_kind = details.failure_kind;
|
|
17
|
+
this.cause = details.cause;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function isRecord(value) {
|
|
21
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
22
|
+
}
|
|
23
|
+
function isPlainRecord(value) {
|
|
24
|
+
if (!isRecord(value)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const prototype = Object.getPrototypeOf(value);
|
|
28
|
+
return prototype === Object.prototype || prototype === null;
|
|
29
|
+
}
|
|
30
|
+
function invalidJSON(path, reason) {
|
|
31
|
+
throw new JudgmentDAGExecutionError('invalid_json', `${path} must be a JSON value: ${reason}`);
|
|
32
|
+
}
|
|
33
|
+
function snapshotJSON(value, path, active = new Set()) {
|
|
34
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === 'number') {
|
|
38
|
+
if (Number.isFinite(value)) {
|
|
39
|
+
return value;
|
|
40
|
+
}
|
|
41
|
+
invalidJSON(path, 'numbers must be finite');
|
|
42
|
+
}
|
|
43
|
+
if (typeof value !== 'object') {
|
|
44
|
+
invalidJSON(path, 'undefined, functions, symbols, and bigint are not supported');
|
|
45
|
+
}
|
|
46
|
+
const objectValue = value;
|
|
47
|
+
if (active.has(objectValue)) {
|
|
48
|
+
invalidJSON(path, 'cyclic values are not supported');
|
|
49
|
+
}
|
|
50
|
+
active.add(objectValue);
|
|
51
|
+
let snapshot;
|
|
52
|
+
if (Array.isArray(value)) {
|
|
53
|
+
const result = [];
|
|
54
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
55
|
+
if (!(index in value)) {
|
|
56
|
+
invalidJSON(`${path}[${index}]`, 'array holes are not supported');
|
|
57
|
+
}
|
|
58
|
+
result.push(snapshotJSON(value[index], `${path}[${index}]`, active));
|
|
59
|
+
}
|
|
60
|
+
snapshot = result;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
if (!isPlainRecord(value)) {
|
|
64
|
+
invalidJSON(path, 'only plain objects are supported');
|
|
65
|
+
}
|
|
66
|
+
const result = {};
|
|
67
|
+
for (const key of Object.keys(value)) {
|
|
68
|
+
Object.defineProperty(result, key, {
|
|
69
|
+
configurable: true,
|
|
70
|
+
enumerable: true,
|
|
71
|
+
value: snapshotJSON(value[key], `${path}.${key}`, active),
|
|
72
|
+
writable: true
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
snapshot = result;
|
|
76
|
+
}
|
|
77
|
+
active.delete(objectValue);
|
|
78
|
+
return snapshot;
|
|
79
|
+
}
|
|
80
|
+
function deepFreeze(value) {
|
|
81
|
+
if (value !== null && typeof value === 'object' && !Object.isFrozen(value)) {
|
|
82
|
+
Object.freeze(value);
|
|
83
|
+
for (const child of Object.values(value)) {
|
|
84
|
+
deepFreeze(child);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
function invalidRequest(message) {
|
|
90
|
+
return new JudgmentDAGExecutionError('invalid_request', message);
|
|
91
|
+
}
|
|
92
|
+
function snapshotRequest(value) {
|
|
93
|
+
let requestRecord;
|
|
94
|
+
try {
|
|
95
|
+
if (!isPlainRecord(value)) {
|
|
96
|
+
throw invalidRequest('request must be a plain object');
|
|
97
|
+
}
|
|
98
|
+
requestRecord = value;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error instanceof JudgmentDAGExecutionError) {
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
throw invalidRequest('request must be a readable plain object');
|
|
105
|
+
}
|
|
106
|
+
let runId;
|
|
107
|
+
let dag;
|
|
108
|
+
let input;
|
|
109
|
+
let runners;
|
|
110
|
+
try {
|
|
111
|
+
// Read each public request field exactly once. The local values below are
|
|
112
|
+
// the only values used for validation, execution, and recording.
|
|
113
|
+
runId = requestRecord.run_id;
|
|
114
|
+
dag = requestRecord.dag;
|
|
115
|
+
input = requestRecord.input;
|
|
116
|
+
runners = requestRecord.runners;
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
throw invalidRequest('request fields must be readable');
|
|
120
|
+
}
|
|
121
|
+
if (typeof runId !== 'string' || runId.trim().length === 0) {
|
|
122
|
+
throw invalidRequest('request.run_id must be a non-empty string');
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
if (!isPlainRecord(runners)) {
|
|
126
|
+
throw invalidRequest('request.runners must be a plain object');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error instanceof JudgmentDAGExecutionError) {
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
throw invalidRequest('request.runners must be a readable plain object');
|
|
134
|
+
}
|
|
135
|
+
return { run_id: runId, dag, input, runners };
|
|
136
|
+
}
|
|
137
|
+
function snapshotRequestJSON(value, path) {
|
|
138
|
+
try {
|
|
139
|
+
return snapshotJSON(value, path);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof JudgmentDAGExecutionError) {
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
throw invalidRequest(`${path} must be readable`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function snapshotRunnerOutput(value, node) {
|
|
149
|
+
try {
|
|
150
|
+
return snapshotJSON(value, `node ${node.id} output`);
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Runner output is an untrusted boundary. Do not expose an arbitrary
|
|
154
|
+
// getter/proxy exception or preserve it as cause; retain only stable node
|
|
155
|
+
// context in the public machine-readable error.
|
|
156
|
+
throw new JudgmentDAGExecutionError('invalid_json', `node ${node.id} output must be a JSON value`, {
|
|
157
|
+
node_id: node.id,
|
|
158
|
+
node_type: node.node_type,
|
|
159
|
+
runner_type: node.runner_type
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function validateRequestDAG(value) {
|
|
164
|
+
try {
|
|
165
|
+
return validateJudgmentDAG(value);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
if (error instanceof JudgmentDAGValidationError) {
|
|
169
|
+
throw error;
|
|
170
|
+
}
|
|
171
|
+
throw invalidRequest('request.dag must be readable');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
function requiredRunnerEntries(dag, executionOrder, runners) {
|
|
175
|
+
const nodeById = new Map(dag.nodes.map((node) => [node.id, node]));
|
|
176
|
+
const required = new Map();
|
|
177
|
+
for (const nodeId of executionOrder) {
|
|
178
|
+
const node = nodeById.get(nodeId);
|
|
179
|
+
if (node === undefined) {
|
|
180
|
+
throw invalidRequest(`validated execution order references unknown node ${nodeId}`);
|
|
181
|
+
}
|
|
182
|
+
const runnerType = node.runner_type;
|
|
183
|
+
if (required.has(runnerType)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
let hasRegistration;
|
|
187
|
+
let registration;
|
|
188
|
+
try {
|
|
189
|
+
hasRegistration = Object.prototype.hasOwnProperty.call(runners, runnerType);
|
|
190
|
+
registration = hasRegistration ? runners[runnerType] : undefined;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
throw new JudgmentDAGExecutionError('invalid_runner', `Runner registration for ${runnerType} is invalid`, { node_id: node.id, runner_type: runnerType });
|
|
194
|
+
}
|
|
195
|
+
if (!hasRegistration || registration === undefined) {
|
|
196
|
+
throw new JudgmentDAGExecutionError('missing_runner', `DAG node ${node.id} requires a ${runnerType} runner registration`, { node_id: node.id, runner_type: runnerType });
|
|
197
|
+
}
|
|
198
|
+
let registrationRecord;
|
|
199
|
+
let version = undefined;
|
|
200
|
+
let run = undefined;
|
|
201
|
+
try {
|
|
202
|
+
registrationRecord = isPlainRecord(registration) ? registration : undefined;
|
|
203
|
+
if (registrationRecord !== undefined) {
|
|
204
|
+
version = registrationRecord.version;
|
|
205
|
+
run = registrationRecord.run;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
throw new JudgmentDAGExecutionError('invalid_runner', `Runner registration for ${runnerType} is invalid`, { node_id: node.id, runner_type: runnerType });
|
|
210
|
+
}
|
|
211
|
+
if (registrationRecord === undefined ||
|
|
212
|
+
typeof version !== 'string' ||
|
|
213
|
+
version.trim().length === 0 ||
|
|
214
|
+
typeof run !== 'function') {
|
|
215
|
+
throw new JudgmentDAGExecutionError('invalid_runner', `Runner registration for ${runnerType} is invalid`, { node_id: node.id, runner_type: runnerType });
|
|
216
|
+
}
|
|
217
|
+
// Capture the registration's immutable contract before the first runner
|
|
218
|
+
// invocation. A runner is allowed to close over the caller's registration
|
|
219
|
+
// object, so retaining that object here would let it rewrite the version
|
|
220
|
+
// recorded for nodes that execute later in the same run.
|
|
221
|
+
required.set(runnerType, {
|
|
222
|
+
version,
|
|
223
|
+
run: run
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return required;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Execute a validated local DAG sequentially using only explicit runner
|
|
230
|
+
* registrations. No clock, randomness, filesystem, network, or persistence
|
|
231
|
+
* is consulted by this function.
|
|
232
|
+
*/
|
|
233
|
+
export async function executeJudgmentDAG(request) {
|
|
234
|
+
const requestSnapshot = snapshotRequest(request);
|
|
235
|
+
const runIdSnapshot = requestSnapshot.run_id;
|
|
236
|
+
// Keep the validator's machine-readable error and details intact. Validate
|
|
237
|
+
// the captured source before cloning, then validate the exact JSON snapshot
|
|
238
|
+
// that will be executed and recorded so no later mutation can change the
|
|
239
|
+
// execution order or node lookup basis.
|
|
240
|
+
validateRequestDAG(requestSnapshot.dag);
|
|
241
|
+
const dagSnapshot = snapshotRequestJSON(requestSnapshot.dag, 'request.dag');
|
|
242
|
+
const validation = validateRequestDAG(dagSnapshot);
|
|
243
|
+
const inputSnapshot = snapshotRequestJSON(requestSnapshot.input, 'request.input');
|
|
244
|
+
const runners = requiredRunnerEntries(dagSnapshot, validation.execution_order, requestSnapshot.runners);
|
|
245
|
+
const nodeById = new Map(dagSnapshot.nodes.map((node) => [node.id, node]));
|
|
246
|
+
const outputByNode = new Map();
|
|
247
|
+
const nodeRecords = [];
|
|
248
|
+
for (const nodeId of validation.execution_order) {
|
|
249
|
+
const node = nodeById.get(nodeId);
|
|
250
|
+
if (node === undefined) {
|
|
251
|
+
throw invalidRequest(`validated execution order references unknown node ${nodeId}`);
|
|
252
|
+
}
|
|
253
|
+
const registration = runners.get(node.runner_type);
|
|
254
|
+
if (registration === undefined) {
|
|
255
|
+
throw new JudgmentDAGExecutionError('missing_runner', `DAG node ${node.id} requires a ${node.runner_type} runner registration`, { node_id: node.id, runner_type: node.runner_type });
|
|
256
|
+
}
|
|
257
|
+
const dependencyOutputs = node.depends_on
|
|
258
|
+
.slice()
|
|
259
|
+
.sort()
|
|
260
|
+
.map((dependencyId) => {
|
|
261
|
+
const output = outputByNode.get(dependencyId);
|
|
262
|
+
if (output === undefined) {
|
|
263
|
+
throw invalidRequest(`dependency ${dependencyId} has not executed before ${node.id}`);
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
node_id: dependencyId,
|
|
267
|
+
output
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
const runnerInput = deepFreeze({
|
|
271
|
+
run_id: runIdSnapshot,
|
|
272
|
+
dag: dagSnapshot,
|
|
273
|
+
node,
|
|
274
|
+
input: inputSnapshot,
|
|
275
|
+
dependency_outputs: dependencyOutputs
|
|
276
|
+
});
|
|
277
|
+
let rawOutput;
|
|
278
|
+
let runnerResult;
|
|
279
|
+
try {
|
|
280
|
+
runnerResult = registration.run(runnerInput);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
throw new JudgmentDAGExecutionError('runner_failed', `Runner ${node.runner_type} failed for node ${node.id}`, {
|
|
284
|
+
node_id: node.id,
|
|
285
|
+
runner_type: node.runner_type,
|
|
286
|
+
failure_kind: 'sync_throw',
|
|
287
|
+
cause: error
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
rawOutput = await runnerResult;
|
|
292
|
+
}
|
|
293
|
+
catch (error) {
|
|
294
|
+
throw new JudgmentDAGExecutionError('runner_failed', `Runner ${node.runner_type} failed for node ${node.id}`, {
|
|
295
|
+
node_id: node.id,
|
|
296
|
+
runner_type: node.runner_type,
|
|
297
|
+
failure_kind: 'async_reject',
|
|
298
|
+
cause: error
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const output = snapshotRunnerOutput(rawOutput, node);
|
|
302
|
+
outputByNode.set(node.id, output);
|
|
303
|
+
nodeRecords.push({
|
|
304
|
+
node_id: node.id,
|
|
305
|
+
runner_type: node.runner_type,
|
|
306
|
+
runner_version: registration.version,
|
|
307
|
+
input_contract: node.input_contract,
|
|
308
|
+
output_contract: node.output_contract,
|
|
309
|
+
input: inputSnapshot,
|
|
310
|
+
dependency_outputs: dependencyOutputs,
|
|
311
|
+
output
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
const runnerVersions = [...runners.entries()]
|
|
315
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
316
|
+
.map(([runner_type, registration]) => ({ runner_type, version: registration.version }));
|
|
317
|
+
return deepFreeze({
|
|
318
|
+
run_id: runIdSnapshot,
|
|
319
|
+
dag: dagSnapshot,
|
|
320
|
+
input: inputSnapshot,
|
|
321
|
+
execution_order: validation.execution_order.slice(),
|
|
322
|
+
runner_versions: runnerVersions,
|
|
323
|
+
nodes: nodeRecords
|
|
324
|
+
});
|
|
325
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect-free public entrypoint for the Judgment DAG core contract.
|
|
3
|
+
*
|
|
4
|
+
* The package root remains the MCP server entrypoint. Consumers that only
|
|
5
|
+
* need to validate a DAG can import this subpath without starting MCP.
|
|
6
|
+
*/
|
|
7
|
+
export { JUDGMENT_DAG_EDGE_RELATIONS, JUDGMENT_DAG_ALLOWED_KEYS, JUDGMENT_DAG_LAYERS, JUDGMENT_DAG_NODE_TYPE_TO_LAYER, JUDGMENT_DAG_NODE_TYPES, JUDGMENT_DAG_RUNNER_TYPES, JUDGMENT_DAG_SCOPE_TYPES, JudgmentDAGValidationError, assertValidJudgmentDAG, validateJudgmentDAG } from './judgment-dag-core.js';
|
|
8
|
+
export { executeJudgmentDAG, JudgmentDAGExecutionError } from './judgment-dag-runner.js';
|
|
9
|
+
export type { JudgmentDAG, JudgmentDAGEdge, JudgmentDAGEdgeRelation, JudgmentDAGLayer, JudgmentDAGMetadata, JudgmentDAGNode, JudgmentDAGNodeType, JudgmentDAGRunnerType, JudgmentDAGScope, JudgmentDAGScopeType, JudgmentDAGValidationCode, JudgmentDAGValidationDetails, JudgmentDAGValidationResult } from './judgment-dag-core.js';
|
|
10
|
+
export type { JudgmentDAGDependencyOutput, JudgmentDAGExecutionCode, JudgmentDAGExecutionErrorDetails, JudgmentDAGJSONValue, JudgmentDAGNodeRunRecord, JudgmentDAGRunRecord, JudgmentDAGRunRequest, JudgmentDAGRunnerFailureKind, JudgmentDAGRunnerInput, JudgmentDAGRunnerRegistration, JudgmentDAGRunnerVersion } from './judgment-dag-runner.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Side-effect-free public entrypoint for the Judgment DAG core contract.
|
|
3
|
+
*
|
|
4
|
+
* The package root remains the MCP server entrypoint. Consumers that only
|
|
5
|
+
* need to validate a DAG can import this subpath without starting MCP.
|
|
6
|
+
*/
|
|
7
|
+
export { JUDGMENT_DAG_EDGE_RELATIONS, JUDGMENT_DAG_ALLOWED_KEYS, JUDGMENT_DAG_LAYERS, JUDGMENT_DAG_NODE_TYPE_TO_LAYER, JUDGMENT_DAG_NODE_TYPES, JUDGMENT_DAG_RUNNER_TYPES, JUDGMENT_DAG_SCOPE_TYPES, JudgmentDAGValidationError, assertValidJudgmentDAG, validateJudgmentDAG } from './judgment-dag-core.js';
|
|
8
|
+
export { executeJudgmentDAG, JudgmentDAGExecutionError } from './judgment-dag-runner.js';
|
package/dist/judgment-host.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type JudgmentAutonomyEvaluation, type JudgmentAutonomyResolver } from './judgment-autonomy.js';
|
|
1
2
|
type Environment = Record<string, string | undefined>;
|
|
2
3
|
export interface ConversationMessage {
|
|
3
4
|
sequence?: number;
|
|
@@ -104,8 +105,12 @@ export interface JudgmentHookOutput {
|
|
|
104
105
|
export interface JudgmentHostOptions {
|
|
105
106
|
env?: Environment;
|
|
106
107
|
resolver?: (request: JudgmentRequest) => Promise<JudgmentReceipt>;
|
|
108
|
+
autonomyResolver?: JudgmentAutonomyResolver;
|
|
109
|
+
autonomyMode?: JudgmentAutonomyMode;
|
|
110
|
+
autonomyCanaryProjects?: readonly string[];
|
|
107
111
|
trustedConversationMessages?: ConversationMessage[];
|
|
108
112
|
}
|
|
113
|
+
export type JudgmentAutonomyMode = 'off' | 'canary' | 'on';
|
|
109
114
|
interface LocalResolverOptions {
|
|
110
115
|
now?: () => Date;
|
|
111
116
|
id?: () => string;
|
|
@@ -179,8 +184,24 @@ export interface JudgmentFinalReceipt {
|
|
|
179
184
|
owner_audit_line_count?: number;
|
|
180
185
|
final_digest: string;
|
|
181
186
|
}
|
|
187
|
+
export interface JudgmentAutonomyReceipt {
|
|
188
|
+
schema_version: 'brainbase-judgment-autonomy-receipt-v1';
|
|
189
|
+
session_ref: string;
|
|
190
|
+
turn_id: string;
|
|
191
|
+
recorded_at: string;
|
|
192
|
+
mode: Exclude<JudgmentAutonomyMode, 'off'>;
|
|
193
|
+
project_code: string | null;
|
|
194
|
+
case_id: string;
|
|
195
|
+
verdict: JudgmentAutonomyEvaluation['verdict'];
|
|
196
|
+
reason_code: JudgmentAutonomyEvaluation['reason_code'];
|
|
197
|
+
source: JudgmentAutonomyEvaluation['source'];
|
|
198
|
+
question_digest: string | null;
|
|
199
|
+
basis_entity_ids: string[];
|
|
200
|
+
autonomy_digest: string;
|
|
201
|
+
}
|
|
182
202
|
export interface JudgmentStopBlock {
|
|
183
203
|
decision: 'block';
|
|
204
|
+
block_kind?: 'autonomy_continue' | 'audit_repair';
|
|
184
205
|
reason: string;
|
|
185
206
|
}
|
|
186
207
|
export declare function canonicalJson(value: unknown): string;
|
package/dist/judgment-host.js
CHANGED
|
@@ -2,6 +2,7 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { closeSync, constants as fsConstants, fsyncSync, linkSync, mkdirSync, openSync, readdirSync, readFileSync, realpathSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
4
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
5
|
+
import { evaluateJudgmentAutonomy, renderJudgmentAutonomyContinuation } from './judgment-autonomy.js';
|
|
5
6
|
const RUNTIME_VERSION = 'portable-judgment-runtime-1.0.0';
|
|
6
7
|
const ZERO_CALL_DISPLAY_LINE = '📚 Brainbase未参照: 必須参照なし・実呼び出し0回 ✓';
|
|
7
8
|
const PORTABLE_KNOWLEDGE_TOOLS = {
|
|
@@ -306,6 +307,7 @@ function journalPaths(sessionRef, turnId, env) {
|
|
|
306
307
|
events: join(directory, `${turnRef}.events`),
|
|
307
308
|
final: join(directory, `${turnRef}.final.json`),
|
|
308
309
|
continuation: join(directory, `${turnRef}.continuation.json`),
|
|
310
|
+
autonomy: join(directory, `${turnRef}.autonomy`),
|
|
309
311
|
lock: join(directory, `${turnRef}.lock`)
|
|
310
312
|
};
|
|
311
313
|
}
|
|
@@ -1334,6 +1336,180 @@ async function recordToolUse(payload, env) {
|
|
|
1334
1336
|
return event;
|
|
1335
1337
|
});
|
|
1336
1338
|
}
|
|
1339
|
+
function resolveAutonomyMode(options, env) {
|
|
1340
|
+
const rawMode = options.autonomyMode ?? env.BRAINBASE_JUDGMENT_AUTONOMY_MODE ?? 'off';
|
|
1341
|
+
if (!['off', 'canary', 'on'].includes(rawMode)) {
|
|
1342
|
+
throw new Error('judgment_autonomy_mode_invalid');
|
|
1343
|
+
}
|
|
1344
|
+
const configuredProjects = options.autonomyCanaryProjects
|
|
1345
|
+
?? (env.BRAINBASE_JUDGMENT_AUTONOMY_CANARY_PROJECTS ?? '').split(',');
|
|
1346
|
+
const canaryProjects = [...new Set(configuredProjects.map((value) => value.trim()).filter(Boolean))];
|
|
1347
|
+
if (rawMode === 'canary' && canaryProjects.length === 0) {
|
|
1348
|
+
throw new Error('judgment_autonomy_canary_project_missing');
|
|
1349
|
+
}
|
|
1350
|
+
return { mode: rawMode, canaryProjects };
|
|
1351
|
+
}
|
|
1352
|
+
function readAutonomyReceipt(path) {
|
|
1353
|
+
const value = readJsonArtifact(path);
|
|
1354
|
+
if (value.schema_version !== 'brainbase-judgment-autonomy-receipt-v1'
|
|
1355
|
+
|| typeof value.session_ref !== 'string'
|
|
1356
|
+
|| typeof value.turn_id !== 'string'
|
|
1357
|
+
|| typeof value.recorded_at !== 'string'
|
|
1358
|
+
|| !['canary', 'on'].includes(String(value.mode))
|
|
1359
|
+
|| (value.project_code !== null && typeof value.project_code !== 'string')
|
|
1360
|
+
|| typeof value.case_id !== 'string'
|
|
1361
|
+
|| !['continue', 'human_required', 'not_applicable'].includes(String(value.verdict))
|
|
1362
|
+
|| typeof value.reason_code !== 'string'
|
|
1363
|
+
|| !['deterministic', 'same_codex', 'independent_resolver'].includes(String(value.source))
|
|
1364
|
+
|| (value.question_digest !== null && typeof value.question_digest !== 'string')
|
|
1365
|
+
|| !Array.isArray(value.basis_entity_ids)
|
|
1366
|
+
|| !value.basis_entity_ids.every((entry) => typeof entry === 'string')
|
|
1367
|
+
|| typeof value.autonomy_digest !== 'string') {
|
|
1368
|
+
throw new Error(`judgment_journal_invalid:${basename(path)}`);
|
|
1369
|
+
}
|
|
1370
|
+
if (value.autonomy_digest !== artifactDigest(value, 'autonomy_digest')) {
|
|
1371
|
+
throw new Error(`judgment_autonomy_receipt_digest_mismatch:${basename(path)}`);
|
|
1372
|
+
}
|
|
1373
|
+
return value;
|
|
1374
|
+
}
|
|
1375
|
+
function autonomyReceiptProjection(receipt) {
|
|
1376
|
+
const { recorded_at: _recordedAt, autonomy_digest: _digest, ...projection } = receipt;
|
|
1377
|
+
return projection;
|
|
1378
|
+
}
|
|
1379
|
+
function persistAutonomyReceipt(paths, sessionRef, snapshot, evaluation, mode) {
|
|
1380
|
+
const path = join(paths.autonomy, `${evaluation.case_id}.json`);
|
|
1381
|
+
const basisEntityIds = evaluation.resolver_decision?.basis.map((entry) => entry.entity_id) ?? [];
|
|
1382
|
+
const receiptWithoutDigest = {
|
|
1383
|
+
schema_version: 'brainbase-judgment-autonomy-receipt-v1',
|
|
1384
|
+
session_ref: sessionRef,
|
|
1385
|
+
turn_id: snapshot.context.turn_id,
|
|
1386
|
+
recorded_at: new Date().toISOString(),
|
|
1387
|
+
mode,
|
|
1388
|
+
project_code: snapshot.context.project_code ?? null,
|
|
1389
|
+
case_id: evaluation.case_id,
|
|
1390
|
+
verdict: evaluation.verdict,
|
|
1391
|
+
reason_code: evaluation.reason_code,
|
|
1392
|
+
source: evaluation.source,
|
|
1393
|
+
question_digest: evaluation.question ? sha256(evaluation.question) : null,
|
|
1394
|
+
basis_entity_ids: basisEntityIds
|
|
1395
|
+
};
|
|
1396
|
+
const receipt = {
|
|
1397
|
+
...receiptWithoutDigest,
|
|
1398
|
+
autonomy_digest: artifactDigest(receiptWithoutDigest, 'autonomy_digest')
|
|
1399
|
+
};
|
|
1400
|
+
try {
|
|
1401
|
+
const existing = readAutonomyReceipt(path);
|
|
1402
|
+
if (canonicalJson(autonomyReceiptProjection(existing)) !== canonicalJson(autonomyReceiptProjection(receipt))) {
|
|
1403
|
+
throw new Error('judgment_autonomy_receipt_conflict');
|
|
1404
|
+
}
|
|
1405
|
+
return existing;
|
|
1406
|
+
}
|
|
1407
|
+
catch (error) {
|
|
1408
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
1409
|
+
throw error;
|
|
1410
|
+
}
|
|
1411
|
+
writeImmutableJson(path, receipt);
|
|
1412
|
+
return receipt;
|
|
1413
|
+
}
|
|
1414
|
+
async function autonomyStopSnapshot(payload, env) {
|
|
1415
|
+
const answer = typeof payload.last_assistant_message === 'string' ? payload.last_assistant_message : '';
|
|
1416
|
+
if (!answer.trim())
|
|
1417
|
+
return null;
|
|
1418
|
+
const { sessionRef, turnId } = hookIdentity(payload);
|
|
1419
|
+
const paths = journalPaths(sessionRef, turnId, env);
|
|
1420
|
+
return withTurnLock(paths.lock, async () => {
|
|
1421
|
+
let episode;
|
|
1422
|
+
try {
|
|
1423
|
+
episode = readEpisode(paths.episode);
|
|
1424
|
+
verifyEpisodeAdoption(episode, paths.target);
|
|
1425
|
+
}
|
|
1426
|
+
catch (error) {
|
|
1427
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') {
|
|
1428
|
+
throw new Error('judgment_orphan_stop');
|
|
1429
|
+
}
|
|
1430
|
+
throw error;
|
|
1431
|
+
}
|
|
1432
|
+
if (episode.session_ref !== sessionRef || episode.turn_id !== turnId) {
|
|
1433
|
+
throw new Error('judgment_episode_binding_mismatch');
|
|
1434
|
+
}
|
|
1435
|
+
try {
|
|
1436
|
+
readFinal(paths.final);
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
catch (error) {
|
|
1440
|
+
if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT'))
|
|
1441
|
+
throw error;
|
|
1442
|
+
}
|
|
1443
|
+
const events = readEvents(paths.events);
|
|
1444
|
+
return {
|
|
1445
|
+
episode_digest: episode.episode_digest,
|
|
1446
|
+
event_set_digest: sha256(canonicalJson(events)),
|
|
1447
|
+
context: {
|
|
1448
|
+
turn_id: turnId,
|
|
1449
|
+
request: episode.adoption.request.request,
|
|
1450
|
+
final_answer: answer,
|
|
1451
|
+
...(episode.adoption.receipt.project_code
|
|
1452
|
+
? { project_code: episode.adoption.receipt.project_code }
|
|
1453
|
+
: {}),
|
|
1454
|
+
selected_dag_ids: [...episode.adoption.receipt.selected_dag_ids]
|
|
1455
|
+
}
|
|
1456
|
+
};
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
async function evaluateAutonomyStop(payload, env, options) {
|
|
1460
|
+
const config = resolveAutonomyMode(options, env);
|
|
1461
|
+
if (config.mode === 'off')
|
|
1462
|
+
return null;
|
|
1463
|
+
const snapshot = await autonomyStopSnapshot(payload, env);
|
|
1464
|
+
if (!snapshot)
|
|
1465
|
+
return null;
|
|
1466
|
+
if (config.mode === 'canary'
|
|
1467
|
+
&& (!snapshot.context.project_code || !config.canaryProjects.includes(snapshot.context.project_code))) {
|
|
1468
|
+
return null;
|
|
1469
|
+
}
|
|
1470
|
+
// A resumed Stop must not invoke an independent LLM twice. Reclassify with
|
|
1471
|
+
// the pure fallback only; repeated routine/semantic escalation fails loud.
|
|
1472
|
+
const evaluation = await evaluateJudgmentAutonomy(snapshot.context, payload.stop_hook_active === true ? undefined : options.autonomyResolver);
|
|
1473
|
+
const { sessionRef, turnId } = hookIdentity(payload);
|
|
1474
|
+
const paths = journalPaths(sessionRef, turnId, env);
|
|
1475
|
+
const stillActive = await withTurnLock(paths.lock, async () => {
|
|
1476
|
+
const episode = readEpisode(paths.episode);
|
|
1477
|
+
verifyEpisodeAdoption(episode, paths.target);
|
|
1478
|
+
if (episode.episode_digest !== snapshot.episode_digest) {
|
|
1479
|
+
throw new Error('judgment_autonomy_episode_changed');
|
|
1480
|
+
}
|
|
1481
|
+
const events = readEvents(paths.events);
|
|
1482
|
+
if (sha256(canonicalJson(events)) !== snapshot.event_set_digest) {
|
|
1483
|
+
throw new Error('judgment_autonomy_event_set_changed');
|
|
1484
|
+
}
|
|
1485
|
+
try {
|
|
1486
|
+
readFinal(paths.final);
|
|
1487
|
+
return false;
|
|
1488
|
+
}
|
|
1489
|
+
catch (error) {
|
|
1490
|
+
if (error instanceof Error && 'code' in error && error.code === 'ENOENT')
|
|
1491
|
+
return true;
|
|
1492
|
+
throw error;
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
if (!stillActive)
|
|
1496
|
+
return null;
|
|
1497
|
+
if (evaluation.verdict === 'continue' && payload.stop_hook_active === true) {
|
|
1498
|
+
throw new Error('judgment_autonomy_continuation_exhausted');
|
|
1499
|
+
}
|
|
1500
|
+
if (evaluation.verdict !== 'not_applicable') {
|
|
1501
|
+
await withTurnLock(paths.lock, async () => {
|
|
1502
|
+
persistAutonomyReceipt(paths, sessionRef, snapshot, evaluation, config.mode);
|
|
1503
|
+
});
|
|
1504
|
+
}
|
|
1505
|
+
if (evaluation.verdict !== 'continue')
|
|
1506
|
+
return null;
|
|
1507
|
+
return {
|
|
1508
|
+
decision: 'block',
|
|
1509
|
+
block_kind: 'autonomy_continue',
|
|
1510
|
+
reason: renderJudgmentAutonomyContinuation(evaluation)
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1337
1513
|
async function finalizeEpisode(payload, env) {
|
|
1338
1514
|
const { sessionRef, turnId } = hookIdentity(payload);
|
|
1339
1515
|
const paths = journalPaths(sessionRef, turnId, env);
|
|
@@ -1423,6 +1599,7 @@ async function finalizeEpisode(payload, env) {
|
|
|
1423
1599
|
];
|
|
1424
1600
|
return {
|
|
1425
1601
|
decision: 'block',
|
|
1602
|
+
block_kind: 'audit_repair',
|
|
1426
1603
|
reason: `Brainbase judgment episodeを完了する前に${reasons.join('。その後')}。監査行の後に、元の回答本文をそのまま続けてください。`
|
|
1427
1604
|
};
|
|
1428
1605
|
}
|
|
@@ -1457,6 +1634,9 @@ export async function processJudgmentHook(payload, options = {}) {
|
|
|
1457
1634
|
return event ? { systemMessage: event.display_line } : {};
|
|
1458
1635
|
}
|
|
1459
1636
|
if (eventName === 'Stop') {
|
|
1637
|
+
const autonomyBlock = await evaluateAutonomyStop(payload, env, options);
|
|
1638
|
+
if (autonomyBlock)
|
|
1639
|
+
return autonomyBlock;
|
|
1460
1640
|
const output = await finalizeEpisode(payload, env);
|
|
1461
1641
|
if ('decision' in output && output.decision === 'block' && payload.stop_hook_active === true) {
|
|
1462
1642
|
throw new Error('judgment_stop_repair_exhausted');
|