@orkestrel/study 0.0.2 → 0.0.3
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/dist/src/core/index.cjs +61 -16
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +67 -8
- package/dist/src/core/index.d.ts +67 -8
- package/dist/src/core/index.js +61 -18
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +2879 -928
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +874 -15
- package/dist/src/server/index.d.ts +874 -15
- package/dist/src/server/index.js +2838 -934
- package/dist/src/server/index.js.map +1 -1
- package/package.json +5 -5
|
@@ -4,13 +4,15 @@ let _orkestrel_contract = require("@orkestrel/contract");
|
|
|
4
4
|
let node_crypto = require("node:crypto");
|
|
5
5
|
let node_path = require("node:path");
|
|
6
6
|
let node_fs = require("node:fs");
|
|
7
|
+
let node_fs_promises = require("node:fs/promises");
|
|
7
8
|
let node_module = require("node:module");
|
|
8
9
|
let node_url = require("node:url");
|
|
9
|
-
let node_fs_promises = require("node:fs/promises");
|
|
10
10
|
let node_child_process = require("node:child_process");
|
|
11
11
|
let node_timers_promises = require("node:timers/promises");
|
|
12
12
|
let _orkestrel_tool = require("@orkestrel/tool");
|
|
13
13
|
let _orkestrel_budget = require("@orkestrel/budget");
|
|
14
|
+
let _orkestrel_abort = require("@orkestrel/abort");
|
|
15
|
+
let _orkestrel_timeout = require("@orkestrel/timeout");
|
|
14
16
|
//#region src/server/helpers.ts
|
|
15
17
|
/** Reports whether a caught value is a Node error carrying the given system code.
|
|
16
18
|
* @remarks Requires an `Error` from this realm. Structured cloning, such as a worker's `postMessage`,
|
|
@@ -93,6 +95,40 @@ var STUDY_RESERVED_NAME = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])$/i;
|
|
|
93
95
|
/** Discloses the limits of neutral identifiers when public text remains visible. */
|
|
94
96
|
var STUDY_PACKET_DISCLOSURE = "Identifiers omit treatment and model names. Input and product text may still reveal design details.";
|
|
95
97
|
//#endregion
|
|
98
|
+
//#region src/server/ollama/validators.ts
|
|
99
|
+
/** Checks whether a value declares independent positive metadata allowances.
|
|
100
|
+
* @param value - The untrusted metadata declaration.
|
|
101
|
+
* @returns True if the declaration has positive safe calls and bytes and finite duration; false otherwise.
|
|
102
|
+
*/
|
|
103
|
+
function isStudyOllamaMetadata(value) {
|
|
104
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.calls === "number" && Number.isSafeInteger(value.calls) && value.calls > 0 && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes > 0 && typeof value.duration === "number" && Number.isFinite(value.duration) && value.duration > 0);
|
|
105
|
+
}
|
|
106
|
+
/** Checks whether a value pins a served model name and SHA-256 digest.
|
|
107
|
+
* @param value - The untrusted model pin.
|
|
108
|
+
* @returns True if the value declares a nonempty name and lowercase SHA-256 digest; false otherwise.
|
|
109
|
+
*/
|
|
110
|
+
function isStudyOllamaModel(value) {
|
|
111
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.name === "string" && value.name.length > 0 && typeof value.digest === "string" && /^[a-f0-9]{64}$/.test(value.digest));
|
|
112
|
+
}
|
|
113
|
+
/** Checks whether a value declares a complete finite model-generation policy.
|
|
114
|
+
* @param value - The untrusted role policy.
|
|
115
|
+
* @returns True if every policy member and output allowance is valid; false otherwise.
|
|
116
|
+
*/
|
|
117
|
+
function isStudyOllamaPolicy(value) {
|
|
118
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.model === "string" && value.model.length > 0 && typeof value.think === "boolean" && typeof value.strict === "boolean" && typeof value.length === "boolean" && (0, _orkestrel_contract.isRecord)(value.options) && Object.values(value.options).every((entry) => typeof entry === "number" && Number.isFinite(entry)) && typeof value.options.num_predict === "number" && Number.isSafeInteger(value.options.num_predict) && value.options.num_predict > 0);
|
|
119
|
+
}
|
|
120
|
+
/** Checks whether a value declares exclusive exact or capture admission with a valid receipt.
|
|
121
|
+
* @param value - The untrusted ordered step.
|
|
122
|
+
* @returns True if identity, policy, admission shape and exact receipt are valid; false otherwise.
|
|
123
|
+
*/
|
|
124
|
+
function isStudyOllamaStep(value) {
|
|
125
|
+
return (0, _orkestrel_contract.holds)(() => {
|
|
126
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.ordinal !== "number" || !Number.isSafeInteger(value.ordinal) || value.ordinal <= 0 || typeof value.goal !== "string" || !_src_core.STUDY_NAME.test(value.goal) || value.role !== "agent" && value.role !== "summary" && value.role !== "judge" || value.path !== (value.role === "judge" ? "/api/generate" : "/api/chat") || !isStudyOllamaPolicy(value.policy) || value.role === "judge" && value.policy.strict) return false;
|
|
127
|
+
if (value.admission === "capture") return typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes > 0 && !Object.hasOwn(value, "body") && !Object.hasOwn(value, "input");
|
|
128
|
+
return value.admission === "exact" && typeof value.body === "string" && value.body.length > 0 && typeof value.input === "string" && value.input === hashStudyBytes(value.body) && !Object.hasOwn(value, "bytes");
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
96
132
|
//#region src/server/stores/validators.ts
|
|
97
133
|
/**
|
|
98
134
|
* Checks whether a trial name is portable on all supported hosts.
|
|
@@ -126,7 +162,7 @@ function isStudyCustodyManifest(value) {
|
|
|
126
162
|
"schedule",
|
|
127
163
|
"bounds",
|
|
128
164
|
"host"
|
|
129
|
-
], ["qualification"]) || !isStudyCustodyName(value.id) || !(0, _src_core.isStudyTimestamp)(value.created) || !(0, _src_core.isStudyDefinition)(value.definition) || !(0, _orkestrel_contract.isArray)(value.scenarios) || !value.scenarios.every(_src_core.isStudyScenario) || !(0, _src_core.isStudyBounds)(value.bounds) || !(0, _orkestrel_contract.isRecord)(value.identity) || !(0, _src_core.matchesStudyKeys)(value.identity, [
|
|
165
|
+
], ["qualification", "record"]) || !isStudyCustodyName(value.id) || !(0, _src_core.isStudyTimestamp)(value.created) || !(0, _src_core.isStudyDefinition)(value.definition) || !(0, _orkestrel_contract.isArray)(value.scenarios) || !value.scenarios.every(_src_core.isStudyScenario) || !(0, _src_core.isStudyBounds)(value.bounds) || value.record !== void 0 && !(0, _src_core.isStudyRecord)(value.record) || !(0, _orkestrel_contract.isRecord)(value.identity) || !(0, _src_core.matchesStudyKeys)(value.identity, [
|
|
130
166
|
"agent",
|
|
131
167
|
"subject",
|
|
132
168
|
"instrument",
|
|
@@ -269,7 +305,7 @@ function isStudyCustodyFile(value) {
|
|
|
269
305
|
"path",
|
|
270
306
|
"bytes",
|
|
271
307
|
"digest"
|
|
272
|
-
]) && typeof value.path === "string" && (value.path === "manifest.json" || /^trials\/[a-z0-9]+(?:-[a-z0-9]+)*\/(?:evidence.jsonl|attempt.json)$/.test(value.path)) && value.path.split("/").every((name) => !STUDY_RESERVED_NAME.test(name)) && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0 && (0, _src_core.isStudyDigest)(value.digest);
|
|
308
|
+
]) && typeof value.path === "string" && (value.path === "manifest.json" || /^trials\/[a-z0-9]+(?:-[a-z0-9]+)*\/(?:evidence.jsonl|attempt.json|exchanges.jsonl)$/.test(value.path)) && value.path.split("/").every((name) => !STUDY_RESERVED_NAME.test(name)) && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0 && (0, _src_core.isStudyDigest)(value.digest);
|
|
273
309
|
});
|
|
274
310
|
}
|
|
275
311
|
/**
|
|
@@ -290,6 +326,106 @@ function isStudyCustodySeal(value) {
|
|
|
290
326
|
]) && isStudyCustodyName(value.result) && (0, _src_core.isStudyDigest)(value.id) && (0, _orkestrel_contract.isArray)(value.files) && value.files.length > 0 && value.files.every(isStudyCustodyFile) && new Set(value.files.map((entry) => entry.path)).size === value.files.length;
|
|
291
327
|
});
|
|
292
328
|
}
|
|
329
|
+
/** Checks whether encoded body bytes have their exact canonical base64 and SHA-256 identity.
|
|
330
|
+
* @param value - The candidate byte envelope.
|
|
331
|
+
* @returns True if decoded length and digest match; false otherwise.
|
|
332
|
+
*/
|
|
333
|
+
function isStudyExchangeBytes(value) {
|
|
334
|
+
return (0, _orkestrel_contract.holds)(() => {
|
|
335
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.body !== "string" || typeof value.size !== "number" || !Number.isSafeInteger(value.size) || value.size < 0 || !(0, _src_core.isStudyDigest)(value.digest)) return false;
|
|
336
|
+
const bytes = Buffer.from(value.body, "base64");
|
|
337
|
+
return bytes.length === value.size && bytes.toString("base64") === value.body && hashStudyBytes(bytes) === value.digest;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
/** Checks whether an exchange carries an admissible ordered HTTP identity.
|
|
341
|
+
* @param value - The candidate identity.
|
|
342
|
+
* @returns True if identity and stage fields are valid; false otherwise.
|
|
343
|
+
*/
|
|
344
|
+
function isStudyExchangeIdentity(value) {
|
|
345
|
+
return (0, _orkestrel_contract.holds)(() => {
|
|
346
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.ordinal !== "number" || !Number.isSafeInteger(value.ordinal) || value.ordinal <= 0 || !(0, _src_core.isStudyName)(value.goal) || !(0, _src_core.isStudyRole)(value.role) || value.method !== "GET" && value.method !== "POST") return false;
|
|
347
|
+
if (value.stage === "execution") return (value.role === "agent" || value.role === "summary" || value.role === "judge") && value.method === "POST" && value.path === (value.role === "judge" ? "/api/generate" : "/api/chat") && typeof value.step === "number" && Number.isSafeInteger(value.step) && value.step > 0;
|
|
348
|
+
if (Object.hasOwn(value, "step")) return false;
|
|
349
|
+
if (value.stage === "maintenance") return value.role === "maintenance" && value.method === "POST" && value.path === "/api/generate";
|
|
350
|
+
return (value.stage === "preflight" || value.stage === "postflight") && value.role === "metadata" && (value.path === "/api/version" || value.path === "/api/tags" ? value.method === "GET" : value.path === "/api/show" && value.method === "POST");
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
/** Checks whether a retained reply describes bounded successful application bytes and chunks.
|
|
354
|
+
* @param value - The candidate reply.
|
|
355
|
+
* @returns True if status, selected headers and chunk lengths are valid; false otherwise.
|
|
356
|
+
*/
|
|
357
|
+
function isStudyExchangeReply(value) {
|
|
358
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && isStudyExchangeBytes(value) && "status" in value && typeof value.status === "number" && Number.isSafeInteger(value.status) && value.status >= 200 && value.status <= 599 && "headers" in value && (0, _orkestrel_contract.isArray)(value.headers) && value.headers.every((header) => (0, _orkestrel_contract.isArray)(header) && header.length === 2 && (header[0] === "content-type" || header[0] === "content-encoding") && typeof header[1] === "string" && !/[\r\n]/.test(header[1])) && "chunks" in value && (0, _orkestrel_contract.isArray)(value.chunks) && value.chunks.every((size) => typeof size === "number" && Number.isSafeInteger(size) && size > 0) && value.chunks.reduce((total, size) => total + (typeof size === "number" ? size : 0), 0) === value.size);
|
|
359
|
+
}
|
|
360
|
+
/** Checks whether the opening row declares a finite ordered procedure and serving identities.
|
|
361
|
+
* @param value - The candidate declaration.
|
|
362
|
+
* @returns True if all declared populations are valid; false otherwise.
|
|
363
|
+
*/
|
|
364
|
+
function isStudyExchangeDeclaration(value) {
|
|
365
|
+
return (0, _orkestrel_contract.holds)(() => {
|
|
366
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _src_core.matchesStudyKeys)(value, [
|
|
367
|
+
"phase",
|
|
368
|
+
"trial",
|
|
369
|
+
"version",
|
|
370
|
+
"models",
|
|
371
|
+
"metadata",
|
|
372
|
+
"procedure",
|
|
373
|
+
"maintenance"
|
|
374
|
+
]) || value.phase !== "declaration" || !isStudyCustodyName(value.trial) || !(0, _orkestrel_contract.isNonEmptyString)(value.version) || !(0, _orkestrel_contract.isRecord)(value.models) || Object.keys(value.models).length === 0 || !Object.values(value.models).every(isStudyOllamaModel) || !isStudyOllamaMetadata(value.metadata) || !(0, _orkestrel_contract.isArray)(value.procedure) || value.procedure.length === 0 || !value.procedure.every(isStudyOllamaStep) || !(0, _orkestrel_contract.isArray)(value.maintenance) || !value.maintenance.every(isStudyOllamaModel)) return false;
|
|
375
|
+
const models = Object.values(value.models).filter(isStudyOllamaModel);
|
|
376
|
+
return value.procedure.every((step, index) => step.ordinal === index + 1 && models.some((model) => model.name === step.policy.model)) && value.maintenance.every((model, index, entries) => entries.findIndex((entry) => entry.name === model.name) === index && models.some((entry) => entry.name === model.name && entry.digest === model.digest)) && models.every((model) => models.every((other) => model.name !== other.name || model.digest === other.digest));
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/** Checks whether a sidecar row has exactly its declared fields and verified byte identities.
|
|
380
|
+
* @param value - The candidate row.
|
|
381
|
+
* @returns True if the row belongs to the closed sidecar grammar; false otherwise.
|
|
382
|
+
*/
|
|
383
|
+
function isStudyExchangeRow(value) {
|
|
384
|
+
return (0, _orkestrel_contract.holds)(() => {
|
|
385
|
+
if (!(0, _orkestrel_contract.isRecord)(value)) return false;
|
|
386
|
+
if (value.phase === "declaration") return isStudyExchangeDeclaration(value);
|
|
387
|
+
if (value.phase === "complete") return (0, _src_core.matchesStudyKeys)(value, ["phase", "count"]) && typeof value.count === "number" && Number.isSafeInteger(value.count) && value.count > 0;
|
|
388
|
+
const identity = [
|
|
389
|
+
"phase",
|
|
390
|
+
"ordinal",
|
|
391
|
+
"goal",
|
|
392
|
+
"role",
|
|
393
|
+
"path",
|
|
394
|
+
"method",
|
|
395
|
+
"stage"
|
|
396
|
+
];
|
|
397
|
+
if (!isStudyExchangeIdentity(value)) return false;
|
|
398
|
+
if (value.phase === "attempt") return (0, _src_core.matchesStudyKeys)(value, [
|
|
399
|
+
...identity,
|
|
400
|
+
"body",
|
|
401
|
+
"size",
|
|
402
|
+
"digest"
|
|
403
|
+
], ["step"]) && isStudyExchangeBytes(value);
|
|
404
|
+
if (value.phase === "response") return (0, _src_core.matchesStudyKeys)(value, [
|
|
405
|
+
...identity,
|
|
406
|
+
"body",
|
|
407
|
+
"size",
|
|
408
|
+
"digest",
|
|
409
|
+
"status",
|
|
410
|
+
"headers",
|
|
411
|
+
"chunks"
|
|
412
|
+
], ["step"]) && isStudyExchangeReply(value) && value.status < 300;
|
|
413
|
+
return value.phase === "failure" && (0, _src_core.matchesStudyKeys)(value, [...identity, "reason"], ["step", "response"]) && typeof value.reason === "string" && (value.response === void 0 || isStudyExchangeReply(value.response));
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
/** Checks whether a history has verified row shapes before its complete relational validation.
|
|
417
|
+
* @param value - The candidate history.
|
|
418
|
+
* @returns True if its envelopes and rows have valid types; false otherwise.
|
|
419
|
+
*/
|
|
420
|
+
function isStudyExchangeHistory(value) {
|
|
421
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.matchesStudyKeys)(value, [
|
|
422
|
+
"seal",
|
|
423
|
+
"file",
|
|
424
|
+
"declaration",
|
|
425
|
+
"exchanges",
|
|
426
|
+
"completion"
|
|
427
|
+
]) && (0, _src_core.isStudyDigest)(value.seal) && isStudyCustodyFile(value.file) && isStudyExchangeDeclaration(value.declaration) && (0, _orkestrel_contract.isArray)(value.exchanges) && value.exchanges.every((pair) => (0, _orkestrel_contract.isRecord)(pair) && (0, _src_core.matchesStudyKeys)(pair, ["attempt", "response"]) && isStudyExchangeRow(pair.attempt) && pair.attempt.phase === "attempt" && isStudyExchangeRow(pair.response) && pair.response.phase === "response") && isStudyExchangeRow(value.completion) && value.completion.phase === "complete");
|
|
428
|
+
}
|
|
293
429
|
//#endregion
|
|
294
430
|
//#region src/server/adapters/validators.ts
|
|
295
431
|
/** Checks whether a factory returned the complete consumer adapter contract.
|
|
@@ -419,28 +555,249 @@ var ARTIFACT_VISITS = 32768;
|
|
|
419
555
|
var ARTIFACT_DEPTH = 64;
|
|
420
556
|
/** Bounds selected package roots before traversal begins. */
|
|
421
557
|
var ARTIFACT_ROOTS = 128;
|
|
558
|
+
/** Marks artifact errors across module formats and installations. */
|
|
559
|
+
var ARTIFACT_ERROR_BRAND = Symbol.for("@orkestrel/study.ArtifactError");
|
|
560
|
+
/** Lists the artifact refusal codes. */
|
|
561
|
+
var ARTIFACT_ERROR_CODES = Object.freeze([
|
|
562
|
+
"ARTIFACT",
|
|
563
|
+
"TRAVERSAL",
|
|
564
|
+
"REDIRECT",
|
|
565
|
+
"ALIAS",
|
|
566
|
+
"NAME",
|
|
567
|
+
"DUPLICATE",
|
|
568
|
+
"OCCUPIED",
|
|
569
|
+
"OVERLAP",
|
|
570
|
+
"STALE",
|
|
571
|
+
"VOLUME",
|
|
572
|
+
"DRIFT",
|
|
573
|
+
"LIMIT"
|
|
574
|
+
]);
|
|
422
575
|
//#endregion
|
|
423
576
|
//#region src/server/artifacts/errors.ts
|
|
424
577
|
/** Reports an invalid or drifted artifact inventory. */
|
|
425
578
|
var ArtifactError = class extends Error {
|
|
426
|
-
code
|
|
579
|
+
code;
|
|
427
580
|
/** Records the refused artifact boundary.
|
|
428
581
|
* @param message - The specific refusal.
|
|
582
|
+
* @param code - The refused boundary. Default: ARTIFACT.
|
|
429
583
|
*/
|
|
430
|
-
constructor(message) {
|
|
584
|
+
constructor(message, code = "ARTIFACT") {
|
|
431
585
|
super(message);
|
|
586
|
+
_orkestrel_contract.INTRINSICS.define(this, ARTIFACT_ERROR_BRAND, { value: this });
|
|
432
587
|
this.name = "ArtifactError";
|
|
588
|
+
this.code = code;
|
|
433
589
|
}
|
|
434
590
|
};
|
|
435
591
|
/** Narrows a caught value to an artifact refusal.
|
|
592
|
+
* @remarks
|
|
593
|
+
* Recognizes subclasses and other module copies by an own identity brand and valid members.
|
|
594
|
+
* The brand is a recognition marker. A transparent proxy, a look-alike without the brand,
|
|
595
|
+
* throwing accessors, and revoked proxies return false.
|
|
436
596
|
* @param value - The caught value.
|
|
437
597
|
* @returns True if the value is an artifact error; false otherwise.
|
|
438
598
|
*/
|
|
439
599
|
function isArtifactError(value) {
|
|
440
|
-
return (0, _orkestrel_contract.
|
|
600
|
+
return (0, _orkestrel_contract.holds)(() => {
|
|
601
|
+
if (!(value instanceof _orkestrel_contract.INTRINSICS.error) || _orkestrel_contract.INTRINSICS.prototype(value) === _orkestrel_contract.INTRINSICS.error.prototype) return false;
|
|
602
|
+
if (_orkestrel_contract.INTRINSICS.describe(value, ARTIFACT_ERROR_BRAND)?.value !== value) return false;
|
|
603
|
+
const code = _orkestrel_contract.INTRINSICS.reflect.read(value, "code");
|
|
604
|
+
const stack = _orkestrel_contract.INTRINSICS.reflect.read(value, "stack");
|
|
605
|
+
return ARTIFACT_ERROR_CODES.some((candidate) => candidate === code) && (0, _orkestrel_contract.isString)(_orkestrel_contract.INTRINSICS.reflect.read(value, "name")) && (0, _orkestrel_contract.isString)(_orkestrel_contract.INTRINSICS.reflect.read(value, "message")) && (stack === void 0 || (0, _orkestrel_contract.isString)(stack));
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/server/artifacts/validators.ts
|
|
610
|
+
/** Checks whether a key names portable, non-credential members within a closure.
|
|
611
|
+
* @param value - The untrusted slash-separated key.
|
|
612
|
+
* @returns True if every segment is safe and contained; false otherwise.
|
|
613
|
+
*/
|
|
614
|
+
function isArtifactKey(value) {
|
|
615
|
+
return typeof value === "string" && value !== "" && !/[\\<>:"|?*]/u.test(value) && !Array.from(value).some((character) => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127) && value.split("/").every((name) => name !== "" && name !== "." && name !== ".." && !/[. ]$/.test(name) && !/^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(name) && !matchesArtifactCredential(name));
|
|
616
|
+
}
|
|
617
|
+
/** Checks the bounded loader selections retained in a publication.
|
|
618
|
+
* @param value - The decoded selections.
|
|
619
|
+
* @returns True if required selections and optional groups have their declared shapes; false otherwise.
|
|
620
|
+
*/
|
|
621
|
+
function isArtifactLoaderOptions(value) {
|
|
622
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.entries) && (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.roots) && value.entries.length <= 8192 && value.roots.length <= 128 && (value.source === void 0 || (0, _orkestrel_contract.isRecord)(value.source) && (0, _orkestrel_contract.isString)(value.source.root) && (0, _orkestrel_contract.isString)(value.source.entry)) && (value.instrument === void 0 || (0, _orkestrel_contract.isString)(value.instrument)) && (value.excluded === void 0 || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.excluded)) && (value.assets === void 0 || (0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.assets) && value.assets.length <= 8192));
|
|
623
|
+
}
|
|
624
|
+
/** Checks the exact file identity and provenance of a closure member.
|
|
625
|
+
* @param value - The decoded member.
|
|
626
|
+
* @returns True if the member has a safe key, source, digest and bounded size; false otherwise.
|
|
627
|
+
*/
|
|
628
|
+
function isArtifactMember(value) {
|
|
629
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.source) && isArtifactKey(value.key) && (0, _src_core.isStudyDigest)(value.digest) && typeof value.size === "number" && Number.isSafeInteger(value.size) && value.size >= 0);
|
|
630
|
+
}
|
|
631
|
+
/** Checks a bounded materialization record before any path is used.
|
|
632
|
+
* @param value - The decoded record.
|
|
633
|
+
* @returns True if its selections and member records have the declared shapes; false otherwise.
|
|
634
|
+
*/
|
|
635
|
+
function isArtifactClosure(value) {
|
|
636
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.destination) && (0, _src_core.isStudyDigest)(value.digest) && isArtifactLoaderOptions(value.options) && (0, _orkestrel_contract.isArray)(value.members) && value.members.length > 0 && value.members.length <= 8192 && value.members.every(isArtifactMember));
|
|
441
637
|
}
|
|
442
638
|
//#endregion
|
|
443
639
|
//#region src/server/artifacts/helpers.ts
|
|
640
|
+
/** Checks whether a portable segment identifies credentials.
|
|
641
|
+
* @param value - One untrusted path segment.
|
|
642
|
+
* @returns True if the segment has a credential filename; false otherwise.
|
|
643
|
+
*/
|
|
644
|
+
function matchesArtifactCredential(value) {
|
|
645
|
+
return /^(?:\.env(?:\..*)?|\.npmrc|auth\.json|id_(?:rsa|dsa|ecdsa|ed25519))$/i.test(value) || /\.(?:key|pem|p12|pfx)$/i.test(value) || /(?:^|[._-])token(?:$|[._-])/i.test(value);
|
|
646
|
+
}
|
|
647
|
+
/** Resolves a portable member key without traversing outside its destination.
|
|
648
|
+
* @param destination - The canonical absolute destination.
|
|
649
|
+
* @param key - The slash-separated member key.
|
|
650
|
+
* @returns The contained host path.
|
|
651
|
+
* @throws ArtifactError - Thrown for traversal or an unsafe name.
|
|
652
|
+
*/
|
|
653
|
+
function resolveArtifactMember(destination, key) {
|
|
654
|
+
if ((0, node_path.isAbsolute)(key) || key.split("/").some((name) => name === ".." || name === ".") || /[\\:]/.test(key)) throw new ArtifactError("Artifact member traverses its boundary", "TRAVERSAL");
|
|
655
|
+
if (!isArtifactKey(key)) throw new ArtifactError("Artifact member name is unsafe", "NAME");
|
|
656
|
+
const path = (0, node_path.resolve)(destination, ...key.split("/"));
|
|
657
|
+
if (!containsArtifact((0, node_path.resolve)(destination), path)) throw new ArtifactError("Artifact member traverses its boundary", "TRAVERSAL");
|
|
658
|
+
return path;
|
|
659
|
+
}
|
|
660
|
+
/** Checks every existing physical ancestor before using a canonical absolute path.
|
|
661
|
+
* @param path - The existing absolute file or directory.
|
|
662
|
+
* @returns Its physical filesystem metadata.
|
|
663
|
+
* @throws ArtifactError - Thrown for traversal, unsafe names, aliases or redirects before reading bytes.
|
|
664
|
+
*/
|
|
665
|
+
function readArtifactPhysical(path) {
|
|
666
|
+
if (!(0, node_path.isAbsolute)(path) || (0, node_path.resolve)(path) !== path) throw new ArtifactError("Artifact path must be canonical and absolute", "TRAVERSAL");
|
|
667
|
+
const origin = (0, node_path.parse)(path).root;
|
|
668
|
+
const names = (0, node_path.relative)(origin, path).split(node_path.sep).filter(Boolean);
|
|
669
|
+
if (names.some((name) => !isArtifactKey(name))) throw new ArtifactError("Artifact physical path has an unsafe name", "NAME");
|
|
670
|
+
for (const [index] of names.entries()) {
|
|
671
|
+
const candidate = (0, node_path.join)(origin, ...names.slice(0, index + 1));
|
|
672
|
+
if ((0, node_fs.lstatSync)(candidate, { bigint: true }).isSymbolicLink()) throw new ArtifactError("Artifact path is redirected", "REDIRECT");
|
|
673
|
+
const physical = (0, node_fs.realpathSync)(candidate);
|
|
674
|
+
if (physical !== candidate) throw new ArtifactError("Artifact path has a physical alias or redirect", physical.toLowerCase() === candidate.toLowerCase() ? "ALIAS" : "REDIRECT");
|
|
675
|
+
}
|
|
676
|
+
return (0, node_fs.lstatSync)(path, { bigint: true });
|
|
677
|
+
}
|
|
678
|
+
/** Finds the common physical parent that preserves every selected path's relative resolution.
|
|
679
|
+
* @param paths - Existing absolute members or selected roots on one volume.
|
|
680
|
+
* @returns Their common ancestor directory.
|
|
681
|
+
* @throws ArtifactError - Thrown when selections span volumes or contain no files.
|
|
682
|
+
*/
|
|
683
|
+
function resolveArtifactBase(paths) {
|
|
684
|
+
const first = paths[0];
|
|
685
|
+
if (first === void 0) throw new ArtifactError("Artifact population is empty");
|
|
686
|
+
if (paths.some((path) => (0, node_path.parse)(path).root !== (0, node_path.parse)(first).root)) throw new ArtifactError("Artifact sources require one volume", "VOLUME");
|
|
687
|
+
const origin = (0, node_path.parse)(first).root;
|
|
688
|
+
const names = (0, node_path.relative)(origin, (0, node_path.dirname)(first)).split(node_path.sep).filter(Boolean);
|
|
689
|
+
const base = Array.from({ length: names.length + 1 }, (_entry, index) => (0, node_path.join)(origin, ...names.slice(0, index))).reverse().find((parent) => paths.every((path) => parent === path || containsArtifact(parent, path)));
|
|
690
|
+
if (base === void 0) throw new ArtifactError("Artifact sources have no common parent", "VOLUME");
|
|
691
|
+
return base;
|
|
692
|
+
}
|
|
693
|
+
/** Relocates an original path through a closure's retained physical forest.
|
|
694
|
+
* @param closure - The verified closure record.
|
|
695
|
+
* @param path - An original member or ancestor within its forest.
|
|
696
|
+
* @returns The corresponding destination path.
|
|
697
|
+
* @throws ArtifactError - Thrown when provenance or containment is inconsistent.
|
|
698
|
+
*/
|
|
699
|
+
function relocateArtifactPath(closure, path) {
|
|
700
|
+
const first = closure.members[0];
|
|
701
|
+
if (first === void 0) throw new ArtifactError("Artifact population is empty");
|
|
702
|
+
const origin = (0, node_path.resolve)(first.source, ...first.key.split("/").map(() => ".."));
|
|
703
|
+
if (path !== origin && !containsArtifact(origin, path)) throw new ArtifactError("Artifact relocation traverses its forest", "TRAVERSAL");
|
|
704
|
+
if (closure.members.some((member) => resolveArtifactMember(origin, member.key) !== member.source)) throw new ArtifactError("Artifact member provenance is inconsistent", "TRAVERSAL");
|
|
705
|
+
return path === origin ? closure.destination : resolveArtifactMember(closure.destination, (0, node_path.relative)(origin, path).split(node_path.sep).join("/"));
|
|
706
|
+
}
|
|
707
|
+
/** Computes a destination-independent digest of members and relative package identities.
|
|
708
|
+
* @param members - Complete copied membership, with source provenance excluded from identity.
|
|
709
|
+
* @param packages - Package identities with relocated roots.
|
|
710
|
+
* @param destination - The common destination directory.
|
|
711
|
+
* @returns The portable SHA-256 identity.
|
|
712
|
+
*/
|
|
713
|
+
function computeArtifactClosureDigest(members, packages, destination) {
|
|
714
|
+
return hashStudyBytes(encodeStudyJSON({
|
|
715
|
+
members: members.map(({ key, digest, size }) => ({
|
|
716
|
+
key,
|
|
717
|
+
digest,
|
|
718
|
+
size
|
|
719
|
+
})).toSorted((left, right) => left.key < right.key ? -1 : left.key > right.key ? 1 : 0),
|
|
720
|
+
packages: packages.map((entry) => ({
|
|
721
|
+
...entry,
|
|
722
|
+
root: (0, node_path.relative)(destination, entry.root).split(node_path.sep).join("/")
|
|
723
|
+
})).toSorted((left, right) => left.root < right.root ? -1 : left.root > right.root ? 1 : 0)
|
|
724
|
+
}));
|
|
725
|
+
}
|
|
726
|
+
/** Verifies complete copied membership, path safety, exact bytes, and portable identity.
|
|
727
|
+
* @param closure - The materialization record to verify independently.
|
|
728
|
+
* @param published - If true, permits the root publication marker; if false, refuses every extra file. Default: false.
|
|
729
|
+
* @returns Nothing when the entire forest matches.
|
|
730
|
+
* @throws ArtifactError - Thrown for malformed records, changed membership, bytes, or physical paths.
|
|
731
|
+
*/
|
|
732
|
+
function verifyArtifactClosure(closure, published = false) {
|
|
733
|
+
if (!isArtifactClosure(closure)) throw new ArtifactError("Artifact closure record is invalid", "NAME");
|
|
734
|
+
if (!readArtifactPhysical(closure.destination).isDirectory()) throw new ArtifactError("Artifact closure requires a directory", "DRIFT");
|
|
735
|
+
const keys = new Set(closure.members.map((member) => member.key));
|
|
736
|
+
if (keys.size !== closure.members.length) throw new ArtifactError("Artifact member key is duplicated", "DUPLICATE");
|
|
737
|
+
if (new Set(closure.members.map((member) => member.key.toLowerCase())).size !== closure.members.length) throw new ArtifactError("Artifact member keys have a case alias", "ALIAS");
|
|
738
|
+
const directories = [{
|
|
739
|
+
path: closure.destination,
|
|
740
|
+
depth: 0
|
|
741
|
+
}];
|
|
742
|
+
const visited = [];
|
|
743
|
+
const files = [];
|
|
744
|
+
for (const directory of directories) {
|
|
745
|
+
if (directory.depth > 64) throw new ArtifactError("Artifact directory depth limit exceeded", "LIMIT");
|
|
746
|
+
const handle = (0, node_fs.opendirSync)(directory.path);
|
|
747
|
+
try {
|
|
748
|
+
for (const entry of iterateArtifactEntries(handle)) {
|
|
749
|
+
const path = (0, node_path.join)(directory.path, entry.name);
|
|
750
|
+
visited.push(path);
|
|
751
|
+
if (visited.length > 32768) throw new ArtifactError("Artifact traversal limit exceeded", "LIMIT");
|
|
752
|
+
const stat = readArtifactPhysical(path);
|
|
753
|
+
if (stat.isDirectory()) {
|
|
754
|
+
if (!closure.members.some((member) => containsArtifact(path, resolveArtifactMember(closure.destination, member.key)))) throw new ArtifactError("Artifact closure membership drifted", "DRIFT");
|
|
755
|
+
directories.push({
|
|
756
|
+
path,
|
|
757
|
+
depth: directory.depth + 1
|
|
758
|
+
});
|
|
759
|
+
continue;
|
|
760
|
+
}
|
|
761
|
+
if (!stat.isFile()) throw new ArtifactError("Artifact closure contains a non-file", "DRIFT");
|
|
762
|
+
if (published && path === (0, node_path.join)(closure.destination, "closure.json")) continue;
|
|
763
|
+
const key = (0, node_path.relative)(closure.destination, path).split(node_path.sep).join("/");
|
|
764
|
+
if (!keys.has(key)) throw new ArtifactError("Artifact closure membership drifted", "DRIFT");
|
|
765
|
+
files.push({
|
|
766
|
+
path,
|
|
767
|
+
identity: `${stat.dev}:${stat.ino}`,
|
|
768
|
+
size: Number(stat.size)
|
|
769
|
+
});
|
|
770
|
+
if (files.length > 8192) throw new ArtifactError("Artifact closure exceeds its capture allowance", "LIMIT");
|
|
771
|
+
}
|
|
772
|
+
} finally {
|
|
773
|
+
handle.closeSync();
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
if (files.length !== closure.members.length) throw new ArtifactError("Artifact closure membership drifted", "DRIFT");
|
|
777
|
+
if (files.reduce((sum, file) => sum + file.size, 0) > 134217728) throw new ArtifactError("Artifact closure exceeds its capture allowance", "LIMIT");
|
|
778
|
+
if (new Set(files.map((file) => file.identity)).size !== files.length) throw new ArtifactError("Artifact members have a physical alias", "ALIAS");
|
|
779
|
+
for (const member of closure.members) {
|
|
780
|
+
const file = readArtifact(resolveArtifactMember(closure.destination, member.key));
|
|
781
|
+
if (file.digest !== member.digest || file.size !== member.size) throw new ArtifactError("Artifact closure bytes drifted", "DRIFT");
|
|
782
|
+
}
|
|
783
|
+
if ([
|
|
784
|
+
...closure.options.entries,
|
|
785
|
+
...closure.options.assets ?? [],
|
|
786
|
+
...closure.options.excluded ?? [],
|
|
787
|
+
...closure.options.source === void 0 ? [] : [closure.options.source.entry]
|
|
788
|
+
].some((path) => !files.some((file) => file.path === path)) || closure.options.roots.some((root) => root !== closure.destination && !containsArtifact(closure.destination, root)) || closure.options.source !== void 0 && closure.options.source.root !== closure.destination && !containsArtifact(closure.destination, closure.options.source.root)) throw new ArtifactError("Artifact closure selection traverses its population", "TRAVERSAL");
|
|
789
|
+
const packages = closure.options.roots.map((root) => {
|
|
790
|
+
const manifest = readArtifact((0, node_path.join)(root, "package.json"));
|
|
791
|
+
const value = (0, _orkestrel_contract.parseJSON)(Buffer.from(manifest.source).toString("utf8"));
|
|
792
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.name !== "string" || typeof value.version !== "string") throw new ArtifactError("Artifact closure manifest is invalid", "DRIFT");
|
|
793
|
+
return {
|
|
794
|
+
root,
|
|
795
|
+
name: value.name,
|
|
796
|
+
version: value.version
|
|
797
|
+
};
|
|
798
|
+
});
|
|
799
|
+
if (computeArtifactClosureDigest(closure.members, packages, closure.destination) !== closure.digest) throw new ArtifactError("Artifact closure identity drifted", "DRIFT");
|
|
800
|
+
}
|
|
444
801
|
/** Fills a buffer from an open descriptor, one positioned read at a time.
|
|
445
802
|
* @param descriptor - The open file descriptor to read.
|
|
446
803
|
* @param target - The buffer to fill.
|
|
@@ -471,6 +828,7 @@ function* iterateArtifactEntries(handle) {
|
|
|
471
828
|
*/
|
|
472
829
|
function readArtifact(path, maximum = ARTIFACT_FILE_BYTES) {
|
|
473
830
|
if (!Number.isSafeInteger(maximum) || maximum < 0 || !(0, node_path.isAbsolute)(path) || (0, node_path.resolve)(path) !== path || (0, node_fs.realpathSync)(path) !== path || !(0, node_fs.lstatSync)(path).isFile()) throw new ArtifactError(`Artifact requires a canonical regular file and byte budget: ${path}`);
|
|
831
|
+
readArtifactPhysical(path);
|
|
474
832
|
const descriptor = (0, node_fs.openSync)(path, "r");
|
|
475
833
|
try {
|
|
476
834
|
const before = (0, node_fs.fstatSync)(descriptor);
|
|
@@ -601,7 +959,7 @@ var Resolution = class {
|
|
|
601
959
|
//#region src/server/artifacts/ArtifactLoader.ts
|
|
602
960
|
/** Captures built ESM and a separate strip-only consumer adapter in a fresh worker.
|
|
603
961
|
* @remarks
|
|
604
|
-
* Loads trusted code
|
|
962
|
+
* Loads trusted code without sandboxing untrusted code. Native process APIs, eval and external
|
|
605
963
|
* services are not replayed. One inventory may load per process; terminate it afterwards.
|
|
606
964
|
*/
|
|
607
965
|
var ArtifactLoader = class ArtifactLoader {
|
|
@@ -610,6 +968,7 @@ var ArtifactLoader = class ArtifactLoader {
|
|
|
610
968
|
#inventory;
|
|
611
969
|
#sources = [];
|
|
612
970
|
#token;
|
|
971
|
+
#references = /* @__PURE__ */ new Map();
|
|
613
972
|
/** Captures selections without reading or executing selected modules.
|
|
614
973
|
* @param options - Canonical built selections and optional consumer source selection.
|
|
615
974
|
*/
|
|
@@ -619,8 +978,12 @@ var ArtifactLoader = class ArtifactLoader {
|
|
|
619
978
|
...options,
|
|
620
979
|
entries: [...options.entries],
|
|
621
980
|
roots: [...options.roots],
|
|
622
|
-
...options.source === void 0 ? {} : { source: {
|
|
623
|
-
|
|
981
|
+
...options.source === void 0 ? {} : { source: {
|
|
982
|
+
root: options.source.root,
|
|
983
|
+
entry: options.source.entry
|
|
984
|
+
} },
|
|
985
|
+
...options.excluded === void 0 ? {} : { excluded: [...options.excluded] },
|
|
986
|
+
...options.assets === void 0 ? {} : { assets: [...options.assets] }
|
|
624
987
|
};
|
|
625
988
|
}
|
|
626
989
|
/** Reads and identifies admitted files without evaluating selected modules.
|
|
@@ -680,9 +1043,14 @@ var ArtifactLoader = class ArtifactLoader {
|
|
|
680
1043
|
path,
|
|
681
1044
|
digest,
|
|
682
1045
|
size
|
|
683
|
-
})))
|
|
1046
|
+
}))),
|
|
1047
|
+
...this.#options.assets === void 0 ? {} : { assets: Object.freeze([...this.#options.assets].sort()) }
|
|
684
1048
|
};
|
|
685
1049
|
this.#sources = sources;
|
|
1050
|
+
this.#references = new Map(sources.map((file) => {
|
|
1051
|
+
const stat = readArtifactPhysical(file.path);
|
|
1052
|
+
return [file.path, `${stat.dev}:${stat.ino}:${stat.mtimeNs}:${stat.ctimeNs}`];
|
|
1053
|
+
}));
|
|
686
1054
|
this.#inventory = Object.freeze({
|
|
687
1055
|
digest: hashStudyBytes(JSON.stringify(evidence)),
|
|
688
1056
|
...evidence
|
|
@@ -712,7 +1080,7 @@ var ArtifactLoader = class ArtifactLoader {
|
|
|
712
1080
|
const token = (0, node_crypto.randomUUID)();
|
|
713
1081
|
const resolution = new Resolution({
|
|
714
1082
|
token,
|
|
715
|
-
files: this.#sources,
|
|
1083
|
+
files: this.#sources.filter((file) => this.#options.roots.some((root) => file.path === (0, node_path.join)(root, "package.json") || containsArtifact((0, node_path.join)(root, "dist"), file.path)) || this.#options.source !== void 0 && containsArtifact(this.#options.source.root, file.path)),
|
|
716
1084
|
...inventory.source === void 0 ? {} : { source: inventory.source },
|
|
717
1085
|
...inventory.instrument === void 0 ? {} : { instrument: inventory.instrument },
|
|
718
1086
|
...this.#options.excluded === void 0 ? {} : { excluded: this.#options.excluded }
|
|
@@ -730,6 +1098,123 @@ var ArtifactLoader = class ArtifactLoader {
|
|
|
730
1098
|
this.verify();
|
|
731
1099
|
return namespace;
|
|
732
1100
|
}
|
|
1101
|
+
/** Copies the inspected physical forest into an exclusive absent destination.
|
|
1102
|
+
* @param destination - The canonical absent absolute directory under an existing physical parent.
|
|
1103
|
+
* @returns The independently verified copied membership and relocated selections.
|
|
1104
|
+
* @throws ArtifactError - Thrown at the refused path, stale-source, volume, or drift boundary.
|
|
1105
|
+
*/
|
|
1106
|
+
async materialize(destination) {
|
|
1107
|
+
if (new Set(this.#options.roots).size !== this.#options.roots.length) throw new ArtifactError("Artifact package root is duplicated", "DUPLICATE");
|
|
1108
|
+
if (!(0, node_path.isAbsolute)(destination) || (0, node_path.resolve)(destination) !== destination) throw new ArtifactError("Artifact destination must be canonical and absolute", "TRAVERSAL");
|
|
1109
|
+
const boundaries = [
|
|
1110
|
+
...this.#options.roots,
|
|
1111
|
+
...this.#options.source === void 0 ? [] : [this.#options.source.root],
|
|
1112
|
+
...this.#options.assets ?? []
|
|
1113
|
+
];
|
|
1114
|
+
if (boundaries.some((path) => (0, node_path.parse)(path).root !== (0, node_path.parse)(destination).root)) throw new ArtifactError("Artifact closure requires one volume", "VOLUME");
|
|
1115
|
+
if (boundaries.some((path) => path === destination || containsArtifact(path, destination) || containsArtifact(destination, path))) throw new ArtifactError("Artifact destination overlaps a source", "OVERLAP");
|
|
1116
|
+
const parent = readArtifactPhysical((0, node_path.dirname)(destination));
|
|
1117
|
+
resolveArtifactMember((0, node_path.dirname)(destination), (0, node_path.relative)((0, node_path.dirname)(destination), destination));
|
|
1118
|
+
if (!parent.isDirectory()) throw new ArtifactError("Artifact destination parent must be a directory", "REDIRECT");
|
|
1119
|
+
try {
|
|
1120
|
+
(0, node_fs.lstatSync)(destination);
|
|
1121
|
+
throw new ArtifactError("Artifact destination must be absent", "OCCUPIED");
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
if (!matchesSystemError(error, "ENOENT")) throw error;
|
|
1124
|
+
}
|
|
1125
|
+
const inspected = this.#inventory !== void 0;
|
|
1126
|
+
try {
|
|
1127
|
+
for (const path of boundaries) readArtifactPhysical(path);
|
|
1128
|
+
this.inspect();
|
|
1129
|
+
for (const file of this.#sources) {
|
|
1130
|
+
const stat = readArtifactPhysical(file.path);
|
|
1131
|
+
if (stat.dev !== parent.dev) throw new ArtifactError("Artifact closure requires one volume", "VOLUME");
|
|
1132
|
+
if (`${stat.dev}:${stat.ino}:${stat.mtimeNs}:${stat.ctimeNs}` !== this.#references.get(file.path)) throw new ArtifactError("Artifact source reference is stale", "STALE");
|
|
1133
|
+
}
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
if (inspected && !(isArtifactError(error) && [
|
|
1136
|
+
"REDIRECT",
|
|
1137
|
+
"ALIAS",
|
|
1138
|
+
"NAME",
|
|
1139
|
+
"VOLUME"
|
|
1140
|
+
].includes(error.code))) throw new ArtifactError("Artifact source inspection is stale", "STALE");
|
|
1141
|
+
throw error;
|
|
1142
|
+
}
|
|
1143
|
+
const base = resolveArtifactBase([
|
|
1144
|
+
...this.#sources.map((file) => file.path),
|
|
1145
|
+
...this.#options.roots,
|
|
1146
|
+
...this.#options.source === void 0 ? [] : [this.#options.source.root]
|
|
1147
|
+
]);
|
|
1148
|
+
const members = Object.freeze(this.#sources.map((file) => Object.freeze({
|
|
1149
|
+
source: file.path,
|
|
1150
|
+
key: (0, node_path.relative)(base, file.path).split(node_path.sep).join("/"),
|
|
1151
|
+
digest: file.digest,
|
|
1152
|
+
size: file.size
|
|
1153
|
+
})));
|
|
1154
|
+
if (new Set(members.map((member) => member.key)).size !== members.length) throw new ArtifactError("Artifact member key is duplicated", "DUPLICATE");
|
|
1155
|
+
if (new Set(members.map((member) => member.key.toLowerCase())).size !== members.length) throw new ArtifactError("Artifact member keys have a case alias", "ALIAS");
|
|
1156
|
+
if (members.some((member) => member.key.split("/").length - 1 > 64)) throw new ArtifactError("Artifact closure directory depth limit exceeded", "LIMIT");
|
|
1157
|
+
if (new Set(members.flatMap((member) => member.key.split("/").slice(0, -1).map((_name, index, names) => names.slice(0, index + 1).join("/")))).size + members.length > 32768) throw new ArtifactError("Artifact closure traversal limit exceeded", "LIMIT");
|
|
1158
|
+
for (const member of members) resolveArtifactMember(destination, member.key);
|
|
1159
|
+
const identities = this.#sources.map((file) => {
|
|
1160
|
+
const stat = readArtifactPhysical(file.path);
|
|
1161
|
+
return `${stat.dev}:${stat.ino}`;
|
|
1162
|
+
});
|
|
1163
|
+
if (new Set(identities).size !== identities.length) throw new ArtifactError("Artifact members have a physical alias", "ALIAS");
|
|
1164
|
+
const relocation = {
|
|
1165
|
+
destination,
|
|
1166
|
+
digest: "",
|
|
1167
|
+
options: this.#options,
|
|
1168
|
+
members
|
|
1169
|
+
};
|
|
1170
|
+
const options = Object.freeze({
|
|
1171
|
+
entries: Object.freeze(this.#options.entries.map((path) => relocateArtifactPath(relocation, path))),
|
|
1172
|
+
roots: Object.freeze(this.#options.roots.map((path) => relocateArtifactPath(relocation, path))),
|
|
1173
|
+
...this.#options.source === void 0 ? {} : { source: Object.freeze({
|
|
1174
|
+
root: relocateArtifactPath(relocation, this.#options.source.root),
|
|
1175
|
+
entry: relocateArtifactPath(relocation, this.#options.source.entry)
|
|
1176
|
+
}) },
|
|
1177
|
+
...this.#options.instrument === void 0 ? {} : { instrument: relocateArtifactPath(relocation, this.#options.instrument) },
|
|
1178
|
+
...this.#options.excluded === void 0 ? {} : { excluded: Object.freeze(this.#options.excluded.map((path) => relocateArtifactPath(relocation, path))) },
|
|
1179
|
+
...this.#options.assets === void 0 ? {} : { assets: Object.freeze(this.#options.assets.map((path) => relocateArtifactPath(relocation, path))) }
|
|
1180
|
+
});
|
|
1181
|
+
const packages = this.inspect().packages.map((entry) => ({
|
|
1182
|
+
...entry,
|
|
1183
|
+
root: relocateArtifactPath(relocation, entry.root)
|
|
1184
|
+
}));
|
|
1185
|
+
const closure = Object.freeze({
|
|
1186
|
+
destination,
|
|
1187
|
+
digest: computeArtifactClosureDigest(members, packages, destination),
|
|
1188
|
+
options,
|
|
1189
|
+
members
|
|
1190
|
+
});
|
|
1191
|
+
(0, node_fs.mkdirSync)(destination);
|
|
1192
|
+
for (const file of this.#sources) {
|
|
1193
|
+
const target = relocateArtifactPath(closure, file.path);
|
|
1194
|
+
this.#directory(closure.destination, (0, node_path.dirname)(target));
|
|
1195
|
+
await (0, node_fs_promises.writeFile)(target, file.source, { flag: "wx" });
|
|
1196
|
+
}
|
|
1197
|
+
try {
|
|
1198
|
+
this.verify();
|
|
1199
|
+
} catch {
|
|
1200
|
+
throw new ArtifactError("Artifact source drifted during copying", "DRIFT");
|
|
1201
|
+
}
|
|
1202
|
+
verifyArtifactClosure(closure);
|
|
1203
|
+
return closure;
|
|
1204
|
+
}
|
|
1205
|
+
#directory(destination, target) {
|
|
1206
|
+
const names = (0, node_path.relative)(destination, target).split(node_path.sep).filter(Boolean);
|
|
1207
|
+
readArtifactPhysical(destination);
|
|
1208
|
+
for (const [index] of names.entries()) {
|
|
1209
|
+
const path = (0, node_path.join)(destination, ...names.slice(0, index + 1));
|
|
1210
|
+
try {
|
|
1211
|
+
(0, node_fs.mkdirSync)(path);
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
if (!matchesSystemError(error, "EEXIST")) throw error;
|
|
1214
|
+
}
|
|
1215
|
+
if (!readArtifactPhysical(path).isDirectory()) throw new ArtifactError("Artifact destination ancestor is redirected", "REDIRECT");
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
733
1218
|
#capture() {
|
|
734
1219
|
const sources = [];
|
|
735
1220
|
const tally = this.#options.roots.reduce((counted, root) => {
|
|
@@ -742,10 +1227,20 @@ var ArtifactLoader = class ArtifactLoader {
|
|
|
742
1227
|
const source = this.#options.source;
|
|
743
1228
|
if (source !== void 0) {
|
|
744
1229
|
if (this.#options.roots.some((root) => (0, node_path.join)(root, "dist") === source.root || containsArtifact((0, node_path.join)(root, "dist"), source.root) || containsArtifact(source.root, (0, node_path.join)(root, "dist")))) throw new ArtifactError("Artifact source and built inventories must not overlap");
|
|
745
|
-
this.#walk(source.root, sources, true, 0, tally);
|
|
746
|
-
|
|
1230
|
+
const counted = this.#walk(source.root, sources, true, 0, tally);
|
|
1231
|
+
this.#assets(sources, counted);
|
|
1232
|
+
} else this.#assets(sources, tally);
|
|
747
1233
|
return sources;
|
|
748
1234
|
}
|
|
1235
|
+
#assets(sources, tally) {
|
|
1236
|
+
const assets = this.#options.assets ?? [];
|
|
1237
|
+
if (assets.length > 8192) throw new ArtifactError("Artifact asset count limit exceeded", "LIMIT");
|
|
1238
|
+
if (new Set(assets).size !== assets.length) throw new ArtifactError("Artifact asset key is duplicated", "DUPLICATE");
|
|
1239
|
+
assets.reduce((counted, path) => {
|
|
1240
|
+
readArtifactPhysical(path);
|
|
1241
|
+
return sources.some((file) => file.path === path) ? counted : this.#append(path, sources, counted);
|
|
1242
|
+
}, tally);
|
|
1243
|
+
}
|
|
749
1244
|
#append(path, sources, tally) {
|
|
750
1245
|
if (sources.length >= 8192) throw new ArtifactError("Artifact file count limit exceeded");
|
|
751
1246
|
const file = readArtifact(path, ARTIFACT_BYTES - tally.bytes);
|
|
@@ -767,7 +1262,7 @@ var ArtifactLoader = class ArtifactLoader {
|
|
|
767
1262
|
});
|
|
768
1263
|
if (visited.visits > 32768) throw new ArtifactError("Artifact traversal limit exceeded");
|
|
769
1264
|
const path = (0, node_path.join)(directory, entry.name);
|
|
770
|
-
if (entry.isSymbolicLink()) throw new ArtifactError(`Artifact links are unsupported: ${path}
|
|
1265
|
+
if (entry.isSymbolicLink()) throw new ArtifactError(`Artifact links are unsupported: ${path}`, "REDIRECT");
|
|
771
1266
|
if (entry.isDirectory()) return this.#walk(path, sources, source, depth + 1, visited);
|
|
772
1267
|
return (source ? [
|
|
773
1268
|
".ts",
|
|
@@ -1018,7 +1513,7 @@ async function writeStudyBytes(path, bytes) {
|
|
|
1018
1513
|
*/
|
|
1019
1514
|
async function scanStudyExecution(directory, manifest) {
|
|
1020
1515
|
await checkStudyDirectory(directory);
|
|
1021
|
-
const files = await scanStudyTree(directory, "", new Set(manifest.schedule.map((trial) => trial.id)), manifest.bounds.series.bytes);
|
|
1516
|
+
const files = await scanStudyTree(directory, "", new Set(manifest.schedule.map((trial) => trial.id)), manifest.bounds.series.bytes, manifest.record !== void 0);
|
|
1022
1517
|
return Object.freeze([...files].sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
|
1023
1518
|
}
|
|
1024
1519
|
/**
|
|
@@ -1030,7 +1525,7 @@ async function scanStudyExecution(directory, manifest) {
|
|
|
1030
1525
|
* @returns The admitted files in read order: this directory's files, then each admitted subdirectory's, last listed first
|
|
1031
1526
|
* @throws StudyError - Thrown when the tree holds a link, an unknown directory or file, or a file beyond the remaining allowance
|
|
1032
1527
|
*/
|
|
1033
|
-
async function scanStudyTree(directory, prefix, known, allowance) {
|
|
1528
|
+
async function scanStudyTree(directory, prefix, known, allowance, recording = false) {
|
|
1034
1529
|
const files = [];
|
|
1035
1530
|
const directories = [];
|
|
1036
1531
|
for (const entry of await (0, node_fs_promises.readdir)((0, node_path.join)(directory, ...prefix.split("/").filter(Boolean)), { withFileTypes: true })) {
|
|
@@ -1042,7 +1537,7 @@ async function scanStudyTree(directory, prefix, known, allowance) {
|
|
|
1042
1537
|
continue;
|
|
1043
1538
|
}
|
|
1044
1539
|
const names = path.split("/");
|
|
1045
|
-
if (!entry.isFile() || !(path === "manifest.json" || names.length === 3 && names[0] === "trials" && known.has(names[1] ?? "") && (names[2] === "attempt.json" || names[2] === "evidence.jsonl"))) throw new _src_core.StudyError("CUSTODY", "Execution contains an unknown file");
|
|
1540
|
+
if (!entry.isFile() || !(path === "manifest.json" || names.length === 3 && names[0] === "trials" && known.has(names[1] ?? "") && (names[2] === "attempt.json" || names[2] === "evidence.jsonl" || recording && names[2] === "exchanges.jsonl"))) throw new _src_core.StudyError("CUSTODY", "Execution contains an unknown file");
|
|
1046
1541
|
const bytes = await readStudyBytes((0, node_path.join)(directory, ...names), files.reduce((left, file) => left - file.bytes, allowance));
|
|
1047
1542
|
files.push(Object.freeze({
|
|
1048
1543
|
path,
|
|
@@ -1054,7 +1549,7 @@ async function scanStudyTree(directory, prefix, known, allowance) {
|
|
|
1054
1549
|
const steps = [];
|
|
1055
1550
|
for (const child of [...directories].reverse()) {
|
|
1056
1551
|
const before = steps.at(-1)?.left ?? remaining;
|
|
1057
|
-
const found = await scanStudyTree(directory, child, known, before);
|
|
1552
|
+
const found = await scanStudyTree(directory, child, known, before, recording);
|
|
1058
1553
|
steps.push({
|
|
1059
1554
|
found,
|
|
1060
1555
|
left: found.reduce((left, file) => left - file.bytes, before)
|
|
@@ -1192,7 +1687,12 @@ async function readStudyResult(directory) {
|
|
|
1192
1687
|
const sealBytes = await readStudyBytes((0, node_path.join)(absolute, "seal.json"), STUDY_CUSTODY_BYTES);
|
|
1193
1688
|
if (files.reduce((total, file) => total + file.bytes, sealBytes.byteLength) > manifest.bounds.series.bytes) throw new _src_core.StudyError("CUSTODY", "Seal and execution exceed the series allowance");
|
|
1194
1689
|
const seal = decodeStudyJSON(sealBytes);
|
|
1195
|
-
if (!isStudyCustodySeal(seal) || seal.result !== manifest.id || seal.id !== computeStudySealId(manifest.id, files) || (0, _orkestrel_contract.canonicalStringify)(seal.files) !== (0, _orkestrel_contract.canonicalStringify)(files) || attempts.length !== manifest.schedule.length || files.length !== 1 + manifest.schedule.length * 2) throw new _src_core.StudyError("CUSTODY", "Execution seal does not match the complete byte inventory");
|
|
1690
|
+
if (!isStudyCustodySeal(seal) || seal.result !== manifest.id || seal.id !== computeStudySealId(manifest.id, files) || (0, _orkestrel_contract.canonicalStringify)(seal.files) !== (0, _orkestrel_contract.canonicalStringify)(files) || attempts.length !== manifest.schedule.length || files.length !== 1 + manifest.schedule.length * (manifest.record === void 0 ? 2 : 3)) throw new _src_core.StudyError("CUSTODY", "Execution seal does not match the complete byte inventory");
|
|
1691
|
+
if (manifest.record !== void 0) for (const trial of manifest.schedule) {
|
|
1692
|
+
const file = files.find((entry) => entry.path === `trials/${trial.id}/exchanges.jsonl`);
|
|
1693
|
+
if (file === void 0) throw new _src_core.StudyError("CUSTODY", "Recorded trial is missing its exchange sidecar");
|
|
1694
|
+
await readStudyTrialExchanges(absolute, manifest, trial.id, file);
|
|
1695
|
+
}
|
|
1196
1696
|
return Object.freeze({
|
|
1197
1697
|
status: "sealed",
|
|
1198
1698
|
directory: absolute,
|
|
@@ -1201,6 +1701,228 @@ async function readStudyResult(directory) {
|
|
|
1201
1701
|
seal
|
|
1202
1702
|
});
|
|
1203
1703
|
}
|
|
1704
|
+
/** Projects the declaration onto its complete ordered HTTP identity population.
|
|
1705
|
+
* @param declaration - The validated opening row.
|
|
1706
|
+
* @returns Maintenance, preflight, model and postflight identities in declaration order.
|
|
1707
|
+
*/
|
|
1708
|
+
function buildStudyExchangeSequence(declaration) {
|
|
1709
|
+
const first = declaration.procedure[0];
|
|
1710
|
+
const last = declaration.procedure.at(-1);
|
|
1711
|
+
if (first === void 0 || last === void 0) throw new _src_core.StudyError("CUSTODY", "Exchange procedure is empty");
|
|
1712
|
+
const models = Object.values(declaration.models).filter((model, index, entries) => entries.findIndex((entry) => entry.name === model.name) === index);
|
|
1713
|
+
const maintenance = declaration.maintenance.map((_model, index) => ({
|
|
1714
|
+
ordinal: index + 1,
|
|
1715
|
+
goal: first.goal,
|
|
1716
|
+
role: "maintenance",
|
|
1717
|
+
path: "/api/generate",
|
|
1718
|
+
method: "POST",
|
|
1719
|
+
stage: "maintenance"
|
|
1720
|
+
}));
|
|
1721
|
+
const preflight = [
|
|
1722
|
+
"/api/version",
|
|
1723
|
+
"/api/tags",
|
|
1724
|
+
...models.map(() => "/api/show")
|
|
1725
|
+
].map((path, index) => ({
|
|
1726
|
+
ordinal: maintenance.length + index + 1,
|
|
1727
|
+
goal: first.goal,
|
|
1728
|
+
role: "metadata",
|
|
1729
|
+
path: path === "/api/version" ? "/api/version" : path === "/api/tags" ? "/api/tags" : "/api/show",
|
|
1730
|
+
method: path === "/api/show" ? "POST" : "GET",
|
|
1731
|
+
stage: "preflight"
|
|
1732
|
+
}));
|
|
1733
|
+
const execution = declaration.procedure.map((step, index) => ({
|
|
1734
|
+
ordinal: maintenance.length + preflight.length + index + 1,
|
|
1735
|
+
goal: step.goal,
|
|
1736
|
+
role: step.role,
|
|
1737
|
+
path: step.path,
|
|
1738
|
+
method: "POST",
|
|
1739
|
+
stage: "execution",
|
|
1740
|
+
step: step.ordinal
|
|
1741
|
+
}));
|
|
1742
|
+
const postflight = preflight.map((identity, index) => ({
|
|
1743
|
+
...identity,
|
|
1744
|
+
ordinal: maintenance.length + preflight.length + execution.length + index + 1,
|
|
1745
|
+
goal: last.goal,
|
|
1746
|
+
stage: "postflight"
|
|
1747
|
+
}));
|
|
1748
|
+
return [
|
|
1749
|
+
...maintenance,
|
|
1750
|
+
...preflight,
|
|
1751
|
+
...execution,
|
|
1752
|
+
...postflight
|
|
1753
|
+
];
|
|
1754
|
+
}
|
|
1755
|
+
/** Projects only the repeated identity fields of a retained HTTP exchange.
|
|
1756
|
+
* @param identity - The row whose body and lifecycle fields are omitted.
|
|
1757
|
+
* @returns The identity repeated by its attempt and terminal.
|
|
1758
|
+
*/
|
|
1759
|
+
function projectStudyExchangeIdentity(identity) {
|
|
1760
|
+
return {
|
|
1761
|
+
ordinal: identity.ordinal,
|
|
1762
|
+
goal: identity.goal,
|
|
1763
|
+
role: identity.role,
|
|
1764
|
+
path: identity.path,
|
|
1765
|
+
method: identity.method,
|
|
1766
|
+
stage: identity.stage,
|
|
1767
|
+
...identity.step === void 0 ? {} : { step: identity.step }
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
/** Validates the exact declaration order and request bytes of an admitted exchange.
|
|
1771
|
+
* @param declaration - The complete opening procedure.
|
|
1772
|
+
* @param request - The admitted request to join.
|
|
1773
|
+
* @returns Nothing after identity, exact-body or capture-length validation.
|
|
1774
|
+
*/
|
|
1775
|
+
function validateStudyExchangeRequest(declaration, request) {
|
|
1776
|
+
const expected = buildStudyExchangeSequence(declaration)[request.ordinal - 1];
|
|
1777
|
+
if (expected === void 0 || (0, _orkestrel_contract.canonicalStringify)(expected) !== (0, _orkestrel_contract.canonicalStringify)(projectStudyExchangeIdentity(request))) throw new _src_core.StudyError("CUSTODY", "Exchange differs from its declared order");
|
|
1778
|
+
const bytes = Buffer.from(request.body, "base64");
|
|
1779
|
+
if (request.stage === "execution") {
|
|
1780
|
+
const step = declaration.procedure.find((entry) => entry.ordinal === request.step);
|
|
1781
|
+
if (step === void 0 || (step.admission === "exact" ? !bytes.equals(Buffer.from(step.body)) || request.digest !== step.input : bytes.length !== step.bytes)) throw new _src_core.StudyError("CUSTODY", "Exchange request differs from its declared admission");
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (request.stage === "maintenance") {
|
|
1785
|
+
const model = declaration.maintenance[request.ordinal - 1];
|
|
1786
|
+
const body = (0, _orkestrel_contract.parseJSON)(bytes.toString("utf8"));
|
|
1787
|
+
if (model === void 0 || !(0, _orkestrel_contract.isRecord)(body) || body.model !== model.name || body.keep_alive !== 0) throw new _src_core.StudyError("CUSTODY", "Maintenance request differs from its declaration");
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
const models = Object.values(declaration.models).filter((model, index, entries) => entries.findIndex((entry) => entry.name === model.name) === index);
|
|
1791
|
+
const population = models.length + 2;
|
|
1792
|
+
const model = models[request.ordinal - declaration.maintenance.length - (request.stage === "postflight" ? population + declaration.procedure.length : 0) - 1 - 2];
|
|
1793
|
+
const body = request.path === "/api/show" && model !== void 0 ? JSON.stringify({ model: model.name }) : "";
|
|
1794
|
+
if (!bytes.equals(Buffer.from(body))) throw new _src_core.StudyError("CUSTODY", "Metadata request differs from its declaration");
|
|
1795
|
+
}
|
|
1796
|
+
/** Validates a serialized journal prefix or its complete successful population.
|
|
1797
|
+
* @param rows - The owned rows in physical append order.
|
|
1798
|
+
* @param complete - If true, requires successful completion; if false, admits an unfinished prefix.
|
|
1799
|
+
* @returns Nothing when every row joins its preceding attempt and declared sequence.
|
|
1800
|
+
*/
|
|
1801
|
+
function validateStudyExchangeRows(rows, complete = true) {
|
|
1802
|
+
const declaration = rows[0];
|
|
1803
|
+
if (declaration?.phase !== "declaration" || !rows.every(isStudyExchangeRow)) throw new _src_core.StudyError("CUSTODY", "Exchange declaration or row is invalid");
|
|
1804
|
+
const attempts = rows.filter((row) => row.phase === "attempt");
|
|
1805
|
+
for (const [index, row] of rows.entries()) {
|
|
1806
|
+
if (index === 0) continue;
|
|
1807
|
+
const previous = rows[index - 1];
|
|
1808
|
+
if (row.phase === "attempt") {
|
|
1809
|
+
if (previous?.phase !== "declaration" && previous?.phase !== "response") throw new _src_core.StudyError("CUSTODY", "Exchange attempt overlaps an unfinished attempt");
|
|
1810
|
+
if (row.ordinal !== rows.slice(0, index).filter((entry) => entry.phase === "attempt").length + 1) throw new _src_core.StudyError("CUSTODY", "Exchange ordinals are not contiguous");
|
|
1811
|
+
validateStudyExchangeRequest(declaration, row);
|
|
1812
|
+
} else if (row.phase === "response" || row.phase === "failure") {
|
|
1813
|
+
if (previous?.phase !== "attempt" || (0, _orkestrel_contract.canonicalStringify)(projectStudyExchangeIdentity(previous)) !== (0, _orkestrel_contract.canonicalStringify)(projectStudyExchangeIdentity(row))) throw new _src_core.StudyError("CUSTODY", "Exchange terminal has no matching attempt");
|
|
1814
|
+
} else if (row.phase === "complete") {
|
|
1815
|
+
if (index !== rows.length - 1 || previous?.phase !== "response" || row.count !== attempts.length || row.count !== buildStudyExchangeSequence(declaration).length || rows.some((entry) => entry.phase === "failure")) throw new _src_core.StudyError("CUSTODY", "Exchange completion is partial or failed");
|
|
1816
|
+
} else throw new _src_core.StudyError("CUSTODY", "Exchange declaration repeats");
|
|
1817
|
+
}
|
|
1818
|
+
if (complete && rows.at(-1)?.phase !== "complete") throw new _src_core.StudyError("CUSTODY", "Exchange history has no successful completion");
|
|
1819
|
+
}
|
|
1820
|
+
/** Reads strict complete LF-framed exchange rows with verified bodies and declaration joins.
|
|
1821
|
+
* @param bytes - The bounded retained sidecar bytes.
|
|
1822
|
+
* @param complete - If true, requires completion; if false, admits a verified unfinished prefix.
|
|
1823
|
+
* @returns Owned rows in physical order.
|
|
1824
|
+
*/
|
|
1825
|
+
function readStudyExchangeRows(bytes, complete = true) {
|
|
1826
|
+
try {
|
|
1827
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
1828
|
+
if (!text.endsWith("\n")) throw new _src_core.StudyError("CUSTODY", "Exchange sidecar ends with a partial record");
|
|
1829
|
+
const rows = text.split(/\r\n|\n/).slice(0, -1).map((line) => {
|
|
1830
|
+
const row = (0, _orkestrel_contract.parseJSON)(line);
|
|
1831
|
+
if (!isStudyExchangeRow(row)) throw new _src_core.StudyError("CUSTODY", "Exchange sidecar contains an invalid row");
|
|
1832
|
+
return row;
|
|
1833
|
+
});
|
|
1834
|
+
validateStudyExchangeRows(rows, complete);
|
|
1835
|
+
return rows;
|
|
1836
|
+
} catch (error) {
|
|
1837
|
+
throw new _src_core.StudyError("CUSTODY", error instanceof Error ? error.message : "Invalid exchange bytes");
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
/** Validates complete history envelopes again before a replay may perform dependent work.
|
|
1841
|
+
* @param history - The strict-reader result, treated as untrusted at this boundary.
|
|
1842
|
+
* @returns Nothing after complete row, file and declaration verification.
|
|
1843
|
+
*/
|
|
1844
|
+
function validateStudyExchangeHistory(history) {
|
|
1845
|
+
if (!isStudyExchangeHistory(history) || history.file.path !== `trials/${history.declaration.trial}/exchanges.jsonl`) throw new _src_core.StudyError("CUSTODY", "Replay requires a verified sealed exchange history");
|
|
1846
|
+
const rows = [
|
|
1847
|
+
history.declaration,
|
|
1848
|
+
...history.exchanges.flatMap((pair) => [pair.attempt, pair.response]),
|
|
1849
|
+
history.completion
|
|
1850
|
+
];
|
|
1851
|
+
validateStudyExchangeRows(rows);
|
|
1852
|
+
const bytes = Buffer.from(rows.map((row) => JSON.stringify(row) + "\n").join(""));
|
|
1853
|
+
if (history.file.bytes !== bytes.length || history.file.digest !== hashStudyBytes(bytes)) throw new _src_core.StudyError("CUSTODY", "Replay history differs from its retained file identity");
|
|
1854
|
+
}
|
|
1855
|
+
/** Joins recorded exchanges to exactly one dispatch and wire evidence identity per ordinal.
|
|
1856
|
+
* @param rows - The complete successful sidecar rows.
|
|
1857
|
+
* @param evidence - The validated trial evidence rows.
|
|
1858
|
+
* @returns Nothing when all retained exchange identities have exact evidence counterparts.
|
|
1859
|
+
*/
|
|
1860
|
+
function validateStudyExchangeEvidence(rows, evidence) {
|
|
1861
|
+
const attempts = rows.filter((row) => row.phase === "attempt");
|
|
1862
|
+
for (const event of ["dispatch", "wire"]) {
|
|
1863
|
+
const joined = evidence.filter((row) => row.event === event && (0, _orkestrel_contract.isRecord)(row.value) && row.value.ordinal !== void 0);
|
|
1864
|
+
if (joined.length !== attempts.length || !joined.every((row, index) => {
|
|
1865
|
+
const request = attempts[index];
|
|
1866
|
+
return request !== void 0 && (0, _orkestrel_contract.isRecord)(row.value) && row.value.ordinal === request.ordinal && row.goal === request.goal && row.value.role === request.role && row.value.path === request.path;
|
|
1867
|
+
})) throw new _src_core.StudyError("CUSTODY", "Exchange sidecar and evidence ordinals do not join exactly");
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
/** Reads and joins a complete trial sidecar before or after sealing.
|
|
1871
|
+
* @param directory - The result directory.
|
|
1872
|
+
* @param manifest - The verified manifest declaring recording and bounds.
|
|
1873
|
+
* @param trial - The exact scheduled trial id.
|
|
1874
|
+
* @param file - The inventory identity read for this sidecar.
|
|
1875
|
+
* @returns Complete rows after byte, declaration and evidence joins.
|
|
1876
|
+
*/
|
|
1877
|
+
async function readStudyTrialExchanges(directory, manifest, trial, file) {
|
|
1878
|
+
if (manifest.record === void 0 || file.path !== `trials/${trial}/exchanges.jsonl`) throw new _src_core.StudyError("CUSTODY", "Sidecar has no recording declaration");
|
|
1879
|
+
const bytes = await readStudyBytes((0, node_path.join)(directory, "execution", "trials", trial, "exchanges.jsonl"), manifest.record.bytes);
|
|
1880
|
+
if (bytes.length !== file.bytes || hashStudyBytes(bytes) !== file.digest) throw new _src_core.StudyError("CUSTODY", "Exchange sidecar changed during reading");
|
|
1881
|
+
const rows = readStudyExchangeRows(bytes);
|
|
1882
|
+
const declaration = rows[0];
|
|
1883
|
+
const scheduled = manifest.schedule.find((entry) => entry.id === trial);
|
|
1884
|
+
const scenario = manifest.scenarios.find((entry) => entry.id === scheduled?.scenario);
|
|
1885
|
+
if (declaration?.phase !== "declaration" || declaration.trial !== trial || scenario === void 0 || !declaration.procedure.every((step) => scenario.goals.some((goal) => goal.id === step.goal))) throw new _src_core.StudyError("CUSTODY", "Exchange declaration differs from its trial");
|
|
1886
|
+
const evidenceBytes = await readStudyBytes((0, node_path.join)(directory, "execution", "trials", trial, "evidence.jsonl"), manifest.bounds.trial.bytes);
|
|
1887
|
+
validateStudyEvidenceBytes(evidenceBytes, manifest, trial);
|
|
1888
|
+
validateStudyExchangeEvidence(rows, new TextDecoder("utf-8", { fatal: true }).decode(evidenceBytes).split(/\r\n|\n/).slice(0, -1).map((line) => {
|
|
1889
|
+
const row = (0, _orkestrel_contract.parseJSON)(line);
|
|
1890
|
+
if (!isStudyCustodyEvidence(row)) throw new _src_core.StudyError("CUSTODY", "Invalid exchange evidence");
|
|
1891
|
+
return row;
|
|
1892
|
+
}));
|
|
1893
|
+
return rows;
|
|
1894
|
+
}
|
|
1895
|
+
/** Reads a sealed trial's exact complete exchange history after reverifying its entire result.
|
|
1896
|
+
* @param result - The sealed execution selected for replay.
|
|
1897
|
+
* @param trial - The scheduled trial id.
|
|
1898
|
+
* @returns Complete ordered request and response pairs with their verified file and seal identities.
|
|
1899
|
+
*/
|
|
1900
|
+
async function readStudyExchanges(result, trial) {
|
|
1901
|
+
const reading = await readStudyResult(result.directory);
|
|
1902
|
+
if (reading.status !== "sealed" || reading.seal.id !== result.seal.id) throw new _src_core.StudyError("CUSTODY", "Replay result is unsealed or changed");
|
|
1903
|
+
const file = reading.seal.files.find((entry) => entry.path === `trials/${trial}/exchanges.jsonl`);
|
|
1904
|
+
if (file === void 0) throw new _src_core.StudyError("CUSTODY", "Recorded trial is missing its exchange sidecar");
|
|
1905
|
+
const rows = await readStudyTrialExchanges(reading.directory, reading.manifest, trial, file);
|
|
1906
|
+
const declaration = rows[0];
|
|
1907
|
+
const completion = rows.at(-1);
|
|
1908
|
+
if (declaration?.phase !== "declaration" || completion?.phase !== "complete") throw new _src_core.StudyError("CUSTODY", "Exchange boundaries are missing");
|
|
1909
|
+
const exchanges = rows.flatMap((row, index) => {
|
|
1910
|
+
const response = rows[index + 1];
|
|
1911
|
+
return row.phase === "attempt" && response?.phase === "response" ? [{
|
|
1912
|
+
attempt: row,
|
|
1913
|
+
response
|
|
1914
|
+
}] : [];
|
|
1915
|
+
});
|
|
1916
|
+
const history = {
|
|
1917
|
+
seal: reading.seal.id,
|
|
1918
|
+
file,
|
|
1919
|
+
declaration,
|
|
1920
|
+
exchanges,
|
|
1921
|
+
completion
|
|
1922
|
+
};
|
|
1923
|
+
validateStudyExchangeHistory(history);
|
|
1924
|
+
return history;
|
|
1925
|
+
}
|
|
1204
1926
|
//#endregion
|
|
1205
1927
|
//#region src/server/stores/StudyEvidenceSink.ts
|
|
1206
1928
|
/** Retains admitted evidence in order and closes admission before terminal records. */
|
|
@@ -1288,6 +2010,93 @@ var StudyEvidenceSink = class {
|
|
|
1288
2010
|
}
|
|
1289
2011
|
};
|
|
1290
2012
|
//#endregion
|
|
2013
|
+
//#region src/server/stores/StudyExchangeSink.ts
|
|
2014
|
+
/** Retains ordered exchange rows under a parent-reserved serialized byte allowance. */
|
|
2015
|
+
var StudyExchangeSink = class {
|
|
2016
|
+
#options;
|
|
2017
|
+
#writes = /* @__PURE__ */ new Set();
|
|
2018
|
+
#rows = [];
|
|
2019
|
+
#bytes = 0;
|
|
2020
|
+
#fault;
|
|
2021
|
+
#closing;
|
|
2022
|
+
/** Owns the descriptor and restores a verified prefix without rewriting it.
|
|
2023
|
+
* @param options - The exclusive descriptor, byte bound and optional retained rows.
|
|
2024
|
+
*/
|
|
2025
|
+
constructor(options) {
|
|
2026
|
+
if (!Number.isSafeInteger(options.record.bytes) || options.record.bytes <= 0) throw new _src_core.StudyError("CONFIG", "Invalid exchange byte allowance");
|
|
2027
|
+
this.#options = {
|
|
2028
|
+
...options,
|
|
2029
|
+
record: { ...options.record }
|
|
2030
|
+
};
|
|
2031
|
+
if (options.retained !== void 0) {
|
|
2032
|
+
const rows = structuredClone(options.retained);
|
|
2033
|
+
validateStudyExchangeRows(rows, false);
|
|
2034
|
+
this.#rows = rows;
|
|
2035
|
+
this.#bytes = rows.reduce((total, row) => total + Buffer.byteLength(JSON.stringify(row) + "\n"), 0);
|
|
2036
|
+
if (this.#bytes > this.#options.record.bytes) throw new _src_core.StudyError("CAPACITY", "Retained exchange prefix exceeds its allowance");
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
/** Holds the next contiguous one-based HTTP ordinal, including the retained prefix. */
|
|
2040
|
+
get next() {
|
|
2041
|
+
return this.#rows.filter((row) => row.phase === "attempt").length + 1;
|
|
2042
|
+
}
|
|
2043
|
+
/** Appends one owned row after validating order and reserving its complete serialized size.
|
|
2044
|
+
* @param row - The next declaration, attempt, terminal or completion.
|
|
2045
|
+
* @returns Completion after synchronized retention; callers await each append before proceeding.
|
|
2046
|
+
*/
|
|
2047
|
+
append(row) {
|
|
2048
|
+
try {
|
|
2049
|
+
if (this.#closing !== void 0 || this.#writes.size !== 0) throw new _src_core.StudyError("CUSTODY", "Exchange append requires drained open admission");
|
|
2050
|
+
const text = JSON.stringify(row) + "\n";
|
|
2051
|
+
const owned = (0, _orkestrel_contract.parseJSON)(text);
|
|
2052
|
+
if (!isStudyExchangeRow(owned)) throw new _src_core.StudyError("CUSTODY", "Invalid exchange row");
|
|
2053
|
+
if (this.#fault !== void 0 && owned.phase !== "failure") throw this.#fault.error;
|
|
2054
|
+
const rows = [...this.#rows, owned];
|
|
2055
|
+
validateStudyExchangeRows(rows, false);
|
|
2056
|
+
const bytes = Buffer.from(text);
|
|
2057
|
+
const reservation = owned.phase === "attempt" ? Buffer.byteLength(JSON.stringify({
|
|
2058
|
+
...owned,
|
|
2059
|
+
phase: "failure",
|
|
2060
|
+
reason: "Exchange failed",
|
|
2061
|
+
body: void 0,
|
|
2062
|
+
digest: void 0,
|
|
2063
|
+
size: void 0
|
|
2064
|
+
}) + "\n") + 1024 : 0;
|
|
2065
|
+
if (this.#bytes + bytes.length + reservation > this.#options.record.bytes) throw new _src_core.StudyError("CAPACITY", "Serialized exchange bytes exceed the trial allowance");
|
|
2066
|
+
this.#rows = rows;
|
|
2067
|
+
this.#bytes += bytes.length;
|
|
2068
|
+
const pending = this.#write(bytes);
|
|
2069
|
+
this.#writes.add(pending);
|
|
2070
|
+
pending.then(() => this.#writes.delete(pending), () => this.#writes.delete(pending));
|
|
2071
|
+
return pending;
|
|
2072
|
+
} catch (error) {
|
|
2073
|
+
this.#fault ??= { error };
|
|
2074
|
+
return Promise.reject(error);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
/** Closes admission, drains accepted writes and synchronizes the owned descriptor.
|
|
2078
|
+
* @returns Completion after descriptor release, with a retained refusal propagated.
|
|
2079
|
+
*/
|
|
2080
|
+
close() {
|
|
2081
|
+
this.#closing ??= Promise.allSettled([...this.#writes]).then(async () => {
|
|
2082
|
+
await this.#options.file.sync();
|
|
2083
|
+
if (this.#fault !== void 0) throw this.#fault.error;
|
|
2084
|
+
}).finally(async () => {
|
|
2085
|
+
await this.#options.file.close();
|
|
2086
|
+
});
|
|
2087
|
+
return this.#closing;
|
|
2088
|
+
}
|
|
2089
|
+
async #write(bytes) {
|
|
2090
|
+
try {
|
|
2091
|
+
await this.#options.file.writeFile(bytes);
|
|
2092
|
+
await this.#options.file.sync();
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
this.#fault ??= { error };
|
|
2095
|
+
throw error;
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
//#endregion
|
|
1291
2100
|
//#region src/server/stores/StudyResultStore.ts
|
|
1292
2101
|
/** Owns append-only execution bytes and seals only a complete terminal population. */
|
|
1293
2102
|
var StudyResultStore = class {
|
|
@@ -1295,6 +2104,7 @@ var StudyResultStore = class {
|
|
|
1295
2104
|
#manifest;
|
|
1296
2105
|
#trials = /* @__PURE__ */ new Map();
|
|
1297
2106
|
#starting = /* @__PURE__ */ new Set();
|
|
2107
|
+
#exchanges = /* @__PURE__ */ new Map();
|
|
1298
2108
|
#phase = "new";
|
|
1299
2109
|
#bytes = 0;
|
|
1300
2110
|
/**
|
|
@@ -1332,6 +2142,21 @@ var StudyResultStore = class {
|
|
|
1332
2142
|
throw error;
|
|
1333
2143
|
}
|
|
1334
2144
|
}
|
|
2145
|
+
/** Reserves a trial's entire exchange allowance before any worker starts.
|
|
2146
|
+
* @param trial - The exact scheduled trial whose manifest declares recording.
|
|
2147
|
+
* @returns Its exclusive sidecar destination, without creating the trial directory.
|
|
2148
|
+
*/
|
|
2149
|
+
async exchanges(trial) {
|
|
2150
|
+
const scheduled = this.#manifest.schedule.find((entry) => entry.id === trial.id);
|
|
2151
|
+
if (this.#phase !== "ready" || !isStudyCustodyTrial(trial) || scheduled === void 0 || this.#manifest.record === void 0 || (0, _orkestrel_contract.canonicalStringify)(scheduled) !== (0, _orkestrel_contract.canonicalStringify)(trial) || this.#exchanges.has(trial.id) || this.#trials.has(trial.id) || this.#starting.has(trial.id)) throw new _src_core.StudyError("CUSTODY", "Trial sidecar cannot be reserved");
|
|
2152
|
+
this.#reserve(this.#manifest.record.bytes);
|
|
2153
|
+
const destination = Object.freeze({
|
|
2154
|
+
path: (0, node_path.join)(this.#directory, "execution", "trials", scheduled.id, "exchanges.jsonl"),
|
|
2155
|
+
record: Object.freeze({ ...this.#manifest.record })
|
|
2156
|
+
});
|
|
2157
|
+
this.#exchanges.set(scheduled.id, destination);
|
|
2158
|
+
return destination;
|
|
2159
|
+
}
|
|
1335
2160
|
/**
|
|
1336
2161
|
* Exclusively creates the evidence sink for an exact scheduled trial.
|
|
1337
2162
|
* @param trial - The scheduled trajectory
|
|
@@ -1342,7 +2167,7 @@ var StudyResultStore = class {
|
|
|
1342
2167
|
if (this.#phase !== "ready" || !isStudyCustodyTrial(trial) || this.#trials.has(trial.id) || this.#starting.has(trial.id)) throw new _src_core.StudyError("CUSTODY", "Trial cannot start in the current state");
|
|
1343
2168
|
const scheduled = this.#manifest.schedule.find((entry) => entry.id === trial.id);
|
|
1344
2169
|
const scenario = this.#manifest.scenarios.find((entry) => entry.id === trial.scenario);
|
|
1345
|
-
if (scheduled === void 0 || scenario === void 0 || (0, _orkestrel_contract.canonicalStringify)(scheduled) !== (0, _orkestrel_contract.canonicalStringify)(trial)) throw new _src_core.StudyError("CUSTODY", "Trial differs from the execution schedule");
|
|
2170
|
+
if (scheduled === void 0 || scenario === void 0 || this.#manifest.record !== void 0 && !this.#exchanges.has(trial.id) || (0, _orkestrel_contract.canonicalStringify)(scheduled) !== (0, _orkestrel_contract.canonicalStringify)(trial)) throw new _src_core.StudyError("CUSTODY", "Trial differs from the execution schedule");
|
|
1346
2171
|
this.#starting.add(scheduled.id);
|
|
1347
2172
|
try {
|
|
1348
2173
|
const directory = (0, node_path.join)(this.#directory, "execution", "trials", scheduled.id);
|
|
@@ -1404,8 +2229,15 @@ var StudyResultStore = class {
|
|
|
1404
2229
|
const reading = await readStudyResult(this.#directory);
|
|
1405
2230
|
if (reading.status !== "interrupted" || reading.manifest === void 0 || reading.attempts.length !== this.#manifest.schedule.length || (0, _orkestrel_contract.canonicalStringify)(reading.manifest) !== (0, _orkestrel_contract.canonicalStringify)(this.#manifest)) throw new _src_core.StudyError("CUSTODY", "Execution changed before sealing");
|
|
1406
2231
|
const files = await scanStudyExecution((0, node_path.join)(this.#directory, "execution"), this.#manifest);
|
|
1407
|
-
if (files.length !== 1 + this.#manifest.schedule.length * 2) throw new _src_core.StudyError("CUSTODY", "Execution inventory is incomplete");
|
|
1408
|
-
for (const trial of this.#manifest.schedule)
|
|
2232
|
+
if (files.length !== 1 + this.#manifest.schedule.length * (this.#manifest.record === void 0 ? 2 : 3)) throw new _src_core.StudyError("CUSTODY", "Execution inventory is incomplete");
|
|
2233
|
+
for (const trial of this.#manifest.schedule) {
|
|
2234
|
+
if (this.#manifest.record !== void 0) {
|
|
2235
|
+
const file = files.find((entry) => entry.path === `trials/${trial.id}/exchanges.jsonl`);
|
|
2236
|
+
if (file === void 0 || !this.#exchanges.has(trial.id)) throw new _src_core.StudyError("CUSTODY", "Recorded trial has no reserved complete sidecar");
|
|
2237
|
+
await readStudyTrialExchanges(this.#directory, this.#manifest, trial.id, file);
|
|
2238
|
+
}
|
|
2239
|
+
validateStudyEvidenceBytes(await readStudyBytes((0, node_path.join)(this.#directory, "execution", "trials", trial.id, "evidence.jsonl"), this.#manifest.bounds.trial.bytes), this.#manifest, trial.id);
|
|
2240
|
+
}
|
|
1409
2241
|
const seal = Object.freeze({
|
|
1410
2242
|
result: this.#manifest.id,
|
|
1411
2243
|
id: computeStudySealId(this.#manifest.id, files),
|
|
@@ -1602,79 +2434,187 @@ var STUDY_COMMAND_DURATION = 864e5;
|
|
|
1602
2434
|
/** Names the trusted command's independent offline predicate. */
|
|
1603
2435
|
var STUDY_COMMAND_GRADER = "consumer-adapter";
|
|
1604
2436
|
/** Defines the complete public CLI reference shared by help and grammar diagnostics. */
|
|
1605
|
-
var STUDY_COMMAND_HELP = `study check <study> [--profile <name>]
|
|
1606
|
-
study
|
|
2437
|
+
var STUDY_COMMAND_HELP = `study check <study> [--profile <name>] [--closure <directory>]
|
|
2438
|
+
study freeze <study> --profile <name> --to <directory>
|
|
2439
|
+
study execute <study> [--profile <name>] [--out <path>] [--expected <sha256>] [--closure <directory>] [--replay <result> --prefix <JSON trial counts> --offline <true|false>]
|
|
1607
2440
|
study grade <result> --rubric <path>
|
|
1608
2441
|
study label <result> --grading <sha256> --labels <path>
|
|
1609
2442
|
study report <result> --grading <sha256>
|
|
1610
2443
|
study --help
|
|
1611
2444
|
study --version
|
|
1612
2445
|
|
|
1613
|
-
Profiles default to local. Flags require values and may appear only once.
|
|
2446
|
+
Profiles default to local; freeze requires --profile. Flags require values and may appear only once.
|
|
1614
2447
|
Results default to tmp/studies/<result>; scratch also uses tmp/studies.
|
|
1615
2448
|
A result is its id under tmp/studies, a root-relative path, or an absolute path.
|
|
1616
2449
|
Relative paths, including --out, must stay inside the consumer working directory.
|
|
1617
2450
|
Output directories must be new; existing results are never overwritten.
|
|
1618
2451
|
--expected pins the fingerprint returned by check. --grading selects an exact grading.
|
|
1619
2452
|
--rubric selects independent criteria. --labels imports caller-supplied semantic labels.
|
|
2453
|
+
--closure selects retained artifacts. Replay requires explicit prefix counts and offline behavior.
|
|
1620
2454
|
Success writes one JSON document; next contains suggested argv, never executed automatically.
|
|
1621
|
-
check, grade, label and report have a fixed allowance of 60 seconds.
|
|
2455
|
+
check, freeze, grade, label and report have a fixed allowance of 60 seconds.
|
|
1622
2456
|
execute checks first, then uses the declared series duration plus 15 seconds for cleanup.
|
|
1623
2457
|
Exit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 operational failure.
|
|
1624
2458
|
`;
|
|
1625
2459
|
//#endregion
|
|
1626
|
-
//#region src/server/
|
|
1627
|
-
/**
|
|
1628
|
-
* @param
|
|
1629
|
-
* @
|
|
1630
|
-
* @returns The exact declaration bytes under the custody metadata allowance.
|
|
1631
|
-
* @throws StudyError - Thrown with code `CONFIG` when the path is missing or linked; other custody refusals retain their codes.
|
|
1632
|
-
* @example Read declaration bytes
|
|
1633
|
-
* ```ts
|
|
1634
|
-
* import { readStudyDeclarationBytes } from '@orkestrel/study/server'
|
|
1635
|
-
*
|
|
1636
|
-
* declare const root: string
|
|
1637
|
-
* declare const path: string
|
|
1638
|
-
* await readStudyDeclarationBytes(root, path)
|
|
1639
|
-
* ```
|
|
2460
|
+
//#region src/server/processes/validators.ts
|
|
2461
|
+
/** Checks whether a worker destination has an absolute sidecar path and finite reservation.
|
|
2462
|
+
* @param value - The decoded destination.
|
|
2463
|
+
* @returns True if its path, allowance and optional retained identity are valid; false otherwise.
|
|
1640
2464
|
*/
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
const absolute = (0, node_path.resolve)(path);
|
|
1644
|
-
const origin = (0, node_path.parse)(absolute).root;
|
|
1645
|
-
const names = (0, node_path.relative)(origin, absolute).split(node_path.sep).filter(Boolean);
|
|
1646
|
-
for (const [index] of names.entries()) {
|
|
1647
|
-
const candidate = (0, node_path.join)(origin, ...names.slice(0, index + 1));
|
|
1648
|
-
const stat = await (0, node_fs_promises.lstat)(candidate);
|
|
1649
|
-
if (stat.isSymbolicLink()) throw new _src_core.StudyError("CONFIG", `Declaration path is linked: ${(0, node_path.relative)(root, candidate).split(node_path.sep).join("/")}`);
|
|
1650
|
-
if (!stat.isDirectory()) break;
|
|
1651
|
-
}
|
|
1652
|
-
return await readStudyBytes(path, STUDY_CUSTODY_BYTES);
|
|
1653
|
-
} catch (error) {
|
|
1654
|
-
if (matchesSystemError(error, "ENOENT")) throw new _src_core.StudyError("CONFIG", `Declaration is missing: ${(0, node_path.relative)(root, path).split(node_path.sep).join("/")}`);
|
|
1655
|
-
throw error;
|
|
1656
|
-
}
|
|
2465
|
+
function isStudyExchangeDestination(value) {
|
|
2466
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.matchesStudyKeys)(value, ["path", "record"], ["retained"]) && typeof value.path === "string" && (0, node_path.isAbsolute)(value.path) && (0, node_path.basename)(value.path) === "exchanges.jsonl" && (0, _src_core.isStudyRecord)(value.record) && (value.retained === void 0 || isStudyCustodyFile(value.retained)));
|
|
1657
2467
|
}
|
|
1658
|
-
/**
|
|
1659
|
-
* @param
|
|
1660
|
-
* @
|
|
1661
|
-
* @param signal - Optional cancellation checked between scenario reads.
|
|
1662
|
-
* @returns The parsed declaration, driver bundle, ordered scenarios, fixtures, and selected rubric.
|
|
1663
|
-
* @throws StudyError - Thrown when declarations are missing, invalid, or incompatible.
|
|
1664
|
-
* @example Read a study declaration
|
|
1665
|
-
* ```ts
|
|
1666
|
-
* import type { StudyDeclaration } from '@orkestrel/study/server'
|
|
1667
|
-
* import { readStudyDeclaration } from '@orkestrel/study/server'
|
|
1668
|
-
*
|
|
1669
|
-
* declare const root: string
|
|
1670
|
-
* const declaration: StudyDeclaration = await readStudyDeclaration(root, 'assessment')
|
|
1671
|
-
* ```
|
|
2468
|
+
/** Checks whether replay explicitly selects a sealed directory and per-trial prefix counts.
|
|
2469
|
+
* @param value - The decoded replay selection.
|
|
2470
|
+
* @returns True if result, offline and every prefix are declared; false otherwise.
|
|
1672
2471
|
*/
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2472
|
+
function isStudyReplaySelection(value) {
|
|
2473
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.matchesStudyKeys)(value, [
|
|
2474
|
+
"result",
|
|
2475
|
+
"prefix",
|
|
2476
|
+
"offline"
|
|
2477
|
+
]) && typeof value.result === "string" && value.result.length > 0 && typeof value.offline === "boolean" && (0, _orkestrel_contract.isRecord)(value.prefix) && Object.keys(value.prefix).length > 0 && Object.entries(value.prefix).every(([trial, count]) => isStudyCustodyName(trial) && typeof count === "number" && Number.isSafeInteger(count) && count > 0));
|
|
2478
|
+
}
|
|
2479
|
+
/** Checks the complete transport envelope before domain handling.
|
|
2480
|
+
* @param value - Native IPC decoded value.
|
|
2481
|
+
* @returns True if the envelope is bounded in shape and JSON-compatible; false otherwise.
|
|
2482
|
+
*/
|
|
2483
|
+
function isStudyFrame(value) {
|
|
2484
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && Object.keys(value).length === 3 && Object.hasOwn(value, "id") && Object.hasOwn(value, "operation") && Object.hasOwn(value, "value") && typeof value.id === "number" && Number.isSafeInteger(value.id) && value.id >= 0 && typeof value.operation === "string" && [
|
|
2485
|
+
"open",
|
|
2486
|
+
"goal",
|
|
2487
|
+
"close",
|
|
2488
|
+
"admit",
|
|
2489
|
+
"finish",
|
|
2490
|
+
"fail",
|
|
2491
|
+
"evidence",
|
|
2492
|
+
"reply",
|
|
2493
|
+
"error",
|
|
2494
|
+
"abort"
|
|
2495
|
+
].includes(value.operation) && (0, _orkestrel_contract.isJSONValue)(value.value));
|
|
2496
|
+
}
|
|
2497
|
+
/** Checks operational failure attribution arriving over IPC.
|
|
2498
|
+
* @param value - Decoded failure value.
|
|
2499
|
+
* @returns True if the failure carries a known cause, an optional role, and an optional stop that satisfies `isStudyStop` on a `capacity` or `deadline` cause; false otherwise.
|
|
2500
|
+
*/
|
|
2501
|
+
function isStudyProcessFailure(value) {
|
|
2502
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && typeof value.message === "string" && (0, _src_core.isStudyCause)(value.cause) && (value.role === void 0 || (0, _src_core.isStudyRole)(value.role)) && (value.stop === void 0 || (value.cause === "capacity" || value.cause === "deadline") && (0, _src_core.isStudyStop)(value.stop)));
|
|
2503
|
+
}
|
|
2504
|
+
/** Checks a request's declared role and finite cost ceilings.
|
|
2505
|
+
* @param value - Decoded charge.
|
|
2506
|
+
* @returns True if all charge leaves are valid; false otherwise.
|
|
2507
|
+
*/
|
|
2508
|
+
function isStudyCharge(value) {
|
|
2509
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyRole)(value.role) && typeof value.tokens === "number" && Number.isSafeInteger(value.tokens) && value.tokens >= 0 && typeof value.bytes === "number" && Number.isSafeInteger(value.bytes) && value.bytes >= 0);
|
|
2510
|
+
}
|
|
2511
|
+
/** Checks append-only public evidence.
|
|
2512
|
+
* @param value - Decoded evidence.
|
|
2513
|
+
* @returns True if event, goal and payload are valid; false otherwise.
|
|
2514
|
+
*/
|
|
2515
|
+
function isStudyEvidence(value) {
|
|
2516
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyName)(value.event) && (0, _src_core.isStudyName)(value.goal) && (0, _orkestrel_contract.isJSONValue)(value.value));
|
|
2517
|
+
}
|
|
2518
|
+
/** Checks a fresh trial's declared identifiers.
|
|
2519
|
+
* @param value - Decoded trial.
|
|
2520
|
+
* @returns True if every identifier and repetition is valid; false otherwise.
|
|
2521
|
+
*/
|
|
2522
|
+
function isStudyTrial(value) {
|
|
2523
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyName)(value.id) && (0, _src_core.isStudyName)(value.scenario) && (0, _src_core.isStudyName)(value.arm) && typeof value.repetition === "number" && Number.isSafeInteger(value.repetition) && value.repetition >= 0);
|
|
2524
|
+
}
|
|
2525
|
+
/** Checks the exact digest fields carried by the execution declaration.
|
|
2526
|
+
* @param value - Decoded identity.
|
|
2527
|
+
* @returns True if all required identities are digests; false otherwise.
|
|
2528
|
+
*/
|
|
2529
|
+
function isStudyIdentity(value) {
|
|
2530
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _src_core.isStudyDigest)(value.agent) && (0, _src_core.isStudyDigest)(value.subject) && (value.provider === void 0 || (0, _src_core.isStudyDigest)(value.provider)) && (0, _src_core.isStudyDigest)(value.instrument) && (0, _src_core.isStudyDigest)(value.fixtures) && (0, _src_core.isStudyDigest)(value.profile));
|
|
2531
|
+
}
|
|
2532
|
+
//#endregion
|
|
2533
|
+
//#region src/server/commands/validators.ts
|
|
2534
|
+
/** Checks a closure publication before using its command inputs.
|
|
2535
|
+
* @param value - The decoded closure.json value.
|
|
2536
|
+
* @returns True if relocation, selectors, and both identities are declared; false otherwise.
|
|
2537
|
+
*/
|
|
2538
|
+
function isStudyClosure(value) {
|
|
2539
|
+
return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && isArtifactClosure(value.closure) && (0, _orkestrel_contract.isString)(value.root) && (0, _orkestrel_contract.isString)(value.entry) && (0, _src_core.isStudyName)(value.target) && (0, _src_core.isStudyName)(value.profile) && isStudyIdentity(value.source) && isStudyIdentity(value.identity) && (0, _src_core.isStudyDigest)(value.fingerprint));
|
|
2540
|
+
}
|
|
2541
|
+
//#endregion
|
|
2542
|
+
//#region src/server/commands/helpers.ts
|
|
2543
|
+
/** Reads and independently verifies an exclusively published closure.
|
|
2544
|
+
* @param directory - The canonical closure destination.
|
|
2545
|
+
* @returns The verified publication and its copied physical forest.
|
|
2546
|
+
* @throws StudyError - Thrown when publication or its command inputs are invalid.
|
|
2547
|
+
*/
|
|
2548
|
+
function readStudyClosure(directory) {
|
|
2549
|
+
const marker = readArtifact((0, node_path.join)(directory, "closure.json"), STUDY_CUSTODY_BYTES);
|
|
2550
|
+
const value = decodeStudyJSON(marker.source);
|
|
2551
|
+
if (!isStudyClosure(value) || value.closure.destination !== directory) throw new _src_core.StudyError("CONFIG", "Study closure publication is invalid");
|
|
2552
|
+
verifyArtifactClosure(value.closure, true);
|
|
2553
|
+
if (value.root !== directory && !containsArtifact(directory, value.root) || !value.closure.members.some((member) => (0, node_path.resolve)(directory, ...member.key.split("/")) === value.entry)) throw new _src_core.StudyError("CONFIG", "Study closure command inputs leave the copied forest");
|
|
2554
|
+
if (readArtifact((0, node_path.join)(directory, "closure.json"), 16777216).digest !== marker.digest) throw new _src_core.StudyError("FAULT", "Study closure publication changed while reading");
|
|
2555
|
+
return value;
|
|
2556
|
+
}
|
|
2557
|
+
/** Resolves a profile path through a retained physical relocation when selected.
|
|
2558
|
+
* @param root - The active consumer root.
|
|
2559
|
+
* @param path - The profile's absolute or consumer-relative selection.
|
|
2560
|
+
* @param closure - The optional verified physical forest.
|
|
2561
|
+
* @returns The active canonical selection.
|
|
2562
|
+
*/
|
|
2563
|
+
function resolveStudyClosurePath(root, path, closure) {
|
|
2564
|
+
const absolute = (0, node_path.resolve)(root, path);
|
|
2565
|
+
return closure === void 0 || absolute === closure.destination || containsArtifact(closure.destination, absolute) ? absolute : relocateArtifactPath(closure, absolute);
|
|
2566
|
+
}
|
|
2567
|
+
/** Reads one declaration file and refuses a missing or linked path with its root-relative path.
|
|
2568
|
+
* @param root - The consumer root used to describe a missing or linked declaration.
|
|
2569
|
+
* @param path - The already resolved and contained declaration path.
|
|
2570
|
+
* @returns The exact declaration bytes under the custody metadata allowance.
|
|
2571
|
+
* @throws StudyError - Thrown with code `CONFIG` when the path is missing or linked; other custody refusals retain their codes.
|
|
2572
|
+
* @example Read declaration bytes
|
|
2573
|
+
* ```ts
|
|
2574
|
+
* import { readStudyDeclarationBytes } from '@orkestrel/study/server'
|
|
2575
|
+
*
|
|
2576
|
+
* declare const root: string
|
|
2577
|
+
* declare const path: string
|
|
2578
|
+
* await readStudyDeclarationBytes(root, path)
|
|
2579
|
+
* ```
|
|
2580
|
+
*/
|
|
2581
|
+
async function readStudyDeclarationBytes(root, path) {
|
|
2582
|
+
try {
|
|
2583
|
+
const absolute = (0, node_path.resolve)(path);
|
|
2584
|
+
const origin = (0, node_path.parse)(absolute).root;
|
|
2585
|
+
const names = (0, node_path.relative)(origin, absolute).split(node_path.sep).filter(Boolean);
|
|
2586
|
+
for (const [index] of names.entries()) {
|
|
2587
|
+
const candidate = (0, node_path.join)(origin, ...names.slice(0, index + 1));
|
|
2588
|
+
const stat = await (0, node_fs_promises.lstat)(candidate);
|
|
2589
|
+
if (stat.isSymbolicLink()) throw new _src_core.StudyError("CONFIG", `Declaration path is linked: ${(0, node_path.relative)(root, candidate).split(node_path.sep).join("/")}`);
|
|
2590
|
+
if (!stat.isDirectory()) break;
|
|
2591
|
+
}
|
|
2592
|
+
return await readStudyBytes(path, STUDY_CUSTODY_BYTES);
|
|
2593
|
+
} catch (error) {
|
|
2594
|
+
if (matchesSystemError(error, "ENOENT")) throw new _src_core.StudyError("CONFIG", `Declaration is missing: ${(0, node_path.relative)(root, path).split(node_path.sep).join("/")}`);
|
|
2595
|
+
throw error;
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
/** Reads a study declaration, its driver's scenarios, and its rubric as the command resolves them.
|
|
2599
|
+
* @param root - The consumer root containing the studies directory.
|
|
2600
|
+
* @param study - The selected study name.
|
|
2601
|
+
* @param signal - Optional cancellation checked between scenario reads.
|
|
2602
|
+
* @returns The parsed declaration, driver bundle, ordered scenarios, fixtures, and selected rubric.
|
|
2603
|
+
* @throws StudyError - Thrown when declarations are missing, invalid, or incompatible.
|
|
2604
|
+
* @example Read a study declaration
|
|
2605
|
+
* ```ts
|
|
2606
|
+
* import type { StudyDeclaration } from '@orkestrel/study/server'
|
|
2607
|
+
* import { readStudyDeclaration } from '@orkestrel/study/server'
|
|
2608
|
+
*
|
|
2609
|
+
* declare const root: string
|
|
2610
|
+
* const declaration: StudyDeclaration = await readStudyDeclaration(root, 'assessment')
|
|
2611
|
+
* ```
|
|
2612
|
+
*/
|
|
2613
|
+
async function readStudyDeclaration(root, study, signal) {
|
|
2614
|
+
const directory = resolveStudyPath((0, node_path.join)(root, "studies"), study);
|
|
2615
|
+
const definition = (0, _src_core.parseStudyDefinition)(decodeStudyJSON(await readStudyDeclarationBytes(root, resolveStudyPath(directory, "study.json"))));
|
|
2616
|
+
if (definition.name !== study) throw new _src_core.StudyError("CONFIG", "Study name differs from its selected directory");
|
|
2617
|
+
const bundle = resolveStudyPath((0, node_path.join)(root, "studies"), definition.driver);
|
|
1678
2618
|
const steps = [];
|
|
1679
2619
|
for (const id of definition.scenarios) {
|
|
1680
2620
|
signal?.throwIfAborted();
|
|
@@ -1721,22 +2661,25 @@ function resolveStudyPath(directory, path) {
|
|
|
1721
2661
|
* Resolves the execution profile's caller-selected built artifact paths.
|
|
1722
2662
|
* @param root - The repository root
|
|
1723
2663
|
* @param profile - The validated source profile
|
|
2664
|
+
* @param closure - The optional verified physical forest used to relocate executable selections
|
|
1724
2665
|
* @returns The reproducible resolved execution profile
|
|
1725
2666
|
* @example
|
|
1726
2667
|
* ```ts
|
|
1727
2668
|
* resolveStudyProfile(root, profile)
|
|
1728
2669
|
* ```
|
|
1729
2670
|
*/
|
|
1730
|
-
function resolveStudyProfile(root, profile) {
|
|
2671
|
+
function resolveStudyProfile(root, profile, closure) {
|
|
2672
|
+
const daemon = profile.provider?.daemon;
|
|
2673
|
+
if (daemon !== void 0 && (!(0, node_path.isAbsolute)(daemon.executable) || (0, node_fs.realpathSync)(daemon.executable) !== daemon.executable)) throw new _src_core.StudyError("CONFIG", "Daemon executable must name its canonical absolute path");
|
|
1731
2674
|
return {
|
|
1732
2675
|
...profile,
|
|
1733
2676
|
qualification: profile.qualification === void 0 ? void 0 : (0, node_path.resolve)(root, profile.qualification),
|
|
1734
|
-
agent: (
|
|
1735
|
-
subject: (
|
|
1736
|
-
roots: profile.roots.map((path) => (
|
|
2677
|
+
agent: resolveStudyClosurePath(root, profile.agent, closure),
|
|
2678
|
+
subject: resolveStudyClosurePath(root, profile.subject, closure),
|
|
2679
|
+
roots: profile.roots.map((path) => resolveStudyClosurePath(root, path, closure)),
|
|
1737
2680
|
provider: profile.provider === void 0 ? void 0 : {
|
|
1738
2681
|
...profile.provider,
|
|
1739
|
-
entry: (
|
|
2682
|
+
entry: resolveStudyClosurePath(root, profile.provider.entry, closure)
|
|
1740
2683
|
}
|
|
1741
2684
|
};
|
|
1742
2685
|
}
|
|
@@ -1998,20 +2941,25 @@ function computeStudyReport(result, rubric, grading) {
|
|
|
1998
2941
|
function parseStudyArguments(arguments_) {
|
|
1999
2942
|
const command = arguments_[0];
|
|
2000
2943
|
const target = arguments_[1];
|
|
2001
|
-
const usage = "study check <study> [--profile <name>]\nstudy execute <study> [--profile <name>] [--out <path>] [--expected <sha256>]\nstudy grade <result> --rubric <path>\nstudy label <result> --grading <sha256> --labels <path>\nstudy report <result> --grading <sha256>\nstudy --help\nstudy --version\n\nProfiles default to local. Flags require values and may appear only once.\nResults default to tmp/studies/<result>; scratch also uses tmp/studies.\nA result is its id under tmp/studies, a root-relative path, or an absolute path.\nRelative paths, including --out, must stay inside the consumer working directory.\nOutput directories must be new; existing results are never overwritten.\n--expected pins the fingerprint returned by check. --grading selects an exact grading.\n--rubric selects independent criteria. --labels imports caller-supplied semantic labels.\nSuccess writes one JSON document; next contains suggested argv, never executed automatically.\ncheck, grade, label and report have a fixed allowance of 60 seconds.\nexecute checks first, then uses the declared series duration plus 15 seconds for cleanup.\nExit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 operational failure.\n".split("\n").find((line) => line.startsWith("study " + command + " ")) ?? "Expected study check, execute, grade, label, or report; use study --help.";
|
|
2944
|
+
const usage = "study check <study> [--profile <name>] [--closure <directory>]\nstudy freeze <study> --profile <name> --to <directory>\nstudy execute <study> [--profile <name>] [--out <path>] [--expected <sha256>] [--closure <directory>] [--replay <result> --prefix <JSON trial counts> --offline <true|false>]\nstudy grade <result> --rubric <path>\nstudy label <result> --grading <sha256> --labels <path>\nstudy report <result> --grading <sha256>\nstudy --help\nstudy --version\n\nProfiles default to local; freeze requires --profile. Flags require values and may appear only once.\nResults default to tmp/studies/<result>; scratch also uses tmp/studies.\nA result is its id under tmp/studies, a root-relative path, or an absolute path.\nRelative paths, including --out, must stay inside the consumer working directory.\nOutput directories must be new; existing results are never overwritten.\n--expected pins the fingerprint returned by check. --grading selects an exact grading.\n--rubric selects independent criteria. --labels imports caller-supplied semantic labels.\n--closure selects retained artifacts. Replay requires explicit prefix counts and offline behavior.\nSuccess writes one JSON document; next contains suggested argv, never executed automatically.\ncheck, freeze, grade, label and report have a fixed allowance of 60 seconds.\nexecute checks first, then uses the declared series duration plus 15 seconds for cleanup.\nExit codes: 0 success, 64 invalid input, 124 deadline, 130 interruption, 1 operational failure.\n".split("\n").find((line) => line.startsWith("study " + command + " ")) ?? "Expected study check, freeze, execute, grade, label, or report; use study --help.";
|
|
2002
2945
|
if (![
|
|
2003
2946
|
"check",
|
|
2947
|
+
"freeze",
|
|
2004
2948
|
"execute",
|
|
2005
2949
|
"grade",
|
|
2006
2950
|
"label",
|
|
2007
2951
|
"report"
|
|
2008
2952
|
].includes(command ?? "")) throw new _src_core.StudyError("CONFIG", "Unknown or missing command " + JSON.stringify(command ?? "<command>") + ". " + usage);
|
|
2009
|
-
if (target === void 0 || target === "" || target.startsWith("-")) throw new _src_core.StudyError("CONFIG", "Missing <" + (command === "check" || command === "execute" ? "study" : "result") + "> before " + JSON.stringify(target ?? "<end>") + ". " + usage);
|
|
2010
|
-
const allowed = command === "check" ? ["--profile"] : command === "execute" ? [
|
|
2953
|
+
if (target === void 0 || target === "" || target.startsWith("-")) throw new _src_core.StudyError("CONFIG", "Missing <" + (command === "check" || command === "execute" || command === "freeze" ? "study" : "result") + "> before " + JSON.stringify(target ?? "<end>") + ". " + usage);
|
|
2954
|
+
const allowed = command === "check" ? ["--profile", "--closure"] : command === "execute" ? [
|
|
2011
2955
|
"--profile",
|
|
2012
2956
|
"--out",
|
|
2013
|
-
"--expected"
|
|
2014
|
-
|
|
2957
|
+
"--expected",
|
|
2958
|
+
"--closure",
|
|
2959
|
+
"--replay",
|
|
2960
|
+
"--prefix",
|
|
2961
|
+
"--offline"
|
|
2962
|
+
] : command === "freeze" ? ["--profile", "--to"] : command === "grade" ? ["--rubric"] : command === "label" ? ["--grading", "--labels"] : ["--grading"];
|
|
2015
2963
|
const tokens = arguments_.slice(2);
|
|
2016
2964
|
const pairs = [...tokens.entries()].flatMap(([index, flag]) => index % 2 === 0 ? [{
|
|
2017
2965
|
flag,
|
|
@@ -2024,24 +2972,50 @@ function parseStudyArguments(arguments_) {
|
|
|
2024
2972
|
if (value === void 0 || value === "" || value.startsWith("-")) throw new _src_core.StudyError("CONFIG", flag + " requires a value before " + JSON.stringify(value ?? "<end>") + ". " + usage);
|
|
2025
2973
|
}
|
|
2026
2974
|
const flags = new Map(pairs.flatMap(({ flag, value }) => flag === void 0 || value === void 0 ? [] : [[flag, value]]));
|
|
2027
|
-
if (command === "check" || command === "execute") {
|
|
2975
|
+
if (command === "check" || command === "execute" || command === "freeze") {
|
|
2028
2976
|
if (!isStudyCustodyName(target)) throw new _src_core.StudyError("CONFIG", "Invalid study token " + JSON.stringify(target) + "; use a portable study name. " + usage);
|
|
2029
2977
|
const profile = flags.get("--profile") ?? "local";
|
|
2030
2978
|
const out = flags.get("--out");
|
|
2031
2979
|
const expected = flags.get("--expected");
|
|
2032
2980
|
if (!isStudyCustodyName(profile)) throw new _src_core.StudyError("CONFIG", "--profile " + JSON.stringify(profile) + " must be a portable name. " + usage);
|
|
2981
|
+
if (command === "freeze") {
|
|
2982
|
+
if (!flags.has("--profile")) throw new _src_core.StudyError("CONFIG", "Missing --profile. " + usage);
|
|
2983
|
+
const destination = flags.get("--to");
|
|
2984
|
+
if (destination === void 0) throw new _src_core.StudyError("CONFIG", "Missing --to. " + usage);
|
|
2985
|
+
if (!(0, node_path.isAbsolute)(destination)) resolveStudyPath((0, node_path.resolve)("."), destination);
|
|
2986
|
+
return {
|
|
2987
|
+
command,
|
|
2988
|
+
target,
|
|
2989
|
+
profile,
|
|
2990
|
+
destination
|
|
2991
|
+
};
|
|
2992
|
+
}
|
|
2033
2993
|
if (out !== void 0 && !(0, node_path.isAbsolute)(out)) try {
|
|
2034
2994
|
resolveStudyPath((0, node_path.resolve)("."), out);
|
|
2035
2995
|
} catch {
|
|
2036
2996
|
throw new _src_core.StudyError("CONFIG", "--out " + JSON.stringify(out) + " must be a portable contained path and cannot escape the consumer root. " + usage);
|
|
2037
2997
|
}
|
|
2038
2998
|
if (expected !== void 0 && !(0, _src_core.isStudyDigest)(expected)) throw new _src_core.StudyError("CONFIG", "--expected " + JSON.stringify(expected) + " must be a SHA-256 fingerprint. " + usage);
|
|
2999
|
+
const closure = flags.get("--closure");
|
|
3000
|
+
if (closure !== void 0 && !(0, node_path.isAbsolute)(closure)) resolveStudyPath((0, node_path.resolve)("."), closure);
|
|
3001
|
+
const result = flags.get("--replay");
|
|
3002
|
+
const prefix = flags.get("--prefix");
|
|
3003
|
+
const offline = flags.get("--offline");
|
|
3004
|
+
if (result === void 0 && (prefix !== void 0 || offline !== void 0)) throw new _src_core.StudyError("CONFIG", "--prefix and --offline require --replay. " + usage);
|
|
3005
|
+
const replay = result === void 0 ? void 0 : (0, _orkestrel_contract.parseJSON)(prefix ?? "");
|
|
3006
|
+
if (result !== void 0 && (!(0, _orkestrel_contract.isRecord)(replay) || Object.keys(replay).length === 0 || !Object.entries(replay).every(([trial, count]) => isStudyCustodyName(trial) && typeof count === "number" && Number.isSafeInteger(count) && count > 0) || offline !== "true" && offline !== "false")) throw new _src_core.StudyError("CONFIG", "--replay requires --prefix trial counts and --offline true or false. " + usage);
|
|
2039
3007
|
return {
|
|
2040
3008
|
command,
|
|
2041
3009
|
target,
|
|
2042
3010
|
profile,
|
|
2043
3011
|
out,
|
|
2044
|
-
expected
|
|
3012
|
+
expected,
|
|
3013
|
+
...closure === void 0 ? {} : { closure },
|
|
3014
|
+
...result === void 0 || !(0, _orkestrel_contract.isRecord)(replay) ? {} : { replay: {
|
|
3015
|
+
result,
|
|
3016
|
+
prefix: Object.fromEntries(Object.entries(replay).map(([trial, count]) => [trial, Number(count)])),
|
|
3017
|
+
offline: offline === "true"
|
|
3018
|
+
} }
|
|
2045
3019
|
};
|
|
2046
3020
|
}
|
|
2047
3021
|
if (command === "grade") {
|
|
@@ -2092,340 +3066,736 @@ var STUDY_COMMAND_OPERATION = "study-command";
|
|
|
2092
3066
|
/** Caps pure domain work independently of its event loop. */
|
|
2093
3067
|
var STUDY_TASK_DURATION = 6e4;
|
|
2094
3068
|
//#endregion
|
|
2095
|
-
//#region src/server/
|
|
2096
|
-
/**
|
|
2097
|
-
*
|
|
2098
|
-
* @
|
|
2099
|
-
* @
|
|
2100
|
-
* ```ts
|
|
2101
|
-
* import { valueToWire } from '@orkestrel/study/server'
|
|
2102
|
-
*
|
|
2103
|
-
* valueToWire({ state: 'ready', failure: undefined }) // { state: 'ready' }
|
|
2104
|
-
* ```
|
|
2105
|
-
*/
|
|
2106
|
-
function valueToWire(value) {
|
|
2107
|
-
const parsed = JSON.parse(JSON.stringify(value));
|
|
2108
|
-
if (!(0, _orkestrel_contract.isJSONValue)(parsed)) throw new _src_core.StudyError("FAULT", "Invalid study wire value");
|
|
2109
|
-
return parsed;
|
|
2110
|
-
}
|
|
2111
|
-
/** Creates attributable protocol infrastructure failure.
|
|
2112
|
-
* @param message - The refused operation or broken boundary.
|
|
2113
|
-
* @returns A domain failure that never awards task credit.
|
|
3069
|
+
//#region src/server/ollama/helpers.ts
|
|
3070
|
+
/**
|
|
3071
|
+
* Declares the pinned serving identities the Ollama transport verifies, keyed by the label their inventory evidence carries.
|
|
3072
|
+
* @param provider - Declared serving identities
|
|
3073
|
+
* @returns The model pin, plus the judge pin when the provider declares both the judge name and its digest
|
|
2114
3074
|
*/
|
|
2115
|
-
function
|
|
3075
|
+
function buildStudyOllamaModels(provider) {
|
|
2116
3076
|
return {
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
3077
|
+
model: {
|
|
3078
|
+
name: provider.model,
|
|
3079
|
+
digest: provider.digest.model
|
|
3080
|
+
},
|
|
3081
|
+
...provider.judge === void 0 || provider.digest.judge === void 0 ? {} : { judge: {
|
|
3082
|
+
name: provider.judge,
|
|
3083
|
+
digest: provider.digest.judge
|
|
3084
|
+
} }
|
|
2121
3085
|
};
|
|
2122
3086
|
}
|
|
2123
|
-
/**
|
|
2124
|
-
*
|
|
2125
|
-
* @
|
|
3087
|
+
/**
|
|
3088
|
+
* Builds the provider options one role's declared wire policy determines.
|
|
3089
|
+
* @remarks
|
|
3090
|
+
* The policy's `options` object keeps its identity and key order. A strict policy adds `truncate`
|
|
3091
|
+
* and `shift` as `false`; any other policy omits both keys. The `length` allowance is read by the
|
|
3092
|
+
* transport and projects nothing.
|
|
3093
|
+
* @param policy - The role's declared wire policy
|
|
3094
|
+
* @returns The model, think flag, options object, and the strict native fields when declared
|
|
2126
3095
|
*/
|
|
2127
|
-
function
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
3096
|
+
function buildStudyOllamaOptions(policy) {
|
|
3097
|
+
return {
|
|
3098
|
+
model: policy.model,
|
|
3099
|
+
options: policy.options,
|
|
3100
|
+
think: policy.think,
|
|
3101
|
+
...policy.strict ? {
|
|
3102
|
+
truncate: false,
|
|
3103
|
+
shift: false
|
|
3104
|
+
} : {}
|
|
3105
|
+
};
|
|
2131
3106
|
}
|
|
2132
|
-
/**
|
|
2133
|
-
*
|
|
2134
|
-
* @
|
|
2135
|
-
* @
|
|
3107
|
+
/**
|
|
3108
|
+
* Validates declared role policies against the pinned inventory before any dispatch.
|
|
3109
|
+
* @param roles - The declared wire policy of each model role
|
|
3110
|
+
* @param models - The pinned served identities
|
|
3111
|
+
* @returns Nothing when every policy names a pinned model and a positive output allowance
|
|
3112
|
+
* @throws StudyError - Thrown with code `CONFIG` when a policy names an unpinned model, declares no positive safe-integer `num_predict`, or makes the raw judge wire strict
|
|
2136
3113
|
*/
|
|
2137
|
-
function
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
3114
|
+
function validateStudyOllamaRoles(roles, models) {
|
|
3115
|
+
const names = Object.values(models).map((model) => model.name);
|
|
3116
|
+
for (const [role, policy] of Object.entries(roles)) {
|
|
3117
|
+
if (policy === void 0) continue;
|
|
3118
|
+
if (!names.includes(policy.model)) throw new _src_core.StudyError("CONFIG", "Ollama role policy names a model the inventory does not pin");
|
|
3119
|
+
const output = policy.options.num_predict;
|
|
3120
|
+
if (output === void 0 || !Number.isSafeInteger(output) || output <= 0) throw new _src_core.StudyError("CONFIG", "Ollama role policy declares no positive num_predict allowance");
|
|
3121
|
+
if (role === "judge" && policy.strict) throw new _src_core.StudyError("CONFIG", "Ollama strict fields apply only to chat roles");
|
|
3122
|
+
}
|
|
2143
3123
|
}
|
|
2144
|
-
/**
|
|
2145
|
-
* @param
|
|
2146
|
-
* @
|
|
2147
|
-
* @returns The identity that validation, grading, and trial children must share.
|
|
3124
|
+
/** Encodes exact application bytes with their decoded size and SHA-256 identity.
|
|
3125
|
+
* @param bytes - The request or response application bytes.
|
|
3126
|
+
* @returns Canonical base64 and the independent byte receipt.
|
|
2148
3127
|
*/
|
|
2149
|
-
function
|
|
2150
|
-
return
|
|
3128
|
+
function buildStudyOllamaBytes(bytes) {
|
|
3129
|
+
return {
|
|
3130
|
+
body: Buffer.from(bytes).toString("base64"),
|
|
3131
|
+
digest: hashStudyBytes(bytes),
|
|
3132
|
+
size: bytes.byteLength
|
|
3133
|
+
};
|
|
2151
3134
|
}
|
|
2152
|
-
/**
|
|
2153
|
-
* @param
|
|
2154
|
-
* @returns
|
|
3135
|
+
/** Validates independent metadata bounds and the complete optional recording group.
|
|
3136
|
+
* @param options - The owned transport configuration.
|
|
3137
|
+
* @returns Nothing when declarations are executable.
|
|
3138
|
+
* @throws StudyError - Thrown with CONFIG for invalid bounds, groups, identities or ordered policies.
|
|
2155
3139
|
*/
|
|
2156
|
-
function
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
return
|
|
3140
|
+
function validateStudyOllamaRecording(options) {
|
|
3141
|
+
if (!isStudyOllamaMetadata(options.metadata)) throw new _src_core.StudyError("CONFIG", "Invalid independent metadata budget");
|
|
3142
|
+
if (!Object.values(options.models).every(isStudyOllamaModel)) throw new _src_core.StudyError("CONFIG", "Invalid pinned model identity");
|
|
3143
|
+
const declared = [
|
|
3144
|
+
options.version,
|
|
3145
|
+
options.procedure,
|
|
3146
|
+
options.exchanges
|
|
3147
|
+
].filter((value) => value !== void 0).length;
|
|
3148
|
+
if (declared === 0) return;
|
|
3149
|
+
if (declared !== 3 || typeof options.version !== "string" || options.version.length === 0 || !Array.isArray(options.procedure) || options.exchanges === void 0 || !Number.isSafeInteger(options.exchanges.next) || options.exchanges.next < 1) throw new _src_core.StudyError("CONFIG", "Recording requires version, procedure and an exchange sink");
|
|
3150
|
+
for (const [index, step] of options.procedure.entries()) {
|
|
3151
|
+
if (!isStudyOllamaStep(step) || step.ordinal !== index + 1 || (0, _orkestrel_contract.canonicalStringify)(step.policy) !== (0, _orkestrel_contract.canonicalStringify)(options.roles[step.role])) throw new _src_core.StudyError("CONFIG", "Invalid ordered model-step declaration");
|
|
3152
|
+
if (step.admission === "exact") readStudyOllamaRequest(options, step.role, new URL(step.path, options.url), {
|
|
3153
|
+
method: "POST",
|
|
3154
|
+
body: step.body
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
2165
3157
|
}
|
|
2166
|
-
/**
|
|
2167
|
-
* @param
|
|
2168
|
-
* @
|
|
2169
|
-
* @
|
|
2170
|
-
* @
|
|
2171
|
-
*
|
|
2172
|
-
*
|
|
2173
|
-
*
|
|
2174
|
-
* declare const entry: string
|
|
2175
|
-
* readStudyVersion(entry)
|
|
2176
|
-
* ```
|
|
3158
|
+
/** Admits one model request against its next exact or capture declaration.
|
|
3159
|
+
* @param step - The next model step, absent after the procedure ends.
|
|
3160
|
+
* @param goal - The bound goal identity.
|
|
3161
|
+
* @param request - The independently validated request policy and byte length.
|
|
3162
|
+
* @param body - The owned encoded request body.
|
|
3163
|
+
* @returns Nothing when identity, policy and bytes match the declaration.
|
|
3164
|
+
* @throws StudyError - Thrown with FAULT before sending any mismatched request.
|
|
2177
3165
|
*/
|
|
2178
|
-
function
|
|
2179
|
-
|
|
2180
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.version !== "string" || value.version.length === 0) throw new _src_core.StudyError("CONFIG", "Study runtime package declares no version");
|
|
2181
|
-
return value.version;
|
|
3166
|
+
function validateStudyOllamaStep(step, goal, request, body) {
|
|
3167
|
+
if (step === void 0 || step.goal !== goal || step.role !== request.role || step.path !== request.path || typeof body !== "string" || (step.admission === "exact" ? !Buffer.from(step.body).equals(Buffer.from(body)) : step.bytes !== request.bytes)) throw new _src_core.StudyError("FAULT", "Request differs from the declared model step");
|
|
2182
3168
|
}
|
|
2183
|
-
/**
|
|
2184
|
-
*
|
|
2185
|
-
* @
|
|
3169
|
+
/**
|
|
3170
|
+
* Reads exact outbound Ollama facts and refuses a request outside its declared policy.
|
|
3171
|
+
* @remarks
|
|
3172
|
+
* The refusal happens before admission, so a refused request is never accounted.
|
|
3173
|
+
* @param options - The admitted origin, pinned inventory, and declared role policies
|
|
3174
|
+
* @param role - The responsible request role
|
|
3175
|
+
* @param input - The requested URL
|
|
3176
|
+
* @param init - The native fetch settings
|
|
3177
|
+
* @returns The validated dispatch facts
|
|
3178
|
+
* @throws StudyError - Thrown with code `CONFIG` when the endpoint, model, options, flags, or strict fields differ from the declaration, or the role declares no policy
|
|
2186
3179
|
*/
|
|
2187
|
-
function
|
|
2188
|
-
|
|
2189
|
-
|
|
3180
|
+
function readStudyOllamaRequest(options, role, input, init) {
|
|
3181
|
+
if (input instanceof Request) throw new _src_core.StudyError("CONFIG", "Ollama transport requires explicit URL requests");
|
|
3182
|
+
const url = new URL(input);
|
|
3183
|
+
const path = role === "metadata" ? init?.method === "POST" ? "/api/show" : url.pathname === "/api/version" ? "/api/version" : "/api/tags" : role === "judge" ? "/api/generate" : "/api/chat";
|
|
3184
|
+
if (url.origin !== new URL(options.url).origin || url.pathname !== path || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "") throw new _src_core.StudyError("CONFIG", "Ollama transport endpoint differs from the admitted origin and path");
|
|
3185
|
+
if (role === "metadata") {
|
|
3186
|
+
if (path === "/api/show") {
|
|
3187
|
+
if (typeof init?.body !== "string") throw new _src_core.StudyError("CONFIG", "Invalid model metadata request");
|
|
3188
|
+
const body = JSON.parse(init.body);
|
|
3189
|
+
if (!(0, _orkestrel_contract.isRecord)(body) || Object.keys(body).length !== 1 || typeof body.model !== "string" || !Object.values(options.models).some((model) => model.name === body.model)) throw new _src_core.StudyError("CONFIG", "Invalid model metadata identity");
|
|
3190
|
+
return {
|
|
3191
|
+
role,
|
|
3192
|
+
path,
|
|
3193
|
+
model: body.model,
|
|
3194
|
+
output: 0,
|
|
3195
|
+
bytes: new TextEncoder().encode(init.body).byteLength,
|
|
3196
|
+
options: {},
|
|
3197
|
+
strict: false,
|
|
3198
|
+
length: false
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
if (init?.method !== "GET" || init.body !== void 0) throw new _src_core.StudyError("CONFIG", "Invalid inventory request");
|
|
3202
|
+
return {
|
|
3203
|
+
role,
|
|
3204
|
+
path,
|
|
3205
|
+
model: void 0,
|
|
3206
|
+
output: 0,
|
|
3207
|
+
bytes: 0,
|
|
3208
|
+
options: {},
|
|
3209
|
+
strict: false,
|
|
3210
|
+
length: false
|
|
3211
|
+
};
|
|
3212
|
+
}
|
|
3213
|
+
if (init?.method !== "POST" || typeof init.body !== "string") throw new _src_core.StudyError("CONFIG", "Ollama transport requires an encoded JSON POST");
|
|
3214
|
+
const policy = role === "tool" || role === "maintenance" ? void 0 : options.roles[role];
|
|
3215
|
+
if (policy === void 0) throw new _src_core.StudyError("CONFIG", "Ollama transport role has no declared wire policy");
|
|
3216
|
+
const output = policy.options.num_predict;
|
|
3217
|
+
if (output === void 0 || !Number.isSafeInteger(output) || output <= 0) throw new _src_core.StudyError("CONFIG", "Ollama role policy declares no positive num_predict allowance");
|
|
3218
|
+
const body = JSON.parse(init.body);
|
|
3219
|
+
if (!(0, _orkestrel_contract.isRecord)(body) || body.model !== policy.model || !(0, _orkestrel_contract.isRecord)(body.options) || (0, _orkestrel_contract.canonicalStringify)(body.options) !== (0, _orkestrel_contract.canonicalStringify)(policy.options) || body.stream !== (role !== "judge")) throw new _src_core.StudyError("CONFIG", "Ollama transport model or effective options differ from the declared policy");
|
|
3220
|
+
if (role === "judge" ? body.raw !== true || body.logprobs !== true : typeof body.think !== "boolean" || body.think === true && !policy.think) throw new _src_core.StudyError("CONFIG", "Ollama transport flags differ from the declared policy");
|
|
3221
|
+
const strict = policy.strict && role !== "judge";
|
|
3222
|
+
if (strict && (body.truncate !== false || body.shift !== false)) throw new _src_core.StudyError("CONFIG", "Ollama transport must disable native truncation and shifting");
|
|
3223
|
+
return {
|
|
3224
|
+
role,
|
|
3225
|
+
path,
|
|
3226
|
+
model: policy.model,
|
|
3227
|
+
output,
|
|
3228
|
+
bytes: new TextEncoder().encode(init.body).byteLength,
|
|
3229
|
+
options: { ...policy.options },
|
|
3230
|
+
strict,
|
|
3231
|
+
length: policy.length
|
|
3232
|
+
};
|
|
2190
3233
|
}
|
|
2191
|
-
/**
|
|
2192
|
-
*
|
|
2193
|
-
* @param
|
|
3234
|
+
/**
|
|
3235
|
+
* Reads the declared model context ceiling without claiming daemon allocation.
|
|
3236
|
+
* @param value - The parsed Ollama show response
|
|
3237
|
+
* @param context - The requested total context
|
|
3238
|
+
* @returns The unambiguous model context ceiling
|
|
3239
|
+
* @throws StudyError - Thrown with code `FAULT` when context metadata is missing, or `CAPACITY` when it is ambiguous or below the requested context
|
|
2194
3240
|
*/
|
|
2195
|
-
function
|
|
2196
|
-
|
|
2197
|
-
const
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
3241
|
+
function readStudyOllamaSupport(value, context) {
|
|
3242
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(value.model_info) || typeof value.model_info["general.architecture"] !== "string") throw new _src_core.StudyError("FAULT", "Missing model context metadata");
|
|
3243
|
+
const architecture = value.model_info["general.architecture"];
|
|
3244
|
+
const keys = Object.keys(value.model_info).filter((key) => key.endsWith(".context_length"));
|
|
3245
|
+
const ceiling = value.model_info[`${architecture}.context_length`];
|
|
3246
|
+
if (keys.length !== 1 || typeof ceiling !== "number" || !Number.isSafeInteger(ceiling) || ceiling < context) {
|
|
3247
|
+
const message = "Model context metadata does not support the requested context";
|
|
3248
|
+
throw new _src_core.StudyError("CAPACITY", message, {
|
|
3249
|
+
cause: "capacity",
|
|
3250
|
+
message,
|
|
3251
|
+
role: void 0,
|
|
3252
|
+
stop: {
|
|
3253
|
+
layer: "model",
|
|
3254
|
+
id: "context_length",
|
|
3255
|
+
max: keys.length === 1 && Number.isSafeInteger(ceiling) ? (0, _src_core.parseStudyReading)(ceiling) : void 0,
|
|
3256
|
+
consumed: context
|
|
3257
|
+
}
|
|
3258
|
+
});
|
|
2209
3259
|
}
|
|
3260
|
+
return ceiling;
|
|
2210
3261
|
}
|
|
2211
|
-
/**
|
|
2212
|
-
*
|
|
2213
|
-
* @remarks
|
|
3262
|
+
/**
|
|
3263
|
+
* Reads a native Ollama error member as the attributable failure it names, without usage.
|
|
3264
|
+
* @remarks
|
|
3265
|
+
* No token usage is read from the member.
|
|
3266
|
+
* The failure is `CAPACITY` when the member, a string or an object, names the exact structured
|
|
3267
|
+
* type `exceed_context_size_error` with a finite positive integer `n_prompt_tokens` above `n_ctx`,
|
|
3268
|
+
* a `code` of 400 if supplied, and a status of 400 if supplied. Every other member, a malformed
|
|
3269
|
+
* one included, is `FAULT` with its readable reason.
|
|
3270
|
+
* @param value - The parsed HTTP error body or wire row
|
|
3271
|
+
* @param role - The responsibility whose request was dispatched; the failure retains it
|
|
3272
|
+
* @param status - The HTTP status of the carrying response; undefined for a row of a stream that answered 200
|
|
3273
|
+
* @returns Undefined when the value carries no `error` member; otherwise the attributable failure
|
|
2214
3274
|
*/
|
|
2215
|
-
function
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
3275
|
+
function readStudyOllamaFailure(value, role, status) {
|
|
3276
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || !("error" in value)) return void 0;
|
|
3277
|
+
const field = value.error;
|
|
3278
|
+
const decoded = typeof field === "string" ? (0, _orkestrel_contract.parseJSON)(field) : field;
|
|
3279
|
+
const structured = (0, _orkestrel_contract.isRecord)(decoded) ? (0, _orkestrel_contract.isRecord)(decoded.error) ? decoded.error : decoded : void 0;
|
|
3280
|
+
const native = structured?.message;
|
|
3281
|
+
const text = typeof native === "string" && native.trim() !== "" ? native : typeof field === "string" ? field : "";
|
|
3282
|
+
const label = structured?.type;
|
|
3283
|
+
const type = typeof label === "string" && label !== "" ? label : void 0;
|
|
3284
|
+
const origin = status === void 0 ? "Ollama error" : `Ollama HTTP ${status} error`;
|
|
3285
|
+
const prompt = structured?.n_prompt_tokens;
|
|
3286
|
+
const limit = structured?.n_ctx;
|
|
3287
|
+
const capacity = type === "exceed_context_size_error" && (0, _orkestrel_contract.isInteger)(prompt) && prompt > 0 && (0, _orkestrel_contract.isInteger)(limit) && limit > 0 && prompt > limit && (structured?.code === void 0 || structured?.code === 400) && (status === void 0 || status === 400);
|
|
3288
|
+
const reason = text.trim() === "" && type === void 0 ? `Malformed ${origin}` : `${origin}${type === void 0 ? "" : ` (${type})`}${text.trim() === "" ? "" : `: ${text}`}`;
|
|
3289
|
+
return new _src_core.StudyError(capacity ? "CAPACITY" : "FAULT", reason, {
|
|
3290
|
+
cause: capacity ? "capacity" : "infrastructure",
|
|
3291
|
+
message: reason,
|
|
3292
|
+
role,
|
|
3293
|
+
stop: capacity ? {
|
|
3294
|
+
layer: "model",
|
|
3295
|
+
id: "num_ctx",
|
|
3296
|
+
max: (0, _src_core.parseStudyReading)(limit),
|
|
3297
|
+
consumed: (0, _src_core.parseStudyReading)(prompt)
|
|
3298
|
+
} : void 0
|
|
2222
3299
|
});
|
|
2223
|
-
child.once("close", () => closed.abort());
|
|
2224
3300
|
}
|
|
2225
|
-
/**
|
|
2226
|
-
*
|
|
3301
|
+
/**
|
|
3302
|
+
* Reads one Ollama response row without inventing absent usage.
|
|
3303
|
+
* @param value - The parsed wire row
|
|
3304
|
+
* @param request - The admitted dispatch
|
|
3305
|
+
* @returns The terminal facts, or undefined for a nonterminal row
|
|
3306
|
+
* @throws StudyError - Thrown with the recognized provider failure when the row carries an `error` member, before any other row check; `FAULT` for a malformed row, model, message, finish reason, or usage
|
|
2227
3307
|
*/
|
|
2228
|
-
function
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
timeout: 2e3,
|
|
2239
|
-
stdio: "ignore"
|
|
2240
|
-
});
|
|
2241
|
-
else {
|
|
2242
|
-
const rows = (0, node_child_process.execFileSync)("ps", ["-eo", "pid=,ppid="], {
|
|
2243
|
-
timeout: 2e3,
|
|
2244
|
-
encoding: "utf8",
|
|
2245
|
-
maxBuffer: 1048576
|
|
2246
|
-
}).trim().split(/\r\n|\n/).map((row) => row.trim().split(/\s+/).map(Number));
|
|
2247
|
-
const descendants = /* @__PURE__ */ new Set([child.pid]);
|
|
2248
|
-
rows.forEach(() => {
|
|
2249
|
-
for (const [pid, parent] of rows) if (pid !== void 0 && parent !== void 0 && descendants.has(parent)) descendants.add(pid);
|
|
2250
|
-
});
|
|
2251
|
-
for (const pid of [...descendants].reverse()) try {
|
|
2252
|
-
process.kill(pid, "SIGKILL");
|
|
2253
|
-
} catch (error) {
|
|
2254
|
-
if (!matchesSystemError(error, "ESRCH")) throw error;
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
} catch (error) {
|
|
2258
|
-
try {
|
|
2259
|
-
process.kill(child.pid, 0);
|
|
2260
|
-
} catch (status) {
|
|
2261
|
-
if (matchesSystemError(status, "ESRCH")) return;
|
|
2262
|
-
throw status;
|
|
2263
|
-
}
|
|
2264
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
2265
|
-
child.kill("SIGKILL");
|
|
2266
|
-
throw error;
|
|
3308
|
+
function readStudyOllamaTerminal(value, request) {
|
|
3309
|
+
const failure = readStudyOllamaFailure(value, request.role);
|
|
3310
|
+
if (failure !== void 0) throw failure;
|
|
3311
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.done !== "boolean") throw new _src_core.StudyError("FAULT", "Malformed Ollama response row");
|
|
3312
|
+
if (value.model !== request.model) throw new _src_core.StudyError("FAULT", "Ollama response model differs from the dispatched model");
|
|
3313
|
+
if (request.role !== "judge") {
|
|
3314
|
+
if (!(0, _orkestrel_contract.isRecord)(value.message) || typeof value.message.content !== "string" || value.message.thinking !== void 0 && typeof value.message.thinking !== "string") throw new _src_core.StudyError("FAULT", "Malformed Ollama message");
|
|
3315
|
+
if (value.message.tool_calls !== void 0) {
|
|
3316
|
+
if (!(0, _orkestrel_contract.isArray)(value.message.tool_calls)) throw new _src_core.StudyError("FAULT", "Malformed Ollama tool calls");
|
|
3317
|
+
for (const call of value.message.tool_calls) if (!(0, _orkestrel_contract.isRecord)(call) || !(0, _orkestrel_contract.isRecord)(call.function) || typeof call.function.name !== "string" || !(0, _orkestrel_contract.isRecord)(call.function.arguments)) throw new _src_core.StudyError("FAULT", "Malformed Ollama tool call");
|
|
2267
3318
|
}
|
|
2268
3319
|
}
|
|
3320
|
+
if (!value.done) return void 0;
|
|
3321
|
+
if (typeof value.done_reason !== "string" || value.done_reason.length === 0) throw new _src_core.StudyError("FAULT", "Missing Ollama finish reason");
|
|
3322
|
+
for (const key of ["prompt_eval_count", "eval_count"]) if (value[key] !== void 0 && (typeof value[key] !== "number" || !Number.isSafeInteger(value[key]) || value[key] < 0)) throw new _src_core.StudyError("FAULT", "Malformed Ollama usage", {
|
|
3323
|
+
cause: "infrastructure",
|
|
3324
|
+
message: "Malformed Ollama usage",
|
|
3325
|
+
role: request.role,
|
|
3326
|
+
stop: void 0
|
|
3327
|
+
});
|
|
3328
|
+
return {
|
|
3329
|
+
prompt: typeof value.prompt_eval_count === "number" ? value.prompt_eval_count : void 0,
|
|
3330
|
+
completion: typeof value.eval_count === "number" ? value.eval_count : void 0,
|
|
3331
|
+
reason: value.done_reason
|
|
3332
|
+
};
|
|
2269
3333
|
}
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
const profile = this.#options.profile;
|
|
2301
|
-
const instrument = resolveStudyInstrument(this.#options.entry);
|
|
2302
|
-
const roots = [.../* @__PURE__ */ new Set([...profile.roots, instrument])];
|
|
2303
|
-
validateStudyRuntime(instrument, roots);
|
|
2304
|
-
const source = {
|
|
2305
|
-
root: (0, node_path.dirname)(this.#options.adapter),
|
|
2306
|
-
entry: this.#options.adapter
|
|
2307
|
-
};
|
|
2308
|
-
this.#instrument = new ArtifactLoader({
|
|
2309
|
-
entries: [],
|
|
2310
|
-
roots,
|
|
2311
|
-
source,
|
|
2312
|
-
instrument
|
|
2313
|
-
});
|
|
2314
|
-
const entry = hashStudyBytes(await readStudyBytes(this.#options.entry, STUDY_CUSTODY_BYTES));
|
|
2315
|
-
this.#entry = entry;
|
|
2316
|
-
this.#loader = new ArtifactLoader({
|
|
2317
|
-
entries: collectStudyEntries(profile),
|
|
2318
|
-
roots,
|
|
2319
|
-
source,
|
|
2320
|
-
instrument
|
|
3334
|
+
/**
|
|
3335
|
+
* Checks terminal policy after the actual wire cost has been retained.
|
|
3336
|
+
* @param terminal - The valid terminal facts
|
|
3337
|
+
* @param room - The reserved context and safety margin
|
|
3338
|
+
* @param request - The admitted dispatch and its declared finish policy; every refusal retains its role
|
|
3339
|
+
* @returns Nothing when the completion satisfies the declared policy
|
|
3340
|
+
* @throws StudyError - Thrown with code `CAPACITY` when the actual prompt exceeds its room, generation exceeds its allowance, or an undeclared `length` finish or capped generation occurs; `FAULT` for another unsupported finish reason
|
|
3341
|
+
*/
|
|
3342
|
+
function validateStudyOllamaCompletion(terminal, room, request) {
|
|
3343
|
+
const { prompt, completion, reason } = terminal;
|
|
3344
|
+
const { role } = request;
|
|
3345
|
+
const capped = {
|
|
3346
|
+
layer: "model",
|
|
3347
|
+
id: "num_predict",
|
|
3348
|
+
max: request.output,
|
|
3349
|
+
consumed: completion
|
|
3350
|
+
};
|
|
3351
|
+
const ceiling = room.context - request.output - room.safety;
|
|
3352
|
+
if (prompt !== void 0 && prompt > ceiling) {
|
|
3353
|
+
const message = "Actual prompt exceeded reserved context capacity";
|
|
3354
|
+
throw new _src_core.StudyError("CAPACITY", message, {
|
|
3355
|
+
cause: "capacity",
|
|
3356
|
+
message,
|
|
3357
|
+
role,
|
|
3358
|
+
stop: {
|
|
3359
|
+
layer: "window",
|
|
3360
|
+
id: "Context room",
|
|
3361
|
+
max: ceiling,
|
|
3362
|
+
consumed: prompt
|
|
3363
|
+
}
|
|
2321
3364
|
});
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
3365
|
+
}
|
|
3366
|
+
if (completion !== void 0 && completion > request.output) {
|
|
3367
|
+
const message = "Actual generation exceeded the output allowance";
|
|
3368
|
+
throw new _src_core.StudyError("CAPACITY", message, {
|
|
3369
|
+
cause: "capacity",
|
|
3370
|
+
message,
|
|
3371
|
+
role,
|
|
3372
|
+
stop: capped
|
|
2328
3373
|
});
|
|
2329
|
-
this.verify();
|
|
2330
|
-
return this.#identity;
|
|
2331
3374
|
}
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
3375
|
+
if (!request.length && (reason === "length" || completion !== void 0 && completion >= request.output)) {
|
|
3376
|
+
const message = "Substantive generation reached its output cap";
|
|
3377
|
+
throw new _src_core.StudyError("CAPACITY", message, {
|
|
3378
|
+
cause: "capacity",
|
|
3379
|
+
message,
|
|
3380
|
+
role,
|
|
3381
|
+
stop: capped
|
|
3382
|
+
});
|
|
2339
3383
|
}
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
const loaded = [];
|
|
2348
|
-
for (const entry of collectStudyEntries(profile)) loaded.push([entry, await this.#loader.load(entry)]);
|
|
2349
|
-
const namespaces = new Map(loaded);
|
|
2350
|
-
this.verify();
|
|
2351
|
-
const provider = profile.provider;
|
|
2352
|
-
return Object.freeze({
|
|
2353
|
-
agent: namespaces.get(profile.agent),
|
|
2354
|
-
subject: namespaces.get(profile.subject),
|
|
2355
|
-
provider: provider === void 0 ? void 0 : namespaces.get(provider.entry)
|
|
3384
|
+
if (reason !== "stop" && !(request.length && reason === "length")) {
|
|
3385
|
+
const message = "Unsupported Ollama finish reason";
|
|
3386
|
+
throw new _src_core.StudyError("FAULT", message, {
|
|
3387
|
+
cause: "infrastructure",
|
|
3388
|
+
message,
|
|
3389
|
+
role,
|
|
3390
|
+
stop: void 0
|
|
2356
3391
|
});
|
|
2357
3392
|
}
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Verifies that each pinned name resolves to one inventory entry with its pinned digest.
|
|
3396
|
+
* @param value - The parsed inventory response
|
|
3397
|
+
* @param models - The pinned served identities
|
|
3398
|
+
* @returns Nothing for a matching inventory
|
|
3399
|
+
* @throws StudyError - Thrown with code `FAULT` when the inventory is unsupported or a pinned identity does not match
|
|
3400
|
+
*/
|
|
3401
|
+
function validateStudyOllamaInventory(value, models) {
|
|
3402
|
+
if (!(0, _orkestrel_contract.isObject)(value) || !("models" in value) || !(0, _orkestrel_contract.isArray)(value.models)) throw new _src_core.StudyError("FAULT", "Unsupported Ollama inventory");
|
|
3403
|
+
const served = value.models;
|
|
3404
|
+
for (const { name, digest } of Object.values(models)) {
|
|
3405
|
+
const matches = served.filter((model) => (0, _orkestrel_contract.isRecord)(model) && model.name === name);
|
|
3406
|
+
const model = matches[0];
|
|
3407
|
+
if (matches.length !== 1 || !(0, _orkestrel_contract.isRecord)(model) || typeof model.digest !== "string" || model.digest.replace(/^sha256:/, "") !== digest.replace(/^sha256:/, "")) throw new _src_core.StudyError("FAULT", "Serving inventory does not match the required model digest");
|
|
2364
3408
|
}
|
|
2365
|
-
}
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Reads the pinned serving digest of an admitted request's model.
|
|
3412
|
+
* @param models - The pinned served identities
|
|
3413
|
+
* @param request - The admitted dispatch
|
|
3414
|
+
* @returns The pinned digest, or undefined for an inventory request or an unpinned model
|
|
3415
|
+
*/
|
|
3416
|
+
function readStudyOllamaDigest(models, request) {
|
|
3417
|
+
if (request.model === void 0) return void 0;
|
|
3418
|
+
return Object.values(models).find((model) => model.name === request.model)?.digest;
|
|
3419
|
+
}
|
|
3420
|
+
/**
|
|
3421
|
+
* Reads daemon timing from a terminal row without letting an observation fail the request.
|
|
3422
|
+
* @param extract - The admitted extractor
|
|
3423
|
+
* @param record - The validated terminal row
|
|
3424
|
+
* @returns The raw timing, or undefined when absent or the extractor throws
|
|
3425
|
+
*/
|
|
3426
|
+
function readStudyOllamaTiming(extract, record) {
|
|
3427
|
+
try {
|
|
3428
|
+
return extract(record);
|
|
3429
|
+
} catch {
|
|
3430
|
+
return;
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
/**
|
|
3434
|
+
* Builds the JSON evidence form of a timing observation with raw nanoseconds and counts.
|
|
3435
|
+
* @param timing - The extracted timing
|
|
3436
|
+
* @returns Null when no terminal timing was observed; otherwise every unreported member as null
|
|
3437
|
+
*/
|
|
3438
|
+
function buildStudyOllamaTiming(timing) {
|
|
3439
|
+
if (timing === void 0) return null;
|
|
3440
|
+
return {
|
|
3441
|
+
durations: {
|
|
3442
|
+
total: timing.durations.total ?? null,
|
|
3443
|
+
load: timing.durations.load ?? null,
|
|
3444
|
+
prompt: timing.durations.prompt ?? null,
|
|
3445
|
+
decode: timing.durations.decode ?? null
|
|
3446
|
+
},
|
|
3447
|
+
counts: {
|
|
3448
|
+
prompt: timing.counts.prompt ?? null,
|
|
3449
|
+
decode: timing.counts.decode ?? null,
|
|
3450
|
+
cached: timing.counts.cached ?? null
|
|
3451
|
+
},
|
|
3452
|
+
invalid: [...timing.invalid]
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
/**
|
|
3456
|
+
* Builds a reading of the raw prompt text an encoded request body carries, as sent.
|
|
3457
|
+
* @param body - The encoded request body
|
|
3458
|
+
* @returns The prompt's UTF-16 code units and UTF-8 bytes, labeled with its encoding, or undefined when there is no string prompt; these are readings of the actual text, not token allocations
|
|
3459
|
+
*/
|
|
3460
|
+
function buildStudyOllamaRaw(body) {
|
|
3461
|
+
if (typeof body !== "string") return void 0;
|
|
3462
|
+
const parsed = (0, _orkestrel_contract.attempt)(() => JSON.parse(body));
|
|
3463
|
+
if (!parsed.success || !(0, _orkestrel_contract.isRecord)(parsed.value) || typeof parsed.value.prompt !== "string") return void 0;
|
|
3464
|
+
return {
|
|
3465
|
+
encoding: "utf-8",
|
|
3466
|
+
units: parsed.value.prompt.length,
|
|
3467
|
+
bytes: new TextEncoder().encode(parsed.value.prompt).byteLength
|
|
3468
|
+
};
|
|
3469
|
+
}
|
|
2366
3470
|
//#endregion
|
|
2367
|
-
//#region src/server/processes/
|
|
2368
|
-
/**
|
|
2369
|
-
* @param
|
|
2370
|
-
* @
|
|
3471
|
+
//#region src/server/processes/helpers.ts
|
|
3472
|
+
/** Reads the executing profile's explicit recording declarations before a worker starts.
|
|
3473
|
+
* @param profile - The validated executing profile.
|
|
3474
|
+
* @param trial - The scheduled trial identity.
|
|
3475
|
+
* @returns A validated opening row, with no inferred metadata or replay allowance.
|
|
2371
3476
|
*/
|
|
2372
|
-
function
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
"
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
3477
|
+
function readStudyRecordingDeclaration(profile, trial) {
|
|
3478
|
+
if (profile.provider === void 0 || profile.settings === void 0) throw new _src_core.StudyError("CONFIG", "Recording requires provider and explicit wire declarations");
|
|
3479
|
+
const declaration = {
|
|
3480
|
+
phase: "declaration",
|
|
3481
|
+
trial: trial.id,
|
|
3482
|
+
version: profile.settings.version,
|
|
3483
|
+
metadata: profile.settings.metadata,
|
|
3484
|
+
procedure: profile.settings.procedure,
|
|
3485
|
+
models: buildStudyOllamaModels(profile.provider),
|
|
3486
|
+
maintenance: []
|
|
3487
|
+
};
|
|
3488
|
+
if (!isStudyExchangeDeclaration(declaration)) throw new _src_core.StudyError("CONFIG", "Recording requires valid version, metadata and ordered procedure settings");
|
|
3489
|
+
return declaration;
|
|
2385
3490
|
}
|
|
2386
|
-
/**
|
|
2387
|
-
* @param
|
|
2388
|
-
* @
|
|
3491
|
+
/** Validates parent preparation output without permitting a destination or allowance substitution.
|
|
3492
|
+
* @param original - The parent-reserved destination, or undefined without recording.
|
|
3493
|
+
* @param prepared - The callback's retained destination.
|
|
3494
|
+
* @returns The validated destination for the worker.
|
|
2389
3495
|
*/
|
|
2390
|
-
function
|
|
2391
|
-
|
|
3496
|
+
function readStudyPreparedDestination(original, prepared) {
|
|
3497
|
+
if (original === void 0 ? prepared !== void 0 : !isStudyExchangeDestination(prepared) || prepared.path !== original.path || (0, _orkestrel_contract.canonicalStringify)(prepared.record) !== (0, _orkestrel_contract.canonicalStringify)(original.record)) throw new _src_core.StudyError("CUSTODY", "Trial preparation changed its reserved destination");
|
|
3498
|
+
return prepared;
|
|
2392
3499
|
}
|
|
2393
|
-
/**
|
|
2394
|
-
* @param
|
|
2395
|
-
* @
|
|
3500
|
+
/** Rechecks the sealed source and this trial's prefix under the executing profile declaration.
|
|
3501
|
+
* @param selection - The explicit sealed source and offline behavior.
|
|
3502
|
+
* @param profile - The executing profile, whose bounds remain authoritative.
|
|
3503
|
+
* @param trial - The trial whose history must be present.
|
|
3504
|
+
* @returns Complete verified history before any dependent sender or worker starts.
|
|
2396
3505
|
*/
|
|
2397
|
-
function
|
|
2398
|
-
|
|
3506
|
+
async function readStudyReplayHistory(selection, profile, trial) {
|
|
3507
|
+
if (!isStudyReplaySelection(selection)) throw new _src_core.StudyError("CONFIG", "Invalid replay selection");
|
|
3508
|
+
const result = await readStudyResult(selection.result);
|
|
3509
|
+
if (result.status !== "sealed") throw new _src_core.StudyError("CUSTODY", "Replay requires a sealed result");
|
|
3510
|
+
const history = await readStudyExchanges(result, trial.id);
|
|
3511
|
+
const prefix = selection.prefix[trial.id];
|
|
3512
|
+
if (prefix === void 0 || prefix > history.exchanges.length || selection.offline && prefix !== history.exchanges.length) throw new _src_core.StudyError("CONFIG", "Replay prefix does not cover its declared trial");
|
|
3513
|
+
const { metadata: _metadata, maintenance: _maintenance, ...current } = readStudyRecordingDeclaration(profile, trial);
|
|
3514
|
+
const { metadata: _recordedMetadata, maintenance: _recordedMaintenance, ...recorded } = history.declaration;
|
|
3515
|
+
if ((0, _orkestrel_contract.canonicalStringify)(current) !== (0, _orkestrel_contract.canonicalStringify)(recorded)) throw new _src_core.StudyError("CONFIG", "Replay declaration differs from recorded execution");
|
|
3516
|
+
return history;
|
|
2399
3517
|
}
|
|
2400
|
-
/**
|
|
2401
|
-
* @param value -
|
|
2402
|
-
* @returns
|
|
3518
|
+
/** Projects optional domain fields into the JSON wire representation.
|
|
3519
|
+
* @param value - An owned domain value with optional undefined fields.
|
|
3520
|
+
* @returns A JSON-compatible snapshot.
|
|
3521
|
+
* @example Project a wire value
|
|
3522
|
+
* ```ts
|
|
3523
|
+
* import { valueToWire } from '@orkestrel/study/server'
|
|
3524
|
+
*
|
|
3525
|
+
* valueToWire({ state: 'ready', failure: undefined }) // { state: 'ready' }
|
|
3526
|
+
* ```
|
|
2403
3527
|
*/
|
|
2404
|
-
function
|
|
2405
|
-
|
|
3528
|
+
function valueToWire(value) {
|
|
3529
|
+
const parsed = JSON.parse(JSON.stringify(value));
|
|
3530
|
+
if (!(0, _orkestrel_contract.isJSONValue)(parsed)) throw new _src_core.StudyError("FAULT", "Invalid study wire value");
|
|
3531
|
+
return parsed;
|
|
2406
3532
|
}
|
|
2407
|
-
/**
|
|
2408
|
-
* @param
|
|
2409
|
-
* @returns
|
|
3533
|
+
/** Creates attributable protocol infrastructure failure.
|
|
3534
|
+
* @param message - The refused operation or broken boundary.
|
|
3535
|
+
* @returns A domain failure that never awards task credit.
|
|
2410
3536
|
*/
|
|
2411
|
-
function
|
|
2412
|
-
return
|
|
3537
|
+
function createStudyProcessFailure(message) {
|
|
3538
|
+
return {
|
|
3539
|
+
cause: "infrastructure",
|
|
3540
|
+
message,
|
|
3541
|
+
role: void 0,
|
|
3542
|
+
stop: void 0
|
|
3543
|
+
};
|
|
2413
3544
|
}
|
|
2414
|
-
/**
|
|
2415
|
-
* @param
|
|
2416
|
-
* @returns
|
|
3545
|
+
/** Binds the canonical fully resolved execution profile carried over IPC.
|
|
3546
|
+
* @param profile - The resolved execution settings, with optional absent leaves.
|
|
3547
|
+
* @returns The SHA256 digest of the canonical JSON wire representation.
|
|
2417
3548
|
*/
|
|
2418
|
-
function
|
|
2419
|
-
|
|
3549
|
+
function computeStudyProfileDigest(profile) {
|
|
3550
|
+
const encoded = (0, _orkestrel_contract.canonicalStringify)(valueToWire(profile));
|
|
3551
|
+
if (encoded === void 0) throw new _src_core.StudyError("CONFIG", "Study profile is not JSON-compatible");
|
|
3552
|
+
return hashStudyBytes(encoded);
|
|
2420
3553
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
3554
|
+
/** Collects the executable entries a profile selects, each canonical path once.
|
|
3555
|
+
* @param profile - The resolved execution profile.
|
|
3556
|
+
* @returns The agent, subject, and provider entries without repeating a shared entry.
|
|
3557
|
+
* @remarks The declared roles stay separate in the profile; only capture and loading deduplicate.
|
|
3558
|
+
*/
|
|
3559
|
+
function collectStudyEntries(profile) {
|
|
3560
|
+
return [.../* @__PURE__ */ new Set([
|
|
3561
|
+
profile.agent,
|
|
3562
|
+
profile.subject,
|
|
3563
|
+
...profile.provider === void 0 ? [] : [profile.provider.entry]
|
|
3564
|
+
])];
|
|
3565
|
+
}
|
|
3566
|
+
/** Computes the instrument identity binding trusted runtime bytes to the adapter inventory.
|
|
3567
|
+
* @param entry - The SHA-256 identity of the trusted runtime entry bytes.
|
|
3568
|
+
* @param inventory - The captured adapter inventory digest.
|
|
3569
|
+
* @returns The identity that validation, grading, and trial children must share.
|
|
3570
|
+
*/
|
|
3571
|
+
function computeStudyInstrumentDigest(entry, inventory) {
|
|
3572
|
+
return hashStudyBytes(JSON.stringify([entry, inventory]));
|
|
3573
|
+
}
|
|
3574
|
+
/** Resolves the actual host package from its trusted executable entry.
|
|
3575
|
+
* @param entry - The runtime's own executable, never a consumer-supplied module.
|
|
3576
|
+
* @returns The canonical study package root.
|
|
3577
|
+
*/
|
|
3578
|
+
function resolveStudyInstrument(entry) {
|
|
3579
|
+
const root = collectStudyAncestors((0, node_path.dirname)((0, node_fs.realpathSync)(entry))).find((directory) => {
|
|
3580
|
+
const manifest = (0, node_path.join)(directory, "package.json");
|
|
3581
|
+
if (!(0, node_fs.existsSync)(manifest)) return false;
|
|
3582
|
+
const value = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
|
|
3583
|
+
return (0, _orkestrel_contract.isRecord)(value) && value.name === "@orkestrel/study";
|
|
3584
|
+
});
|
|
3585
|
+
if (root === void 0) throw new _src_core.StudyError("CONFIG", "Cannot locate the study runtime package");
|
|
3586
|
+
return root;
|
|
3587
|
+
}
|
|
3588
|
+
/** Reads the version of the study package that owns an executable entry.
|
|
3589
|
+
* @param entry - The installed runtime's executable entry.
|
|
3590
|
+
* @returns The non-empty version declared by the owning study package.
|
|
3591
|
+
* @throws StudyError - Thrown when no study package owns the entry or its version is absent or empty.
|
|
3592
|
+
* @example Read the installed version
|
|
3593
|
+
* ```ts
|
|
3594
|
+
* import { readStudyVersion } from '@orkestrel/study/server'
|
|
3595
|
+
*
|
|
3596
|
+
* declare const entry: string
|
|
3597
|
+
* readStudyVersion(entry)
|
|
3598
|
+
* ```
|
|
3599
|
+
*/
|
|
3600
|
+
function readStudyVersion(entry) {
|
|
3601
|
+
const value = JSON.parse((0, node_fs.readFileSync)((0, node_path.join)(resolveStudyInstrument(entry), "package.json"), "utf8"));
|
|
3602
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.version !== "string" || value.version.length === 0) throw new _src_core.StudyError("CONFIG", "Study runtime package declares no version");
|
|
3603
|
+
return value.version;
|
|
3604
|
+
}
|
|
3605
|
+
/** Collects a directory and each of its ancestors up to the filesystem root, nearest first.
|
|
3606
|
+
* @param directory - The absolute directory to start from.
|
|
3607
|
+
* @returns The directory followed by every parent directory, ending at the root.
|
|
3608
|
+
*/
|
|
3609
|
+
function collectStudyAncestors(directory) {
|
|
3610
|
+
const parent = (0, node_path.dirname)(directory);
|
|
3611
|
+
return parent === directory ? [directory] : [directory, ...collectStudyAncestors(parent)];
|
|
3612
|
+
}
|
|
3613
|
+
/** Refuses a profile that omits the actual host runtime's declared dependency bytes.
|
|
3614
|
+
* @param instrument - The canonical package owning the trusted runtime entry.
|
|
3615
|
+
* @param roots - Explicitly admitted package roots, including the instrument.
|
|
3616
|
+
*/
|
|
3617
|
+
function validateStudyRuntime(instrument, roots) {
|
|
3618
|
+
const pending = [instrument];
|
|
3619
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3620
|
+
while (pending.length > 0) {
|
|
3621
|
+
const root = pending.pop();
|
|
3622
|
+
if (root === void 0 || visited.has(root)) continue;
|
|
3623
|
+
visited.add(root);
|
|
3624
|
+
if (!roots.includes(root)) throw new _src_core.StudyError("CONFIG", `Profile roots omit a runtime dependency: ${root}`);
|
|
3625
|
+
const manifest = (0, node_path.join)(root, "package.json");
|
|
3626
|
+
const value = JSON.parse((0, node_fs.readFileSync)(manifest, "utf8"));
|
|
3627
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || value.dependencies === void 0) continue;
|
|
3628
|
+
if (!(0, _orkestrel_contract.isRecord)(value.dependencies)) throw new _src_core.StudyError("CONFIG", "Runtime dependency declarations are invalid");
|
|
3629
|
+
const resolver = (0, node_module.createRequire)(manifest);
|
|
3630
|
+
for (const dependency of Object.keys(value.dependencies)) pending.push((0, node_path.dirname)((0, node_fs.realpathSync)(resolver.resolve(`${dependency}/package.json`))));
|
|
3631
|
+
}
|
|
3632
|
+
}
|
|
3633
|
+
/** Releases inherited output handles a bounded time after native exit.
|
|
3634
|
+
* @param child - A newly spawned child, subscribed before native exit.
|
|
3635
|
+
* @remarks A descendant that outlives an exited root is outside discoverable-tree cleanup; this only stops the parent from waiting on its pipes.
|
|
3636
|
+
*/
|
|
3637
|
+
function drainStudyChild(child) {
|
|
3638
|
+
const closed = new AbortController();
|
|
3639
|
+
child.once("exit", () => {
|
|
3640
|
+
(0, node_timers_promises.setTimeout)(STUDY_DRAIN_DURATION, void 0, { signal: closed.signal }).then(() => {
|
|
3641
|
+
child.stdout?.destroy();
|
|
3642
|
+
child.stderr?.destroy();
|
|
3643
|
+
}, () => void 0);
|
|
3644
|
+
});
|
|
3645
|
+
child.once("close", () => closed.abort());
|
|
3646
|
+
}
|
|
3647
|
+
/** Terminates the recorded native process tree under a finite cleanup allowance.
|
|
3648
|
+
* @param child - The exclusively owned child process.
|
|
3649
|
+
*/
|
|
3650
|
+
function terminateStudyChild(child) {
|
|
3651
|
+
if (child.pid === void 0 || child.exitCode !== null || child.signalCode !== null) return;
|
|
3652
|
+
try {
|
|
3653
|
+
if (process.platform === "win32") (0, node_child_process.execFileSync)("taskkill", [
|
|
3654
|
+
"/PID",
|
|
3655
|
+
String(child.pid),
|
|
3656
|
+
"/T",
|
|
3657
|
+
"/F"
|
|
3658
|
+
], {
|
|
3659
|
+
windowsHide: true,
|
|
3660
|
+
timeout: 2e3,
|
|
3661
|
+
stdio: "ignore"
|
|
3662
|
+
});
|
|
3663
|
+
else {
|
|
3664
|
+
const rows = (0, node_child_process.execFileSync)("ps", ["-eo", "pid=,ppid="], {
|
|
3665
|
+
timeout: 2e3,
|
|
3666
|
+
encoding: "utf8",
|
|
3667
|
+
maxBuffer: 1048576
|
|
3668
|
+
}).trim().split(/\r\n|\n/).map((row) => row.trim().split(/\s+/).map(Number));
|
|
3669
|
+
const descendants = /* @__PURE__ */ new Set([child.pid]);
|
|
3670
|
+
rows.forEach(() => {
|
|
3671
|
+
for (const [pid, parent] of rows) if (pid !== void 0 && parent !== void 0 && descendants.has(parent)) descendants.add(pid);
|
|
3672
|
+
});
|
|
3673
|
+
for (const pid of [...descendants].reverse()) try {
|
|
3674
|
+
process.kill(pid, "SIGKILL");
|
|
3675
|
+
} catch (error) {
|
|
3676
|
+
if (!matchesSystemError(error, "ESRCH")) throw error;
|
|
3677
|
+
}
|
|
3678
|
+
}
|
|
3679
|
+
} catch (error) {
|
|
3680
|
+
try {
|
|
3681
|
+
process.kill(child.pid, 0);
|
|
3682
|
+
} catch (status) {
|
|
3683
|
+
if (matchesSystemError(status, "ESRCH")) return;
|
|
3684
|
+
throw status;
|
|
3685
|
+
}
|
|
3686
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
3687
|
+
child.kill("SIGKILL");
|
|
3688
|
+
throw error;
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
//#endregion
|
|
3693
|
+
//#region src/server/processes/StudyArtifactLoader.ts
|
|
3694
|
+
/** Binds consumer instrumentation separately from selected executable identities. */
|
|
3695
|
+
var StudyArtifactLoader = class {
|
|
3696
|
+
#options;
|
|
3697
|
+
#loader;
|
|
3698
|
+
#instrument;
|
|
3699
|
+
#identity;
|
|
3700
|
+
#entry;
|
|
3701
|
+
/** Owns resolved built and consumer source selections.
|
|
3702
|
+
* @param options - The trusted host, adapter, and profile.
|
|
3703
|
+
*/
|
|
3704
|
+
constructor(options) {
|
|
3705
|
+
this.#options = structuredClone(options);
|
|
3706
|
+
if (![
|
|
3707
|
+
options.entry,
|
|
3708
|
+
options.root,
|
|
3709
|
+
options.adapter,
|
|
3710
|
+
...collectStudyEntries(options.profile),
|
|
3711
|
+
...options.profile.roots
|
|
3712
|
+
].every(node_path.isAbsolute)) throw new _src_core.StudyError("CONFIG", "Study artifact paths must be absolute");
|
|
3713
|
+
}
|
|
3714
|
+
/** Inventories bytes without importing selected code.
|
|
3715
|
+
* @returns Bound executable and instrumentation identities.
|
|
3716
|
+
*/
|
|
3717
|
+
async inspect() {
|
|
3718
|
+
const closure = this.#options.closure;
|
|
3719
|
+
if (closure !== void 0) verifyArtifactClosure(closure.closure, true);
|
|
3720
|
+
if (this.#identity !== void 0) {
|
|
3721
|
+
this.verify();
|
|
3722
|
+
return this.#identity;
|
|
3723
|
+
}
|
|
3724
|
+
const profile = this.#options.profile;
|
|
3725
|
+
const instrument = resolveStudyInstrument(this.#options.entry);
|
|
3726
|
+
const roots = [.../* @__PURE__ */ new Set([...profile.roots, instrument])];
|
|
3727
|
+
validateStudyRuntime(instrument, roots);
|
|
3728
|
+
const source = {
|
|
3729
|
+
root: (0, node_path.dirname)(this.#options.adapter),
|
|
3730
|
+
entry: this.#options.adapter
|
|
3731
|
+
};
|
|
3732
|
+
this.#instrument = new ArtifactLoader({
|
|
3733
|
+
entries: [],
|
|
3734
|
+
roots,
|
|
3735
|
+
source,
|
|
3736
|
+
instrument
|
|
3737
|
+
});
|
|
3738
|
+
const entry = hashStudyBytes(await readStudyBytes(this.#options.entry, STUDY_CUSTODY_BYTES));
|
|
3739
|
+
this.#entry = entry;
|
|
3740
|
+
this.#loader = new ArtifactLoader(closure === void 0 ? {
|
|
3741
|
+
entries: collectStudyEntries(profile),
|
|
3742
|
+
roots,
|
|
3743
|
+
source,
|
|
3744
|
+
instrument
|
|
3745
|
+
} : closure.closure.options);
|
|
3746
|
+
const inventory = this.#loader.inspect();
|
|
3747
|
+
this.#identity = Object.freeze({
|
|
3748
|
+
agent: inventory.digest,
|
|
3749
|
+
subject: inventory.digest,
|
|
3750
|
+
provider: profile.provider === void 0 ? void 0 : inventory.digest,
|
|
3751
|
+
instrument: computeStudyInstrumentDigest(entry, this.#instrument.inspect().digest)
|
|
3752
|
+
});
|
|
3753
|
+
this.verify();
|
|
3754
|
+
return this.#identity;
|
|
3755
|
+
}
|
|
3756
|
+
/** Loads the adapter from the trial inventory.
|
|
3757
|
+
* @returns The complete consumer adapter.
|
|
3758
|
+
*/
|
|
3759
|
+
async adapter() {
|
|
3760
|
+
await this.inspect();
|
|
3761
|
+
if (this.#loader === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
|
|
3762
|
+
return readStudyAdapter(await this.#loader.load(this.#options.adapter));
|
|
3763
|
+
}
|
|
3764
|
+
/** Loads admitted built namespaces inside the trial child.
|
|
3765
|
+
* @returns The agent, subject and optional provider; a shared entry loads once and keeps each role.
|
|
3766
|
+
*/
|
|
3767
|
+
async load() {
|
|
3768
|
+
await this.inspect();
|
|
3769
|
+
if (this.#loader === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
|
|
3770
|
+
const profile = this.#options.profile;
|
|
3771
|
+
const loaded = [];
|
|
3772
|
+
for (const entry of collectStudyEntries(profile)) loaded.push([entry, await this.#loader.load(entry)]);
|
|
3773
|
+
const namespaces = new Map(loaded);
|
|
3774
|
+
this.verify();
|
|
3775
|
+
const provider = profile.provider;
|
|
3776
|
+
return Object.freeze({
|
|
3777
|
+
agent: namespaces.get(profile.agent),
|
|
3778
|
+
subject: namespaces.get(profile.subject),
|
|
3779
|
+
provider: provider === void 0 ? void 0 : namespaces.get(provider.entry)
|
|
3780
|
+
});
|
|
3781
|
+
}
|
|
3782
|
+
/** Refuses instrument and inventory drift after inspection. */
|
|
3783
|
+
verify() {
|
|
3784
|
+
if (this.#loader === void 0 || this.#instrument === void 0) throw new _src_core.StudyError("CONFIG", "Artifact inspection is required");
|
|
3785
|
+
if (hashStudyBytes((0, node_fs.readFileSync)(this.#options.entry)) !== this.#entry) throw new _src_core.StudyError("FAULT", "Study instrument bytes drifted");
|
|
3786
|
+
this.#loader.verify();
|
|
3787
|
+
this.#instrument.verify();
|
|
3788
|
+
if (this.#options.closure !== void 0) verifyArtifactClosure(this.#options.closure.closure, true);
|
|
3789
|
+
}
|
|
3790
|
+
};
|
|
3791
|
+
//#endregion
|
|
3792
|
+
//#region src/server/processes/StudyChannel.ts
|
|
3793
|
+
/** Correlates bounded IPC work and rejects every outstanding request on transport failure. */
|
|
3794
|
+
var StudyChannel = class {
|
|
3795
|
+
#options;
|
|
3796
|
+
#control = new _src_core.StudyControl();
|
|
3797
|
+
#pending = /* @__PURE__ */ new Map();
|
|
3798
|
+
#next = 0;
|
|
2429
3799
|
#received = 0;
|
|
2430
3800
|
#working = /* @__PURE__ */ new Set();
|
|
2431
3801
|
#message;
|
|
@@ -2621,6 +3991,8 @@ var StudyProcessSession = class {
|
|
|
2621
3991
|
this.#options.root,
|
|
2622
3992
|
this.#options.directory
|
|
2623
3993
|
].every(node_path.isAbsolute)) throw new _src_core.StudyError("CONFIG", "Study process paths must be absolute");
|
|
3994
|
+
const destination = this.#options.exchanges?.[this.#options.trial.id];
|
|
3995
|
+
if (this.#options.profile.record === void 0 ? destination !== void 0 : !isStudyExchangeDestination(destination) || destination.record.bytes !== this.#options.profile.record.bytes) throw new _src_core.StudyError("CONFIG", "Session recording differs from its reserved allowance");
|
|
2624
3996
|
const directory = await (0, node_fs_promises.mkdtemp)((0, node_path.join)(this.#options.directory, "trial-"));
|
|
2625
3997
|
if (this.#control.failure !== void 0) throw (0, _src_core.failureToStudyError)(this.#control.failure);
|
|
2626
3998
|
const child = (0, node_child_process.fork)(this.#options.entry, [STUDY_WORKER_OPERATION], {
|
|
@@ -2827,11 +4199,16 @@ var StudyProcessSession = class {
|
|
|
2827
4199
|
/** Opens one fresh native child for each scheduled trial. */
|
|
2828
4200
|
var StudyProcessDriver = class {
|
|
2829
4201
|
#options;
|
|
4202
|
+
#prepare;
|
|
2830
4203
|
/** Owns immutable resolved launch settings.
|
|
2831
4204
|
* @param options - Built entry, admitted identities and exclusive scratch parent.
|
|
2832
4205
|
*/
|
|
2833
4206
|
constructor(options) {
|
|
2834
|
-
|
|
4207
|
+
const { prepare, ...configuration } = options;
|
|
4208
|
+
if (prepare !== void 0 && typeof prepare !== "function") throw new _src_core.StudyError("CONFIG", "Trial preparation must be callable");
|
|
4209
|
+
if (options.replay?.offline === true && prepare !== void 0) throw new _src_core.StudyError("CONFIG", "Offline replay cannot prepare external resources");
|
|
4210
|
+
this.#options = structuredClone(configuration);
|
|
4211
|
+
this.#prepare = prepare;
|
|
2835
4212
|
}
|
|
2836
4213
|
/** Opens isolated child state and waits for its verified readiness.
|
|
2837
4214
|
* @param trial - The public scheduled trial.
|
|
@@ -2839,10 +4216,19 @@ var StudyProcessDriver = class {
|
|
|
2839
4216
|
* @returns A ready process-backed session.
|
|
2840
4217
|
*/
|
|
2841
4218
|
async open(trial, signal) {
|
|
4219
|
+
signal.throwIfAborted();
|
|
4220
|
+
const owned = structuredClone(trial);
|
|
4221
|
+
const destination = this.#options.exchanges?.[owned.id];
|
|
4222
|
+
if (this.#options.profile.record !== void 0 && (!isStudyExchangeDestination(destination) || destination.record.bytes !== this.#options.profile.record.bytes)) throw new _src_core.StudyError("CONFIG", "Recorded trial requires its reserved destination");
|
|
4223
|
+
if (this.#options.profile.record === void 0 && destination !== void 0) throw new _src_core.StudyError("CONFIG", "Unrecorded trial cannot receive a sidecar");
|
|
4224
|
+
if (this.#options.replay !== void 0) await readStudyReplayHistory(this.#options.replay, this.#options.profile, owned);
|
|
4225
|
+
const prepared = this.#prepare === void 0 ? destination : readStudyPreparedDestination(destination, await this.#prepare(structuredClone(owned), signal, structuredClone(destination)));
|
|
4226
|
+
signal.throwIfAborted();
|
|
2842
4227
|
const session = new StudyProcessSession({
|
|
2843
4228
|
...this.#options,
|
|
2844
|
-
trial,
|
|
2845
|
-
signal
|
|
4229
|
+
trial: owned,
|
|
4230
|
+
signal,
|
|
4231
|
+
...prepared === void 0 ? {} : { exchanges: { [owned.id]: prepared } }
|
|
2846
4232
|
});
|
|
2847
4233
|
try {
|
|
2848
4234
|
await session.start();
|
|
@@ -2854,15 +4240,261 @@ var StudyProcessDriver = class {
|
|
|
2854
4240
|
}
|
|
2855
4241
|
};
|
|
2856
4242
|
//#endregion
|
|
2857
|
-
//#region src/server/
|
|
2858
|
-
/**
|
|
2859
|
-
|
|
4243
|
+
//#region src/server/commands/StudyDaemonTrial.ts
|
|
4244
|
+
/** Records one trial's daemon maintenance before handing its verified prefix to a worker.
|
|
4245
|
+
* @example
|
|
4246
|
+
* ```ts
|
|
4247
|
+
* const trial = new StudyDaemonTrial(options)
|
|
4248
|
+
* await trial.execute(daemon)
|
|
4249
|
+
* ```
|
|
4250
|
+
*/
|
|
4251
|
+
var StudyDaemonTrial = class {
|
|
2860
4252
|
#options;
|
|
2861
|
-
#
|
|
2862
|
-
#
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
4253
|
+
#pending = /* @__PURE__ */ new Map();
|
|
4254
|
+
#sink;
|
|
4255
|
+
/** Owns the declaration and reserved destination for one preparation.
|
|
4256
|
+
* @param options - The immutable trial declaration, destination, and cancellation.
|
|
4257
|
+
*/
|
|
4258
|
+
constructor(options) {
|
|
4259
|
+
this.#options = {
|
|
4260
|
+
...options,
|
|
4261
|
+
declaration: structuredClone(options.declaration),
|
|
4262
|
+
destination: structuredClone(options.destination)
|
|
4263
|
+
};
|
|
4264
|
+
}
|
|
4265
|
+
/** Unloads declared models, drains the journal, and returns its exact retained identity.
|
|
4266
|
+
* @remarks The daemon's unload sender must call this instance's fetch method. Startup metadata must forward without recording.
|
|
4267
|
+
* @param daemon - The verified execution-owned daemon.
|
|
4268
|
+
* @returns The original destination with its closed maintenance prefix identity.
|
|
4269
|
+
*/
|
|
4270
|
+
async execute(daemon) {
|
|
4271
|
+
if (this.#sink !== void 0) throw new _src_core.StudyError("FAULT", "Daemon preparation cannot repeat");
|
|
4272
|
+
const file = await (0, node_fs_promises.open)(this.#options.destination.path, "wx");
|
|
4273
|
+
this.#sink = new StudyExchangeSink({
|
|
4274
|
+
file,
|
|
4275
|
+
record: this.#options.destination.record
|
|
4276
|
+
});
|
|
4277
|
+
try {
|
|
4278
|
+
await this.#sink.append(this.#options.declaration);
|
|
4279
|
+
await daemon.unload();
|
|
4280
|
+
await this.#finish();
|
|
4281
|
+
} catch (error) {
|
|
4282
|
+
await this.#finish(error);
|
|
4283
|
+
throw error;
|
|
4284
|
+
} finally {
|
|
4285
|
+
await this.#sink.close();
|
|
4286
|
+
}
|
|
4287
|
+
const bytes = await readStudyBytes(this.#options.destination.path, this.#options.destination.record.bytes);
|
|
4288
|
+
return {
|
|
4289
|
+
...this.#options.destination,
|
|
4290
|
+
retained: {
|
|
4291
|
+
path: `trials/${this.#options.declaration.trial}/exchanges.jsonl`,
|
|
4292
|
+
bytes: bytes.length,
|
|
4293
|
+
digest: hashStudyBytes(bytes)
|
|
4294
|
+
}
|
|
4295
|
+
};
|
|
4296
|
+
}
|
|
4297
|
+
/** Retains each request before sending and preserves the sender's native response chunks.
|
|
4298
|
+
* @param input - The daemon's exact HTTP input.
|
|
4299
|
+
* @param init - The daemon's method, body, redirect policy, and cancellation.
|
|
4300
|
+
* @returns An unread response carrying the same body bytes for daemon validation.
|
|
4301
|
+
*/
|
|
4302
|
+
async fetch(input, init) {
|
|
4303
|
+
const sink = this.#sink;
|
|
4304
|
+
if (sink === void 0) throw new _src_core.StudyError("FAULT", "Daemon journal is not open");
|
|
4305
|
+
await this.#finish();
|
|
4306
|
+
const request = new Request(input, {
|
|
4307
|
+
...init,
|
|
4308
|
+
signal: AbortSignal.any([this.#options.signal, ...init?.signal === void 0 || init.signal === null ? [] : [init.signal]])
|
|
4309
|
+
});
|
|
4310
|
+
const first = this.#options.declaration.procedure[0];
|
|
4311
|
+
if (first === void 0 || new URL(request.url).pathname !== "/api/generate" || request.method !== "POST") throw new _src_core.StudyError("FAULT", "Unexpected maintenance request");
|
|
4312
|
+
const identity = {
|
|
4313
|
+
ordinal: sink.next,
|
|
4314
|
+
goal: first.goal,
|
|
4315
|
+
role: "maintenance",
|
|
4316
|
+
path: "/api/generate",
|
|
4317
|
+
method: "POST",
|
|
4318
|
+
stage: "maintenance"
|
|
4319
|
+
};
|
|
4320
|
+
await sink.append({
|
|
4321
|
+
...identity,
|
|
4322
|
+
phase: "attempt",
|
|
4323
|
+
...buildStudyOllamaBytes(Buffer.from(await request.clone().arrayBuffer()))
|
|
4324
|
+
});
|
|
4325
|
+
const chunks = [];
|
|
4326
|
+
try {
|
|
4327
|
+
const response = await globalThis.fetch(request);
|
|
4328
|
+
const headers = [...response.headers].filter(([name]) => name === "content-type" || name === "content-encoding");
|
|
4329
|
+
const reader = response.body?.getReader();
|
|
4330
|
+
try {
|
|
4331
|
+
if (reader !== void 0) for (;;) {
|
|
4332
|
+
const step = await reader.read();
|
|
4333
|
+
if (step.done) break;
|
|
4334
|
+
if (chunks.reduce((sum, chunk) => sum + chunk.byteLength, 0) + step.value.byteLength > this.#options.declaration.metadata.bytes) throw new _src_core.StudyError("CAPACITY", "Maintenance reply exceeds its declared byte allowance");
|
|
4335
|
+
if (step.value.byteLength > 0) chunks.push(Buffer.from(step.value));
|
|
4336
|
+
}
|
|
4337
|
+
} finally {
|
|
4338
|
+
await reader?.cancel().catch(() => void 0);
|
|
4339
|
+
reader?.releaseLock();
|
|
4340
|
+
}
|
|
4341
|
+
const bytes = Buffer.concat(chunks);
|
|
4342
|
+
this.#pending.set(identity, {
|
|
4343
|
+
status: response.status,
|
|
4344
|
+
headers,
|
|
4345
|
+
chunks: chunks.map((chunk) => chunk.byteLength),
|
|
4346
|
+
...buildStudyOllamaBytes(bytes)
|
|
4347
|
+
});
|
|
4348
|
+
return new Response(response.body === null ? null : bytes, {
|
|
4349
|
+
status: response.status,
|
|
4350
|
+
statusText: response.statusText,
|
|
4351
|
+
headers: response.headers
|
|
4352
|
+
});
|
|
4353
|
+
} catch (error) {
|
|
4354
|
+
await sink.append({
|
|
4355
|
+
...identity,
|
|
4356
|
+
phase: "failure",
|
|
4357
|
+
reason: error instanceof Error ? error.message : "Maintenance request failed"
|
|
4358
|
+
});
|
|
4359
|
+
throw error;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
async #finish(error) {
|
|
4363
|
+
for (const [identity, reply] of this.#pending) try {
|
|
4364
|
+
await this.#sink?.append(error === void 0 ? {
|
|
4365
|
+
...identity,
|
|
4366
|
+
...reply,
|
|
4367
|
+
phase: "response"
|
|
4368
|
+
} : {
|
|
4369
|
+
...identity,
|
|
4370
|
+
phase: "failure",
|
|
4371
|
+
reason: error instanceof Error ? error.message : "Maintenance refused",
|
|
4372
|
+
response: reply
|
|
4373
|
+
});
|
|
4374
|
+
} finally {
|
|
4375
|
+
this.#pending.delete(identity);
|
|
4376
|
+
}
|
|
4377
|
+
}
|
|
4378
|
+
};
|
|
4379
|
+
//#endregion
|
|
4380
|
+
//#region src/server/commands/StudyDaemonSession.ts
|
|
4381
|
+
/** Owns the declared daemon and its serial trial preparation through permanent cleanup.
|
|
4382
|
+
* @example
|
|
4383
|
+
* ```ts
|
|
4384
|
+
* const session = new StudyDaemonSession(options)
|
|
4385
|
+
* try { await session.start() } finally { await session.stop() }
|
|
4386
|
+
* ```
|
|
4387
|
+
*/
|
|
4388
|
+
var StudyDaemonSession = class {
|
|
4389
|
+
#options;
|
|
4390
|
+
#daemon;
|
|
4391
|
+
#stopped = false;
|
|
4392
|
+
#trials = /* @__PURE__ */ new Set();
|
|
4393
|
+
/** Captures resolved declarations without loading the daemon runtime.
|
|
4394
|
+
* @param options - The execution profile, scenarios, workspace, and cancellation.
|
|
4395
|
+
*/
|
|
4396
|
+
constructor(options) {
|
|
4397
|
+
const provider = options.profile.provider;
|
|
4398
|
+
const daemon = provider?.daemon;
|
|
4399
|
+
if (provider === void 0 || daemon === void 0) throw new _src_core.StudyError("CONFIG", "Execution has no declared daemon");
|
|
4400
|
+
if (options.profile.record !== void 0 && options.profile.settings?.version !== daemon.version) throw new _src_core.StudyError("CONFIG", "Daemon and recording versions differ");
|
|
4401
|
+
this.#options = {
|
|
4402
|
+
...options,
|
|
4403
|
+
profile: structuredClone(options.profile),
|
|
4404
|
+
scenarios: structuredClone(options.scenarios)
|
|
4405
|
+
};
|
|
4406
|
+
}
|
|
4407
|
+
/** Starts the daemon and returns its verified execution provenance.
|
|
4408
|
+
* @returns The executable, endpoint, owned pid, and model receipt.
|
|
4409
|
+
*/
|
|
4410
|
+
async start() {
|
|
4411
|
+
this.#options.signal.throwIfAborted();
|
|
4412
|
+
const { createOllamaDaemon } = await import("@orkestrel/ollama/server");
|
|
4413
|
+
if (this.#stopped) throw new _src_core.StudyError("FAULT", "Daemon session has stopped");
|
|
4414
|
+
const provider = this.#options.profile.provider;
|
|
4415
|
+
const daemon = provider?.daemon;
|
|
4416
|
+
if (provider === void 0 || daemon === void 0) throw new _src_core.StudyError("CONFIG", "Execution has no declared daemon");
|
|
4417
|
+
this.#daemon ??= createOllamaDaemon({
|
|
4418
|
+
executable: {
|
|
4419
|
+
path: daemon.executable,
|
|
4420
|
+
digest: daemon.digest,
|
|
4421
|
+
version: daemon.version
|
|
4422
|
+
},
|
|
4423
|
+
url: provider.url,
|
|
4424
|
+
models: daemon.models,
|
|
4425
|
+
workspace: this.#options.workspace,
|
|
4426
|
+
...daemon.environment === void 0 ? {} : { environment: daemon.environment },
|
|
4427
|
+
signal: this.#options.signal,
|
|
4428
|
+
readiness: this.#options.profile.bounds.request.duration,
|
|
4429
|
+
metadata: {
|
|
4430
|
+
timeout: this.#options.profile.bounds.request.duration,
|
|
4431
|
+
bytes: this.#options.profile.bounds.request.bytes
|
|
4432
|
+
},
|
|
4433
|
+
fetch: (input, init) => this.#send(input, init)
|
|
4434
|
+
});
|
|
4435
|
+
return this.#daemon.start();
|
|
4436
|
+
}
|
|
4437
|
+
/** Unloads the declared population before opening one trial worker.
|
|
4438
|
+
* @param trial - The scheduled trial.
|
|
4439
|
+
* @param signal - The trial's cancellation.
|
|
4440
|
+
* @param destination - The optional reserved recording destination.
|
|
4441
|
+
* @returns The unchanged destination with its verified maintenance prefix when recording.
|
|
4442
|
+
*/
|
|
4443
|
+
async prepare(trial, signal, destination) {
|
|
4444
|
+
signal.throwIfAborted();
|
|
4445
|
+
const daemon = this.#daemon;
|
|
4446
|
+
if (daemon === void 0) throw new _src_core.StudyError("FAULT", "Daemon startup is required");
|
|
4447
|
+
if (this.#trials.size !== 0) throw new _src_core.StudyError("FAULT", "Daemon preparations cannot overlap");
|
|
4448
|
+
if (destination === void 0) {
|
|
4449
|
+
if (this.#options.profile.record !== void 0) throw new _src_core.StudyError("CUSTODY", "Daemon maintenance requires its reserved sidecar");
|
|
4450
|
+
await daemon.unload();
|
|
4451
|
+
return;
|
|
4452
|
+
}
|
|
4453
|
+
const declaration = readStudyRecordingDeclaration(this.#options.profile, trial);
|
|
4454
|
+
const models = this.#options.profile.provider?.daemon?.models;
|
|
4455
|
+
const first = this.#options.scenarios.find((scenario) => scenario.id === trial.scenario)?.goals[0];
|
|
4456
|
+
if (models === void 0 || first?.id !== declaration.procedure[0]?.goal) throw new _src_core.StudyError("CONFIG", "Maintenance must join the first declared goal");
|
|
4457
|
+
const preparation = new StudyDaemonTrial({
|
|
4458
|
+
destination,
|
|
4459
|
+
declaration: {
|
|
4460
|
+
...declaration,
|
|
4461
|
+
maintenance: models
|
|
4462
|
+
},
|
|
4463
|
+
signal
|
|
4464
|
+
});
|
|
4465
|
+
this.#trials.add(preparation);
|
|
4466
|
+
try {
|
|
4467
|
+
return await preparation.execute(daemon);
|
|
4468
|
+
} finally {
|
|
4469
|
+
this.#trials.delete(preparation);
|
|
4470
|
+
}
|
|
4471
|
+
}
|
|
4472
|
+
/** Stops and joins the owned tree and refuses an unconfirmed cleanup.
|
|
4473
|
+
* @returns Completion after daemon and emitter teardown.
|
|
4474
|
+
*/
|
|
4475
|
+
async stop() {
|
|
4476
|
+
this.#stopped = true;
|
|
4477
|
+
const daemon = this.#daemon;
|
|
4478
|
+
if (daemon === void 0) return;
|
|
4479
|
+
const confirmed = await daemon.stop();
|
|
4480
|
+
await daemon.destroy();
|
|
4481
|
+
if (!confirmed) throw new _src_core.StudyError("FAULT", "Daemon cleanup was not confirmed");
|
|
4482
|
+
}
|
|
4483
|
+
#send(input, init) {
|
|
4484
|
+
for (const trial of this.#trials) return trial.fetch(input, init);
|
|
4485
|
+
return globalThis.fetch(input, init);
|
|
4486
|
+
}
|
|
4487
|
+
};
|
|
4488
|
+
//#endregion
|
|
4489
|
+
//#region src/server/processes/StudyTaskChild.ts
|
|
4490
|
+
/** Supervises one forked adapter child until it closes with one verified response. */
|
|
4491
|
+
var StudyTaskChild = class {
|
|
4492
|
+
#options;
|
|
4493
|
+
#duration;
|
|
4494
|
+
#signal;
|
|
4495
|
+
#failure;
|
|
4496
|
+
#result;
|
|
4497
|
+
#logs = 0;
|
|
2866
4498
|
#executed = false;
|
|
2867
4499
|
/** Binds one adapter evaluation to its owned route, validated deadline and caller cancellation.
|
|
2868
4500
|
* @param options - The owning task's snapshotted entry, root and selection.
|
|
@@ -3020,32 +4652,109 @@ var StudyCommand = class {
|
|
|
3020
4652
|
signal.throwIfAborted();
|
|
3021
4653
|
if (request.command === "check") return this.#check(await this.#prepare(request, signal));
|
|
3022
4654
|
if (request.command === "execute") return this.#execution(request, signal);
|
|
4655
|
+
if (request.command === "freeze") return this.#freeze(request, signal);
|
|
3023
4656
|
if (request.command === "grade") return this.#grade(request, signal);
|
|
3024
4657
|
if (request.command === "label") return this.#label(request, signal);
|
|
3025
4658
|
if (request.command === "report") return this.#report(request, signal);
|
|
3026
4659
|
throw new _src_core.StudyError("CONFIG", "Unsupported study operation");
|
|
3027
4660
|
}
|
|
3028
|
-
async #
|
|
3029
|
-
const
|
|
3030
|
-
|
|
4661
|
+
async #freeze(request, signal) {
|
|
4662
|
+
const selection = {
|
|
4663
|
+
command: "check",
|
|
4664
|
+
target: request.target,
|
|
4665
|
+
profile: request.profile,
|
|
4666
|
+
out: void 0,
|
|
4667
|
+
expected: void 0
|
|
4668
|
+
};
|
|
4669
|
+
const original = await this.#prepare(selection, signal);
|
|
4670
|
+
const instrument = resolveStudyInstrument(this.#entry);
|
|
4671
|
+
const directory = resolveStudyPath((0, node_path.join)(this.#root, "studies"), request.target);
|
|
4672
|
+
const assets = [.../* @__PURE__ */ new Set([
|
|
4673
|
+
this.#entry,
|
|
4674
|
+
(0, node_path.join)(directory, "study.json"),
|
|
4675
|
+
resolveStudyPath(directory, original.definition.rubric),
|
|
4676
|
+
resolveStudyPath(original.bundle, `profiles/${request.profile}.json`),
|
|
4677
|
+
...original.definition.scenarios.map((id) => resolveStudyPath(original.bundle, `scenarios/${id}.json`))
|
|
4678
|
+
])];
|
|
4679
|
+
const loader = new ArtifactLoader({
|
|
4680
|
+
entries: [.../* @__PURE__ */ new Set([
|
|
4681
|
+
original.profile.agent,
|
|
4682
|
+
original.profile.subject,
|
|
4683
|
+
...original.profile.provider === void 0 ? [] : [original.profile.provider.entry]
|
|
4684
|
+
])],
|
|
4685
|
+
roots: [.../* @__PURE__ */ new Set([...original.profile.roots, instrument])],
|
|
4686
|
+
source: {
|
|
4687
|
+
root: original.bundle,
|
|
4688
|
+
entry: (0, node_path.join)(original.bundle, "index.ts")
|
|
4689
|
+
},
|
|
4690
|
+
instrument,
|
|
4691
|
+
assets
|
|
4692
|
+
});
|
|
4693
|
+
loader.inspect();
|
|
4694
|
+
signal.throwIfAborted();
|
|
4695
|
+
const closure = await loader.materialize(request.destination);
|
|
4696
|
+
const candidate = {
|
|
4697
|
+
closure,
|
|
4698
|
+
root: relocateArtifactPath(closure, this.#root),
|
|
4699
|
+
entry: relocateArtifactPath(closure, this.#entry),
|
|
4700
|
+
target: request.target,
|
|
4701
|
+
profile: request.profile,
|
|
4702
|
+
source: original.identity,
|
|
4703
|
+
identity: original.identity,
|
|
4704
|
+
fingerprint: computeStudyCommandFingerprint(original.identity, original.profile.bounds, original.definition)
|
|
4705
|
+
};
|
|
4706
|
+
const checked = await this.#prepare(selection, signal, candidate);
|
|
4707
|
+
if (checked.source !== original.source || checked.identity.fixtures !== original.identity.fixtures || hashStudyBytes(encodeStudyJSON(checked.definition)) !== hashStudyBytes(encodeStudyJSON(original.definition)) || hashStudyBytes(encodeStudyJSON(checked.rubric)) !== hashStudyBytes(encodeStudyJSON(original.rubric))) throw new _src_core.StudyError("FAULT", "Study declarations changed during freezing");
|
|
4708
|
+
const publication = {
|
|
4709
|
+
...candidate,
|
|
4710
|
+
identity: checked.identity,
|
|
4711
|
+
fingerprint: computeStudyCommandFingerprint(checked.identity, checked.profile.bounds, checked.definition)
|
|
4712
|
+
};
|
|
4713
|
+
original.artifacts.verify();
|
|
4714
|
+
loader.verify();
|
|
4715
|
+
checked.artifacts.verify();
|
|
4716
|
+
verifyArtifactClosure(closure);
|
|
4717
|
+
signal.throwIfAborted();
|
|
4718
|
+
await (0, node_fs_promises.writeFile)((0, node_path.join)(closure.destination, "closure.json"), encodeStudyJSON(publication), { flag: "wx" });
|
|
4719
|
+
return valueToWire({
|
|
4720
|
+
command: "freeze",
|
|
4721
|
+
path: closure.destination,
|
|
4722
|
+
digest: closure.digest,
|
|
4723
|
+
fingerprint: publication.fingerprint
|
|
4724
|
+
});
|
|
4725
|
+
}
|
|
4726
|
+
#closure(request) {
|
|
4727
|
+
if (request.closure === void 0) throw new _src_core.StudyError("CONFIG", "Study closure selection is absent");
|
|
4728
|
+
const closure = readStudyClosure((0, node_path.resolve)(this.#root, request.closure));
|
|
4729
|
+
if (closure.target !== request.target || closure.profile !== request.profile) throw new _src_core.StudyError("CONFIG", "Study closure selectors differ from the checked selection");
|
|
4730
|
+
return closure;
|
|
4731
|
+
}
|
|
4732
|
+
async #prepare(request, signal, retained) {
|
|
4733
|
+
const closure = retained ?? (request.closure === void 0 ? void 0 : this.#closure(request));
|
|
4734
|
+
const root = closure?.root ?? this.#root;
|
|
4735
|
+
const entry = closure?.entry ?? this.#entry;
|
|
4736
|
+
const { bundle, definition, scenarios, fixtures, rubric } = await readStudyDeclaration(root, request.target, signal);
|
|
4737
|
+
const profileBytes = await readStudyDeclarationBytes(root, resolveStudyPath(bundle, `profiles/${request.profile}.json`));
|
|
3031
4738
|
const source = (0, _src_core.parseStudyProfile)(decodeStudyJSON(profileBytes));
|
|
3032
4739
|
if (source.name !== request.profile) throw new _src_core.StudyError("CONFIG", "Profile name differs from its selected file");
|
|
3033
4740
|
if (source.bounds.series.duration > 864e5) throw new _src_core.StudyError("CONFIG", "Series duration exceeds the 24-hour host ceiling");
|
|
3034
|
-
const profile = resolveStudyProfile(
|
|
4741
|
+
const profile = resolveStudyProfile(root, source, closure?.closure);
|
|
3035
4742
|
(0, _src_core.validateStudyInputs)(definition, scenarios, rubric, profile);
|
|
3036
4743
|
signal.throwIfAborted();
|
|
3037
4744
|
const artifacts = new StudyArtifactLoader({
|
|
3038
|
-
root
|
|
3039
|
-
entry
|
|
4745
|
+
root,
|
|
4746
|
+
entry,
|
|
3040
4747
|
profile,
|
|
3041
|
-
adapter: (0, node_path.join)(bundle, "index.ts")
|
|
4748
|
+
adapter: (0, node_path.join)(bundle, "index.ts"),
|
|
4749
|
+
...closure === void 0 ? {} : { closure }
|
|
3042
4750
|
});
|
|
3043
4751
|
const selected = await artifacts.inspect();
|
|
3044
4752
|
const validation = await new StudyTask({
|
|
3045
|
-
root
|
|
3046
|
-
entry
|
|
4753
|
+
root,
|
|
4754
|
+
entry,
|
|
3047
4755
|
adapter: (0, node_path.join)(bundle, "index.ts"),
|
|
3048
|
-
roots: profile.roots
|
|
4756
|
+
roots: profile.roots,
|
|
4757
|
+
...closure === void 0 ? {} : { closure }
|
|
3049
4758
|
}).execute({
|
|
3050
4759
|
operation: "validate",
|
|
3051
4760
|
input: {
|
|
@@ -3057,7 +4766,7 @@ var StudyCommand = class {
|
|
|
3057
4766
|
}, signal);
|
|
3058
4767
|
if (validation.value !== true || validation.instrument !== selected.instrument) throw new _src_core.StudyError("FAULT", "Adapter validation identity differs");
|
|
3059
4768
|
artifacts.verify();
|
|
3060
|
-
|
|
4769
|
+
const preparation = {
|
|
3061
4770
|
bundle,
|
|
3062
4771
|
definition,
|
|
3063
4772
|
scenarios: Object.freeze(scenarios),
|
|
@@ -3069,8 +4778,11 @@ var StudyCommand = class {
|
|
|
3069
4778
|
profile: computeStudyProfileDigest(profile)
|
|
3070
4779
|
},
|
|
3071
4780
|
artifacts,
|
|
3072
|
-
source: hashStudyBytes(profileBytes)
|
|
4781
|
+
source: hashStudyBytes(profileBytes),
|
|
4782
|
+
...closure === void 0 ? {} : { closure }
|
|
3073
4783
|
};
|
|
4784
|
+
if (request.closure !== void 0 && closure !== void 0 && (computeStudyCommandFingerprint(preparation.identity, profile.bounds, definition) !== closure.fingerprint || hashStudyBytes(encodeStudyJSON(preparation.identity)) !== hashStudyBytes(encodeStudyJSON(closure.identity)))) throw new _src_core.StudyError("FAULT", `Study closure checked identity differs: ${Object.entries(preparation.identity).filter(([name, digest]) => Object.entries(closure.identity).some(([key, expected]) => key === name && digest !== expected)).map(([name]) => name).join(", ")}`);
|
|
4785
|
+
return preparation;
|
|
3074
4786
|
}
|
|
3075
4787
|
#check(preparation) {
|
|
3076
4788
|
const fingerprint = computeStudyCommandFingerprint(preparation.identity, preparation.profile.bounds, preparation.definition);
|
|
@@ -3083,6 +4795,10 @@ var StudyCommand = class {
|
|
|
3083
4795
|
bounds: preparation.profile.bounds,
|
|
3084
4796
|
identity: preparation.identity,
|
|
3085
4797
|
provenance: { profile: preparation.source },
|
|
4798
|
+
...preparation.closure === void 0 ? {} : { closure: {
|
|
4799
|
+
digest: preparation.closure.closure.digest,
|
|
4800
|
+
source: preparation.closure.source
|
|
4801
|
+
} },
|
|
3086
4802
|
fingerprint,
|
|
3087
4803
|
next: [
|
|
3088
4804
|
"execute",
|
|
@@ -3090,12 +4806,14 @@ var StudyCommand = class {
|
|
|
3090
4806
|
"--profile",
|
|
3091
4807
|
preparation.profile.name,
|
|
3092
4808
|
"--expected",
|
|
3093
|
-
fingerprint
|
|
4809
|
+
fingerprint,
|
|
4810
|
+
...preparation.closure === void 0 ? [] : ["--closure", preparation.closure.closure.destination]
|
|
3094
4811
|
],
|
|
3095
4812
|
schedule: (0, _src_core.buildStudySchedule)(preparation.definition, preparation.scenarios)
|
|
3096
4813
|
});
|
|
3097
4814
|
}
|
|
3098
4815
|
async #execution(request, signal) {
|
|
4816
|
+
const replay = request.replay === void 0 ? void 0 : await this.#replay(request, signal);
|
|
3099
4817
|
const preparation = await this.#prepare(request, signal);
|
|
3100
4818
|
const fingerprint = computeStudyCommandFingerprint(preparation.identity, preparation.profile.bounds, preparation.definition);
|
|
3101
4819
|
if (request.expected !== void 0 && request.expected !== fingerprint) throw new _src_core.StudyError("CONFIG", "Validated execution changed after the supervisor check");
|
|
@@ -3106,61 +4824,80 @@ var StudyCommand = class {
|
|
|
3106
4824
|
const parent = await this.#temporary();
|
|
3107
4825
|
signal.throwIfAborted();
|
|
3108
4826
|
const directory = request.out === void 0 ? (0, node_path.join)(parent, id) : this.#file(request.out);
|
|
3109
|
-
const manifest = {
|
|
3110
|
-
id,
|
|
3111
|
-
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3112
|
-
definition: preparation.definition,
|
|
3113
|
-
scenarios: preparation.scenarios,
|
|
3114
|
-
identity: preparation.identity,
|
|
3115
|
-
schedule: (0, _src_core.buildStudySchedule)(preparation.definition, preparation.scenarios),
|
|
3116
|
-
bounds: preparation.profile.bounds,
|
|
3117
|
-
qualification,
|
|
3118
|
-
host: {
|
|
3119
|
-
node: process.version,
|
|
3120
|
-
platform: process.platform,
|
|
3121
|
-
architecture: process.arch,
|
|
3122
|
-
profile: preparation.source,
|
|
3123
|
-
roots: JSON.stringify(preparation.profile.roots)
|
|
3124
|
-
}
|
|
3125
|
-
};
|
|
3126
|
-
const store = new StudyResultStore({
|
|
3127
|
-
directory,
|
|
3128
|
-
manifest
|
|
3129
|
-
});
|
|
3130
|
-
await store.create();
|
|
3131
4827
|
const scratch = await (0, node_fs_promises.mkdtemp)((0, node_path.join)(parent, "scratch-"));
|
|
3132
4828
|
try {
|
|
3133
|
-
const
|
|
3134
|
-
root: this.#root,
|
|
3135
|
-
entry: this.#entry,
|
|
4829
|
+
const daemon = preparation.profile.provider?.daemon === void 0 || replay?.offline === true ? void 0 : new StudyDaemonSession({
|
|
3136
4830
|
profile: preparation.profile,
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
adapter: (0, node_path.join)(preparation.bundle, "index.ts"),
|
|
3140
|
-
directory: scratch
|
|
3141
|
-
});
|
|
3142
|
-
const attempts = await new _src_core.StudySeries({
|
|
3143
|
-
manifest,
|
|
3144
|
-
driver,
|
|
3145
|
-
store,
|
|
4831
|
+
scenarios: preparation.scenarios,
|
|
4832
|
+
workspace: scratch,
|
|
3146
4833
|
signal
|
|
3147
|
-
}).execute();
|
|
3148
|
-
preparation.artifacts.verify();
|
|
3149
|
-
const seal = await store.seal();
|
|
3150
|
-
return valueToWire({
|
|
3151
|
-
command: "execute",
|
|
3152
|
-
status: attempts.some((attempt) => attempt.failure !== void 0) ? "operational-failure" : "completed",
|
|
3153
|
-
result: id,
|
|
3154
|
-
path: directory,
|
|
3155
|
-
seal: seal.id,
|
|
3156
|
-
fingerprint,
|
|
3157
|
-
next: [
|
|
3158
|
-
"grade",
|
|
3159
|
-
directory,
|
|
3160
|
-
"--rubric",
|
|
3161
|
-
resolveStudyPath(resolveStudyPath((0, node_path.join)(this.#root, "studies"), preparation.definition.name), preparation.definition.rubric)
|
|
3162
|
-
]
|
|
3163
4834
|
});
|
|
4835
|
+
try {
|
|
4836
|
+
const startup = await daemon?.start();
|
|
4837
|
+
const manifest = {
|
|
4838
|
+
id,
|
|
4839
|
+
created: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4840
|
+
definition: preparation.definition,
|
|
4841
|
+
scenarios: preparation.scenarios,
|
|
4842
|
+
identity: preparation.identity,
|
|
4843
|
+
schedule: (0, _src_core.buildStudySchedule)(preparation.definition, preparation.scenarios),
|
|
4844
|
+
bounds: preparation.profile.bounds,
|
|
4845
|
+
...preparation.profile.record === void 0 ? {} : { record: preparation.profile.record },
|
|
4846
|
+
qualification,
|
|
4847
|
+
host: {
|
|
4848
|
+
node: process.version,
|
|
4849
|
+
platform: process.platform,
|
|
4850
|
+
architecture: process.arch,
|
|
4851
|
+
profile: preparation.source,
|
|
4852
|
+
roots: JSON.stringify(preparation.profile.roots),
|
|
4853
|
+
...startup === void 0 ? {} : { daemon: JSON.stringify(startup) }
|
|
4854
|
+
}
|
|
4855
|
+
};
|
|
4856
|
+
const store = new StudyResultStore({
|
|
4857
|
+
directory,
|
|
4858
|
+
manifest
|
|
4859
|
+
});
|
|
4860
|
+
await store.create();
|
|
4861
|
+
const destinations = preparation.profile.record === void 0 ? void 0 : Object.fromEntries(await Promise.all(manifest.schedule.map(async (trial) => [trial.id, await store.exchanges(trial)])));
|
|
4862
|
+
const driver = new StudyProcessDriver({
|
|
4863
|
+
root: preparation.closure?.root ?? this.#root,
|
|
4864
|
+
entry: preparation.closure?.entry ?? this.#entry,
|
|
4865
|
+
profile: preparation.profile,
|
|
4866
|
+
identity: preparation.identity,
|
|
4867
|
+
driver: preparation.definition.driver,
|
|
4868
|
+
adapter: (0, node_path.join)(preparation.bundle, "index.ts"),
|
|
4869
|
+
directory: scratch,
|
|
4870
|
+
...preparation.closure === void 0 ? {} : { closure: preparation.closure },
|
|
4871
|
+
...destinations === void 0 ? {} : { exchanges: destinations },
|
|
4872
|
+
...replay === void 0 ? {} : { replay },
|
|
4873
|
+
...daemon === void 0 ? {} : { prepare: daemon.prepare.bind(daemon) }
|
|
4874
|
+
});
|
|
4875
|
+
const attempts = await new _src_core.StudySeries({
|
|
4876
|
+
manifest,
|
|
4877
|
+
driver,
|
|
4878
|
+
store,
|
|
4879
|
+
signal
|
|
4880
|
+
}).execute();
|
|
4881
|
+
await daemon?.stop();
|
|
4882
|
+
preparation.artifacts.verify();
|
|
4883
|
+
const seal = await store.seal();
|
|
4884
|
+
return valueToWire({
|
|
4885
|
+
command: "execute",
|
|
4886
|
+
status: attempts.some((attempt) => attempt.failure !== void 0) ? "operational-failure" : "completed",
|
|
4887
|
+
result: id,
|
|
4888
|
+
path: directory,
|
|
4889
|
+
seal: seal.id,
|
|
4890
|
+
fingerprint,
|
|
4891
|
+
next: [
|
|
4892
|
+
"grade",
|
|
4893
|
+
directory,
|
|
4894
|
+
"--rubric",
|
|
4895
|
+
resolveStudyPath(resolveStudyPath((0, node_path.join)(this.#root, "studies"), preparation.definition.name), preparation.definition.rubric)
|
|
4896
|
+
]
|
|
4897
|
+
});
|
|
4898
|
+
} finally {
|
|
4899
|
+
await daemon?.stop();
|
|
4900
|
+
}
|
|
3164
4901
|
} finally {
|
|
3165
4902
|
await (0, node_fs_promises.rm)(scratch, {
|
|
3166
4903
|
recursive: true,
|
|
@@ -3169,6 +4906,44 @@ var StudyCommand = class {
|
|
|
3169
4906
|
});
|
|
3170
4907
|
}
|
|
3171
4908
|
}
|
|
4909
|
+
async #replay(request, signal) {
|
|
4910
|
+
if (!isStudyReplaySelection(request.replay)) throw new _src_core.StudyError("CONFIG", "Replay requires an explicit sealed source, prefixes and offline behavior");
|
|
4911
|
+
const selection = {
|
|
4912
|
+
...structuredClone(request.replay),
|
|
4913
|
+
result: this.#file(request.replay.result)
|
|
4914
|
+
};
|
|
4915
|
+
const source = await readStudyResult(selection.result);
|
|
4916
|
+
if (source.status !== "sealed") throw new _src_core.StudyError("CUSTODY", "Replay source is not sealed");
|
|
4917
|
+
const closure = request.closure === void 0 ? void 0 : this.#closure(request);
|
|
4918
|
+
const root = closure?.root ?? this.#root;
|
|
4919
|
+
const entry = closure?.entry ?? this.#entry;
|
|
4920
|
+
const { bundle, definition, scenarios, fixtures } = await readStudyDeclaration(root, request.target, signal);
|
|
4921
|
+
const bytes = await readStudyDeclarationBytes(root, resolveStudyPath(bundle, `profiles/${request.profile}.json`));
|
|
4922
|
+
const profile = resolveStudyProfile(root, (0, _src_core.parseStudyProfile)(decodeStudyJSON(bytes)), closure?.closure);
|
|
4923
|
+
const schedule = (0, _src_core.buildStudySchedule)(definition, scenarios);
|
|
4924
|
+
if ((0, _orkestrel_contract.canonicalStringify)(definition) !== (0, _orkestrel_contract.canonicalStringify)(source.manifest.definition) || (0, _orkestrel_contract.canonicalStringify)(scenarios) !== (0, _orkestrel_contract.canonicalStringify)(source.manifest.scenarios) || (0, _orkestrel_contract.canonicalStringify)(schedule) !== (0, _orkestrel_contract.canonicalStringify)(source.manifest.schedule) || computeStudyFixtureDigest(definition, fixtures) !== source.manifest.identity.fixtures || Object.keys(selection.prefix).length !== schedule.length || !schedule.every((trial) => Object.hasOwn(selection.prefix, trial.id))) throw new _src_core.StudyError("CONFIG", "Replay declaration or scheduled membership changed");
|
|
4925
|
+
for (const trial of schedule) {
|
|
4926
|
+
signal.throwIfAborted();
|
|
4927
|
+
await readStudyReplayHistory(selection, profile, trial);
|
|
4928
|
+
}
|
|
4929
|
+
const artifacts = new StudyArtifactLoader({
|
|
4930
|
+
root,
|
|
4931
|
+
entry,
|
|
4932
|
+
profile,
|
|
4933
|
+
adapter: (0, node_path.join)(bundle, "index.ts"),
|
|
4934
|
+
...closure === void 0 ? {} : { closure }
|
|
4935
|
+
});
|
|
4936
|
+
const selected = await artifacts.inspect();
|
|
4937
|
+
const identity = {
|
|
4938
|
+
...selected,
|
|
4939
|
+
fixtures: computeStudyFixtureDigest(definition, fixtures),
|
|
4940
|
+
profile: computeStudyProfileDigest(profile)
|
|
4941
|
+
};
|
|
4942
|
+
if (selected.agent !== source.manifest.identity.agent || selected.subject !== source.manifest.identity.subject || selected.instrument !== source.manifest.identity.instrument || selected.provider !== source.manifest.identity.provider) throw new _src_core.StudyError("CONFIG", "Replay artifact declaration changed");
|
|
4943
|
+
if (request.expected !== void 0 && request.expected !== computeStudyCommandFingerprint(identity, profile.bounds, definition)) throw new _src_core.StudyError("CONFIG", "Replay execution bounds or declaration changed after check");
|
|
4944
|
+
artifacts.verify();
|
|
4945
|
+
return selection;
|
|
4946
|
+
}
|
|
3172
4947
|
async #grade(request, signal) {
|
|
3173
4948
|
const result = await this.#result(request.target);
|
|
3174
4949
|
const bytes = await readStudyBytes(this.#file(request.rubric), STUDY_CUSTODY_BYTES);
|
|
@@ -3488,6 +5263,138 @@ var StudyRemoteMeter = class {
|
|
|
3488
5263
|
}
|
|
3489
5264
|
};
|
|
3490
5265
|
//#endregion
|
|
5266
|
+
//#region src/server/ollama/StudyOllamaReplay.ts
|
|
5267
|
+
/** Answers declared HTTP prefixes from complete ordered recorded application bytes. */
|
|
5268
|
+
var StudyOllamaReplay = class {
|
|
5269
|
+
#options;
|
|
5270
|
+
#readers = /* @__PURE__ */ new Set();
|
|
5271
|
+
#pending = /* @__PURE__ */ new Set();
|
|
5272
|
+
#started = performance.now();
|
|
5273
|
+
#cursor;
|
|
5274
|
+
#bytes = 0;
|
|
5275
|
+
#calls = 0;
|
|
5276
|
+
#fault;
|
|
5277
|
+
#closed = false;
|
|
5278
|
+
#procedure;
|
|
5279
|
+
/** Owns and revalidates the complete selected history before any sender can be reached.
|
|
5280
|
+
* @param options - Verified history, explicit prefix, executing origin and independent bounds.
|
|
5281
|
+
*/
|
|
5282
|
+
constructor(options) {
|
|
5283
|
+
const { signal, fetch, ...configuration } = options;
|
|
5284
|
+
const owned = structuredClone(configuration);
|
|
5285
|
+
validateStudyExchangeHistory(owned.history);
|
|
5286
|
+
const url = new URL(owned.url);
|
|
5287
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.pathname !== "/" || !Number.isSafeInteger(owned.prefix) || owned.prefix <= 0 || owned.prefix > owned.history.exchanges.length || fetch === void 0 && owned.prefix !== owned.history.exchanges.length || fetch !== void 0 && typeof fetch !== "function" || !isStudyOllamaMetadata(owned.limit)) throw new _src_core.StudyError("CONFIG", "Replay requires an explicit valid origin, prefix and current bounds");
|
|
5288
|
+
if (owned.prefix < owned.history.declaration.maintenance.length) throw new _src_core.StudyError("CONFIG", "Replay prefix omits recorded maintenance");
|
|
5289
|
+
signal.throwIfAborted();
|
|
5290
|
+
this.#options = {
|
|
5291
|
+
...owned,
|
|
5292
|
+
signal,
|
|
5293
|
+
...fetch === void 0 ? {} : { fetch }
|
|
5294
|
+
};
|
|
5295
|
+
this.#cursor = owned.history.declaration.maintenance.length;
|
|
5296
|
+
this.#procedure = owned.history.declaration.procedure.map((step) => {
|
|
5297
|
+
const pair = owned.history.exchanges.find((entry) => entry.attempt.step === step.ordinal && entry.attempt.ordinal <= owned.prefix);
|
|
5298
|
+
if (pair === void 0) return step;
|
|
5299
|
+
const { admission: _admission, ...identity } = step;
|
|
5300
|
+
const { bytes: _bytes, ...exact } = identity;
|
|
5301
|
+
return {
|
|
5302
|
+
...exact,
|
|
5303
|
+
admission: "exact",
|
|
5304
|
+
body: new TextDecoder("utf-8", { fatal: true }).decode(Buffer.from(pair.attempt.body, "base64")),
|
|
5305
|
+
input: pair.attempt.digest
|
|
5306
|
+
};
|
|
5307
|
+
});
|
|
5308
|
+
}
|
|
5309
|
+
/** Holds exact recorded bodies for model requests inside the declared prefix. */
|
|
5310
|
+
get procedure() {
|
|
5311
|
+
return structuredClone(this.#procedure);
|
|
5312
|
+
}
|
|
5313
|
+
/** Supplies recorded native responses, forwarding only after the exact declared prefix.
|
|
5314
|
+
* @param role - The validated Study responsibility.
|
|
5315
|
+
* @param input - The executing origin and exact route.
|
|
5316
|
+
* @param init - The method, encoded body and bounded signal.
|
|
5317
|
+
* @returns The recorded response or the explicit live collaborator's response.
|
|
5318
|
+
*/
|
|
5319
|
+
fetch = (role, input, init) => {
|
|
5320
|
+
if (this.#closed || this.#fault !== void 0 || this.#pending.size !== 0 || this.#readers.size !== 0) {
|
|
5321
|
+
const error = this.#fault?.error ?? new _src_core.StudyError("FAULT", "Replay request is closed, concurrent or follows an unread response");
|
|
5322
|
+
this.#fault ??= { error };
|
|
5323
|
+
return Promise.reject(error);
|
|
5324
|
+
}
|
|
5325
|
+
const pending = this.#fetch(role, input, init).catch((error) => {
|
|
5326
|
+
this.#fault ??= { error };
|
|
5327
|
+
throw error;
|
|
5328
|
+
});
|
|
5329
|
+
this.#pending.add(pending);
|
|
5330
|
+
pending.then(() => this.#pending.delete(pending), () => this.#pending.delete(pending));
|
|
5331
|
+
return pending;
|
|
5332
|
+
};
|
|
5333
|
+
/** Refuses a mismatch, an unread response or an incompletely consumed prefix. */
|
|
5334
|
+
check() {
|
|
5335
|
+
if (this.#fault !== void 0) throw this.#fault.error;
|
|
5336
|
+
this.#options.signal.throwIfAborted();
|
|
5337
|
+
if (this.#cursor < this.#options.prefix || this.#readers.size !== 0 || this.#pending.size !== 0) throw new _src_core.StudyError("FAULT", "Replay prefix is incomplete or still being read");
|
|
5338
|
+
}
|
|
5339
|
+
/** Closes admission and cancels retained replay responses.
|
|
5340
|
+
* @returns Completion after pending sends and owned readers settle.
|
|
5341
|
+
*/
|
|
5342
|
+
async close() {
|
|
5343
|
+
this.#closed = true;
|
|
5344
|
+
await Promise.allSettled([...this.#readers].map((reader) => reader.cancel()));
|
|
5345
|
+
this.#readers.clear();
|
|
5346
|
+
await Promise.allSettled([...this.#pending]);
|
|
5347
|
+
}
|
|
5348
|
+
async #fetch(role, input, init) {
|
|
5349
|
+
this.#options.signal.throwIfAborted();
|
|
5350
|
+
init.signal?.throwIfAborted();
|
|
5351
|
+
if (this.#cursor >= this.#options.prefix) {
|
|
5352
|
+
if (this.#options.fetch === void 0) throw new _src_core.StudyError("FAULT", "Offline replay has no later sender");
|
|
5353
|
+
return this.#options.fetch(role, input, init);
|
|
5354
|
+
}
|
|
5355
|
+
const pair = this.#options.history.exchanges[this.#cursor];
|
|
5356
|
+
const url = new URL(input);
|
|
5357
|
+
const request = new Request(url, init);
|
|
5358
|
+
const body = Buffer.from(await request.arrayBuffer());
|
|
5359
|
+
if (pair === void 0 || url.origin !== new URL(this.#options.url).origin || url.username !== "" || url.password !== "" || url.search !== "" || url.hash !== "" || url.pathname !== pair.attempt.path || request.method !== pair.attempt.method || role !== pair.attempt.role || !body.equals(Buffer.from(pair.attempt.body, "base64"))) throw new _src_core.StudyError("FAULT", "Replay request differs from the next recorded exchange");
|
|
5360
|
+
this.#calls += 1;
|
|
5361
|
+
this.#bytes += body.length + pair.response.size;
|
|
5362
|
+
if (this.#calls > this.#options.limit.calls || this.#bytes > this.#options.limit.bytes) throw new _src_core.StudyError("CAPACITY", "Replay exceeds the executing allowance");
|
|
5363
|
+
if (performance.now() - this.#started >= this.#options.limit.duration) throw new _src_core.StudyError("DEADLINE", "Replay exceeds the executing duration");
|
|
5364
|
+
this.#cursor += 1;
|
|
5365
|
+
const bytes = Buffer.from(pair.response.body, "base64");
|
|
5366
|
+
const source = pair.response.chunks.map((size, index, sizes) => {
|
|
5367
|
+
const offset = sizes.slice(0, index).reduce((total, length) => total + length, 0);
|
|
5368
|
+
return bytes.subarray(offset, offset + size);
|
|
5369
|
+
}).values();
|
|
5370
|
+
const stream = new ReadableStream({
|
|
5371
|
+
pull: (controller) => {
|
|
5372
|
+
if (this.#options.signal.aborted || init.signal?.aborted || this.#closed) {
|
|
5373
|
+
const error = this.#options.signal.reason ?? init.signal?.reason ?? new _src_core.StudyError("FAULT", "Replay response was closed");
|
|
5374
|
+
this.#fault ??= { error };
|
|
5375
|
+
this.#readers.delete(stream);
|
|
5376
|
+
controller.error(error);
|
|
5377
|
+
return;
|
|
5378
|
+
}
|
|
5379
|
+
const chunk = source.next();
|
|
5380
|
+
if (chunk.done) {
|
|
5381
|
+
controller.close();
|
|
5382
|
+
this.#readers.delete(stream);
|
|
5383
|
+
} else controller.enqueue(chunk.value);
|
|
5384
|
+
},
|
|
5385
|
+
cancel: () => {
|
|
5386
|
+
this.#readers.delete(stream);
|
|
5387
|
+
this.#fault ??= { error: new _src_core.StudyError("FAULT", "Replay response was abandoned") };
|
|
5388
|
+
}
|
|
5389
|
+
}, { highWaterMark: 0 });
|
|
5390
|
+
this.#readers.add(stream);
|
|
5391
|
+
return new Response(stream, {
|
|
5392
|
+
status: pair.response.status,
|
|
5393
|
+
headers: pair.response.headers.filter(([name]) => name !== "content-encoding").map(([name, value]) => [name, value])
|
|
5394
|
+
});
|
|
5395
|
+
}
|
|
5396
|
+
};
|
|
5397
|
+
//#endregion
|
|
3491
5398
|
//#region src/server/processes/StudyWorker.ts
|
|
3492
5399
|
/** Executes one verified trial behind parent-authorized admission and custody. */
|
|
3493
5400
|
var StudyWorker = class {
|
|
@@ -3500,6 +5407,9 @@ var StudyWorker = class {
|
|
|
3500
5407
|
#closing = false;
|
|
3501
5408
|
#duration = 1e3;
|
|
3502
5409
|
#executed = false;
|
|
5410
|
+
#exchanges;
|
|
5411
|
+
#replay;
|
|
5412
|
+
#maintenance = [];
|
|
3503
5413
|
#evidence = /* @__PURE__ */ new Set();
|
|
3504
5414
|
/** Serves the inherited IPC channel until its parent disconnects.
|
|
3505
5415
|
* @returns Completion after channel and acquired session cleanup.
|
|
@@ -3521,6 +5431,7 @@ var StudyWorker = class {
|
|
|
3521
5431
|
label: STUDY_PROCESS_LABEL
|
|
3522
5432
|
}).wait(this.#session.close()).catch(() => void 0);
|
|
3523
5433
|
this.#session = void 0;
|
|
5434
|
+
await Promise.allSettled([this.#exchanges?.close(), this.#replay?.close()]);
|
|
3524
5435
|
this.#channel.close();
|
|
3525
5436
|
this.#control.close();
|
|
3526
5437
|
}
|
|
@@ -3532,7 +5443,7 @@ var StudyWorker = class {
|
|
|
3532
5443
|
if (operation === "close") {
|
|
3533
5444
|
if (this.#closing) throw new _src_core.StudyError("FAULT", "Worker cleanup repeated");
|
|
3534
5445
|
this.#closing = true;
|
|
3535
|
-
this.#control.fail({
|
|
5446
|
+
if (this.#meters.size !== 0 || this.#control.failure !== void 0) this.#control.fail({
|
|
3536
5447
|
cause: "interrupted",
|
|
3537
5448
|
message: "Worker cleanup requested",
|
|
3538
5449
|
role: void 0,
|
|
@@ -3543,6 +5454,12 @@ var StudyWorker = class {
|
|
|
3543
5454
|
label: STUDY_PROCESS_LABEL
|
|
3544
5455
|
}).wait(this.#session.close());
|
|
3545
5456
|
this.#session = void 0;
|
|
5457
|
+
try {
|
|
5458
|
+
this.#replay?.check();
|
|
5459
|
+
} finally {
|
|
5460
|
+
await this.#replay?.close();
|
|
5461
|
+
await this.#exchanges?.close();
|
|
5462
|
+
}
|
|
3546
5463
|
this.#artifacts?.verify();
|
|
3547
5464
|
return true;
|
|
3548
5465
|
}
|
|
@@ -3557,11 +5474,58 @@ var StudyWorker = class {
|
|
|
3557
5474
|
this.#opening = true;
|
|
3558
5475
|
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.entry !== "string" || typeof value.root !== "string" || typeof value.directory !== "string" || typeof value.driver !== "string" || typeof value.adapter !== "string" || !(0, _src_core.isStudyProfile)(value.profile) || !isStudyIdentity(value.identity) || !isStudyTrial(value.trial)) throw new _src_core.StudyError("CONFIG", "Worker opening declaration is invalid or driver is unsupported");
|
|
3559
5476
|
this.#duration = value.profile.bounds.request.duration;
|
|
5477
|
+
if (value.closure !== void 0 && !isStudyClosure(value.closure)) throw new _src_core.StudyError("CONFIG", "Worker closure selection is malformed");
|
|
5478
|
+
if (value.replay !== void 0) {
|
|
5479
|
+
if (!isStudyReplaySelection(value.replay) || value.profile.provider === void 0) throw new _src_core.StudyError("CONFIG", "Worker replay selection is invalid");
|
|
5480
|
+
const history = await readStudyReplayHistory(value.replay, value.profile, value.trial);
|
|
5481
|
+
const prefix = value.replay.prefix[value.trial.id];
|
|
5482
|
+
if (prefix === void 0) throw new _src_core.StudyError("CONFIG", "Worker replay prefix is absent");
|
|
5483
|
+
this.#replay = new StudyOllamaReplay({
|
|
5484
|
+
history,
|
|
5485
|
+
prefix,
|
|
5486
|
+
url: value.profile.provider.url,
|
|
5487
|
+
limit: readStudyRecordingDeclaration(value.profile, value.trial).metadata,
|
|
5488
|
+
signal: this.#control.signal,
|
|
5489
|
+
...value.replay.offline ? {} : { fetch: this.#forward.bind(this) }
|
|
5490
|
+
});
|
|
5491
|
+
}
|
|
5492
|
+
const destination = (0, _orkestrel_contract.isRecord)(value.exchanges) ? value.exchanges[value.trial.id] : void 0;
|
|
5493
|
+
if (value.profile.record === void 0 && destination !== void 0) throw new _src_core.StudyError("CONFIG", "Unrecorded worker received a sidecar");
|
|
5494
|
+
if (value.profile.record !== void 0) {
|
|
5495
|
+
if (!isStudyExchangeDestination(destination) || destination.record.bytes !== value.profile.record.bytes) throw new _src_core.StudyError("CONFIG", "Recorded worker has no matching reserved sidecar");
|
|
5496
|
+
const declaration = readStudyRecordingDeclaration(value.profile, value.trial);
|
|
5497
|
+
const retained = destination.retained === void 0 ? void 0 : await readStudyBytes(destination.path, destination.record.bytes);
|
|
5498
|
+
if (retained !== void 0 && (hashStudyBytes(retained) !== destination.retained?.digest || retained.length !== destination.retained.bytes || destination.retained.path !== `trials/${value.trial.id}/exchanges.jsonl`)) throw new _src_core.StudyError("CUSTODY", "Maintenance prefix changed before worker opening");
|
|
5499
|
+
const rows = retained === void 0 ? void 0 : readStudyExchangeRows(retained, false);
|
|
5500
|
+
if (rows !== void 0 && (rows[0]?.phase !== "declaration" || rows[0].trial !== value.trial.id || rows.some((row) => row.phase !== "declaration" && (row.phase !== "attempt" && row.phase !== "response" || row.stage !== "maintenance")) || rows.at(-1)?.phase !== "response")) throw new _src_core.StudyError("CUSTODY", "Retained prefix is not completed maintenance");
|
|
5501
|
+
if (rows !== void 0 && rows[0]?.phase === "declaration") {
|
|
5502
|
+
const { maintenance: _retainedMaintenance, ...opening } = rows[0];
|
|
5503
|
+
const { maintenance: _maintenance, ...expected } = declaration;
|
|
5504
|
+
if ((0, _orkestrel_contract.canonicalStringify)(opening) !== (0, _orkestrel_contract.canonicalStringify)(expected) || rows.filter((row) => row.phase === "response").length !== rows[0].maintenance.length) throw new _src_core.StudyError("CUSTODY", "Retained maintenance declaration differs from the executing profile");
|
|
5505
|
+
}
|
|
5506
|
+
const file = await (0, node_fs_promises.open)(destination.path, rows === void 0 ? "wx" : "a");
|
|
5507
|
+
try {
|
|
5508
|
+
this.#exchanges = new StudyExchangeSink({
|
|
5509
|
+
file,
|
|
5510
|
+
record: destination.record,
|
|
5511
|
+
...rows === void 0 ? {} : { retained: rows }
|
|
5512
|
+
});
|
|
5513
|
+
} catch (error) {
|
|
5514
|
+
await file.close();
|
|
5515
|
+
throw error;
|
|
5516
|
+
}
|
|
5517
|
+
if (rows === void 0) await this.#exchanges.append({
|
|
5518
|
+
...declaration,
|
|
5519
|
+
procedure: this.#replay?.procedure ?? declaration.procedure
|
|
5520
|
+
});
|
|
5521
|
+
else this.#maintenance = rows;
|
|
5522
|
+
}
|
|
3560
5523
|
this.#artifacts = new StudyArtifactLoader({
|
|
3561
5524
|
entry: value.entry,
|
|
3562
5525
|
root: value.root,
|
|
3563
5526
|
profile: value.profile,
|
|
3564
|
-
adapter: value.adapter
|
|
5527
|
+
adapter: value.adapter,
|
|
5528
|
+
...value.closure === void 0 ? {} : { closure: value.closure }
|
|
3565
5529
|
});
|
|
3566
5530
|
const identity = await this.#artifacts.inspect();
|
|
3567
5531
|
if (identity.agent !== value.identity.agent || identity.subject !== value.identity.subject || identity.provider !== value.identity.provider || identity.instrument !== value.identity.instrument || computeStudyProfileDigest(value.profile) !== value.identity.profile) throw new _src_core.StudyError("FAULT", "Worker artifact identity differs from parent declaration");
|
|
@@ -3569,7 +5533,9 @@ var StudyWorker = class {
|
|
|
3569
5533
|
const driver = (await this.#artifacts.adapter()).driver({
|
|
3570
5534
|
...modules,
|
|
3571
5535
|
profile: value.profile,
|
|
3572
|
-
directory: value.directory
|
|
5536
|
+
directory: value.directory,
|
|
5537
|
+
...this.#exchanges === void 0 ? {} : { exchanges: this.#exchanges },
|
|
5538
|
+
...this.#replay === void 0 ? {} : { replay: this.#replay }
|
|
3573
5539
|
});
|
|
3574
5540
|
this.#session = await driver.open(value.trial, this.#control.signal);
|
|
3575
5541
|
if (this.#control.failure !== void 0) throw (0, _src_core.failureToStudyError)(this.#control.failure);
|
|
@@ -3586,6 +5552,16 @@ var StudyWorker = class {
|
|
|
3586
5552
|
});
|
|
3587
5553
|
this.#meters.add(meter);
|
|
3588
5554
|
try {
|
|
5555
|
+
for (const row of this.#maintenance) if (row.phase === "attempt" || row.phase === "response") await this.#append(valueToWire({
|
|
5556
|
+
event: row.phase === "attempt" ? "dispatch" : "wire",
|
|
5557
|
+
goal: value.id,
|
|
5558
|
+
value: {
|
|
5559
|
+
ordinal: row.ordinal,
|
|
5560
|
+
role: row.role,
|
|
5561
|
+
path: row.path
|
|
5562
|
+
}
|
|
5563
|
+
}));
|
|
5564
|
+
this.#maintenance = [];
|
|
3589
5565
|
const product = await this.#control.wait(this.#session.execute(value, {
|
|
3590
5566
|
signal: this.#control.signal,
|
|
3591
5567
|
meter,
|
|
@@ -3621,6 +5597,12 @@ var StudyWorker = class {
|
|
|
3621
5597
|
meter.abort(value.id, value.failure);
|
|
3622
5598
|
}
|
|
3623
5599
|
}
|
|
5600
|
+
#forward(_role, input, init) {
|
|
5601
|
+
return globalThis.fetch(input, {
|
|
5602
|
+
...init,
|
|
5603
|
+
signal: AbortSignal.any([this.#control.signal, ...init.signal === void 0 || init.signal === null ? [] : [init.signal]])
|
|
5604
|
+
});
|
|
5605
|
+
}
|
|
3624
5606
|
};
|
|
3625
5607
|
//#endregion
|
|
3626
5608
|
//#region src/server/processes/StudyTaskWorker.ts
|
|
@@ -3643,6 +5625,9 @@ var StudyTaskWorker = class {
|
|
|
3643
5625
|
}
|
|
3644
5626
|
async #execute(value) {
|
|
3645
5627
|
if (!(0, _orkestrel_contract.isRecord)(value) || Buffer.byteLength(JSON.stringify(value)) > 16777216 || typeof value.adapter !== "string" || typeof value.entry !== "string" || !(0, _orkestrel_contract.arrayOf)(_orkestrel_contract.isString)(value.roots) || !(0, _orkestrel_contract.isRecord)(value.input)) throw new _src_core.StudyError("CONFIG", "Adapter request is malformed");
|
|
5628
|
+
if (value.closure !== void 0 && !isStudyClosure(value.closure)) throw new _src_core.StudyError("CONFIG", "Adapter closure selection is malformed");
|
|
5629
|
+
const closure = value.closure;
|
|
5630
|
+
if (closure !== void 0) verifyArtifactClosure(closure.closure, true);
|
|
3646
5631
|
const runtime = resolveStudyInstrument(value.entry);
|
|
3647
5632
|
const roots = [.../* @__PURE__ */ new Set([...value.roots, runtime])];
|
|
3648
5633
|
validateStudyRuntime(runtime, roots);
|
|
@@ -3662,6 +5647,7 @@ var StudyTaskWorker = class {
|
|
|
3662
5647
|
if (profile === void 0 && value.operation !== "grade") throw new _src_core.StudyError("CONFIG", "Unknown adapter operation");
|
|
3663
5648
|
const output = profile === void 0 ? this.#grade(adapter, value.input) : this.#validate(adapter, value.input, profile);
|
|
3664
5649
|
loader.verify();
|
|
5650
|
+
if (closure !== void 0) verifyArtifactClosure(closure.closure, true);
|
|
3665
5651
|
return valueToWire({
|
|
3666
5652
|
instrument,
|
|
3667
5653
|
value: output
|
|
@@ -3817,7 +5803,8 @@ var StudyLauncher = class {
|
|
|
3817
5803
|
"check",
|
|
3818
5804
|
request.target,
|
|
3819
5805
|
"--profile",
|
|
3820
|
-
request.profile
|
|
5806
|
+
request.profile,
|
|
5807
|
+
...request.closure === void 0 ? [] : ["--closure", request.closure]
|
|
3821
5808
|
];
|
|
3822
5809
|
const reading = await this.#launch(entry, check, STUDY_LAUNCH_DURATION, false);
|
|
3823
5810
|
if (reading.code !== 0) {
|
|
@@ -3835,6 +5822,15 @@ var StudyLauncher = class {
|
|
|
3835
5822
|
"--profile",
|
|
3836
5823
|
request.profile,
|
|
3837
5824
|
...request.out === void 0 ? [] : ["--out", request.out],
|
|
5825
|
+
...request.closure === void 0 ? [] : ["--closure", request.closure],
|
|
5826
|
+
...request.replay === void 0 ? [] : [
|
|
5827
|
+
"--replay",
|
|
5828
|
+
request.replay.result,
|
|
5829
|
+
"--prefix",
|
|
5830
|
+
JSON.stringify(request.replay.prefix),
|
|
5831
|
+
"--offline",
|
|
5832
|
+
String(request.replay.offline)
|
|
5833
|
+
],
|
|
3838
5834
|
"--expected",
|
|
3839
5835
|
value.fingerprint
|
|
3840
5836
|
];
|
|
@@ -4448,7 +6444,7 @@ var StudyAgentScriptGoal = class {
|
|
|
4448
6444
|
return this.#plan;
|
|
4449
6445
|
}
|
|
4450
6446
|
/** Takes the next declared response and advances past it.
|
|
4451
|
-
* @returns The response; undefined
|
|
6447
|
+
* @returns The response; undefined after the sequence is exhausted, which it then stays.
|
|
4452
6448
|
*/
|
|
4453
6449
|
take() {
|
|
4454
6450
|
const response = this.#plan.responses[this.#cursor];
|
|
@@ -4585,433 +6581,77 @@ var StudyAgentScript = class {
|
|
|
4585
6581
|
};
|
|
4586
6582
|
bounded.throwIfAborted();
|
|
4587
6583
|
ticket.finish({
|
|
4588
|
-
role: "agent",
|
|
4589
|
-
calls: 1,
|
|
4590
|
-
tokens: void 0,
|
|
4591
|
-
bytes,
|
|
4592
|
-
duration: Math.ceil(performance.now() - started)
|
|
4593
|
-
});
|
|
4594
|
-
if (response.abort === true) {
|
|
4595
|
-
await plan.context.evidence.append({
|
|
4596
|
-
event: "workload",
|
|
4597
|
-
goal: plan.goal,
|
|
4598
|
-
value: { aborted: true }
|
|
4599
|
-
});
|
|
4600
|
-
plan.workload.abort(/* @__PURE__ */ new Error("Declared workload cancellation"));
|
|
4601
|
-
}
|
|
4602
|
-
return result;
|
|
4603
|
-
} catch (error) {
|
|
4604
|
-
if ((0, _orkestrel_budget.isBudgetError)(error)) this.#cut(ticket, bytes, started);
|
|
4605
|
-
else (0, _src_core.failStudyTicket)(ticket, error, "agent");
|
|
4606
|
-
throw error;
|
|
4607
|
-
} finally {
|
|
4608
|
-
if ((0, _orkestrel_budget.isBudgetError)(signal.reason)) this.#cut(ticket, bytes, started);
|
|
4609
|
-
else (0, _src_core.failStudyTicket)(ticket, signal.reason ?? new _src_core.StudyError("FAULT", "Scripted response was abandoned before completion"), "agent");
|
|
4610
|
-
}
|
|
4611
|
-
} finally {
|
|
4612
|
-
this.#serving.delete(response);
|
|
4613
|
-
}
|
|
4614
|
-
}
|
|
4615
|
-
#cut(ticket, bytes, started) {
|
|
4616
|
-
if (ticket.settled || ticket.signal.aborted) return;
|
|
4617
|
-
ticket.finish({
|
|
4618
|
-
role: "agent",
|
|
4619
|
-
calls: 1,
|
|
4620
|
-
tokens: void 0,
|
|
4621
|
-
bytes,
|
|
4622
|
-
duration: Math.ceil(performance.now() - started)
|
|
4623
|
-
});
|
|
4624
|
-
}
|
|
4625
|
-
async #admit(plan, response) {
|
|
4626
|
-
try {
|
|
4627
|
-
return await plan.context.meter.admit({
|
|
4628
|
-
role: "agent",
|
|
4629
|
-
tokens: response.tokens ?? 1,
|
|
4630
|
-
bytes: response.bytes ?? this.#bytes
|
|
4631
|
-
});
|
|
4632
|
-
} catch (error) {
|
|
4633
|
-
await plan.context.evidence.append({
|
|
4634
|
-
event: "admission",
|
|
4635
|
-
goal: plan.goal,
|
|
4636
|
-
value: {
|
|
4637
|
-
role: "agent",
|
|
4638
|
-
refused: true
|
|
4639
|
-
}
|
|
4640
|
-
});
|
|
4641
|
-
throw error;
|
|
4642
|
-
}
|
|
4643
|
-
}
|
|
4644
|
-
};
|
|
4645
|
-
//#endregion
|
|
4646
|
-
//#region src/server/ollama/helpers.ts
|
|
4647
|
-
/**
|
|
4648
|
-
* Declares the pinned serving identities the Ollama transport verifies, keyed by the label their inventory evidence carries.
|
|
4649
|
-
* @param provider - Declared serving identities
|
|
4650
|
-
* @returns The model pin, plus the judge pin when the provider declares both the judge name and its digest
|
|
4651
|
-
*/
|
|
4652
|
-
function buildStudyOllamaModels(provider) {
|
|
4653
|
-
return {
|
|
4654
|
-
model: {
|
|
4655
|
-
name: provider.model,
|
|
4656
|
-
digest: provider.digest.model
|
|
4657
|
-
},
|
|
4658
|
-
...provider.judge === void 0 || provider.digest.judge === void 0 ? {} : { judge: {
|
|
4659
|
-
name: provider.judge,
|
|
4660
|
-
digest: provider.digest.judge
|
|
4661
|
-
} }
|
|
4662
|
-
};
|
|
4663
|
-
}
|
|
4664
|
-
/**
|
|
4665
|
-
* Builds the provider options one role's declared wire policy determines.
|
|
4666
|
-
* @remarks
|
|
4667
|
-
* The policy's `options` object keeps its identity and key order. A strict policy adds `truncate`
|
|
4668
|
-
* and `shift` as `false`; any other policy omits both keys. The `length` allowance is read by the
|
|
4669
|
-
* transport and projects nothing.
|
|
4670
|
-
* @param policy - The role's declared wire policy
|
|
4671
|
-
* @returns The model, think flag, options object, and the strict native fields when declared
|
|
4672
|
-
*/
|
|
4673
|
-
function buildStudyOllamaOptions(policy) {
|
|
4674
|
-
return {
|
|
4675
|
-
model: policy.model,
|
|
4676
|
-
options: policy.options,
|
|
4677
|
-
think: policy.think,
|
|
4678
|
-
...policy.strict ? {
|
|
4679
|
-
truncate: false,
|
|
4680
|
-
shift: false
|
|
4681
|
-
} : {}
|
|
4682
|
-
};
|
|
4683
|
-
}
|
|
4684
|
-
/**
|
|
4685
|
-
* Validates declared role policies against the pinned inventory before any dispatch.
|
|
4686
|
-
* @param roles - The declared wire policy of each model role
|
|
4687
|
-
* @param models - The pinned served identities
|
|
4688
|
-
* @returns Nothing when every policy names a pinned model and a positive output allowance
|
|
4689
|
-
* @throws StudyError - Thrown with code `CONFIG` when a policy names an unpinned model, declares no positive safe-integer `num_predict`, or makes the raw judge wire strict
|
|
4690
|
-
*/
|
|
4691
|
-
function validateStudyOllamaRoles(roles, models) {
|
|
4692
|
-
const names = Object.values(models).map((model) => model.name);
|
|
4693
|
-
for (const [role, policy] of Object.entries(roles)) {
|
|
4694
|
-
if (policy === void 0) continue;
|
|
4695
|
-
if (!names.includes(policy.model)) throw new _src_core.StudyError("CONFIG", "Ollama role policy names a model the inventory does not pin");
|
|
4696
|
-
const output = policy.options.num_predict;
|
|
4697
|
-
if (output === void 0 || !Number.isSafeInteger(output) || output <= 0) throw new _src_core.StudyError("CONFIG", "Ollama role policy declares no positive num_predict allowance");
|
|
4698
|
-
if (role === "judge" && policy.strict) throw new _src_core.StudyError("CONFIG", "Ollama strict fields apply only to chat roles");
|
|
4699
|
-
}
|
|
4700
|
-
}
|
|
4701
|
-
/**
|
|
4702
|
-
* Reads exact outbound Ollama facts and refuses a request outside its declared policy.
|
|
4703
|
-
* @remarks
|
|
4704
|
-
* The refusal happens before admission, so a refused request is never accounted.
|
|
4705
|
-
* @param options - The admitted origin, pinned inventory, and declared role policies
|
|
4706
|
-
* @param role - The responsible request role
|
|
4707
|
-
* @param input - The requested URL
|
|
4708
|
-
* @param init - The native fetch settings
|
|
4709
|
-
* @returns The validated dispatch facts
|
|
4710
|
-
* @throws StudyError - Thrown with code `CONFIG` when the endpoint, model, options, flags, or strict fields differ from the declaration, or the role declares no policy
|
|
4711
|
-
*/
|
|
4712
|
-
function readStudyOllamaRequest(options, role, input, init) {
|
|
4713
|
-
if (input instanceof Request) throw new _src_core.StudyError("CONFIG", "Ollama transport requires explicit URL requests");
|
|
4714
|
-
const url = new URL(input);
|
|
4715
|
-
const path = role === "metadata" ? init?.method === "POST" ? "/api/show" : "/api/tags" : role === "judge" ? "/api/generate" : "/api/chat";
|
|
4716
|
-
if (url.origin !== new URL(options.url).origin || url.pathname !== path || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "") throw new _src_core.StudyError("CONFIG", "Ollama transport endpoint differs from the admitted origin and path");
|
|
4717
|
-
if (role === "metadata") {
|
|
4718
|
-
if (path === "/api/show") {
|
|
4719
|
-
if (typeof init?.body !== "string") throw new _src_core.StudyError("CONFIG", "Invalid model metadata request");
|
|
4720
|
-
const body = JSON.parse(init.body);
|
|
4721
|
-
if (!(0, _orkestrel_contract.isRecord)(body) || Object.keys(body).length !== 1 || typeof body.model !== "string" || !Object.values(options.models).some((model) => model.name === body.model)) throw new _src_core.StudyError("CONFIG", "Invalid model metadata identity");
|
|
4722
|
-
return {
|
|
4723
|
-
role,
|
|
4724
|
-
path,
|
|
4725
|
-
model: body.model,
|
|
4726
|
-
output: 0,
|
|
4727
|
-
bytes: new TextEncoder().encode(init.body).byteLength,
|
|
4728
|
-
options: {},
|
|
4729
|
-
strict: false,
|
|
4730
|
-
length: false
|
|
4731
|
-
};
|
|
4732
|
-
}
|
|
4733
|
-
if (init?.method !== "GET" || init.body !== void 0) throw new _src_core.StudyError("CONFIG", "Invalid inventory request");
|
|
4734
|
-
return {
|
|
4735
|
-
role,
|
|
4736
|
-
path,
|
|
4737
|
-
model: void 0,
|
|
4738
|
-
output: 0,
|
|
4739
|
-
bytes: 0,
|
|
4740
|
-
options: {},
|
|
4741
|
-
strict: false,
|
|
4742
|
-
length: false
|
|
4743
|
-
};
|
|
4744
|
-
}
|
|
4745
|
-
if (init?.method !== "POST" || typeof init.body !== "string") throw new _src_core.StudyError("CONFIG", "Ollama transport requires an encoded JSON POST");
|
|
4746
|
-
const policy = role === "tool" ? void 0 : options.roles[role];
|
|
4747
|
-
if (policy === void 0) throw new _src_core.StudyError("CONFIG", "Ollama transport role has no declared wire policy");
|
|
4748
|
-
const output = policy.options.num_predict;
|
|
4749
|
-
if (output === void 0 || !Number.isSafeInteger(output) || output <= 0) throw new _src_core.StudyError("CONFIG", "Ollama role policy declares no positive num_predict allowance");
|
|
4750
|
-
const body = JSON.parse(init.body);
|
|
4751
|
-
if (!(0, _orkestrel_contract.isRecord)(body) || body.model !== policy.model || !(0, _orkestrel_contract.isRecord)(body.options) || (0, _orkestrel_contract.canonicalStringify)(body.options) !== (0, _orkestrel_contract.canonicalStringify)(policy.options) || body.stream !== (role !== "judge")) throw new _src_core.StudyError("CONFIG", "Ollama transport model or effective options differ from the declared policy");
|
|
4752
|
-
if (role === "judge" ? body.raw !== true || body.logprobs !== true : typeof body.think !== "boolean" || body.think === true && !policy.think) throw new _src_core.StudyError("CONFIG", "Ollama transport flags differ from the declared policy");
|
|
4753
|
-
const strict = policy.strict && role !== "judge";
|
|
4754
|
-
if (strict && (body.truncate !== false || body.shift !== false)) throw new _src_core.StudyError("CONFIG", "Ollama transport must disable native truncation and shifting");
|
|
4755
|
-
return {
|
|
4756
|
-
role,
|
|
4757
|
-
path,
|
|
4758
|
-
model: policy.model,
|
|
4759
|
-
output,
|
|
4760
|
-
bytes: new TextEncoder().encode(init.body).byteLength,
|
|
4761
|
-
options: { ...policy.options },
|
|
4762
|
-
strict,
|
|
4763
|
-
length: policy.length
|
|
4764
|
-
};
|
|
4765
|
-
}
|
|
4766
|
-
/**
|
|
4767
|
-
* Reads the declared model context ceiling without claiming daemon allocation.
|
|
4768
|
-
* @param value - The parsed Ollama show response
|
|
4769
|
-
* @param context - The requested total context
|
|
4770
|
-
* @returns The unambiguous model context ceiling
|
|
4771
|
-
* @throws StudyError - Thrown with code `FAULT` when context metadata is missing, or `CAPACITY` when it is ambiguous or below the requested context
|
|
4772
|
-
*/
|
|
4773
|
-
function readStudyOllamaSupport(value, context) {
|
|
4774
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !(0, _orkestrel_contract.isRecord)(value.model_info) || typeof value.model_info["general.architecture"] !== "string") throw new _src_core.StudyError("FAULT", "Missing model context metadata");
|
|
4775
|
-
const architecture = value.model_info["general.architecture"];
|
|
4776
|
-
const keys = Object.keys(value.model_info).filter((key) => key.endsWith(".context_length"));
|
|
4777
|
-
const ceiling = value.model_info[`${architecture}.context_length`];
|
|
4778
|
-
if (keys.length !== 1 || typeof ceiling !== "number" || !Number.isSafeInteger(ceiling) || ceiling < context) {
|
|
4779
|
-
const message = "Model context metadata does not support the requested context";
|
|
4780
|
-
throw new _src_core.StudyError("CAPACITY", message, {
|
|
4781
|
-
cause: "capacity",
|
|
4782
|
-
message,
|
|
4783
|
-
role: void 0,
|
|
4784
|
-
stop: {
|
|
4785
|
-
layer: "model",
|
|
4786
|
-
id: "context_length",
|
|
4787
|
-
max: keys.length === 1 && Number.isSafeInteger(ceiling) ? (0, _src_core.parseStudyReading)(ceiling) : void 0,
|
|
4788
|
-
consumed: context
|
|
4789
|
-
}
|
|
4790
|
-
});
|
|
4791
|
-
}
|
|
4792
|
-
return ceiling;
|
|
4793
|
-
}
|
|
4794
|
-
/**
|
|
4795
|
-
* Reads a native Ollama error member as the attributable failure it names, without usage.
|
|
4796
|
-
* @remarks
|
|
4797
|
-
* No token usage is read from the member.
|
|
4798
|
-
* The failure is `CAPACITY` when the member, a string or an object, names the exact structured
|
|
4799
|
-
* type `exceed_context_size_error` with a finite positive integer `n_prompt_tokens` above `n_ctx`,
|
|
4800
|
-
* a `code` of 400 if supplied, and a status of 400 if supplied. Every other member, a malformed
|
|
4801
|
-
* one included, is `FAULT` with its readable reason.
|
|
4802
|
-
* @param value - The parsed HTTP error body or wire row
|
|
4803
|
-
* @param role - The responsibility whose request was dispatched; the failure retains it
|
|
4804
|
-
* @param status - The HTTP status of the carrying response; undefined for a row of a stream that answered 200
|
|
4805
|
-
* @returns Undefined when the value carries no `error` member; otherwise the attributable failure
|
|
4806
|
-
*/
|
|
4807
|
-
function readStudyOllamaFailure(value, role, status) {
|
|
4808
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || !("error" in value)) return void 0;
|
|
4809
|
-
const field = value.error;
|
|
4810
|
-
const decoded = typeof field === "string" ? (0, _orkestrel_contract.parseJSON)(field) : field;
|
|
4811
|
-
const structured = (0, _orkestrel_contract.isRecord)(decoded) ? (0, _orkestrel_contract.isRecord)(decoded.error) ? decoded.error : decoded : void 0;
|
|
4812
|
-
const native = structured?.message;
|
|
4813
|
-
const text = typeof native === "string" && native.trim() !== "" ? native : typeof field === "string" ? field : "";
|
|
4814
|
-
const label = structured?.type;
|
|
4815
|
-
const type = typeof label === "string" && label !== "" ? label : void 0;
|
|
4816
|
-
const origin = status === void 0 ? "Ollama error" : `Ollama HTTP ${status} error`;
|
|
4817
|
-
const prompt = structured?.n_prompt_tokens;
|
|
4818
|
-
const limit = structured?.n_ctx;
|
|
4819
|
-
const capacity = type === "exceed_context_size_error" && (0, _orkestrel_contract.isInteger)(prompt) && prompt > 0 && (0, _orkestrel_contract.isInteger)(limit) && limit > 0 && prompt > limit && (structured?.code === void 0 || structured?.code === 400) && (status === void 0 || status === 400);
|
|
4820
|
-
const reason = text.trim() === "" && type === void 0 ? `Malformed ${origin}` : `${origin}${type === void 0 ? "" : ` (${type})`}${text.trim() === "" ? "" : `: ${text}`}`;
|
|
4821
|
-
return new _src_core.StudyError(capacity ? "CAPACITY" : "FAULT", reason, {
|
|
4822
|
-
cause: capacity ? "capacity" : "infrastructure",
|
|
4823
|
-
message: reason,
|
|
4824
|
-
role,
|
|
4825
|
-
stop: capacity ? {
|
|
4826
|
-
layer: "model",
|
|
4827
|
-
id: "num_ctx",
|
|
4828
|
-
max: (0, _src_core.parseStudyReading)(limit),
|
|
4829
|
-
consumed: (0, _src_core.parseStudyReading)(prompt)
|
|
4830
|
-
} : void 0
|
|
4831
|
-
});
|
|
4832
|
-
}
|
|
4833
|
-
/**
|
|
4834
|
-
* Reads one Ollama response row without inventing absent usage.
|
|
4835
|
-
* @param value - The parsed wire row
|
|
4836
|
-
* @param request - The admitted dispatch
|
|
4837
|
-
* @returns The terminal facts, or undefined for a nonterminal row
|
|
4838
|
-
* @throws StudyError - Thrown with the recognized provider failure when the row carries an `error` member, before any other row check; `FAULT` for a malformed row, model, message, finish reason, or usage
|
|
4839
|
-
*/
|
|
4840
|
-
function readStudyOllamaTerminal(value, request) {
|
|
4841
|
-
const failure = readStudyOllamaFailure(value, request.role);
|
|
4842
|
-
if (failure !== void 0) throw failure;
|
|
4843
|
-
if (!(0, _orkestrel_contract.isRecord)(value) || typeof value.done !== "boolean") throw new _src_core.StudyError("FAULT", "Malformed Ollama response row");
|
|
4844
|
-
if (value.model !== request.model) throw new _src_core.StudyError("FAULT", "Ollama response model differs from the dispatched model");
|
|
4845
|
-
if (request.role !== "judge") {
|
|
4846
|
-
if (!(0, _orkestrel_contract.isRecord)(value.message) || typeof value.message.content !== "string" || value.message.thinking !== void 0 && typeof value.message.thinking !== "string") throw new _src_core.StudyError("FAULT", "Malformed Ollama message");
|
|
4847
|
-
if (value.message.tool_calls !== void 0) {
|
|
4848
|
-
if (!(0, _orkestrel_contract.isArray)(value.message.tool_calls)) throw new _src_core.StudyError("FAULT", "Malformed Ollama tool calls");
|
|
4849
|
-
for (const call of value.message.tool_calls) if (!(0, _orkestrel_contract.isRecord)(call) || !(0, _orkestrel_contract.isRecord)(call.function) || typeof call.function.name !== "string" || !(0, _orkestrel_contract.isRecord)(call.function.arguments)) throw new _src_core.StudyError("FAULT", "Malformed Ollama tool call");
|
|
4850
|
-
}
|
|
4851
|
-
}
|
|
4852
|
-
if (!value.done) return void 0;
|
|
4853
|
-
if (typeof value.done_reason !== "string" || value.done_reason.length === 0) throw new _src_core.StudyError("FAULT", "Missing Ollama finish reason");
|
|
4854
|
-
for (const key of ["prompt_eval_count", "eval_count"]) if (value[key] !== void 0 && (typeof value[key] !== "number" || !Number.isSafeInteger(value[key]) || value[key] < 0)) throw new _src_core.StudyError("FAULT", "Malformed Ollama usage", {
|
|
4855
|
-
cause: "infrastructure",
|
|
4856
|
-
message: "Malformed Ollama usage",
|
|
4857
|
-
role: request.role,
|
|
4858
|
-
stop: void 0
|
|
4859
|
-
});
|
|
4860
|
-
return {
|
|
4861
|
-
prompt: typeof value.prompt_eval_count === "number" ? value.prompt_eval_count : void 0,
|
|
4862
|
-
completion: typeof value.eval_count === "number" ? value.eval_count : void 0,
|
|
4863
|
-
reason: value.done_reason
|
|
4864
|
-
};
|
|
4865
|
-
}
|
|
4866
|
-
/**
|
|
4867
|
-
* Checks terminal policy after the actual wire cost has been retained.
|
|
4868
|
-
* @param terminal - The valid terminal facts
|
|
4869
|
-
* @param room - The reserved context and safety margin
|
|
4870
|
-
* @param request - The admitted dispatch and its declared finish policy; every refusal retains its role
|
|
4871
|
-
* @returns Nothing when the completion satisfies the declared policy
|
|
4872
|
-
* @throws StudyError - Thrown with code `CAPACITY` when the actual prompt exceeds its room, generation exceeds its allowance, or an undeclared `length` finish or capped generation occurs; `FAULT` for another unsupported finish reason
|
|
4873
|
-
*/
|
|
4874
|
-
function validateStudyOllamaCompletion(terminal, room, request) {
|
|
4875
|
-
const { prompt, completion, reason } = terminal;
|
|
4876
|
-
const { role } = request;
|
|
4877
|
-
const capped = {
|
|
4878
|
-
layer: "model",
|
|
4879
|
-
id: "num_predict",
|
|
4880
|
-
max: request.output,
|
|
4881
|
-
consumed: completion
|
|
4882
|
-
};
|
|
4883
|
-
const ceiling = room.context - request.output - room.safety;
|
|
4884
|
-
if (prompt !== void 0 && prompt > ceiling) {
|
|
4885
|
-
const message = "Actual prompt exceeded reserved context capacity";
|
|
4886
|
-
throw new _src_core.StudyError("CAPACITY", message, {
|
|
4887
|
-
cause: "capacity",
|
|
4888
|
-
message,
|
|
4889
|
-
role,
|
|
4890
|
-
stop: {
|
|
4891
|
-
layer: "window",
|
|
4892
|
-
id: "Context room",
|
|
4893
|
-
max: ceiling,
|
|
4894
|
-
consumed: prompt
|
|
6584
|
+
role: "agent",
|
|
6585
|
+
calls: 1,
|
|
6586
|
+
tokens: void 0,
|
|
6587
|
+
bytes,
|
|
6588
|
+
duration: Math.ceil(performance.now() - started)
|
|
6589
|
+
});
|
|
6590
|
+
if (response.abort === true) {
|
|
6591
|
+
await plan.context.evidence.append({
|
|
6592
|
+
event: "workload",
|
|
6593
|
+
goal: plan.goal,
|
|
6594
|
+
value: { aborted: true }
|
|
6595
|
+
});
|
|
6596
|
+
plan.workload.abort(/* @__PURE__ */ new Error("Declared workload cancellation"));
|
|
6597
|
+
}
|
|
6598
|
+
return result;
|
|
6599
|
+
} catch (error) {
|
|
6600
|
+
if ((0, _orkestrel_budget.isBudgetError)(error)) this.#cut(ticket, bytes, started);
|
|
6601
|
+
else (0, _src_core.failStudyTicket)(ticket, error, "agent");
|
|
6602
|
+
throw error;
|
|
6603
|
+
} finally {
|
|
6604
|
+
if ((0, _orkestrel_budget.isBudgetError)(signal.reason)) this.#cut(ticket, bytes, started);
|
|
6605
|
+
else (0, _src_core.failStudyTicket)(ticket, signal.reason ?? new _src_core.StudyError("FAULT", "Scripted response was abandoned before completion"), "agent");
|
|
4895
6606
|
}
|
|
4896
|
-
}
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
const message = "Actual generation exceeded the output allowance";
|
|
4900
|
-
throw new _src_core.StudyError("CAPACITY", message, {
|
|
4901
|
-
cause: "capacity",
|
|
4902
|
-
message,
|
|
4903
|
-
role,
|
|
4904
|
-
stop: capped
|
|
4905
|
-
});
|
|
4906
|
-
}
|
|
4907
|
-
if (!request.length && (reason === "length" || completion !== void 0 && completion >= request.output)) {
|
|
4908
|
-
const message = "Substantive generation reached its output cap";
|
|
4909
|
-
throw new _src_core.StudyError("CAPACITY", message, {
|
|
4910
|
-
cause: "capacity",
|
|
4911
|
-
message,
|
|
4912
|
-
role,
|
|
4913
|
-
stop: capped
|
|
4914
|
-
});
|
|
6607
|
+
} finally {
|
|
6608
|
+
this.#serving.delete(response);
|
|
6609
|
+
}
|
|
4915
6610
|
}
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
6611
|
+
#cut(ticket, bytes, started) {
|
|
6612
|
+
if (ticket.settled || ticket.signal.aborted) return;
|
|
6613
|
+
ticket.finish({
|
|
6614
|
+
role: "agent",
|
|
6615
|
+
calls: 1,
|
|
6616
|
+
tokens: void 0,
|
|
6617
|
+
bytes,
|
|
6618
|
+
duration: Math.ceil(performance.now() - started)
|
|
4923
6619
|
});
|
|
4924
6620
|
}
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
* Reads the pinned serving digest of an admitted request's model.
|
|
4944
|
-
* @param models - The pinned served identities
|
|
4945
|
-
* @param request - The admitted dispatch
|
|
4946
|
-
* @returns The pinned digest, or undefined for an inventory request or an unpinned model
|
|
4947
|
-
*/
|
|
4948
|
-
function readStudyOllamaDigest(models, request) {
|
|
4949
|
-
if (request.model === void 0) return void 0;
|
|
4950
|
-
return Object.values(models).find((model) => model.name === request.model)?.digest;
|
|
4951
|
-
}
|
|
4952
|
-
/**
|
|
4953
|
-
* Reads daemon timing from a terminal row without letting an observation fail the request.
|
|
4954
|
-
* @param extract - The admitted extractor
|
|
4955
|
-
* @param record - The validated terminal row
|
|
4956
|
-
* @returns The raw timing, or undefined when absent or the extractor throws
|
|
4957
|
-
*/
|
|
4958
|
-
function readStudyOllamaTiming(extract, record) {
|
|
4959
|
-
try {
|
|
4960
|
-
return extract(record);
|
|
4961
|
-
} catch {
|
|
4962
|
-
return;
|
|
6621
|
+
async #admit(plan, response) {
|
|
6622
|
+
try {
|
|
6623
|
+
return await plan.context.meter.admit({
|
|
6624
|
+
role: "agent",
|
|
6625
|
+
tokens: response.tokens ?? 1,
|
|
6626
|
+
bytes: response.bytes ?? this.#bytes
|
|
6627
|
+
});
|
|
6628
|
+
} catch (error) {
|
|
6629
|
+
await plan.context.evidence.append({
|
|
6630
|
+
event: "admission",
|
|
6631
|
+
goal: plan.goal,
|
|
6632
|
+
value: {
|
|
6633
|
+
role: "agent",
|
|
6634
|
+
refused: true
|
|
6635
|
+
}
|
|
6636
|
+
});
|
|
6637
|
+
throw error;
|
|
6638
|
+
}
|
|
4963
6639
|
}
|
|
4964
|
-
}
|
|
4965
|
-
/**
|
|
4966
|
-
* Builds the JSON evidence form of a timing observation with raw nanoseconds and counts.
|
|
4967
|
-
* @param timing - The extracted timing
|
|
4968
|
-
* @returns Null when no terminal timing was observed; otherwise every unreported member as null
|
|
4969
|
-
*/
|
|
4970
|
-
function buildStudyOllamaTiming(timing) {
|
|
4971
|
-
if (timing === void 0) return null;
|
|
4972
|
-
return {
|
|
4973
|
-
durations: {
|
|
4974
|
-
total: timing.durations.total ?? null,
|
|
4975
|
-
load: timing.durations.load ?? null,
|
|
4976
|
-
prompt: timing.durations.prompt ?? null,
|
|
4977
|
-
decode: timing.durations.decode ?? null
|
|
4978
|
-
},
|
|
4979
|
-
counts: {
|
|
4980
|
-
prompt: timing.counts.prompt ?? null,
|
|
4981
|
-
decode: timing.counts.decode ?? null,
|
|
4982
|
-
cached: timing.counts.cached ?? null
|
|
4983
|
-
},
|
|
4984
|
-
invalid: [...timing.invalid]
|
|
4985
|
-
};
|
|
4986
|
-
}
|
|
4987
|
-
/**
|
|
4988
|
-
* Builds a reading of the raw prompt text an encoded request body carries, as sent.
|
|
4989
|
-
* @param body - The encoded request body
|
|
4990
|
-
* @returns The prompt's UTF-16 code units and UTF-8 bytes, labeled with its encoding, or undefined when there is no string prompt; these are readings of the actual text, not token allocations
|
|
4991
|
-
*/
|
|
4992
|
-
function buildStudyOllamaRaw(body) {
|
|
4993
|
-
if (typeof body !== "string") return void 0;
|
|
4994
|
-
const parsed = (0, _orkestrel_contract.attempt)(() => JSON.parse(body));
|
|
4995
|
-
if (!parsed.success || !(0, _orkestrel_contract.isRecord)(parsed.value) || typeof parsed.value.prompt !== "string") return void 0;
|
|
4996
|
-
return {
|
|
4997
|
-
encoding: "utf-8",
|
|
4998
|
-
units: parsed.value.prompt.length,
|
|
4999
|
-
bytes: new TextEncoder().encode(parsed.value.prompt).byteLength
|
|
5000
|
-
};
|
|
5001
|
-
}
|
|
6640
|
+
};
|
|
5002
6641
|
//#endregion
|
|
5003
6642
|
//#region src/server/ollama/cloners.ts
|
|
5004
6643
|
/**
|
|
5005
6644
|
* Builds a frozen owned copy of transport configuration that later caller mutation cannot reach.
|
|
5006
6645
|
* @remarks
|
|
5007
6646
|
* Every caller member is read once, outer fields before nested data. Every nested data object is
|
|
5008
|
-
* copied and frozen; the caller's objects are never frozen. The cancellation signal
|
|
5009
|
-
* extractor keep their live identity.
|
|
6647
|
+
* copied and frozen; the caller's objects are never frozen. The cancellation signal, timing
|
|
6648
|
+
* extractor, and fetch collaborator keep their live identity.
|
|
5010
6649
|
* @param options - The caller's transport configuration
|
|
5011
6650
|
* @returns The owned configuration, not yet validated
|
|
5012
6651
|
*/
|
|
5013
6652
|
function buildStudyOllamaTransportOptions(options) {
|
|
5014
|
-
const { url, room, limit, models, roles, signal, observe, timing } = options;
|
|
6653
|
+
const { url, room, limit, models, roles, signal, observe, timing, fetch, metadata, version, procedure, exchanges } = options;
|
|
6654
|
+
if (Object.hasOwn(roles, "maintenance")) throw new _src_core.StudyError("CONFIG", "Maintenance cannot declare a generation policy");
|
|
5015
6655
|
const owned = Object.fromEntries(Object.entries(models).map(([label, { name, digest }]) => [label, Object.freeze({
|
|
5016
6656
|
name,
|
|
5017
6657
|
digest
|
|
@@ -5048,7 +6688,23 @@ function buildStudyOllamaTransportOptions(options) {
|
|
|
5048
6688
|
roles: Object.freeze(policies),
|
|
5049
6689
|
signal,
|
|
5050
6690
|
observe,
|
|
5051
|
-
timing
|
|
6691
|
+
timing,
|
|
6692
|
+
metadata: Object.freeze({ ...metadata }),
|
|
6693
|
+
...version === void 0 ? {} : { version },
|
|
6694
|
+
...procedure === void 0 ? {} : { procedure: Object.freeze(procedure.map((step) => {
|
|
6695
|
+
const { policy, ...identity } = step;
|
|
6696
|
+
if (identity.admission === "exact" && Object.hasOwn(step, "bytes") || identity.admission === "capture" && (Object.hasOwn(step, "body") || Object.hasOwn(step, "input"))) throw new _src_core.StudyError("CONFIG", "Conflicting model-step admission declarations");
|
|
6697
|
+
const { options: wire, ...settings } = policy;
|
|
6698
|
+
return Object.freeze({
|
|
6699
|
+
...identity,
|
|
6700
|
+
policy: Object.freeze({
|
|
6701
|
+
...settings,
|
|
6702
|
+
options: Object.freeze({ ...wire })
|
|
6703
|
+
})
|
|
6704
|
+
});
|
|
6705
|
+
})) },
|
|
6706
|
+
...exchanges === void 0 ? {} : { exchanges },
|
|
6707
|
+
...fetch === void 0 ? {} : { fetch }
|
|
5052
6708
|
});
|
|
5053
6709
|
}
|
|
5054
6710
|
/**
|
|
@@ -5082,6 +6738,10 @@ function buildStudyOllamaSnapshot(input, init, dispatch) {
|
|
|
5082
6738
|
//#region src/server/ollama/StudyOllamaExchange.ts
|
|
5083
6739
|
/** Tracks one owned HTTP response through its counted bytes, decoded rows, terminal and settlement. */
|
|
5084
6740
|
var StudyOllamaExchange = class {
|
|
6741
|
+
slot;
|
|
6742
|
+
identity;
|
|
6743
|
+
headers;
|
|
6744
|
+
ceiling;
|
|
5085
6745
|
activity;
|
|
5086
6746
|
request;
|
|
5087
6747
|
ticket;
|
|
@@ -5098,10 +6758,16 @@ var StudyOllamaExchange = class {
|
|
|
5098
6758
|
#terminal;
|
|
5099
6759
|
#timing;
|
|
5100
6760
|
#settled = false;
|
|
6761
|
+
#chunks = [];
|
|
6762
|
+
#recorded = false;
|
|
5101
6763
|
/** Binds the facts of one dispatched request and its open response.
|
|
5102
6764
|
* @param wire - The request, accounting boundary, reader and identities; its byte count starts the tally.
|
|
5103
6765
|
*/
|
|
5104
6766
|
constructor(wire) {
|
|
6767
|
+
this.slot = wire.slot;
|
|
6768
|
+
this.identity = wire.identity;
|
|
6769
|
+
this.headers = wire.headers;
|
|
6770
|
+
this.ceiling = wire.ceiling;
|
|
5105
6771
|
this.activity = wire.activity;
|
|
5106
6772
|
this.request = wire.request;
|
|
5107
6773
|
this.ticket = wire.ticket;
|
|
@@ -5135,6 +6801,29 @@ var StudyOllamaExchange = class {
|
|
|
5135
6801
|
get settled() {
|
|
5136
6802
|
return this.#settled;
|
|
5137
6803
|
}
|
|
6804
|
+
/** Reports whether a sidecar terminal has already been admitted. */
|
|
6805
|
+
get recorded() {
|
|
6806
|
+
return this.#recorded;
|
|
6807
|
+
}
|
|
6808
|
+
/** Holds the exact bounded bytes and native chunk lengths observed so far. */
|
|
6809
|
+
get reply() {
|
|
6810
|
+
return {
|
|
6811
|
+
status: this.status,
|
|
6812
|
+
headers: this.headers,
|
|
6813
|
+
...buildStudyOllamaBytes(Buffer.concat(this.#chunks)),
|
|
6814
|
+
chunks: this.#chunks.map((chunk) => chunk.byteLength)
|
|
6815
|
+
};
|
|
6816
|
+
}
|
|
6817
|
+
/** Retains one bounded chunk before decoding changes its representation.
|
|
6818
|
+
* @param chunk - The native response bytes already counted under the ceiling.
|
|
6819
|
+
*/
|
|
6820
|
+
retain(chunk) {
|
|
6821
|
+
if (this.identity !== void 0 && chunk.byteLength > 0) this.#chunks = [...this.#chunks, Buffer.from(chunk)];
|
|
6822
|
+
}
|
|
6823
|
+
/** Marks a sidecar terminal admitted so failure cleanup cannot append another. */
|
|
6824
|
+
record() {
|
|
6825
|
+
this.#recorded = true;
|
|
6826
|
+
}
|
|
5138
6827
|
/** Adds a response chunk's bytes to the tally and refuses a tally beyond the allowance.
|
|
5139
6828
|
* @param length - The chunk's byte length.
|
|
5140
6829
|
* @param limit - The response byte allowance.
|
|
@@ -5223,7 +6912,15 @@ var StudyOllamaGoal = class {
|
|
|
5223
6912
|
var StudyOllamaTransport = class {
|
|
5224
6913
|
#options;
|
|
5225
6914
|
#abort = new AbortController();
|
|
6915
|
+
#sends = /* @__PURE__ */ new Set();
|
|
5226
6916
|
#wires = /* @__PURE__ */ new Set();
|
|
6917
|
+
#requests = /* @__PURE__ */ new Set();
|
|
6918
|
+
#verifications = /* @__PURE__ */ new Set();
|
|
6919
|
+
#metadataControl;
|
|
6920
|
+
#metadataScope;
|
|
6921
|
+
#metadataMeter;
|
|
6922
|
+
#passes = 0;
|
|
6923
|
+
#step = 0;
|
|
5227
6924
|
#goal;
|
|
5228
6925
|
#fault;
|
|
5229
6926
|
#count = 0;
|
|
@@ -5233,8 +6930,31 @@ var StudyOllamaTransport = class {
|
|
|
5233
6930
|
*/
|
|
5234
6931
|
constructor(options) {
|
|
5235
6932
|
const owned = buildStudyOllamaTransportOptions(options);
|
|
6933
|
+
if (owned.fetch !== void 0 && !(0, _orkestrel_contract.isFunction)(owned.fetch)) throw new _src_core.StudyError("CONFIG", "Ollama fetch collaborator must be callable");
|
|
5236
6934
|
validateStudyOllamaRoles(owned.roles, owned.models);
|
|
6935
|
+
validateStudyOllamaRecording(owned);
|
|
5237
6936
|
this.#options = owned;
|
|
6937
|
+
this.#metadataControl = new _src_core.StudyControl(owned.signal);
|
|
6938
|
+
const duration = Math.min(_orkestrel_timeout.MAX_TIMEOUT_MS, Math.ceil(owned.metadata.duration));
|
|
6939
|
+
this.#metadataScope = new _src_core.StudyScope({
|
|
6940
|
+
limit: {
|
|
6941
|
+
...owned.metadata,
|
|
6942
|
+
duration: _orkestrel_timeout.MAX_TIMEOUT_MS,
|
|
6943
|
+
tokens: 1
|
|
6944
|
+
},
|
|
6945
|
+
control: this.#metadataControl,
|
|
6946
|
+
label: "Metadata"
|
|
6947
|
+
});
|
|
6948
|
+
this.#metadataMeter = new _src_core.StudyMeter({
|
|
6949
|
+
request: {
|
|
6950
|
+
duration: Math.min(duration, owned.limit.duration),
|
|
6951
|
+
calls: 1,
|
|
6952
|
+
tokens: 1,
|
|
6953
|
+
bytes: owned.metadata.bytes
|
|
6954
|
+
},
|
|
6955
|
+
scopes: [this.#metadataScope],
|
|
6956
|
+
control: this.#metadataControl
|
|
6957
|
+
});
|
|
5238
6958
|
}
|
|
5239
6959
|
/** Binds subsequent wire requests to one live goal until `clear` or `close` ends it.
|
|
5240
6960
|
* @param activity - The goal's accounting boundary.
|
|
@@ -5272,7 +6992,14 @@ var StudyOllamaTransport = class {
|
|
|
5272
6992
|
* @throws StudyError - Thrown with code `FAULT` when no goal is bound; the transport retains this refusal as its failure, as it retains every verification failure.
|
|
5273
6993
|
*/
|
|
5274
6994
|
async verify() {
|
|
6995
|
+
const verification = Symbol("verification");
|
|
5275
6996
|
try {
|
|
6997
|
+
this.check();
|
|
6998
|
+
if (this.#requests.size !== 0 || this.#verifications.size !== 0) throw new _src_core.StudyError("FAULT", "Verification cannot overlap an admitted request");
|
|
6999
|
+
if (this.#goal === void 0) throw new _src_core.StudyError("FAULT", "Model dispatch outside an active goal");
|
|
7000
|
+
if (this.#options.procedure !== void 0 && (this.#passes > 1 || this.#passes === 1 && this.#step !== this.#options.procedure.length)) throw new _src_core.StudyError("FAULT", "Postflight requires the complete model procedure");
|
|
7001
|
+
this.#verifications.add(verification);
|
|
7002
|
+
if (this.#options.version !== void 0) await (await this.fetch("metadata", new URL("/api/version", this.#options.url), { method: "GET" })).text();
|
|
5276
7003
|
validateStudyOllamaInventory(await (await this.fetch("metadata", new URL("/api/tags", this.#options.url), { method: "GET" })).json(), this.#options.models);
|
|
5277
7004
|
const goal = this.#goal;
|
|
5278
7005
|
if (goal === void 0) throw new _src_core.StudyError("FAULT", "Inventory has no goal attribution");
|
|
@@ -5299,9 +7026,20 @@ var StudyOllamaTransport = class {
|
|
|
5299
7026
|
goal: activity.goal,
|
|
5300
7027
|
value: Object.fromEntries(Object.entries(this.#options.models).map(([label, model]) => [label, model.digest]))
|
|
5301
7028
|
});
|
|
7029
|
+
this.check();
|
|
7030
|
+
this.#passes += 1;
|
|
7031
|
+
if (this.#options.exchanges !== void 0 && this.#passes === 2) {
|
|
7032
|
+
if (this.#requests.size !== 0 || this.#sends.size !== 0 || this.#wires.size !== 0) throw new _src_core.StudyError("FAULT", "Cannot complete while an exchange is pending");
|
|
7033
|
+
await this.#options.exchanges.append({
|
|
7034
|
+
phase: "complete",
|
|
7035
|
+
count: this.#options.exchanges.next - 1
|
|
7036
|
+
});
|
|
7037
|
+
}
|
|
5302
7038
|
} catch (error) {
|
|
5303
7039
|
this.fail(error);
|
|
5304
7040
|
throw error;
|
|
7041
|
+
} finally {
|
|
7042
|
+
this.#verifications.delete(verification);
|
|
5305
7043
|
}
|
|
5306
7044
|
}
|
|
5307
7045
|
/** Dispatches exactly one admitted request with bounded streaming reads.
|
|
@@ -5318,52 +7056,22 @@ var StudyOllamaTransport = class {
|
|
|
5318
7056
|
const goal = this.#goal;
|
|
5319
7057
|
if (goal === void 0) throw new _src_core.StudyError("FAULT", "Model dispatch outside an active goal");
|
|
5320
7058
|
const activity = goal.activity;
|
|
7059
|
+
const slot = Symbol("request");
|
|
5321
7060
|
try {
|
|
7061
|
+
if (this.#requests.size !== 0) throw new _src_core.StudyError("FAULT", "A previous Ollama request is still in flight");
|
|
7062
|
+
if (this.#verifications.size !== 0 && role !== "metadata") throw new _src_core.StudyError("FAULT", "Model dispatch cannot overlap verification");
|
|
7063
|
+
this.#requests.add(slot);
|
|
5322
7064
|
const snapshot = buildStudyOllamaSnapshot(input, init, original);
|
|
5323
7065
|
const settings = snapshot.init;
|
|
5324
7066
|
const request = readStudyOllamaRequest(this.#options, role, snapshot.url, settings);
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5328
|
-
throw new _src_core.StudyError("CAPACITY", message, {
|
|
5329
|
-
cause: "capacity",
|
|
5330
|
-
message,
|
|
5331
|
-
role: void 0,
|
|
5332
|
-
stop: {
|
|
5333
|
-
layer: "scope",
|
|
5334
|
-
id: "Wire bytes",
|
|
5335
|
-
max: limit.bytes,
|
|
5336
|
-
consumed: request.bytes
|
|
5337
|
-
}
|
|
5338
|
-
});
|
|
7067
|
+
if (this.#options.procedure !== void 0 && role !== "metadata") {
|
|
7068
|
+
if (this.#passes !== 1) throw new _src_core.StudyError("FAULT", "Model dispatch requires completed preflight and open execution");
|
|
7069
|
+
validateStudyOllamaStep(this.#options.procedure[this.#step], activity.goal, request, settings?.body);
|
|
5339
7070
|
}
|
|
5340
|
-
|
|
5341
|
-
|
|
5342
|
-
tokens: role === "metadata" ? 0 : this.#options.room.context - this.#options.room.safety,
|
|
5343
|
-
bytes: limit.bytes
|
|
5344
|
-
});
|
|
5345
|
-
const id = ++this.#count;
|
|
5346
|
-
const started = performance.now();
|
|
5347
|
-
const signal = AbortSignal.any([
|
|
5348
|
-
this.#options.signal,
|
|
5349
|
-
this.#abort.signal,
|
|
5350
|
-
activity.context.signal,
|
|
5351
|
-
activity.context.meter.signal,
|
|
5352
|
-
ticket.signal,
|
|
5353
|
-
AbortSignal.timeout(limit.duration),
|
|
5354
|
-
...settings?.signal === void 0 || settings.signal === null ? [] : [settings.signal]
|
|
5355
|
-
]);
|
|
5356
|
-
const wire = await this.#send(role, goal, ticket, request, snapshot, signal, id, started);
|
|
5357
|
-
this.#wires.add(wire);
|
|
5358
|
-
if (wire.status < 200 || wire.status > 299) return await this.#refuse(wire);
|
|
5359
|
-
return new Response(new ReadableStream({
|
|
5360
|
-
pull: (controller) => this.#pull(wire, controller),
|
|
5361
|
-
cancel: (reason) => (0, _orkestrel_budget.isBudgetError)(reason) ? this.#cut(wire, reason) : this.#failWire(wire, reason === void 0 ? new _src_core.StudyError("FAULT", "Ollama response cancelled before its end") : reason)
|
|
5362
|
-
}), {
|
|
5363
|
-
status: wire.status,
|
|
5364
|
-
headers: { "content-type": role === "judge" || role === "metadata" ? "application/json" : "application/x-ndjson" }
|
|
5365
|
-
});
|
|
7071
|
+
if (this.#options.exchanges !== void 0 && role === "metadata" && this.#verifications.size === 0) throw new _src_core.StudyError("FAULT", "Recorded metadata requires a verification pass");
|
|
7072
|
+
return await this.#request(slot, role, goal, request, snapshot);
|
|
5366
7073
|
} catch (error) {
|
|
7074
|
+
this.#requests.delete(slot);
|
|
5367
7075
|
this.fail(error);
|
|
5368
7076
|
throw error;
|
|
5369
7077
|
}
|
|
@@ -5373,8 +7081,87 @@ var StudyOllamaTransport = class {
|
|
|
5373
7081
|
*/
|
|
5374
7082
|
async close() {
|
|
5375
7083
|
this.#abort.abort(new _src_core.StudyError("FAULT", "Ollama transport closed"));
|
|
7084
|
+
await Promise.all([...this.#sends].map((sending) => sending.catch((error) => this.fail(error))));
|
|
5376
7085
|
await Promise.all([...this.#wires].map((wire) => this.#failWire(wire, this.#abort.signal.reason)));
|
|
5377
7086
|
this.#goal = void 0;
|
|
7087
|
+
this.#metadataMeter.close();
|
|
7088
|
+
this.#metadataScope.close();
|
|
7089
|
+
this.#metadataControl.close();
|
|
7090
|
+
}
|
|
7091
|
+
async #request(slot, role, goal, request, snapshot) {
|
|
7092
|
+
const activity = goal.activity;
|
|
7093
|
+
const settings = snapshot.init;
|
|
7094
|
+
const metadata = role === "metadata";
|
|
7095
|
+
const available = this.#options.metadata.bytes - this.#metadataMeter.usage.reduce((total, usage) => total + (usage.bytes ?? this.#options.metadata.bytes), 0);
|
|
7096
|
+
const remaining = this.#remaining;
|
|
7097
|
+
if (metadata && remaining <= 0) throw new _src_core.StudyError("DEADLINE", "Metadata duration allowance exhausted", {
|
|
7098
|
+
cause: "deadline",
|
|
7099
|
+
message: "Metadata duration allowance exhausted",
|
|
7100
|
+
role,
|
|
7101
|
+
stop: {
|
|
7102
|
+
layer: "scope",
|
|
7103
|
+
id: "Metadata duration",
|
|
7104
|
+
max: this.#options.metadata.duration,
|
|
7105
|
+
consumed: this.#options.metadata.duration - remaining
|
|
7106
|
+
}
|
|
7107
|
+
});
|
|
7108
|
+
const limit = {
|
|
7109
|
+
duration: metadata ? Math.min(this.#options.limit.duration, Math.ceil(remaining)) : this.#options.limit.duration,
|
|
7110
|
+
bytes: metadata ? Math.min(this.#options.limit.bytes, available) : this.#options.limit.bytes
|
|
7111
|
+
};
|
|
7112
|
+
if (request.bytes >= limit.bytes) {
|
|
7113
|
+
const message = "Encoded request consumes the response byte allowance";
|
|
7114
|
+
throw new _src_core.StudyError("CAPACITY", message, {
|
|
7115
|
+
cause: "capacity",
|
|
7116
|
+
message,
|
|
7117
|
+
role,
|
|
7118
|
+
stop: {
|
|
7119
|
+
layer: "scope",
|
|
7120
|
+
id: "Wire bytes",
|
|
7121
|
+
max: limit.bytes,
|
|
7122
|
+
consumed: request.bytes
|
|
7123
|
+
}
|
|
7124
|
+
});
|
|
7125
|
+
}
|
|
7126
|
+
const ticket = await (metadata ? this.#metadataMeter : activity.context.meter).admit({
|
|
7127
|
+
role,
|
|
7128
|
+
tokens: role === "metadata" ? 0 : this.#options.room.context - this.#options.room.safety,
|
|
7129
|
+
bytes: limit.bytes
|
|
7130
|
+
});
|
|
7131
|
+
const id = this.#options.exchanges?.next ?? ++this.#count;
|
|
7132
|
+
const started = performance.now();
|
|
7133
|
+
const signal = AbortSignal.any([
|
|
7134
|
+
this.#options.signal,
|
|
7135
|
+
this.#abort.signal,
|
|
7136
|
+
activity.context.signal,
|
|
7137
|
+
...metadata ? [this.#metadataMeter.signal] : [activity.context.meter.signal],
|
|
7138
|
+
ticket.signal,
|
|
7139
|
+
AbortSignal.timeout(limit.duration),
|
|
7140
|
+
...settings?.signal === void 0 || settings.signal === null ? [] : [settings.signal]
|
|
7141
|
+
]);
|
|
7142
|
+
const identity = this.#options.exchanges === void 0 ? void 0 : {
|
|
7143
|
+
ordinal: id,
|
|
7144
|
+
goal: activity.goal,
|
|
7145
|
+
role,
|
|
7146
|
+
path: role === "metadata" ? request.path === "/api/version" ? "/api/version" : request.path === "/api/tags" ? "/api/tags" : "/api/show" : role === "judge" ? "/api/generate" : "/api/chat",
|
|
7147
|
+
method: settings?.method === "GET" ? "GET" : "POST",
|
|
7148
|
+
stage: role === "metadata" ? this.#passes === 0 ? "preflight" : "postflight" : "execution",
|
|
7149
|
+
...role === "metadata" ? {} : { step: this.#step + 1 }
|
|
7150
|
+
};
|
|
7151
|
+
const sending = this.#send(role, goal, ticket, request, snapshot, signal, id, started, slot, identity, limit.bytes);
|
|
7152
|
+
this.#sends.add(sending);
|
|
7153
|
+
const wire = await sending.finally(() => this.#sends.delete(sending));
|
|
7154
|
+
if (wire.status < 200 || wire.status > 299) return await this.#refuse(wire);
|
|
7155
|
+
return new Response(new ReadableStream({
|
|
7156
|
+
pull: (controller) => this.#pull(wire, controller),
|
|
7157
|
+
cancel: (reason) => (0, _orkestrel_budget.isBudgetError)(reason) ? this.#cut(wire, reason) : this.#failWire(wire, reason === void 0 ? new _src_core.StudyError("FAULT", "Ollama response cancelled before its end") : reason)
|
|
7158
|
+
}), {
|
|
7159
|
+
status: wire.status,
|
|
7160
|
+
headers: { "content-type": role === "judge" || role === "metadata" ? "application/json" : "application/x-ndjson" }
|
|
7161
|
+
});
|
|
7162
|
+
}
|
|
7163
|
+
get #remaining() {
|
|
7164
|
+
return this.#options.metadata.duration - this.#metadataMeter.usage.reduce((total, usage) => total + usage.duration, 0);
|
|
5378
7165
|
}
|
|
5379
7166
|
async #refuse(wire) {
|
|
5380
7167
|
const failure = await this.#diagnose(wire);
|
|
@@ -5387,10 +7174,11 @@ var StudyOllamaTransport = class {
|
|
|
5387
7174
|
try {
|
|
5388
7175
|
for (;;) {
|
|
5389
7176
|
wire.signal.throwIfAborted();
|
|
5390
|
-
const chunk = await
|
|
7177
|
+
const chunk = await this.#read(wire);
|
|
5391
7178
|
wire.signal.throwIfAborted();
|
|
5392
7179
|
if (chunk.done) break;
|
|
5393
|
-
wire.count(chunk.value.byteLength,
|
|
7180
|
+
wire.count(chunk.value.byteLength, wire.ceiling);
|
|
7181
|
+
wire.retain(chunk.value);
|
|
5394
7182
|
parts.push(decoder.decode(chunk.value, { stream: true }));
|
|
5395
7183
|
}
|
|
5396
7184
|
const body = (0, _orkestrel_contract.parseJSON)(parts.join("") + decoder.decode());
|
|
@@ -5408,10 +7196,11 @@ var StudyOllamaTransport = class {
|
|
|
5408
7196
|
async #pull(wire, controller) {
|
|
5409
7197
|
try {
|
|
5410
7198
|
wire.signal.throwIfAborted();
|
|
5411
|
-
const chunk = await
|
|
7199
|
+
const chunk = await this.#read(wire);
|
|
5412
7200
|
wire.signal.throwIfAborted();
|
|
5413
7201
|
if (!chunk.done) {
|
|
5414
|
-
wire.count(chunk.value.byteLength,
|
|
7202
|
+
wire.count(chunk.value.byteLength, wire.ceiling);
|
|
7203
|
+
wire.retain(chunk.value);
|
|
5415
7204
|
wire.decode(chunk.value);
|
|
5416
7205
|
if (wire.request.role !== "metadata" && wire.request.role !== "judge") this.#rows(wire, false);
|
|
5417
7206
|
controller.enqueue(chunk.value);
|
|
@@ -5421,19 +7210,48 @@ var StudyOllamaTransport = class {
|
|
|
5421
7210
|
if (wire.request.role === "metadata") {
|
|
5422
7211
|
const value = JSON.parse(wire.text);
|
|
5423
7212
|
if (wire.request.path === "/api/tags") validateStudyOllamaInventory(value, this.#options.models);
|
|
5424
|
-
else
|
|
7213
|
+
else if (wire.request.path === "/api/version") {
|
|
7214
|
+
if (!(0, _orkestrel_contract.isRecord)(value) || value.version !== this.#options.version) throw new _src_core.StudyError("FAULT", "Serving version differs from the declared version");
|
|
7215
|
+
} else readStudyOllamaSupport(value, this.#options.room.context);
|
|
5425
7216
|
} else if (wire.request.role === "judge") this.#observe(wire, JSON.parse(wire.text));
|
|
5426
7217
|
else this.#rows(wire, true);
|
|
5427
7218
|
if (wire.request.role !== "metadata" && wire.terminal === void 0) throw new _src_core.StudyError("FAULT", "Ollama response has no terminal row");
|
|
5428
7219
|
await this.#finish(wire);
|
|
7220
|
+
if (wire.request.role === "metadata" && this.#remaining < 0) {
|
|
7221
|
+
const message = "Metadata duration allowance exhausted";
|
|
7222
|
+
throw new _src_core.StudyError("DEADLINE", message, {
|
|
7223
|
+
cause: "deadline",
|
|
7224
|
+
message,
|
|
7225
|
+
role: "metadata",
|
|
7226
|
+
stop: {
|
|
7227
|
+
layer: "scope",
|
|
7228
|
+
id: "Metadata duration",
|
|
7229
|
+
max: this.#options.metadata.duration,
|
|
7230
|
+
consumed: this.#options.metadata.duration - this.#remaining
|
|
7231
|
+
}
|
|
7232
|
+
});
|
|
7233
|
+
}
|
|
5429
7234
|
if (wire.terminal !== void 0) await this.#judge(wire, wire.terminal);
|
|
7235
|
+
if (wire.identity !== void 0) {
|
|
7236
|
+
this.check();
|
|
7237
|
+
await this.#options.exchanges?.append({
|
|
7238
|
+
phase: "response",
|
|
7239
|
+
...wire.identity,
|
|
7240
|
+
...wire.reply
|
|
7241
|
+
});
|
|
7242
|
+
wire.record();
|
|
7243
|
+
if (wire.identity.step !== void 0) this.#step += 1;
|
|
7244
|
+
}
|
|
7245
|
+
this.#requests.delete(wire.slot);
|
|
5430
7246
|
controller.close();
|
|
5431
7247
|
} catch (error) {
|
|
5432
7248
|
this.fail(error);
|
|
5433
7249
|
try {
|
|
7250
|
+
if (wire.settled) await this.#recordFailure(wire, error);
|
|
5434
7251
|
if ((0, _orkestrel_budget.isBudgetError)(error)) await this.#cut(wire, error);
|
|
5435
7252
|
else await this.#failWire(wire, error);
|
|
5436
7253
|
} finally {
|
|
7254
|
+
this.#requests.delete(wire.slot);
|
|
5437
7255
|
controller.error(error);
|
|
5438
7256
|
}
|
|
5439
7257
|
}
|
|
@@ -5473,10 +7291,16 @@ var StudyOllamaTransport = class {
|
|
|
5473
7291
|
#time(row) {
|
|
5474
7292
|
return this.#options.observe && (0, _orkestrel_contract.isRecord)(row) ? readStudyOllamaTiming(this.#options.timing, row) : void 0;
|
|
5475
7293
|
}
|
|
5476
|
-
async #send(role, goal, ticket, request, snapshot, signal, id, started) {
|
|
7294
|
+
async #send(role, goal, ticket, request, snapshot, signal, id, started, slot, identity, ceiling) {
|
|
5477
7295
|
const { url, init: settings, dispatch } = snapshot;
|
|
5478
7296
|
try {
|
|
7297
|
+
this.check();
|
|
5479
7298
|
signal.throwIfAborted();
|
|
7299
|
+
if (identity !== void 0) await this.#options.exchanges?.append({
|
|
7300
|
+
phase: "attempt",
|
|
7301
|
+
...identity,
|
|
7302
|
+
...buildStudyOllamaBytes(new TextEncoder().encode(typeof settings?.body === "string" ? settings.body : ""))
|
|
7303
|
+
});
|
|
5480
7304
|
await goal.activity.context.evidence.append({
|
|
5481
7305
|
event: "dispatch",
|
|
5482
7306
|
goal: goal.activity.goal,
|
|
@@ -5486,18 +7310,28 @@ var StudyOllamaTransport = class {
|
|
|
5486
7310
|
model: request.model ?? null,
|
|
5487
7311
|
options: { ...request.options },
|
|
5488
7312
|
bytes: request.bytes,
|
|
7313
|
+
...identity === void 0 ? {} : { ordinal: identity.ordinal },
|
|
5489
7314
|
...this.#options.observe ? this.#dispatch(goal, request, id, settings, dispatch) : {}
|
|
5490
7315
|
}
|
|
5491
7316
|
});
|
|
5492
7317
|
signal.throwIfAborted();
|
|
7318
|
+
this.check();
|
|
5493
7319
|
const sent = performance.now();
|
|
5494
|
-
const response = await globalThis.fetch(url, {
|
|
7320
|
+
const response = this.#options.fetch === void 0 ? await globalThis.fetch(url, {
|
|
5495
7321
|
...settings,
|
|
5496
7322
|
signal,
|
|
5497
7323
|
redirect: "error"
|
|
5498
|
-
});
|
|
7324
|
+
}) : await this.#fetch(this.#options.fetch, role, url, settings, signal);
|
|
7325
|
+
if (signal.aborted) {
|
|
7326
|
+
response.body?.cancel(signal.reason).catch(() => void 0);
|
|
7327
|
+
signal.throwIfAborted();
|
|
7328
|
+
}
|
|
5499
7329
|
if (response.body === null) throw new _src_core.StudyError("FAULT", `Ollama HTTP response refused: ${response.status}`);
|
|
5500
|
-
|
|
7330
|
+
const wire = new StudyOllamaExchange({
|
|
7331
|
+
slot,
|
|
7332
|
+
identity,
|
|
7333
|
+
ceiling,
|
|
7334
|
+
headers: [...response.headers.entries()].filter(([name]) => name === "content-type" || name === "content-encoding"),
|
|
5501
7335
|
activity: goal.activity,
|
|
5502
7336
|
request,
|
|
5503
7337
|
ticket,
|
|
@@ -5511,23 +7345,88 @@ var StudyOllamaTransport = class {
|
|
|
5511
7345
|
dispatch,
|
|
5512
7346
|
bytes: request.bytes
|
|
5513
7347
|
});
|
|
7348
|
+
this.#wires.add(wire);
|
|
7349
|
+
return wire;
|
|
5514
7350
|
} catch (error) {
|
|
5515
|
-
|
|
5516
|
-
if (
|
|
5517
|
-
|
|
5518
|
-
|
|
5519
|
-
|
|
5520
|
-
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
|
|
7351
|
+
try {
|
|
7352
|
+
if (identity !== void 0 && this.#options.exchanges !== void 0 && this.#options.exchanges.next > identity.ordinal) {
|
|
7353
|
+
this.#fault ??= { error };
|
|
7354
|
+
await this.#options.exchanges.append({
|
|
7355
|
+
phase: "failure",
|
|
7356
|
+
...identity,
|
|
7357
|
+
reason: (0, _src_core.readStudyFailure)(error).message.slice(0, 256)
|
|
7358
|
+
});
|
|
7359
|
+
}
|
|
7360
|
+
} finally {
|
|
7361
|
+
if (!ticket.settled && !ticket.signal.aborted) {
|
|
7362
|
+
if ((0, _orkestrel_budget.isBudgetError)(error)) ticket.finish({
|
|
7363
|
+
role,
|
|
7364
|
+
calls: 1,
|
|
7365
|
+
tokens: void 0,
|
|
7366
|
+
bytes: void 0,
|
|
7367
|
+
duration: Math.ceil(performance.now() - started)
|
|
7368
|
+
});
|
|
7369
|
+
else ticket.fail({
|
|
7370
|
+
...(0, _src_core.readStudyFailure)(error),
|
|
7371
|
+
role
|
|
7372
|
+
});
|
|
7373
|
+
}
|
|
5527
7374
|
}
|
|
5528
7375
|
throw error;
|
|
5529
7376
|
}
|
|
5530
7377
|
}
|
|
7378
|
+
#read(wire) {
|
|
7379
|
+
const listeners = new _orkestrel_abort.Abort();
|
|
7380
|
+
return new Promise((resolve, reject) => {
|
|
7381
|
+
wire.signal.addEventListener("abort", () => reject(wire.signal.reason), {
|
|
7382
|
+
once: true,
|
|
7383
|
+
signal: listeners.signal
|
|
7384
|
+
});
|
|
7385
|
+
wire.signal.throwIfAborted();
|
|
7386
|
+
wire.reader.read().then(resolve, reject);
|
|
7387
|
+
}).finally(() => listeners.abort());
|
|
7388
|
+
}
|
|
7389
|
+
async #recordFailure(wire, error) {
|
|
7390
|
+
if (wire.identity === void 0 || wire.recorded) return;
|
|
7391
|
+
this.#fault ??= { error };
|
|
7392
|
+
const reason = (0, _src_core.readStudyFailure)(error).message;
|
|
7393
|
+
try {
|
|
7394
|
+
await this.#options.exchanges?.append({
|
|
7395
|
+
phase: "failure",
|
|
7396
|
+
...wire.identity,
|
|
7397
|
+
reason: reason.slice(0, 256),
|
|
7398
|
+
response: wire.reply
|
|
7399
|
+
});
|
|
7400
|
+
} catch (failure) {
|
|
7401
|
+
if (!(0, _src_core.isStudyError)(failure) || failure.code !== "CAPACITY") throw failure;
|
|
7402
|
+
await this.#options.exchanges?.append({
|
|
7403
|
+
phase: "failure",
|
|
7404
|
+
...wire.identity,
|
|
7405
|
+
reason: `The reply was not retained because it exceeded the sidecar allowance; ${reason.slice(0, 128)}`
|
|
7406
|
+
});
|
|
7407
|
+
}
|
|
7408
|
+
wire.record();
|
|
7409
|
+
}
|
|
7410
|
+
#fetch(fetch, role, url, settings, signal) {
|
|
7411
|
+
const listeners = new _orkestrel_abort.Abort();
|
|
7412
|
+
return new Promise((resolve, reject) => {
|
|
7413
|
+
signal.addEventListener("abort", () => reject(signal.reason), {
|
|
7414
|
+
once: true,
|
|
7415
|
+
signal: listeners.signal
|
|
7416
|
+
});
|
|
7417
|
+
signal.throwIfAborted();
|
|
7418
|
+
fetch(role, url, {
|
|
7419
|
+
...settings,
|
|
7420
|
+
signal,
|
|
7421
|
+
redirect: "error"
|
|
7422
|
+
}).then((response) => {
|
|
7423
|
+
if (signal.aborted) {
|
|
7424
|
+
response.body?.cancel(signal.reason).catch(() => void 0);
|
|
7425
|
+
reject(signal.reason);
|
|
7426
|
+
} else resolve(response);
|
|
7427
|
+
}, reject);
|
|
7428
|
+
}).finally(() => listeners.abort());
|
|
7429
|
+
}
|
|
5531
7430
|
#dispatch(goal, request, id, init, dispatch) {
|
|
5532
7431
|
const previous = this.#previous;
|
|
5533
7432
|
const model = request.role !== "metadata";
|
|
@@ -5575,6 +7474,7 @@ var StudyOllamaTransport = class {
|
|
|
5575
7474
|
prompt: terminal?.prompt ?? null,
|
|
5576
7475
|
completion: terminal?.completion ?? null,
|
|
5577
7476
|
bytes: wire.bytes,
|
|
7477
|
+
...wire.identity === void 0 ? {} : { ordinal: wire.identity.ordinal },
|
|
5578
7478
|
...observation
|
|
5579
7479
|
}
|
|
5580
7480
|
});
|
|
@@ -5598,6 +7498,7 @@ var StudyOllamaTransport = class {
|
|
|
5598
7498
|
const observation = this.#options.observe ? this.#timeline(wire) : {};
|
|
5599
7499
|
const stop = (0, _src_core.readStudyFailure)(error).stop;
|
|
5600
7500
|
try {
|
|
7501
|
+
await this.#recordFailure(wire, error);
|
|
5601
7502
|
await wire.activity.context.evidence.append({
|
|
5602
7503
|
event: "cut",
|
|
5603
7504
|
goal: wire.activity.goal,
|
|
@@ -5629,6 +7530,7 @@ var StudyOllamaTransport = class {
|
|
|
5629
7530
|
});
|
|
5630
7531
|
throw failure;
|
|
5631
7532
|
} finally {
|
|
7533
|
+
this.#requests.delete(wire.slot);
|
|
5632
7534
|
this.#wires.delete(wire);
|
|
5633
7535
|
await wire.reader.cancel(error).catch(() => void 0);
|
|
5634
7536
|
wire.reader.releaseLock();
|
|
@@ -5645,6 +7547,7 @@ var StudyOllamaTransport = class {
|
|
|
5645
7547
|
...failure,
|
|
5646
7548
|
role: wire.request.role
|
|
5647
7549
|
});
|
|
7550
|
+
await this.#recordFailure(wire, error);
|
|
5648
7551
|
await wire.activity.context.evidence.append({
|
|
5649
7552
|
event: "failure",
|
|
5650
7553
|
goal: wire.activity.goal,
|
|
@@ -5658,6 +7561,7 @@ var StudyOllamaTransport = class {
|
|
|
5658
7561
|
}
|
|
5659
7562
|
});
|
|
5660
7563
|
} finally {
|
|
7564
|
+
this.#requests.delete(wire.slot);
|
|
5661
7565
|
this.#wires.delete(wire);
|
|
5662
7566
|
await wire.reader.cancel(error).catch(() => void 0);
|
|
5663
7567
|
wire.reader.releaseLock();
|
|
@@ -5667,6 +7571,8 @@ var StudyOllamaTransport = class {
|
|
|
5667
7571
|
//#endregion
|
|
5668
7572
|
exports.ARTIFACT_BYTES = ARTIFACT_BYTES;
|
|
5669
7573
|
exports.ARTIFACT_DEPTH = ARTIFACT_DEPTH;
|
|
7574
|
+
exports.ARTIFACT_ERROR_BRAND = ARTIFACT_ERROR_BRAND;
|
|
7575
|
+
exports.ARTIFACT_ERROR_CODES = ARTIFACT_ERROR_CODES;
|
|
5670
7576
|
exports.ARTIFACT_FILES = ARTIFACT_FILES;
|
|
5671
7577
|
exports.ARTIFACT_FILE_BYTES = ARTIFACT_FILE_BYTES;
|
|
5672
7578
|
exports.ARTIFACT_ROOTS = ARTIFACT_ROOTS;
|
|
@@ -5699,9 +7605,13 @@ exports.StudyArtifactLoader = StudyArtifactLoader;
|
|
|
5699
7605
|
exports.StudyCLI = StudyCLI;
|
|
5700
7606
|
exports.StudyChannel = StudyChannel;
|
|
5701
7607
|
exports.StudyCommand = StudyCommand;
|
|
7608
|
+
exports.StudyDaemonSession = StudyDaemonSession;
|
|
7609
|
+
exports.StudyDaemonTrial = StudyDaemonTrial;
|
|
5702
7610
|
exports.StudyEvidenceSink = StudyEvidenceSink;
|
|
7611
|
+
exports.StudyExchangeSink = StudyExchangeSink;
|
|
5703
7612
|
exports.StudyGradingStore = StudyGradingStore;
|
|
5704
7613
|
exports.StudyLauncher = StudyLauncher;
|
|
7614
|
+
exports.StudyOllamaReplay = StudyOllamaReplay;
|
|
5705
7615
|
exports.StudyOllamaTransport = StudyOllamaTransport;
|
|
5706
7616
|
exports.StudyProcessDriver = StudyProcessDriver;
|
|
5707
7617
|
exports.StudyProcessGoal = StudyProcessGoal;
|
|
@@ -5716,6 +7626,8 @@ exports.buildStudyAgentReadings = buildStudyAgentReadings;
|
|
|
5716
7626
|
exports.buildStudyAgentRevision = buildStudyAgentRevision;
|
|
5717
7627
|
exports.buildStudyAgentToolDenial = buildStudyAgentToolDenial;
|
|
5718
7628
|
exports.buildStudyAgentToolResult = buildStudyAgentToolResult;
|
|
7629
|
+
exports.buildStudyExchangeSequence = buildStudyExchangeSequence;
|
|
7630
|
+
exports.buildStudyOllamaBytes = buildStudyOllamaBytes;
|
|
5719
7631
|
exports.buildStudyOllamaModels = buildStudyOllamaModels;
|
|
5720
7632
|
exports.buildStudyOllamaOptions = buildStudyOllamaOptions;
|
|
5721
7633
|
exports.buildStudyOllamaRaw = buildStudyOllamaRaw;
|
|
@@ -5729,6 +7641,7 @@ exports.checkStudyDirectory = checkStudyDirectory;
|
|
|
5729
7641
|
exports.collectStudyAncestors = collectStudyAncestors;
|
|
5730
7642
|
exports.collectStudyEntries = collectStudyEntries;
|
|
5731
7643
|
exports.collectStudyEvidence = collectStudyEvidence;
|
|
7644
|
+
exports.computeArtifactClosureDigest = computeArtifactClosureDigest;
|
|
5732
7645
|
exports.computeStudyAttributions = computeStudyAttributions;
|
|
5733
7646
|
exports.computeStudyCommandFingerprint = computeStudyCommandFingerprint;
|
|
5734
7647
|
exports.computeStudyFixtureDigest = computeStudyFixtureDigest;
|
|
@@ -5745,10 +7658,15 @@ exports.drainStudyChild = drainStudyChild;
|
|
|
5745
7658
|
exports.encodeStudyJSON = encodeStudyJSON;
|
|
5746
7659
|
exports.gradeStudyEvidence = gradeStudyEvidence;
|
|
5747
7660
|
exports.hashStudyBytes = hashStudyBytes;
|
|
7661
|
+
exports.isArtifactClosure = isArtifactClosure;
|
|
5748
7662
|
exports.isArtifactError = isArtifactError;
|
|
7663
|
+
exports.isArtifactKey = isArtifactKey;
|
|
7664
|
+
exports.isArtifactLoaderOptions = isArtifactLoaderOptions;
|
|
7665
|
+
exports.isArtifactMember = isArtifactMember;
|
|
5749
7666
|
exports.isStudyAdapter = isStudyAdapter;
|
|
5750
7667
|
exports.isStudyAgentResponse = isStudyAgentResponse;
|
|
5751
7668
|
exports.isStudyCharge = isStudyCharge;
|
|
7669
|
+
exports.isStudyClosure = isStudyClosure;
|
|
5752
7670
|
exports.isStudyCustodyAttempt = isStudyCustodyAttempt;
|
|
5753
7671
|
exports.isStudyCustodyEvidence = isStudyCustodyEvidence;
|
|
5754
7672
|
exports.isStudyCustodyFailure = isStudyCustodyFailure;
|
|
@@ -5759,22 +7677,40 @@ exports.isStudyCustodyObservation = isStudyCustodyObservation;
|
|
|
5759
7677
|
exports.isStudyCustodySeal = isStudyCustodySeal;
|
|
5760
7678
|
exports.isStudyCustodyTrial = isStudyCustodyTrial;
|
|
5761
7679
|
exports.isStudyEvidence = isStudyEvidence;
|
|
7680
|
+
exports.isStudyExchangeBytes = isStudyExchangeBytes;
|
|
7681
|
+
exports.isStudyExchangeDeclaration = isStudyExchangeDeclaration;
|
|
7682
|
+
exports.isStudyExchangeDestination = isStudyExchangeDestination;
|
|
7683
|
+
exports.isStudyExchangeHistory = isStudyExchangeHistory;
|
|
7684
|
+
exports.isStudyExchangeIdentity = isStudyExchangeIdentity;
|
|
7685
|
+
exports.isStudyExchangeReply = isStudyExchangeReply;
|
|
7686
|
+
exports.isStudyExchangeRow = isStudyExchangeRow;
|
|
5762
7687
|
exports.isStudyFrame = isStudyFrame;
|
|
5763
7688
|
exports.isStudyIdentity = isStudyIdentity;
|
|
7689
|
+
exports.isStudyOllamaMetadata = isStudyOllamaMetadata;
|
|
7690
|
+
exports.isStudyOllamaModel = isStudyOllamaModel;
|
|
7691
|
+
exports.isStudyOllamaPolicy = isStudyOllamaPolicy;
|
|
7692
|
+
exports.isStudyOllamaStep = isStudyOllamaStep;
|
|
5764
7693
|
exports.isStudyProcessFailure = isStudyProcessFailure;
|
|
7694
|
+
exports.isStudyReplaySelection = isStudyReplaySelection;
|
|
5765
7695
|
exports.isStudyTrial = isStudyTrial;
|
|
5766
7696
|
exports.iterateArtifactEntries = iterateArtifactEntries;
|
|
7697
|
+
exports.matchesArtifactCredential = matchesArtifactCredential;
|
|
5767
7698
|
exports.matchesSystemError = matchesSystemError;
|
|
5768
7699
|
exports.parseStudyArguments = parseStudyArguments;
|
|
7700
|
+
exports.projectStudyExchangeIdentity = projectStudyExchangeIdentity;
|
|
5769
7701
|
exports.readArtifact = readArtifact;
|
|
5770
7702
|
exports.readArtifactBytes = readArtifactBytes;
|
|
7703
|
+
exports.readArtifactPhysical = readArtifactPhysical;
|
|
5771
7704
|
exports.readStudyAdapter = readStudyAdapter;
|
|
5772
7705
|
exports.readStudyAgentMessages = readStudyAgentMessages;
|
|
5773
7706
|
exports.readStudyAgentResult = readStudyAgentResult;
|
|
5774
7707
|
exports.readStudyAgentToolCall = readStudyAgentToolCall;
|
|
5775
7708
|
exports.readStudyBytes = readStudyBytes;
|
|
7709
|
+
exports.readStudyClosure = readStudyClosure;
|
|
5776
7710
|
exports.readStudyDeclaration = readStudyDeclaration;
|
|
5777
7711
|
exports.readStudyDeclarationBytes = readStudyDeclarationBytes;
|
|
7712
|
+
exports.readStudyExchangeRows = readStudyExchangeRows;
|
|
7713
|
+
exports.readStudyExchanges = readStudyExchanges;
|
|
5778
7714
|
exports.readStudyHandle = readStudyHandle;
|
|
5779
7715
|
exports.readStudyOllamaDigest = readStudyOllamaDigest;
|
|
5780
7716
|
exports.readStudyOllamaFailure = readStudyOllamaFailure;
|
|
@@ -5782,9 +7718,17 @@ exports.readStudyOllamaRequest = readStudyOllamaRequest;
|
|
|
5782
7718
|
exports.readStudyOllamaSupport = readStudyOllamaSupport;
|
|
5783
7719
|
exports.readStudyOllamaTerminal = readStudyOllamaTerminal;
|
|
5784
7720
|
exports.readStudyOllamaTiming = readStudyOllamaTiming;
|
|
7721
|
+
exports.readStudyPreparedDestination = readStudyPreparedDestination;
|
|
7722
|
+
exports.readStudyRecordingDeclaration = readStudyRecordingDeclaration;
|
|
7723
|
+
exports.readStudyReplayHistory = readStudyReplayHistory;
|
|
5785
7724
|
exports.readStudyResult = readStudyResult;
|
|
7725
|
+
exports.readStudyTrialExchanges = readStudyTrialExchanges;
|
|
5786
7726
|
exports.readStudyVersion = readStudyVersion;
|
|
7727
|
+
exports.relocateArtifactPath = relocateArtifactPath;
|
|
7728
|
+
exports.resolveArtifactBase = resolveArtifactBase;
|
|
7729
|
+
exports.resolveArtifactMember = resolveArtifactMember;
|
|
5787
7730
|
exports.resolveStudyAgentContent = resolveStudyAgentContent;
|
|
7731
|
+
exports.resolveStudyClosurePath = resolveStudyClosurePath;
|
|
5788
7732
|
exports.resolveStudyInstrument = resolveStudyInstrument;
|
|
5789
7733
|
exports.resolveStudyPath = resolveStudyPath;
|
|
5790
7734
|
exports.resolveStudyProfile = resolveStudyProfile;
|
|
@@ -5793,11 +7737,18 @@ exports.scanStudyTree = scanStudyTree;
|
|
|
5793
7737
|
exports.terminateStudyChild = terminateStudyChild;
|
|
5794
7738
|
exports.translateStudyPacketLabeling = translateStudyPacketLabeling;
|
|
5795
7739
|
exports.validateStudyEvidenceBytes = validateStudyEvidenceBytes;
|
|
7740
|
+
exports.validateStudyExchangeEvidence = validateStudyExchangeEvidence;
|
|
7741
|
+
exports.validateStudyExchangeHistory = validateStudyExchangeHistory;
|
|
7742
|
+
exports.validateStudyExchangeRequest = validateStudyExchangeRequest;
|
|
7743
|
+
exports.validateStudyExchangeRows = validateStudyExchangeRows;
|
|
5796
7744
|
exports.validateStudyOllamaCompletion = validateStudyOllamaCompletion;
|
|
5797
7745
|
exports.validateStudyOllamaInventory = validateStudyOllamaInventory;
|
|
7746
|
+
exports.validateStudyOllamaRecording = validateStudyOllamaRecording;
|
|
5798
7747
|
exports.validateStudyOllamaRoles = validateStudyOllamaRoles;
|
|
7748
|
+
exports.validateStudyOllamaStep = validateStudyOllamaStep;
|
|
5799
7749
|
exports.validateStudyRuntime = validateStudyRuntime;
|
|
5800
7750
|
exports.valueToWire = valueToWire;
|
|
7751
|
+
exports.verifyArtifactClosure = verifyArtifactClosure;
|
|
5801
7752
|
exports.writeStudyBytes = writeStudyBytes;
|
|
5802
7753
|
|
|
5803
7754
|
//# sourceMappingURL=index.cjs.map
|