@matamt-dbg/dsh-studio 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/client.js +892 -402
- package/index.js +653 -331
- package/judge.js +530 -0
- package/package.json +5 -2
- package/roles.js +1 -0
- package/skills/studio-pixie-hook-guide/SKILL.md +53 -0
- package/skills/studio-pixie-hook-guide/guide.md +267 -0
package/index.js
CHANGED
|
@@ -53,10 +53,11 @@ var LEGACY_PACKAGE_NAME = "@local/dsh-studio-shell";
|
|
|
53
53
|
var POLICY_PLUGIN = `${PACKAGE_NAME}/policy`;
|
|
54
54
|
var ROLE_PLUGIN = `${PACKAGE_NAME}/roles`;
|
|
55
55
|
var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
|
|
56
|
+
var JUDGE_PLUGIN = `${PACKAGE_NAME}/judge`;
|
|
56
57
|
function currentPluginName(name) {
|
|
57
58
|
return name === LEGACY_PACKAGE_NAME || name.startsWith(`${LEGACY_PACKAGE_NAME}/`) ? PACKAGE_NAME + name.slice(LEGACY_PACKAGE_NAME.length) : name;
|
|
58
59
|
}
|
|
59
|
-
var STUDIO_VERSION = "0.
|
|
60
|
+
var STUDIO_VERSION = "0.4.0";
|
|
60
61
|
|
|
61
62
|
// src/delegation.ts
|
|
62
63
|
var ROLE_TOOL = "subagent_role";
|
|
@@ -255,6 +256,7 @@ function studioLayer() {
|
|
|
255
256
|
return [
|
|
256
257
|
plugin("spill-policy"),
|
|
257
258
|
{ id: "studio-batch", name: BATCH_PLUGIN },
|
|
259
|
+
{ id: "studio-judge", name: JUDGE_PLUGIN },
|
|
258
260
|
plugin("tool-cordis"),
|
|
259
261
|
plugin("tool-plugin-manager", void 0, "plugin-manager/tools")
|
|
260
262
|
];
|
|
@@ -485,7 +487,7 @@ var MasterExtensions = class {
|
|
|
485
487
|
};
|
|
486
488
|
|
|
487
489
|
// src/host/shell.ts
|
|
488
|
-
import { dirname as dirname8, join as
|
|
490
|
+
import { dirname as dirname8, join as join8 } from "node:path";
|
|
489
491
|
import { randomUUID as randomUUID12 } from "node:crypto";
|
|
490
492
|
import { Remote as Remote2, TypertRemoteService as TypertRemoteService2 } from "@deepseek-ai/dsh-typert-protocol";
|
|
491
493
|
|
|
@@ -1607,15 +1609,82 @@ import { dirname as dirname3, resolve as resolve2 } from "node:path";
|
|
|
1607
1609
|
|
|
1608
1610
|
// src/pixie-gate-task.ts
|
|
1609
1611
|
var GATE_WHENS = ["user-message", "agent-message", "tool-before", "tool-after", "turn-end"];
|
|
1610
|
-
var HOOK_EXIT = { continue: 0, skip: 10 };
|
|
1612
|
+
var HOOK_EXIT = { continue: 0, skip: 10, direct: 11 };
|
|
1611
1613
|
var enabledFor = (task, preset) => task.enabled && task.presetIds.includes(preset) && !task.disabledPresetIds?.includes(preset);
|
|
1612
1614
|
function taskProblem(task) {
|
|
1613
1615
|
if (!task.name.trim()) return "name";
|
|
1614
|
-
if (!task.instruction.trim()) return "instruction";
|
|
1616
|
+
if (!task.instruction.trim() && task.advanced.script === void 0) return "instruction";
|
|
1615
1617
|
if (!(task.advanced.timeoutMs > 0) || !Number.isInteger(task.advanced.maxSteers) || task.advanced.maxSteers < 0) return "limits";
|
|
1616
1618
|
return void 0;
|
|
1617
1619
|
}
|
|
1618
1620
|
|
|
1621
|
+
// src/pixie-gate-task-contracts.ts
|
|
1622
|
+
var object3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
1623
|
+
var text = (value) => typeof value === "string" && !!value.trim();
|
|
1624
|
+
function validateGateTask(value) {
|
|
1625
|
+
if (!object3(value) || !text(value.id) || !text(value.name) || typeof value.enabled !== "boolean" || !GATE_WHENS.includes(value.when) || typeof value.instruction !== "string") throw new Error("Invalid gate task");
|
|
1626
|
+
const advanced = value.advanced;
|
|
1627
|
+
if (!object3(advanced) || typeof advanced.confirmReplace !== "boolean" || !["pass", "block"].includes(advanced.onError) || !["root", "child", "any"].includes(advanced.agents) || typeof advanced.timeoutMs !== "number" || !Number.isFinite(advanced.timeoutMs) || typeof advanced.maxSteers !== "number") throw new Error("Invalid gate advanced options");
|
|
1628
|
+
if (advanced.script !== void 0 && (typeof advanced.script !== "string" || (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(advanced.script) || advanced.script.includes("..")))) throw new Error("Invalid gate script filename");
|
|
1629
|
+
if (!Array.isArray(value.presetIds) || !value.presetIds.every(text)) throw new Error("Invalid gate presetIds");
|
|
1630
|
+
if (value.disabledPresetIds !== void 0 && (!Array.isArray(value.disabledPresetIds) || !value.disabledPresetIds.every(text))) throw new Error("Invalid gate disabledPresetIds");
|
|
1631
|
+
const name = typeof advanced.script === "string" ? advanced.script.replace(/\.[^.]+$/, "") : void 0;
|
|
1632
|
+
for (const match of value.instruction.matchAll(/{{\s*([^{}]+?)\s*}}/g)) {
|
|
1633
|
+
if (!name || ![`hooks.${name}.text`, `hooks.${name}.data`].includes(match[1])) throw new Error(`Unknown prompt variable: ${match[1]}`);
|
|
1634
|
+
}
|
|
1635
|
+
const task = structuredClone(value);
|
|
1636
|
+
delete task.advanced.judge;
|
|
1637
|
+
const problem = taskProblem(task);
|
|
1638
|
+
if (problem) throw new Error(`Invalid gate ${problem}`);
|
|
1639
|
+
task.presetIds = [...new Set(task.presetIds)];
|
|
1640
|
+
const disabled = [...new Set(task.disabledPresetIds ?? [])].filter((id) => task.presetIds.includes(id));
|
|
1641
|
+
if (disabled.length) task.disabledPresetIds = disabled;
|
|
1642
|
+
else delete task.disabledPresetIds;
|
|
1643
|
+
return task;
|
|
1644
|
+
}
|
|
1645
|
+
function validateGateTasks(value) {
|
|
1646
|
+
if (!Array.isArray(value)) throw new Error("Gate tasks must be an array");
|
|
1647
|
+
const ids = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Set();
|
|
1648
|
+
return value.map((raw) => {
|
|
1649
|
+
const task = validateGateTask(raw);
|
|
1650
|
+
if (ids.has(task.id)) throw new Error("Duplicate gate id");
|
|
1651
|
+
ids.add(task.id);
|
|
1652
|
+
for (const preset of task.presetIds) {
|
|
1653
|
+
const key = JSON.stringify([preset, task.when]);
|
|
1654
|
+
if (!enabledFor(task, preset)) continue;
|
|
1655
|
+
if (bindings.has(key)) throw new Error("Only one enabled gate per preset and when is supported");
|
|
1656
|
+
bindings.add(key);
|
|
1657
|
+
}
|
|
1658
|
+
return task;
|
|
1659
|
+
});
|
|
1660
|
+
}
|
|
1661
|
+
function parseGateOutput(text2) {
|
|
1662
|
+
const value = JSON.parse(text2);
|
|
1663
|
+
if (!object3(value) || Object.hasOwn(value, "verdict") && !["pass", "stop"].includes(value.verdict)) throw new Error("Gate output needs a pass/stop verdict");
|
|
1664
|
+
for (const [key, field2] of Object.entries(value)) {
|
|
1665
|
+
if (!["verdict", "reason", "replace", "append", "note"].includes(key) || typeof field2 !== "string") throw new Error(`Invalid gate output field: ${key}`);
|
|
1666
|
+
}
|
|
1667
|
+
return { verdict: "pass", ...value };
|
|
1668
|
+
}
|
|
1669
|
+
function gateOutputPlan(task, output, original) {
|
|
1670
|
+
const message = task.when === "user-message" || task.when === "agent-message";
|
|
1671
|
+
const mutable = message || task.when === "tool-after";
|
|
1672
|
+
const changed = output.replace !== void 0 && output.replace !== original;
|
|
1673
|
+
const confirm = message && task.advanced.confirmReplace && changed;
|
|
1674
|
+
const stop = output.verdict === "stop" || confirm;
|
|
1675
|
+
const notes = output.note === void 0 ? [] : [output.note];
|
|
1676
|
+
if (output.replace !== void 0 && (!mutable || stop && !message)) notes.push(output.replace);
|
|
1677
|
+
if (output.append !== void 0 && (!mutable || stop)) notes.push(output.append);
|
|
1678
|
+
return {
|
|
1679
|
+
stop,
|
|
1680
|
+
notes,
|
|
1681
|
+
...output.reason !== void 0 ? { reason: output.reason } : {},
|
|
1682
|
+
...stop && message && output.replace !== void 0 ? { suggestion: output.replace } : {},
|
|
1683
|
+
...!stop && mutable && changed ? { replacement: output.replace } : {},
|
|
1684
|
+
...!stop && mutable && output.append !== void 0 ? { addition: output.append } : {}
|
|
1685
|
+
};
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1619
1688
|
// src/host/pixie-gate-script.ts
|
|
1620
1689
|
function validScriptName(name) {
|
|
1621
1690
|
return /^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(name) && !name.includes("..");
|
|
@@ -1675,8 +1744,16 @@ async function runHookScript(task, target, signal, deps, context, helperEnv = as
|
|
|
1675
1744
|
if (output.aborted) throw new Error("Hook script aborted");
|
|
1676
1745
|
if (output.stdout.truncated || Buffer.byteLength(output.stdout.text) > 256 * 1024) throw new Error("Hook stdout exceeds 256KB");
|
|
1677
1746
|
if (output.exitCode === HOOK_EXIT.skip) return { skipped: true, instruction: task.instruction };
|
|
1678
|
-
if (output.exitCode !== HOOK_EXIT.continue) throw new Error(`Hook script exit ${output.exitCode}: ${output.stderr.text}`);
|
|
1747
|
+
if (output.exitCode !== HOOK_EXIT.continue && output.exitCode !== HOOK_EXIT.direct) throw new Error(`Hook script exit ${output.exitCode}: ${output.stderr.text}`);
|
|
1679
1748
|
const text2 = output.stdout.text.replace(/^[\r\n]+|[\r\n]+$/g, "");
|
|
1749
|
+
if (output.exitCode === HOOK_EXIT.direct) {
|
|
1750
|
+
try {
|
|
1751
|
+
return { skipped: false, direct: parseGateOutput(text2) };
|
|
1752
|
+
} catch (error) {
|
|
1753
|
+
throw new Error(`Hook script exit 11 needs a gate output JSON object on stdout: ${error instanceof Error ? error.message : String(error)}`);
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (!task.instruction.trim()) throw new Error("Hook script asked for Pixie (exit 0) but the gate has no instruction");
|
|
1680
1757
|
let data = "";
|
|
1681
1758
|
try {
|
|
1682
1759
|
const value = JSON.parse(text2);
|
|
@@ -1726,6 +1803,7 @@ function threadMessages(thread) {
|
|
|
1726
1803
|
var DEFAULT_JUDGE_SYSTEM_PROMPT = [
|
|
1727
1804
|
"You are a judgment component. Evaluate the given state against each question independently.",
|
|
1728
1805
|
'Reply with exactly one JSON object and nothing else: {"answers": {<question id>: <answer>}}.',
|
|
1806
|
+
"Each question has instructions (what to decide) and criteria: choice criteria map option names to descriptions; score criteria list ordered level descriptions; noul criteria optionally describe what true and false mean.",
|
|
1729
1807
|
'choice answer: {"choice": <option name>, "probabilities": {<every option name>: number}} \u2014 probabilities sum to 1.',
|
|
1730
1808
|
'score answer: {"score": number from 0 to levels-1 (fractional allowed), "probabilities": [one number per level, in order]} \u2014 probabilities sum to 1.',
|
|
1731
1809
|
'noul answer: {"noul": probability from 0 to 1 that the answer is yes}.'
|
|
@@ -1746,16 +1824,18 @@ function validateJudgeRequest(value) {
|
|
|
1746
1824
|
for (const [id, raw] of entries) {
|
|
1747
1825
|
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(id) || ["__proto__", "constructor", "prototype"].includes(id)) throw new JudgeValidationError("questionId", `Invalid judge question id: ${id}`);
|
|
1748
1826
|
const question = raw;
|
|
1749
|
-
if (question
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
if (
|
|
1758
|
-
} else
|
|
1827
|
+
if (!question || typeof question !== "object" || Array.isArray(question)) throw new JudgeValidationError("questionType", `Question "${id}" must be choice, score, or noul`);
|
|
1828
|
+
if (!["choice", "score", "noul"].includes(question.type)) throw new JudgeValidationError("questionType", `Question "${id}" must be choice, score, or noul`);
|
|
1829
|
+
if (typeof question.instructions !== "string" || !question.instructions.trim()) throw new JudgeValidationError("instructions", `Question "${id}" needs instructions`);
|
|
1830
|
+
const criteria = question.criteria;
|
|
1831
|
+
if (question.type === "choice") {
|
|
1832
|
+
const names3 = criteria && typeof criteria === "object" && !Array.isArray(criteria) ? Object.keys(criteria) : [];
|
|
1833
|
+
if (names3.length < 2 || names3.length > 255 || names3.some((name) => !name.trim() || typeof criteria[name] !== "string")) throw new JudgeValidationError("criteria", `Choice "${id}" needs 2\u2013255 named options with string descriptions`);
|
|
1834
|
+
} else if (question.type === "score") {
|
|
1835
|
+
if (!Array.isArray(criteria) || criteria.length < 2 || criteria.length > 10 || criteria.some((item) => typeof item !== "string" || !item.trim())) throw new JudgeValidationError("criteria", `Score "${id}" needs 2\u201310 level descriptions`);
|
|
1836
|
+
} else if (criteria !== void 0 && (!criteria || typeof criteria !== "object" || Array.isArray(criteria) || Object.entries(criteria).some(([key, value2]) => !["true", "false"].includes(key) || typeof value2 !== "string"))) {
|
|
1837
|
+
throw new JudgeValidationError("criteria", `Invalid noul criteria: ${id}`);
|
|
1838
|
+
}
|
|
1759
1839
|
}
|
|
1760
1840
|
return { state, questions };
|
|
1761
1841
|
}
|
|
@@ -1879,7 +1959,7 @@ var PixieGateStore = class {
|
|
|
1879
1959
|
if (error.code === "ENOENT") return { sessionId, runs: [] };
|
|
1880
1960
|
throw error;
|
|
1881
1961
|
}
|
|
1882
|
-
if (!record5 || typeof record5 !== "object" || Array.isArray(record5) || record5.sessionId !== sessionId || !Array.isArray(record5.runs) || record5.runs.some((run) => !run || typeof run !== "object" || Array.isArray(run) || run.sessionId !== sessionId)) {
|
|
1962
|
+
if (!record5 || typeof record5 !== "object" || Array.isArray(record5) || record5.sessionId !== sessionId || !Array.isArray(record5.runs) || record5.runs.some((run) => !run || typeof run !== "object" || Array.isArray(run) || run.sessionId !== sessionId) || record5.disabledGateIds !== void 0 && (!Array.isArray(record5.disabledGateIds) || record5.disabledGateIds.some((id) => typeof id !== "string"))) {
|
|
1883
1963
|
throw new Error(`Invalid Pixie Gate record for session ${sessionId}`);
|
|
1884
1964
|
}
|
|
1885
1965
|
for (const run of record5.runs) if (run.status === "running" && !this.owned.has(run.id)) run.status = "cancelled";
|
|
@@ -1960,6 +2040,26 @@ var PixieGateStore = class {
|
|
|
1960
2040
|
return structuredClone(run);
|
|
1961
2041
|
});
|
|
1962
2042
|
}
|
|
2043
|
+
/** Gates switched off for this session; a missing record means none. */
|
|
2044
|
+
async disabledGates(sessionId) {
|
|
2045
|
+
return [...(await this.read(sessionId)).disabledGateIds ?? []];
|
|
2046
|
+
}
|
|
2047
|
+
/** Switch one gate for this session only. Shares the run queue, so it never races a run commit. */
|
|
2048
|
+
setGateEnabled(sessionId, gateId, enabled) {
|
|
2049
|
+
if (typeof gateId !== "string" || !gateId) throw new Error("Gate id is required");
|
|
2050
|
+
return this.enqueue(sessionId, async () => {
|
|
2051
|
+
const record5 = await this.read(sessionId);
|
|
2052
|
+
const current = new Set(record5.disabledGateIds ?? []);
|
|
2053
|
+
if (enabled) current.delete(gateId);
|
|
2054
|
+
else current.add(gateId);
|
|
2055
|
+
const next = [...current];
|
|
2056
|
+
if (next.length) record5.disabledGateIds = next;
|
|
2057
|
+
else delete record5.disabledGateIds;
|
|
2058
|
+
await this.save(sessionId, record5);
|
|
2059
|
+
for (const listener of this.listeners.get(sessionId) ?? []) listener({ type: "session", disabledGateIds: [...next] });
|
|
2060
|
+
return next;
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
1963
2063
|
/** Sync means refetch durable history; only committed running executions are replayed. No await between snapshot and subscribe. */
|
|
1964
2064
|
async *follow(sessionId, signal) {
|
|
1965
2065
|
this.file(sessionId);
|
|
@@ -2411,6 +2511,14 @@ function parsePixieSettings(value) {
|
|
|
2411
2511
|
if (typeof raw.judgeSystemPrompt !== "string") throw new Error("Pixie settings judgeSystemPrompt must be a string");
|
|
2412
2512
|
settings.judgeSystemPrompt = raw.judgeSystemPrompt;
|
|
2413
2513
|
}
|
|
2514
|
+
if (raw.judgeEngine !== void 0) {
|
|
2515
|
+
if (raw.judgeEngine !== "llm" && raw.judgeEngine !== "jev") throw new Error("Pixie settings judgeEngine must be llm or jev");
|
|
2516
|
+
settings.judgeEngine = raw.judgeEngine;
|
|
2517
|
+
}
|
|
2518
|
+
if (raw.jevModel !== void 0) {
|
|
2519
|
+
if (typeof raw.jevModel !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(raw.jevModel)) throw new Error("Pixie settings jevModel must be a model ID");
|
|
2520
|
+
settings.jevModel = raw.jevModel;
|
|
2521
|
+
}
|
|
2414
2522
|
return settings;
|
|
2415
2523
|
}
|
|
2416
2524
|
function parsePixieModel(value) {
|
|
@@ -2461,75 +2569,6 @@ var PixieSettingsStore = class {
|
|
|
2461
2569
|
import { mkdir as mkdir5, readFile as readFile6, rename as rename5, writeFile as writeFile5 } from "node:fs/promises";
|
|
2462
2570
|
import { dirname as dirname5 } from "node:path";
|
|
2463
2571
|
import { randomUUID as randomUUID6 } from "node:crypto";
|
|
2464
|
-
|
|
2465
|
-
// src/pixie-gate-task-contracts.ts
|
|
2466
|
-
var object3 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
2467
|
-
var text = (value) => typeof value === "string" && !!value.trim();
|
|
2468
|
-
function validateGateTask(value) {
|
|
2469
|
-
if (!object3(value) || !text(value.id) || !text(value.name) || typeof value.enabled !== "boolean" || !GATE_WHENS.includes(value.when) || typeof value.instruction !== "string") throw new Error("Invalid gate task");
|
|
2470
|
-
const advanced = value.advanced;
|
|
2471
|
-
if (!object3(advanced) || typeof advanced.confirmReplace !== "boolean" || !["pass", "block"].includes(advanced.onError) || !["root", "child", "any"].includes(advanced.agents) || typeof advanced.timeoutMs !== "number" || !Number.isFinite(advanced.timeoutMs) || typeof advanced.maxSteers !== "number") throw new Error("Invalid gate advanced options");
|
|
2472
|
-
if (advanced.script !== void 0 && (typeof advanced.script !== "string" || (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(advanced.script) || advanced.script.includes("..")))) throw new Error("Invalid gate script filename");
|
|
2473
|
-
if (!Array.isArray(value.presetIds) || !value.presetIds.every(text)) throw new Error("Invalid gate presetIds");
|
|
2474
|
-
if (value.disabledPresetIds !== void 0 && (!Array.isArray(value.disabledPresetIds) || !value.disabledPresetIds.every(text))) throw new Error("Invalid gate disabledPresetIds");
|
|
2475
|
-
const name = typeof advanced.script === "string" ? advanced.script.replace(/\.[^.]+$/, "") : void 0;
|
|
2476
|
-
for (const match of value.instruction.matchAll(/{{\s*([^{}]+?)\s*}}/g)) {
|
|
2477
|
-
if (!name || ![`hooks.${name}.text`, `hooks.${name}.data`].includes(match[1])) throw new Error(`Unknown prompt variable: ${match[1]}`);
|
|
2478
|
-
}
|
|
2479
|
-
const task = structuredClone(value);
|
|
2480
|
-
delete task.advanced.judge;
|
|
2481
|
-
const problem = taskProblem(task);
|
|
2482
|
-
if (problem) throw new Error(`Invalid gate ${problem}`);
|
|
2483
|
-
task.presetIds = [...new Set(task.presetIds)];
|
|
2484
|
-
const disabled = [...new Set(task.disabledPresetIds ?? [])].filter((id) => task.presetIds.includes(id));
|
|
2485
|
-
if (disabled.length) task.disabledPresetIds = disabled;
|
|
2486
|
-
else delete task.disabledPresetIds;
|
|
2487
|
-
return task;
|
|
2488
|
-
}
|
|
2489
|
-
function validateGateTasks(value) {
|
|
2490
|
-
if (!Array.isArray(value)) throw new Error("Gate tasks must be an array");
|
|
2491
|
-
const ids = /* @__PURE__ */ new Set(), bindings = /* @__PURE__ */ new Set();
|
|
2492
|
-
return value.map((raw) => {
|
|
2493
|
-
const task = validateGateTask(raw);
|
|
2494
|
-
if (ids.has(task.id)) throw new Error("Duplicate gate id");
|
|
2495
|
-
ids.add(task.id);
|
|
2496
|
-
for (const preset of task.presetIds) {
|
|
2497
|
-
const key = JSON.stringify([preset, task.when]);
|
|
2498
|
-
if (!enabledFor(task, preset)) continue;
|
|
2499
|
-
if (bindings.has(key)) throw new Error("Only one enabled gate per preset and when is supported");
|
|
2500
|
-
bindings.add(key);
|
|
2501
|
-
}
|
|
2502
|
-
return task;
|
|
2503
|
-
});
|
|
2504
|
-
}
|
|
2505
|
-
function parseGateOutput(text2) {
|
|
2506
|
-
const value = JSON.parse(text2);
|
|
2507
|
-
if (!object3(value) || Object.hasOwn(value, "verdict") && !["pass", "stop"].includes(value.verdict)) throw new Error("Gate output needs a pass/stop verdict");
|
|
2508
|
-
for (const [key, field2] of Object.entries(value)) {
|
|
2509
|
-
if (!["verdict", "reason", "replace", "append", "note"].includes(key) || typeof field2 !== "string") throw new Error(`Invalid gate output field: ${key}`);
|
|
2510
|
-
}
|
|
2511
|
-
return { verdict: "pass", ...value };
|
|
2512
|
-
}
|
|
2513
|
-
function gateOutputPlan(task, output, original) {
|
|
2514
|
-
const message = task.when === "user-message" || task.when === "agent-message";
|
|
2515
|
-
const mutable = message || task.when === "tool-after";
|
|
2516
|
-
const changed = output.replace !== void 0 && output.replace !== original;
|
|
2517
|
-
const confirm = message && task.advanced.confirmReplace && changed;
|
|
2518
|
-
const stop = output.verdict === "stop" || confirm;
|
|
2519
|
-
const notes = output.note === void 0 ? [] : [output.note];
|
|
2520
|
-
if (output.replace !== void 0 && (!mutable || stop && !message)) notes.push(output.replace);
|
|
2521
|
-
if (output.append !== void 0 && (!mutable || stop)) notes.push(output.append);
|
|
2522
|
-
return {
|
|
2523
|
-
stop,
|
|
2524
|
-
notes,
|
|
2525
|
-
...output.reason !== void 0 ? { reason: output.reason } : {},
|
|
2526
|
-
...stop && message && output.replace !== void 0 ? { suggestion: output.replace } : {},
|
|
2527
|
-
...!stop && mutable && changed ? { replacement: output.replace } : {},
|
|
2528
|
-
...!stop && mutable && output.append !== void 0 ? { addition: output.append } : {}
|
|
2529
|
-
};
|
|
2530
|
-
}
|
|
2531
|
-
|
|
2532
|
-
// src/host/pixie-gate-definitions.ts
|
|
2533
2572
|
var diagnostic = (error, code) => ({ code: typeof error?.code === "string" ? error.code : code, message: error instanceof Error ? error.message : String(error) });
|
|
2534
2573
|
function decode(text2) {
|
|
2535
2574
|
let value;
|
|
@@ -2599,6 +2638,15 @@ async function readRuntimeGateDefinitions(source) {
|
|
|
2599
2638
|
return [];
|
|
2600
2639
|
}
|
|
2601
2640
|
}
|
|
2641
|
+
async function readSessionGateDefinitions(source, sessionId) {
|
|
2642
|
+
const [rows, disabled] = await Promise.all([readRuntimeGateDefinitions(source), source.disabledGates?.(sessionId).catch((error) => {
|
|
2643
|
+
source.warn?.(`studio pixie: session gate switches unreadable for ${sessionId}: ${String(error)}`);
|
|
2644
|
+
return [];
|
|
2645
|
+
}) ?? Promise.resolve([])]);
|
|
2646
|
+
if (!disabled.length) return rows;
|
|
2647
|
+
const off = new Set(disabled);
|
|
2648
|
+
return rows.filter((row) => !off.has(row.id));
|
|
2649
|
+
}
|
|
2602
2650
|
|
|
2603
2651
|
// src/host/pixie-gate-input.ts
|
|
2604
2652
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
@@ -2622,6 +2670,7 @@ async function executeTask(task, original, signal, runner, context) {
|
|
|
2622
2670
|
records: execution.helperCalls ?? [],
|
|
2623
2671
|
hooks: {},
|
|
2624
2672
|
appliedPolicy: task.advanced.onError,
|
|
2673
|
+
...execution.concludedBy ? { concludedBy: execution.concludedBy } : {},
|
|
2625
2674
|
...plan.stop || execution.error ? { forwardError: { verdict: execution.error ? "error" : "fail", reason: plan.reason ?? "Revision requires confirmation", ...plan.suggestion !== void 0 ? { text: plan.suggestion } : {} } } : {},
|
|
2626
2675
|
forward: plan.stop ? { kind: "none" } : plan.replacement !== void 0 ? { kind: "replace", text: plan.replacement } : plan.addition !== void 0 ? { kind: "append", text: original, contexts: [plan.addition], source: "studio-pixie-gate" } : { kind: "original", text: original }
|
|
2627
2676
|
};
|
|
@@ -2649,7 +2698,7 @@ function gateInput(deps) {
|
|
|
2649
2698
|
if (decision.kind === "reject") return decision;
|
|
2650
2699
|
const preset = deps.preset(payload.agent);
|
|
2651
2700
|
if (!preset) return decision;
|
|
2652
|
-
const rows = await
|
|
2701
|
+
const rows = await readSessionGateDefinitions(deps, payload.agent.id);
|
|
2653
2702
|
const root = deps.isRoot(payload.agent);
|
|
2654
2703
|
const targets = decision.messages.flatMap((message) => {
|
|
2655
2704
|
const when = message.source.kind === "user" ? "user-message" : message.source.kind === "agent-message" ? "agent-message" : void 0;
|
|
@@ -2720,7 +2769,7 @@ function gateTurn(deps) {
|
|
|
2720
2769
|
return async ({ agent, turn, signal }) => {
|
|
2721
2770
|
const preset = deps.preset(agent);
|
|
2722
2771
|
if (!preset) return;
|
|
2723
|
-
const gate = (await
|
|
2772
|
+
const gate = (await readSessionGateDefinitions(deps, agent.id)).find((row) => row.when === "turn-end" && taskMatches(row, preset, deps.isRoot(agent)));
|
|
2724
2773
|
if (!gate) return;
|
|
2725
2774
|
const original = [...agent.session.deriveMessages()].reverse().find((message2) => message2.role === "assistant");
|
|
2726
2775
|
const subject = original ? textOf3(original) : "";
|
|
@@ -2759,7 +2808,7 @@ function gateTools(deps) {
|
|
|
2759
2808
|
if (!agent) return decision;
|
|
2760
2809
|
const preset = deps.preset(agent);
|
|
2761
2810
|
if (!preset) return decision;
|
|
2762
|
-
const gate = (await
|
|
2811
|
+
const gate = (await readSessionGateDefinitions(deps, agent.id)).find((row) => row.when === point && taskMatches(row, preset, deps.isRoot(agent)));
|
|
2763
2812
|
if (!gate) return decision;
|
|
2764
2813
|
const turn = [...agent.session.snapshotEvents()].reverse().find((event) => event.type === "turn/start")?.data.turn ?? 0;
|
|
2765
2814
|
const contexts = "additionalContexts" in decision ? decision.additionalContexts : void 0;
|
|
@@ -2822,89 +2871,184 @@ function gateTools(deps) {
|
|
|
2822
2871
|
};
|
|
2823
2872
|
}
|
|
2824
2873
|
|
|
2825
|
-
// src/host/pixie-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
const
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
return { type: "choice", choice: answer.choice, probabilities: Object.fromEntries(names3.map((name) => [name, probabilities[name]])), confidence: Math.max(...values) };
|
|
2840
|
-
}
|
|
2841
|
-
if (question.type === "score") {
|
|
2842
|
-
const probabilities = answer.probabilities;
|
|
2843
|
-
const top = question.levels.length - 1;
|
|
2844
|
-
if (typeof answer.score !== "number" || !Number.isFinite(answer.score) || answer.score < 0 || answer.score > top) throw new Error(`Answer "${id}" score must be within 0..${top}`);
|
|
2845
|
-
if (!Array.isArray(probabilities) || probabilities.length !== question.levels.length || !probabilities.every(isProbability) || !sumsToOne(probabilities)) throw new Error(`Answer "${id}" needs one probability per level summing to 1`);
|
|
2846
|
-
return { type: "score", score: answer.score, probabilities, confidence: Math.max(...probabilities) };
|
|
2847
|
-
}
|
|
2848
|
-
if (!isProbability(answer.noul)) throw new Error(`Answer "${id}" noul must be a probability in 0..1`);
|
|
2849
|
-
return { type: "noul", noul: answer.noul };
|
|
2874
|
+
// src/host/pixie-gate-helper.ts
|
|
2875
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2876
|
+
import { chmod, mkdir as mkdir6, rename as rename6, writeFile as writeFile6 } from "node:fs/promises";
|
|
2877
|
+
import { join as join5 } from "node:path";
|
|
2878
|
+
var HELPER_ROUTE = "/studio/pixie-gate/helper";
|
|
2879
|
+
var HELPER_LIMITS = { calls: 20, bodyBytes: 256 * 1024 };
|
|
2880
|
+
function helperJudgement(request, answer, meta = {}) {
|
|
2881
|
+
const extra = { ...meta.engine ? { engine: meta.engine } : {}, ...meta.model ? { model: meta.model } : {} };
|
|
2882
|
+
if (answer.type === "noul") {
|
|
2883
|
+
const pass = answer.noul >= request.threshold;
|
|
2884
|
+
return { exit: pass ? 0 : 1, text: String(answer.noul), json: { type: "bool", label: pass ? "yes" : "no", answer, threshold: request.threshold, ...extra } };
|
|
2885
|
+
}
|
|
2886
|
+
if (answer.type === "choice") return { exit: 0, text: answer.choice, json: { type: "select", label: answer.choice, answer, ...extra } };
|
|
2887
|
+
return { exit: 0, text: String(answer.score), json: { type: "score", label: String(Math.round(answer.score)), answer, ...extra } };
|
|
2850
2888
|
}
|
|
2851
|
-
function
|
|
2852
|
-
const
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2889
|
+
function helperJudgeRequest(form) {
|
|
2890
|
+
const type = form.get("type") ?? "bool";
|
|
2891
|
+
if (!["bool", "select", "score"].includes(type)) throw new Error("type must be bool, select, or score");
|
|
2892
|
+
const instructions2 = form.get("question")?.trim();
|
|
2893
|
+
if (!instructions2) throw new Error("question is required");
|
|
2894
|
+
const raw = form.get("threshold"), threshold = raw === null ? 0.5 : Number(raw);
|
|
2895
|
+
if (!(threshold >= 0 && threshold <= 1)) throw new Error("threshold must be between 0 and 1");
|
|
2896
|
+
let question;
|
|
2897
|
+
if (type === "bool") {
|
|
2898
|
+
const yes = form.get("true"), no = form.get("false");
|
|
2899
|
+
question = { type: "noul", instructions: instructions2, ...yes !== null || no !== null ? { criteria: { ...yes !== null ? { true: yes } : {}, ...no !== null ? { false: no } : {} } } : {} };
|
|
2900
|
+
} else if (type === "select") {
|
|
2901
|
+
const criteria = {};
|
|
2902
|
+
for (const option of form.getAll("option")) {
|
|
2903
|
+
const at = option.indexOf("=");
|
|
2904
|
+
const name = (at < 0 ? option : option.slice(0, at)).trim();
|
|
2905
|
+
if (!name || Object.hasOwn(criteria, name)) throw new Error(`option names must be unique and non-empty: ${option}`);
|
|
2906
|
+
criteria[name] = at < 0 ? name : option.slice(at + 1);
|
|
2907
|
+
}
|
|
2908
|
+
question = { type: "choice", instructions: instructions2, criteria };
|
|
2909
|
+
} else question = { type: "score", instructions: instructions2, criteria: form.getAll("level") };
|
|
2910
|
+
validateJudgeRequest({ state: "", questions: { helper: question } });
|
|
2911
|
+
return { type, question, threshold };
|
|
2862
2912
|
}
|
|
2863
|
-
var
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2913
|
+
var HELPER_SCRIPT = `#!/usr/bin/env bash
|
|
2914
|
+
# Pixie Gate hook helper, generated by DSH Studio. Valid only inside one hook script run.
|
|
2915
|
+
# pixie ask "<instruction>" [--input]
|
|
2916
|
+
# pixie judge [--type bool|select|score] --question "<question>" [--threshold 0.5] [--true ..] [--false ..]
|
|
2917
|
+
# [--option name=desc ..] [--level desc ..] [--json] [--input]
|
|
2918
|
+
# --input reads the helper input from stdin; otherwise the gate target is used.
|
|
2919
|
+
set -u
|
|
2920
|
+
fail() { printf 'pixie: %s\\n' "$1" >&2; exit 2; }
|
|
2921
|
+
[ -n "\${PIXIE_URL:-}" ] && [ -n "\${PIXIE_TOKEN:-}" ] || fail 'helper is not available outside a Pixie Gate hook script run'
|
|
2922
|
+
command -v curl >/dev/null 2>&1 || fail 'curl is required'
|
|
2923
|
+
command="\${1:-}"; [ $# -gt 0 ] && shift
|
|
2924
|
+
input=0; args=()
|
|
2925
|
+
case "$command" in
|
|
2926
|
+
ask)
|
|
2927
|
+
[ $# -ge 1 ] || fail 'usage: pixie ask "<instruction>" [--input]'
|
|
2928
|
+
args+=(--data-urlencode "instruction=$1"); shift
|
|
2929
|
+
while [ $# -gt 0 ]; do case "$1" in --input) input=1 ;; *) fail "unknown option: $1" ;; esac; shift; done ;;
|
|
2930
|
+
judge)
|
|
2931
|
+
question=''; json=0
|
|
2932
|
+
while [ $# -gt 0 ]; do case "$1" in
|
|
2933
|
+
--type|--question|--threshold|--true|--false|--option|--level)
|
|
2934
|
+
[ $# -ge 2 ] || fail "$1 needs a value"
|
|
2935
|
+
[ "$1" = --question ] && question="$2"
|
|
2936
|
+
args+=(--data-urlencode "\${1#--}=$2"); shift ;;
|
|
2937
|
+
--json) json=1 ;;
|
|
2938
|
+
--input) input=1 ;;
|
|
2939
|
+
*) fail "unknown option: $1" ;;
|
|
2940
|
+
esac; shift; done
|
|
2941
|
+
[ -n "$question" ] || fail 'usage: pixie judge [--type bool|select|score] --question "<question>" [--threshold 0.5] [--true ..] [--false ..] [--option name=desc ..] [--level desc ..] [--json] [--input]' ;;
|
|
2942
|
+
*) fail 'usage: pixie ask|judge ...' ;;
|
|
2943
|
+
esac
|
|
2944
|
+
[ "$input" = 1 ] && args+=(--data-urlencode 'input@-')
|
|
2945
|
+
# The token travels in a config on a file descriptor, never on the command line.
|
|
2946
|
+
out=$(curl -sS --noproxy '*' -X POST -K <(printf 'header = "Authorization: Bearer %s"\\n' "$PIXIE_TOKEN") -w '\\n%{http_code}' "\${args[@]}" "$PIXIE_URL/$command") || fail "request failed"
|
|
2947
|
+
code=\${out##*$'\\n'}; body=\${out%$'\\n'*}
|
|
2948
|
+
[ "$code" = 200 ] || fail "HTTP $code: $body"
|
|
2949
|
+
if [ "$command" = ask ]; then printf '%s\\n' "$body"; exit 0; fi
|
|
2950
|
+
# judge body: exit code, plain answer, then the JSON line.
|
|
2951
|
+
status=\${body%%$'\\n'*}; rest=\${body#*$'\\n'}; text=\${rest%%$'\\n'*}; line=\${rest#*$'\\n'}
|
|
2952
|
+
if [ "$json" = 1 ]; then printf '%s\\n' "$line"; else printf '%s\\n' "$text"; fi
|
|
2953
|
+
exit "$status"
|
|
2954
|
+
`;
|
|
2955
|
+
var hash = (token) => createHash("sha256").update(token).digest("hex");
|
|
2956
|
+
var send = (res, status, text2) => {
|
|
2957
|
+
if (res.headersSent) return;
|
|
2958
|
+
res.writeHead(status, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
2959
|
+
res.end(text2);
|
|
2960
|
+
};
|
|
2961
|
+
var PixieGateHelper = class {
|
|
2962
|
+
constructor(options) {
|
|
2963
|
+
this.options = options;
|
|
2964
|
+
this.limits = options.limits ?? HELPER_LIMITS;
|
|
2868
2965
|
}
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2966
|
+
grants = /* @__PURE__ */ new Map();
|
|
2967
|
+
installed;
|
|
2968
|
+
limits;
|
|
2969
|
+
/** Environment for one script run. Without a reachable route only PATH is given and `pixie` explains why. */
|
|
2970
|
+
async issue(grant) {
|
|
2971
|
+
await (this.installed ??= this.install().catch((error) => {
|
|
2972
|
+
this.installed = void 0;
|
|
2973
|
+
throw error;
|
|
2974
|
+
}));
|
|
2975
|
+
const env = { PATH: `${this.options.binDirectory}:${process.env.PATH ?? "/usr/bin:/bin"}` };
|
|
2976
|
+
const url = this.options.url();
|
|
2977
|
+
if (!url) return { env, revoke() {
|
|
2978
|
+
} };
|
|
2979
|
+
const token = randomBytes(32).toString("hex"), key = hash(token);
|
|
2980
|
+
const entry = { grant, calls: 0, revoked: new AbortController() };
|
|
2981
|
+
this.grants.set(key, entry);
|
|
2982
|
+
return { env: { ...env, PIXIE_URL: url, PIXIE_TOKEN: token }, revoke: () => {
|
|
2983
|
+
this.grants.delete(key);
|
|
2984
|
+
entry.revoked.abort(new Error("Pixie helper token revoked"));
|
|
2985
|
+
} };
|
|
2986
|
+
}
|
|
2987
|
+
handle = async (req, res) => {
|
|
2988
|
+
const action = new URL(req.url ?? "/", "http://x").pathname.slice(HELPER_ROUTE.length);
|
|
2989
|
+
if (action !== "/ask" && action !== "/judge") return send(res, 404, "unknown helper");
|
|
2990
|
+
if (req.method !== "POST") return send(res, 405, "POST only");
|
|
2991
|
+
const header = req.headers.authorization;
|
|
2992
|
+
const entry = typeof header === "string" && header.startsWith("Bearer ") ? this.grants.get(hash(header.slice(7))) : void 0;
|
|
2993
|
+
if (!entry) return send(res, 401, "invalid or expired token");
|
|
2994
|
+
if (entry.calls >= this.limits.calls) return send(res, 429, `helper call limit (${this.limits.calls}) reached`);
|
|
2995
|
+
const body = await this.read(req);
|
|
2996
|
+
if (body === void 0) return send(res, 413, `helper request exceeds ${this.limits.bodyBytes} bytes`);
|
|
2997
|
+
if (!this.grants.has(hash(header.slice(7)))) return send(res, 401, "invalid or expired token");
|
|
2998
|
+
const form = new URLSearchParams(body);
|
|
2999
|
+
const input = form.get("input") ?? entry.grant.target;
|
|
3000
|
+
const closed = new AbortController();
|
|
3001
|
+
res.on("close", () => {
|
|
3002
|
+
if (!res.writableFinished) closed.abort(new Error("Helper client disconnected"));
|
|
3003
|
+
});
|
|
3004
|
+
const signal = AbortSignal.any([entry.grant.signal, entry.revoked.signal, closed.signal]);
|
|
3005
|
+
let call;
|
|
3006
|
+
if (action === "/ask") {
|
|
3007
|
+
const instruction = form.get("instruction")?.trim();
|
|
3008
|
+
if (!instruction) return send(res, 400, "instruction is required");
|
|
3009
|
+
entry.calls++;
|
|
3010
|
+
call = entry.grant.ask(instruction, input, signal);
|
|
3011
|
+
} else {
|
|
3012
|
+
let request;
|
|
3013
|
+
try {
|
|
3014
|
+
request = helperJudgeRequest(form);
|
|
3015
|
+
} catch (error) {
|
|
3016
|
+
return send(res, 400, error instanceof Error ? error.message : String(error));
|
|
2901
3017
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
3018
|
+
entry.calls++;
|
|
3019
|
+
call = entry.grant.judge(request, input, signal).then((result) => `${result.exit}
|
|
3020
|
+
${result.text.replace(/\n/g, " ")}
|
|
3021
|
+
${JSON.stringify(result.json)}`);
|
|
3022
|
+
}
|
|
3023
|
+
try {
|
|
3024
|
+
send(res, 200, await call);
|
|
2905
3025
|
} catch (error) {
|
|
2906
|
-
|
|
3026
|
+
send(res, signal.aborted ? 503 : 502, error instanceof Error ? error.message : String(error));
|
|
2907
3027
|
}
|
|
3028
|
+
};
|
|
3029
|
+
/** Body text, or undefined above the size limit (the rest is drained, then the socket closed past 4×). */
|
|
3030
|
+
read(req) {
|
|
3031
|
+
return new Promise((resolve6, reject) => {
|
|
3032
|
+
const chunks = [];
|
|
3033
|
+
let size = 0;
|
|
3034
|
+
req.on("data", (chunk) => {
|
|
3035
|
+
size += chunk.length;
|
|
3036
|
+
if (size <= this.limits.bodyBytes) chunks.push(chunk);
|
|
3037
|
+
else if (size > this.limits.bodyBytes * 4) {
|
|
3038
|
+
req.destroy();
|
|
3039
|
+
resolve6(void 0);
|
|
3040
|
+
}
|
|
3041
|
+
});
|
|
3042
|
+
req.on("end", () => resolve6(size > this.limits.bodyBytes ? void 0 : Buffer.concat(chunks).toString("utf8")));
|
|
3043
|
+
req.on("error", reject);
|
|
3044
|
+
});
|
|
3045
|
+
}
|
|
3046
|
+
async install() {
|
|
3047
|
+
await mkdir6(this.options.binDirectory, { recursive: true });
|
|
3048
|
+
const path = join5(this.options.binDirectory, "pixie"), temp = `${path}.${process.pid}.tmp`;
|
|
3049
|
+
await writeFile6(temp, HELPER_SCRIPT, { mode: 493 });
|
|
3050
|
+
await chmod(temp, 493);
|
|
3051
|
+
await rename6(temp, path);
|
|
2908
3052
|
}
|
|
2909
3053
|
};
|
|
2910
3054
|
|
|
@@ -2959,7 +3103,8 @@ var GateTaskRunner = class {
|
|
|
2959
3103
|
} finally {
|
|
2960
3104
|
session?.revoke();
|
|
2961
3105
|
}
|
|
2962
|
-
if (hook.skipped) return { status: "done", skipped: true, output: { verdict: "pass", note: "skipped by hook" }, usage };
|
|
3106
|
+
if (hook.skipped) return { status: "done", skipped: true, concludedBy: "skipped", output: { verdict: "pass", note: "skipped by hook" }, usage };
|
|
3107
|
+
if ("direct" in hook) return { status: "done", concludedBy: "script", output: hook.direct, usage };
|
|
2963
3108
|
instruction = hook.instruction;
|
|
2964
3109
|
}
|
|
2965
3110
|
if (!this.deps.model) throw new Error("No Pixie model configured");
|
|
@@ -2990,7 +3135,7 @@ Instruction: ${instruction}`,
|
|
|
2990
3135
|
signal.throwIfAborted();
|
|
2991
3136
|
if (!finished) throw new Error("Gate model stream ended without a finish");
|
|
2992
3137
|
const output = parseGateOutput(text2);
|
|
2993
|
-
return { status: "done", output, usage };
|
|
3138
|
+
return { status: "done", concludedBy: "pixie", output, usage };
|
|
2994
3139
|
} catch (error) {
|
|
2995
3140
|
return { status: parent.aborted ? "cancelled" : "error", error: String(signal.aborted ? signal.reason : error), usage };
|
|
2996
3141
|
} finally {
|
|
@@ -3034,168 +3179,34 @@ Instruction: ${instruction}`,
|
|
|
3034
3179
|
record5("ask", input, { verdict: "note", text: text2, reason: instruction, ...usage ? { usage } : {} });
|
|
3035
3180
|
return text2;
|
|
3036
3181
|
},
|
|
3037
|
-
judge: async (
|
|
3038
|
-
const
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3182
|
+
judge: async (request, input, callSignal) => {
|
|
3183
|
+
const reason = request.question.instructions;
|
|
3184
|
+
const engine = this.deps.judge;
|
|
3185
|
+
if (!engine) {
|
|
3186
|
+
record5("judge", input, { verdict: "error", error: "No judge engine configured", reason });
|
|
3187
|
+
throw new Error("No judge engine configured");
|
|
3042
3188
|
}
|
|
3043
|
-
const result = await
|
|
3189
|
+
const result = await engine.judge({ state: input, questions: { helper: request.question } }, callSignal);
|
|
3044
3190
|
const usage = result.usage ? { usage: result.usage } : {};
|
|
3045
3191
|
const answer = result.ok ? result.answers.helper : void 0;
|
|
3046
|
-
if (answer
|
|
3047
|
-
const error = result.ok ? "Judge
|
|
3048
|
-
record5("judge", input, { verdict: "error", error, reason:
|
|
3192
|
+
if (!answer) {
|
|
3193
|
+
const error = result.ok ? "Judge returned no answer" : result.error;
|
|
3194
|
+
record5("judge", input, { verdict: "error", error, reason, data: { type: request.type, engine: engine.label }, ...usage });
|
|
3049
3195
|
throw new Error(error);
|
|
3050
3196
|
}
|
|
3051
|
-
const
|
|
3052
|
-
record5("judge", input, {
|
|
3053
|
-
|
|
3197
|
+
const judgement = helperJudgement(request, answer, { engine: engine.label });
|
|
3198
|
+
record5("judge", input, {
|
|
3199
|
+
verdict: judgement.exit === 0 ? "pass" : "fail",
|
|
3200
|
+
reason,
|
|
3201
|
+
...usage,
|
|
3202
|
+
data: { type: request.type, label: judgement.json.label, engine: engine.label, ...request.type === "bool" && answer.type === "noul" ? { noul: answer.noul, threshold: request.threshold } : { answer } }
|
|
3203
|
+
});
|
|
3204
|
+
return judgement;
|
|
3054
3205
|
}
|
|
3055
3206
|
};
|
|
3056
3207
|
}
|
|
3057
3208
|
};
|
|
3058
3209
|
|
|
3059
|
-
// src/host/pixie-gate-helper.ts
|
|
3060
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
3061
|
-
import { chmod, mkdir as mkdir6, rename as rename6, writeFile as writeFile6 } from "node:fs/promises";
|
|
3062
|
-
import { join as join5 } from "node:path";
|
|
3063
|
-
var HELPER_ROUTE = "/studio/pixie-gate/helper";
|
|
3064
|
-
var HELPER_LIMITS = { calls: 20, bodyBytes: 256 * 1024 };
|
|
3065
|
-
var HELPER_SCRIPT = `#!/usr/bin/env bash
|
|
3066
|
-
# Pixie Gate hook helper, generated by DSH Studio. Valid only inside one hook script run.
|
|
3067
|
-
# pixie ask "<instruction>" [--input]
|
|
3068
|
-
# pixie judge --question "<question>" [--threshold 0.5] [--input]
|
|
3069
|
-
# --input reads the helper input from stdin; otherwise the gate target is used.
|
|
3070
|
-
set -u
|
|
3071
|
-
fail() { printf 'pixie: %s\\n' "$1" >&2; exit 2; }
|
|
3072
|
-
[ -n "\${PIXIE_URL:-}" ] && [ -n "\${PIXIE_TOKEN:-}" ] || fail 'helper is not available outside a Pixie Gate hook script run'
|
|
3073
|
-
command -v curl >/dev/null 2>&1 || fail 'curl is required'
|
|
3074
|
-
command="\${1:-}"; [ $# -gt 0 ] && shift
|
|
3075
|
-
input=0; args=()
|
|
3076
|
-
case "$command" in
|
|
3077
|
-
ask)
|
|
3078
|
-
[ $# -ge 1 ] || fail 'usage: pixie ask "<instruction>" [--input]'
|
|
3079
|
-
args+=(--data-urlencode "instruction=$1"); shift
|
|
3080
|
-
while [ $# -gt 0 ]; do case "$1" in --input) input=1 ;; *) fail "unknown option: $1" ;; esac; shift; done ;;
|
|
3081
|
-
judge)
|
|
3082
|
-
question=''; threshold=''
|
|
3083
|
-
while [ $# -gt 0 ]; do case "$1" in
|
|
3084
|
-
--question) [ $# -ge 2 ] || fail '--question needs a value'; question="$2"; shift ;;
|
|
3085
|
-
--threshold) [ $# -ge 2 ] || fail '--threshold needs a value'; threshold="$2"; shift ;;
|
|
3086
|
-
--input) input=1 ;;
|
|
3087
|
-
*) fail "unknown option: $1" ;;
|
|
3088
|
-
esac; shift; done
|
|
3089
|
-
[ -n "$question" ] || fail 'usage: pixie judge --question "<question>" [--threshold 0.5] [--input]'
|
|
3090
|
-
args+=(--data-urlencode "question=$question")
|
|
3091
|
-
[ -z "$threshold" ] || args+=(--data-urlencode "threshold=$threshold") ;;
|
|
3092
|
-
*) fail 'usage: pixie ask|judge ...' ;;
|
|
3093
|
-
esac
|
|
3094
|
-
[ "$input" = 1 ] && args+=(--data-urlencode 'input@-')
|
|
3095
|
-
# The token travels in a config on a file descriptor, never on the command line.
|
|
3096
|
-
out=$(curl -sS --noproxy '*' -X POST -K <(printf 'header = "Authorization: Bearer %s"\\n' "$PIXIE_TOKEN") -w '\\n%{http_code}' "\${args[@]}" "$PIXIE_URL/$command") || fail "request failed"
|
|
3097
|
-
code=\${out##*$'\\n'}; body=\${out%$'\\n'*}
|
|
3098
|
-
[ "$code" = 200 ] || fail "HTTP $code: $body"
|
|
3099
|
-
if [ "$command" = ask ]; then printf '%s\\n' "$body"; exit 0; fi
|
|
3100
|
-
verdict=\${body%% *}; printf '%s\\n' "\${body#* }"
|
|
3101
|
-
[ "$verdict" = pass ]
|
|
3102
|
-
`;
|
|
3103
|
-
var hash = (token) => createHash("sha256").update(token).digest("hex");
|
|
3104
|
-
var send = (res, status, text2) => {
|
|
3105
|
-
if (res.headersSent) return;
|
|
3106
|
-
res.writeHead(status, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" });
|
|
3107
|
-
res.end(text2);
|
|
3108
|
-
};
|
|
3109
|
-
var PixieGateHelper = class {
|
|
3110
|
-
constructor(options) {
|
|
3111
|
-
this.options = options;
|
|
3112
|
-
this.limits = options.limits ?? HELPER_LIMITS;
|
|
3113
|
-
}
|
|
3114
|
-
grants = /* @__PURE__ */ new Map();
|
|
3115
|
-
installed;
|
|
3116
|
-
limits;
|
|
3117
|
-
/** Environment for one script run. Without a reachable route only PATH is given and `pixie` explains why. */
|
|
3118
|
-
async issue(grant) {
|
|
3119
|
-
await (this.installed ??= this.install().catch((error) => {
|
|
3120
|
-
this.installed = void 0;
|
|
3121
|
-
throw error;
|
|
3122
|
-
}));
|
|
3123
|
-
const env = { PATH: `${this.options.binDirectory}:${process.env.PATH ?? "/usr/bin:/bin"}` };
|
|
3124
|
-
const url = this.options.url();
|
|
3125
|
-
if (!url) return { env, revoke() {
|
|
3126
|
-
} };
|
|
3127
|
-
const token = randomBytes(32).toString("hex"), key = hash(token);
|
|
3128
|
-
const entry = { grant, calls: 0, revoked: new AbortController() };
|
|
3129
|
-
this.grants.set(key, entry);
|
|
3130
|
-
return { env: { ...env, PIXIE_URL: url, PIXIE_TOKEN: token }, revoke: () => {
|
|
3131
|
-
this.grants.delete(key);
|
|
3132
|
-
entry.revoked.abort(new Error("Pixie helper token revoked"));
|
|
3133
|
-
} };
|
|
3134
|
-
}
|
|
3135
|
-
handle = async (req, res) => {
|
|
3136
|
-
const action = new URL(req.url ?? "/", "http://x").pathname.slice(HELPER_ROUTE.length);
|
|
3137
|
-
if (action !== "/ask" && action !== "/judge") return send(res, 404, "unknown helper");
|
|
3138
|
-
if (req.method !== "POST") return send(res, 405, "POST only");
|
|
3139
|
-
const header = req.headers.authorization;
|
|
3140
|
-
const entry = typeof header === "string" && header.startsWith("Bearer ") ? this.grants.get(hash(header.slice(7))) : void 0;
|
|
3141
|
-
if (!entry) return send(res, 401, "invalid or expired token");
|
|
3142
|
-
if (entry.calls >= this.limits.calls) return send(res, 429, `helper call limit (${this.limits.calls}) reached`);
|
|
3143
|
-
const body = await this.read(req);
|
|
3144
|
-
if (body === void 0) return send(res, 413, `helper request exceeds ${this.limits.bodyBytes} bytes`);
|
|
3145
|
-
if (!this.grants.has(hash(header.slice(7)))) return send(res, 401, "invalid or expired token");
|
|
3146
|
-
const form = new URLSearchParams(body);
|
|
3147
|
-
const input = form.get("input") ?? entry.grant.target;
|
|
3148
|
-
const closed = new AbortController();
|
|
3149
|
-
res.on("close", () => {
|
|
3150
|
-
if (!res.writableFinished) closed.abort(new Error("Helper client disconnected"));
|
|
3151
|
-
});
|
|
3152
|
-
const signal = AbortSignal.any([entry.grant.signal, entry.revoked.signal, closed.signal]);
|
|
3153
|
-
let call;
|
|
3154
|
-
if (action === "/ask") {
|
|
3155
|
-
const instruction = form.get("instruction")?.trim();
|
|
3156
|
-
if (!instruction) return send(res, 400, "instruction is required");
|
|
3157
|
-
entry.calls++;
|
|
3158
|
-
call = entry.grant.ask(instruction, input, signal);
|
|
3159
|
-
} else {
|
|
3160
|
-
const question = form.get("question")?.trim();
|
|
3161
|
-
const raw = form.get("threshold"), threshold = raw === null ? 0.5 : Number(raw);
|
|
3162
|
-
if (!question) return send(res, 400, "question is required");
|
|
3163
|
-
if (!(threshold >= 0 && threshold <= 1)) return send(res, 400, "threshold must be between 0 and 1");
|
|
3164
|
-
entry.calls++;
|
|
3165
|
-
call = entry.grant.judge(question, threshold, input, signal).then((result) => `${result.pass ? "pass" : "fail"} ${result.noul}`);
|
|
3166
|
-
}
|
|
3167
|
-
try {
|
|
3168
|
-
send(res, 200, await call);
|
|
3169
|
-
} catch (error) {
|
|
3170
|
-
send(res, signal.aborted ? 503 : 502, error instanceof Error ? error.message : String(error));
|
|
3171
|
-
}
|
|
3172
|
-
};
|
|
3173
|
-
/** Body text, or undefined above the size limit (the rest is drained, then the socket closed past 4×). */
|
|
3174
|
-
read(req) {
|
|
3175
|
-
return new Promise((resolve6, reject) => {
|
|
3176
|
-
const chunks = [];
|
|
3177
|
-
let size = 0;
|
|
3178
|
-
req.on("data", (chunk) => {
|
|
3179
|
-
size += chunk.length;
|
|
3180
|
-
if (size <= this.limits.bodyBytes) chunks.push(chunk);
|
|
3181
|
-
else if (size > this.limits.bodyBytes * 4) {
|
|
3182
|
-
req.destroy();
|
|
3183
|
-
resolve6(void 0);
|
|
3184
|
-
}
|
|
3185
|
-
});
|
|
3186
|
-
req.on("end", () => resolve6(size > this.limits.bodyBytes ? void 0 : Buffer.concat(chunks).toString("utf8")));
|
|
3187
|
-
req.on("error", reject);
|
|
3188
|
-
});
|
|
3189
|
-
}
|
|
3190
|
-
async install() {
|
|
3191
|
-
await mkdir6(this.options.binDirectory, { recursive: true });
|
|
3192
|
-
const path = join5(this.options.binDirectory, "pixie"), temp = `${path}.${process.pid}.tmp`;
|
|
3193
|
-
await writeFile6(temp, HELPER_SCRIPT, { mode: 493 });
|
|
3194
|
-
await chmod(temp, 493);
|
|
3195
|
-
await rename6(temp, path);
|
|
3196
|
-
}
|
|
3197
|
-
};
|
|
3198
|
-
|
|
3199
3210
|
// src/host/pixie-gate-settings.ts
|
|
3200
3211
|
import { randomUUID as randomUUID10 } from "node:crypto";
|
|
3201
3212
|
import { mkdir as mkdir7, readFile as readFile7, rename as rename7, writeFile as writeFile7 } from "node:fs/promises";
|
|
@@ -3256,17 +3267,203 @@ async function trialGate(payload, deps, signal) {
|
|
|
3256
3267
|
return { output: execution.output, plan: gateOutputPlan(task, execution.output, raw.text), usage: execution.usage };
|
|
3257
3268
|
}
|
|
3258
3269
|
|
|
3270
|
+
// src/host/pixie-judge.ts
|
|
3271
|
+
var TOLERANCE = 0.02;
|
|
3272
|
+
var isProbability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
3273
|
+
var sumsToOne = (values) => Math.abs(values.reduce((sum, value) => sum + value, 0) - 1) <= TOLERANCE;
|
|
3274
|
+
function checkAnswer(id, question, raw) {
|
|
3275
|
+
const answer = raw;
|
|
3276
|
+
if (!answer || typeof answer !== "object") throw new Error(`Missing answer for "${id}"`);
|
|
3277
|
+
if (question.type === "choice") {
|
|
3278
|
+
const names3 = Object.keys(question.criteria);
|
|
3279
|
+
const probabilities = answer.probabilities;
|
|
3280
|
+
if (typeof answer.choice !== "string" || !names3.includes(answer.choice)) throw new Error(`Answer "${id}" chose an unknown option`);
|
|
3281
|
+
if (!probabilities || names3.some((name) => !isProbability(probabilities[name])) || Object.keys(probabilities).some((name) => !names3.includes(name))) throw new Error(`Answer "${id}" needs a probability per option`);
|
|
3282
|
+
const values = names3.map((name) => probabilities[name]);
|
|
3283
|
+
if (!sumsToOne(values)) throw new Error(`Answer "${id}" probabilities must sum to 1`);
|
|
3284
|
+
return { type: "choice", choice: answer.choice, probabilities: Object.fromEntries(names3.map((name) => [name, probabilities[name]])), confidence: isProbability(answer.confidence) ? answer.confidence : Math.max(...values) };
|
|
3285
|
+
}
|
|
3286
|
+
if (question.type === "score") {
|
|
3287
|
+
const probabilities = answer.probabilities;
|
|
3288
|
+
const top = question.criteria.length - 1;
|
|
3289
|
+
if (typeof answer.score !== "number" || !Number.isFinite(answer.score) || answer.score < 0 || answer.score > top) throw new Error(`Answer "${id}" score must be within 0..${top}`);
|
|
3290
|
+
if (!Array.isArray(probabilities) || probabilities.length !== question.criteria.length || !probabilities.every(isProbability) || !sumsToOne(probabilities)) throw new Error(`Answer "${id}" needs one probability per level summing to 1`);
|
|
3291
|
+
return { type: "score", score: answer.score, probabilities, confidence: isProbability(answer.confidence) ? answer.confidence : Math.max(...probabilities) };
|
|
3292
|
+
}
|
|
3293
|
+
if (!isProbability(answer.noul)) throw new Error(`Answer "${id}" noul must be a probability in 0..1`);
|
|
3294
|
+
return { type: "noul", noul: answer.noul };
|
|
3295
|
+
}
|
|
3296
|
+
function parseJudgeOutput(text2, questions) {
|
|
3297
|
+
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(text2.trim());
|
|
3298
|
+
let parsed;
|
|
3299
|
+
try {
|
|
3300
|
+
parsed = JSON.parse(fenced ? fenced[1] : text2.trim());
|
|
3301
|
+
} catch {
|
|
3302
|
+
throw new Error("Judge model did not return JSON");
|
|
3303
|
+
}
|
|
3304
|
+
const answers = parsed?.answers;
|
|
3305
|
+
if (!answers || typeof answers !== "object") throw new Error("Judge model output has no answers object");
|
|
3306
|
+
return Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, answers[id])]));
|
|
3307
|
+
}
|
|
3308
|
+
var LlmJudgeEngine = class {
|
|
3309
|
+
constructor(llm, model, system = DEFAULT_JUDGE_SYSTEM_PROMPT) {
|
|
3310
|
+
this.llm = llm;
|
|
3311
|
+
this.model = model;
|
|
3312
|
+
this.system = system;
|
|
3313
|
+
}
|
|
3314
|
+
kind = "judge";
|
|
3315
|
+
label = "llm";
|
|
3316
|
+
async judge(request, signal) {
|
|
3317
|
+
let usage;
|
|
3318
|
+
try {
|
|
3319
|
+
const { state, questions } = validateJudgeRequest(request);
|
|
3320
|
+
const prompt = `State:
|
|
3321
|
+
${typeof state === "string" ? state : JSON.stringify(state, null, 2)}
|
|
3322
|
+
|
|
3323
|
+
Questions:
|
|
3324
|
+
${JSON.stringify(questions, null, 2)}`;
|
|
3325
|
+
let text2 = "";
|
|
3326
|
+
let finished = false;
|
|
3327
|
+
for await (const chunk of untilAborted(this.llm.stream({
|
|
3328
|
+
provider: this.model.provider,
|
|
3329
|
+
model: this.model.model,
|
|
3330
|
+
...this.model.reasoningEffort ? { reasoningEffort: this.model.reasoningEffort } : {},
|
|
3331
|
+
...this.model.maxTokens ? { maxTokens: this.model.maxTokens } : {},
|
|
3332
|
+
system: this.system,
|
|
3333
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
3334
|
+
signal
|
|
3335
|
+
}), signal)) {
|
|
3336
|
+
if (chunk.type === "text-delta" && typeof chunk.text === "string") text2 += chunk.text;
|
|
3337
|
+
if (chunk.type === "usage" && chunk.usage && typeof chunk.usage === "object") {
|
|
3338
|
+
const raw = chunk.usage;
|
|
3339
|
+
usage = { inputTokens: typeof raw.inputTokens === "number" ? raw.inputTokens : 0, outputTokens: typeof raw.outputTokens === "number" ? raw.outputTokens : 0 };
|
|
3340
|
+
for (const key of ["cacheReadTokens", "cacheWriteTokens", "reasoningTokens"]) if (typeof raw[key] === "number") usage[key] = raw[key];
|
|
3341
|
+
}
|
|
3342
|
+
if (chunk.type === "finish") {
|
|
3343
|
+
const reason = chunk.reason;
|
|
3344
|
+
if (reason?.kind !== "stop") throw new Error(reason?.failure?.message ?? `Judge model ended with ${reason?.kind ?? "unknown"}`);
|
|
3345
|
+
finished = true;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
signal.throwIfAborted();
|
|
3349
|
+
if (!finished) throw new Error("Judge model stream ended without a finish");
|
|
3350
|
+
return { ok: true, answers: parseJudgeOutput(text2, questions), ...usage ? { usage } : {} };
|
|
3351
|
+
} catch (error) {
|
|
3352
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error), ...usage ? { usage } : {} };
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
};
|
|
3356
|
+
|
|
3357
|
+
// src/host/jev-judge.ts
|
|
3358
|
+
var JEV_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
|
|
3359
|
+
var JEV_KEY_REF = "TYPESAFE_API_KEY";
|
|
3360
|
+
var DEFAULT_JEV_MODEL = "jev-latest";
|
|
3361
|
+
var RETRIES = 2;
|
|
3362
|
+
var MAX_WAIT_MS = 4e3;
|
|
3363
|
+
function fromWire(question, raw) {
|
|
3364
|
+
if (question.type !== "score" || !raw || typeof raw !== "object") return raw;
|
|
3365
|
+
const answer = raw;
|
|
3366
|
+
const probabilities = answer.probabilities;
|
|
3367
|
+
if (!probabilities || typeof probabilities !== "object" || Array.isArray(probabilities)) return raw;
|
|
3368
|
+
const keyed = probabilities;
|
|
3369
|
+
if (Object.keys(keyed).length !== question.criteria.length) return { ...answer, probabilities: void 0 };
|
|
3370
|
+
return { ...answer, probabilities: question.criteria.map((_, index) => keyed[String(index)]) };
|
|
3371
|
+
}
|
|
3372
|
+
var wait = (ms, signal) => new Promise((resolve6, reject) => {
|
|
3373
|
+
if (signal.aborted) {
|
|
3374
|
+
reject(signal.reason);
|
|
3375
|
+
return;
|
|
3376
|
+
}
|
|
3377
|
+
const timer = setTimeout(() => {
|
|
3378
|
+
signal.removeEventListener("abort", abort);
|
|
3379
|
+
resolve6();
|
|
3380
|
+
}, ms);
|
|
3381
|
+
const abort = () => {
|
|
3382
|
+
clearTimeout(timer);
|
|
3383
|
+
reject(signal.reason);
|
|
3384
|
+
};
|
|
3385
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
3386
|
+
});
|
|
3387
|
+
function retryDelay(retryAfter, attempt) {
|
|
3388
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter);
|
|
3389
|
+
return Math.min(Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : 500 * 2 ** attempt, MAX_WAIT_MS);
|
|
3390
|
+
}
|
|
3391
|
+
var JevJudgeEngine = class {
|
|
3392
|
+
constructor(deps) {
|
|
3393
|
+
this.deps = deps;
|
|
3394
|
+
}
|
|
3395
|
+
kind = "judge";
|
|
3396
|
+
label = "jev";
|
|
3397
|
+
async judge(request, signal) {
|
|
3398
|
+
let usage;
|
|
3399
|
+
try {
|
|
3400
|
+
const { state, questions } = validateJudgeRequest(request);
|
|
3401
|
+
if (!this.deps.credentials) throw new Error("DSH credentials service is unavailable");
|
|
3402
|
+
const key = (await this.deps.credentials.resolve(JEV_KEY_REF))?.value;
|
|
3403
|
+
if (!key) throw new Error(`Jev API key is not configured (${JEV_KEY_REF})`);
|
|
3404
|
+
signal.throwIfAborted();
|
|
3405
|
+
const send2 = this.deps.fetch ?? globalThis.fetch;
|
|
3406
|
+
const body = JSON.stringify({ model: this.deps.model, state, questions });
|
|
3407
|
+
for (let attempt = 0; ; attempt++) {
|
|
3408
|
+
const response = await send2(JEV_ENDPOINT, {
|
|
3409
|
+
method: "POST",
|
|
3410
|
+
signal,
|
|
3411
|
+
body,
|
|
3412
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }
|
|
3413
|
+
});
|
|
3414
|
+
const text2 = await response.text();
|
|
3415
|
+
if ((response.status === 429 || response.status === 529) && attempt < RETRIES) {
|
|
3416
|
+
await wait(retryDelay(response.headers.get("retry-after"), attempt), signal);
|
|
3417
|
+
continue;
|
|
3418
|
+
}
|
|
3419
|
+
if (response.status !== 200) throw new Error(`Jev request failed: HTTP ${response.status}${text2 ? `: ${text2.slice(0, 500)}` : ""}`);
|
|
3420
|
+
let parsed;
|
|
3421
|
+
try {
|
|
3422
|
+
parsed = JSON.parse(text2);
|
|
3423
|
+
} catch {
|
|
3424
|
+
throw new Error("Jev response is not JSON");
|
|
3425
|
+
}
|
|
3426
|
+
const raw = parsed.usage;
|
|
3427
|
+
if (raw && typeof raw.input_tokens === "number" && typeof raw.output_tokens === "number") usage = { inputTokens: raw.input_tokens, outputTokens: raw.output_tokens };
|
|
3428
|
+
const answers = parsed.answers;
|
|
3429
|
+
if (!answers || typeof answers !== "object") throw new Error("Jev response has no answers object");
|
|
3430
|
+
const typed = Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, fromWire(question, answers[id]))]));
|
|
3431
|
+
return { ok: true, answers: typed, ...usage ? { usage } : {} };
|
|
3432
|
+
}
|
|
3433
|
+
} catch (error) {
|
|
3434
|
+
const message = signal.aborted ? String(signal.reason instanceof Error ? signal.reason.message : signal.reason ?? "aborted") : error instanceof Error ? error.message : String(error);
|
|
3435
|
+
return { ok: false, error: message, ...usage ? { usage } : {} };
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
};
|
|
3439
|
+
function judgeEngineFor(settings, deps) {
|
|
3440
|
+
if (settings.judgeEngine === "jev") return new JevJudgeEngine({ credentials: deps.credentials, model: settings.jevModel ?? DEFAULT_JEV_MODEL, ...deps.fetch ? { fetch: deps.fetch } : {} });
|
|
3441
|
+
const model = settings.judgeModel ?? settings.model;
|
|
3442
|
+
return model ? new LlmJudgeEngine(deps.llm, model, settings.judgeSystemPrompt || void 0) : void 0;
|
|
3443
|
+
}
|
|
3444
|
+
|
|
3259
3445
|
// src/host/pixie-service.ts
|
|
3260
3446
|
var PRUNE_INTERVAL_MS = 10 * 60 * 1e3;
|
|
3261
3447
|
var json2 = (payload) => JSON.parse(payload);
|
|
3448
|
+
async function credentialWrite(write) {
|
|
3449
|
+
try {
|
|
3450
|
+
await write();
|
|
3451
|
+
} catch (error) {
|
|
3452
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3453
|
+
if (/Expected YAML collection at refs\b/.test(message)) throw new Error(`[pixie/credentials-refs] ${message}`);
|
|
3454
|
+
throw error;
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3262
3457
|
var llmOf = (ctx) => ctx.get("llm");
|
|
3263
|
-
var _models_dec, _saveSettings_dec, _readSettings_dec, _followGates_dec, _resolveGateRejection_dec, _gateRuns_dec, _saveGateDefinitions_dec, _gateDefinitions_dec, _saveGateScriptDirectory_dec, _gateScripts_dec, _trialGate_dec, _follow_dec, _cancel_dec, _ask_dec, _commandTurn_dec, _deleteThread_dec, _activate_dec, _createThread_dec, _view_dec, _a, _init;
|
|
3264
|
-
var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote], _createThread_dec = [Remote], _activate_dec = [Remote], _deleteThread_dec = [Remote], _commandTurn_dec = [Remote], _ask_dec = [Remote], _cancel_dec = [Remote], _follow_dec = [Remote({ mode: "stream" })], _trialGate_dec = [Remote], _gateScripts_dec = [Remote], _saveGateScriptDirectory_dec = [Remote], _gateDefinitions_dec = [Remote], _saveGateDefinitions_dec = [Remote], _gateRuns_dec = [Remote], _resolveGateRejection_dec = [Remote], _followGates_dec = [Remote({ mode: "stream" })], _readSettings_dec = [Remote], _saveSettings_dec = [Remote], _models_dec = [Remote], _a) {
|
|
3458
|
+
var _models_dec, _deleteJevKey_dec, _saveJevKey_dec, _jevKeyStatus_dec, _saveSettings_dec, _readSettings_dec, _followGates_dec, _setSessionGate_dec, _sessionGates_dec, _resolveGateRejection_dec, _gateRuns_dec, _saveGateDefinitions_dec, _gateDefinitions_dec, _saveGateScriptDirectory_dec, _gateScripts_dec, _trialGate_dec, _follow_dec, _cancel_dec, _ask_dec, _commandTurn_dec, _deleteThread_dec, _activate_dec, _createThread_dec, _view_dec, _a, _init;
|
|
3459
|
+
var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote], _createThread_dec = [Remote], _activate_dec = [Remote], _deleteThread_dec = [Remote], _commandTurn_dec = [Remote], _ask_dec = [Remote], _cancel_dec = [Remote], _follow_dec = [Remote({ mode: "stream" })], _trialGate_dec = [Remote], _gateScripts_dec = [Remote], _saveGateScriptDirectory_dec = [Remote], _gateDefinitions_dec = [Remote], _saveGateDefinitions_dec = [Remote], _gateRuns_dec = [Remote], _resolveGateRejection_dec = [Remote], _sessionGates_dec = [Remote], _setSessionGate_dec = [Remote], _followGates_dec = [Remote({ mode: "stream" })], _readSettings_dec = [Remote], _saveSettings_dec = [Remote], _jevKeyStatus_dec = [Remote], _saveJevKey_dec = [Remote], _deleteJevKey_dec = [Remote], _models_dec = [Remote], _a) {
|
|
3265
3460
|
constructor(ctx, dataDir) {
|
|
3266
3461
|
super(ctx, "studioPixie");
|
|
3267
3462
|
__runInitializers(_init, 5, this);
|
|
3268
3463
|
__publicField(this, "defaultScriptDirectory");
|
|
3269
3464
|
__publicField(this, "gateSettings");
|
|
3465
|
+
/** Optional: the DSH credentials service, read per call so a later mount applies. */
|
|
3466
|
+
__publicField(this, "credentials", () => this.ctx.get("credentials"));
|
|
3270
3467
|
__publicField(this, "scriptSupported", () => process.platform !== "win32" && !!this.ctx.get("shell"));
|
|
3271
3468
|
__publicField(this, "store");
|
|
3272
3469
|
__publicField(this, "settings");
|
|
@@ -3297,6 +3494,7 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
|
|
|
3297
3494
|
});
|
|
3298
3495
|
const gateDeps = {
|
|
3299
3496
|
definitions: () => this.definitions.read(),
|
|
3497
|
+
disabledGates: (sessionId) => this.gates.disabledGates(sessionId),
|
|
3300
3498
|
warn: (message) => ctx.logger.warn(message),
|
|
3301
3499
|
preset: (agent) => ctx.get("agentPresets")?.composedPreset(agent.ctx),
|
|
3302
3500
|
isRoot: (agent) => ctx.get("agents")?.roots().some((root) => root.id === agent.id) ?? false,
|
|
@@ -3312,14 +3510,17 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
|
|
|
3312
3510
|
};
|
|
3313
3511
|
},
|
|
3314
3512
|
runner: async () => {
|
|
3315
|
-
const [
|
|
3513
|
+
const [settings, scriptDirectory] = await Promise.all([this.settings.read(), this.scriptDirectory()]);
|
|
3514
|
+
const llm = { stream: (options) => llmOf(ctx).stream(options) };
|
|
3515
|
+
const judge = judgeEngineFor(settings, { llm, credentials: this.credentials() });
|
|
3316
3516
|
return new GateTaskRunner({
|
|
3317
3517
|
scriptDirectory,
|
|
3318
3518
|
scriptSupported: this.scriptSupported(),
|
|
3319
3519
|
shell: ctx.get("shell"),
|
|
3320
3520
|
helper: this.helper,
|
|
3321
|
-
llm
|
|
3322
|
-
...model ? { model } : {}
|
|
3521
|
+
llm,
|
|
3522
|
+
...settings.model ? { model: settings.model } : {},
|
|
3523
|
+
...judge ? { judge } : {}
|
|
3323
3524
|
});
|
|
3324
3525
|
},
|
|
3325
3526
|
recorder: this.gates,
|
|
@@ -3469,6 +3670,24 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
|
|
|
3469
3670
|
async resolveGateRejection(sessionId, runId, status) {
|
|
3470
3671
|
return this.gates.resolveRejection(sessionId, runId, status);
|
|
3471
3672
|
}
|
|
3673
|
+
async sessionGates(sessionId) {
|
|
3674
|
+
const agent = this.ctx.get("agents")?.get(sessionId);
|
|
3675
|
+
const preset = agent ? this.ctx.get("agentPresets")?.composedPreset(agent.ctx) : void 0;
|
|
3676
|
+
if (!preset) return { gates: [] };
|
|
3677
|
+
const [read, disabled] = await Promise.all([this.definitions.read(), this.gates.disabledGates(sessionId)]);
|
|
3678
|
+
const off = new Set(disabled);
|
|
3679
|
+
return { preset, gates: read.entries.filter((task) => enabledFor(task, preset)).map((task) => ({
|
|
3680
|
+
id: task.id,
|
|
3681
|
+
name: task.name,
|
|
3682
|
+
when: task.when,
|
|
3683
|
+
...task.advanced.script ? { script: task.advanced.script } : {},
|
|
3684
|
+
sessionEnabled: !off.has(task.id)
|
|
3685
|
+
})) };
|
|
3686
|
+
}
|
|
3687
|
+
async setSessionGate(sessionId, gateId, enabled) {
|
|
3688
|
+
if (typeof enabled !== "boolean") throw new Error("enabled must be a boolean");
|
|
3689
|
+
return this.gates.setGateEnabled(sessionId, gateId, enabled);
|
|
3690
|
+
}
|
|
3472
3691
|
async *followGates(sessionId, signal) {
|
|
3473
3692
|
yield* this.gates.follow(sessionId, signal);
|
|
3474
3693
|
}
|
|
@@ -3478,6 +3697,26 @@ var PixieService = class extends (_a = TypertRemoteService, _view_dec = [Remote]
|
|
|
3478
3697
|
async saveSettings(payload) {
|
|
3479
3698
|
return this.settings.save(json2(payload));
|
|
3480
3699
|
}
|
|
3700
|
+
async jevKeyStatus() {
|
|
3701
|
+
const credentials = this.credentials();
|
|
3702
|
+
if (!credentials) return { available: false, configured: false, writable: false };
|
|
3703
|
+
const info = await credentials.describe(JEV_KEY_REF);
|
|
3704
|
+
return { available: true, configured: info.configured, writable: info.writable, ...info.source ? { source: info.source } : {} };
|
|
3705
|
+
}
|
|
3706
|
+
async saveJevKey(payload) {
|
|
3707
|
+
const value = json2(payload).value;
|
|
3708
|
+
if (typeof value !== "string" || !value.trim()) throw new Error("Jev API key is required");
|
|
3709
|
+
const credentials = this.credentials();
|
|
3710
|
+
if (!credentials) throw new Error("DSH credentials service is unavailable");
|
|
3711
|
+
await credentialWrite(() => credentials.set(JEV_KEY_REF, value.trim()));
|
|
3712
|
+
return this.jevKeyStatus();
|
|
3713
|
+
}
|
|
3714
|
+
async deleteJevKey() {
|
|
3715
|
+
const credentials = this.credentials();
|
|
3716
|
+
if (!credentials) throw new Error("DSH credentials service is unavailable");
|
|
3717
|
+
await credentialWrite(() => credentials.unset(JEV_KEY_REF));
|
|
3718
|
+
return this.jevKeyStatus();
|
|
3719
|
+
}
|
|
3481
3720
|
async models() {
|
|
3482
3721
|
return this.ctx.get("sessionController").modelCatalog();
|
|
3483
3722
|
}
|
|
@@ -3498,16 +3737,91 @@ __decorateElement(_init, 1, "gateDefinitions", _gateDefinitions_dec, PixieServic
|
|
|
3498
3737
|
__decorateElement(_init, 1, "saveGateDefinitions", _saveGateDefinitions_dec, PixieService);
|
|
3499
3738
|
__decorateElement(_init, 1, "gateRuns", _gateRuns_dec, PixieService);
|
|
3500
3739
|
__decorateElement(_init, 1, "resolveGateRejection", _resolveGateRejection_dec, PixieService);
|
|
3740
|
+
__decorateElement(_init, 1, "sessionGates", _sessionGates_dec, PixieService);
|
|
3741
|
+
__decorateElement(_init, 1, "setSessionGate", _setSessionGate_dec, PixieService);
|
|
3501
3742
|
__decorateElement(_init, 1, "followGates", _followGates_dec, PixieService);
|
|
3502
3743
|
__decorateElement(_init, 1, "readSettings", _readSettings_dec, PixieService);
|
|
3503
3744
|
__decorateElement(_init, 1, "saveSettings", _saveSettings_dec, PixieService);
|
|
3745
|
+
__decorateElement(_init, 1, "jevKeyStatus", _jevKeyStatus_dec, PixieService);
|
|
3746
|
+
__decorateElement(_init, 1, "saveJevKey", _saveJevKey_dec, PixieService);
|
|
3747
|
+
__decorateElement(_init, 1, "deleteJevKey", _deleteJevKey_dec, PixieService);
|
|
3504
3748
|
__decorateElement(_init, 1, "models", _models_dec, PixieService);
|
|
3505
3749
|
__decoratorMetadata(_init, PixieService);
|
|
3506
3750
|
__publicField(PixieService, "inject", ["llm", "sessionController", "commands"]);
|
|
3507
3751
|
|
|
3752
|
+
// src/host/skill-install.ts
|
|
3753
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
3754
|
+
import { mkdir as mkdir8, readFile as readFile8, rename as rename8, writeFile as writeFile8 } from "node:fs/promises";
|
|
3755
|
+
import { join as join7 } from "node:path";
|
|
3756
|
+
var MARKER = "studio-managed";
|
|
3757
|
+
var MANIFEST = ".studio-managed.json";
|
|
3758
|
+
var digest = (text2) => createHash2("sha256").update(text2).digest("hex");
|
|
3759
|
+
async function installSkill(skillsRoot, skill) {
|
|
3760
|
+
const directory = join7(skillsRoot, skill.name);
|
|
3761
|
+
await mkdir8(directory, { recursive: true });
|
|
3762
|
+
const manifestPath = join7(directory, MANIFEST);
|
|
3763
|
+
let recorded = {};
|
|
3764
|
+
try {
|
|
3765
|
+
const parsed = JSON.parse(await readFile8(manifestPath, "utf8"));
|
|
3766
|
+
if (parsed.kind === MARKER && parsed.files && typeof parsed.files === "object") recorded = parsed.files;
|
|
3767
|
+
} catch {
|
|
3768
|
+
}
|
|
3769
|
+
const results = {};
|
|
3770
|
+
const next = {};
|
|
3771
|
+
for (const [relative, content] of Object.entries(skill.files)) {
|
|
3772
|
+
const path = join7(directory, relative);
|
|
3773
|
+
const wanted = digest(content);
|
|
3774
|
+
let current;
|
|
3775
|
+
try {
|
|
3776
|
+
current = digest(await readFile8(path, "utf8"));
|
|
3777
|
+
} catch {
|
|
3778
|
+
current = void 0;
|
|
3779
|
+
}
|
|
3780
|
+
if (current === wanted) {
|
|
3781
|
+
results[relative] = "current";
|
|
3782
|
+
next[relative] = wanted;
|
|
3783
|
+
continue;
|
|
3784
|
+
}
|
|
3785
|
+
if (current !== void 0 && current !== recorded[relative]) {
|
|
3786
|
+
results[relative] = "kept-user-edit";
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
const temporary2 = `${path}.${process.pid}.tmp`;
|
|
3790
|
+
await writeFile8(temporary2, content);
|
|
3791
|
+
await rename8(temporary2, path);
|
|
3792
|
+
results[relative] = "installed";
|
|
3793
|
+
next[relative] = wanted;
|
|
3794
|
+
}
|
|
3795
|
+
const temporary = `${manifestPath}.${process.pid}.tmp`;
|
|
3796
|
+
await writeFile8(temporary, `${JSON.stringify({ kind: MARKER, files: next }, null, 2)}
|
|
3797
|
+
`);
|
|
3798
|
+
await rename8(temporary, manifestPath);
|
|
3799
|
+
return results;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// src/host/pixie-hook-skill.ts
|
|
3803
|
+
import { readFileSync } from "node:fs";
|
|
3804
|
+
import { fileURLToPath } from "node:url";
|
|
3805
|
+
function pixieHookSkill() {
|
|
3806
|
+
const name = "studio-pixie-hook-guide";
|
|
3807
|
+
const read = (packaged, source) => {
|
|
3808
|
+
for (const candidate of [new URL(packaged, import.meta.url), new URL(source, import.meta.url), new URL(source.replace("release/docs/", "docs/"), import.meta.url)]) {
|
|
3809
|
+
try {
|
|
3810
|
+
return readFileSync(fileURLToPath(candidate), "utf8");
|
|
3811
|
+
} catch {
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
throw new Error(`Studio skill file missing: ${packaged}`);
|
|
3815
|
+
};
|
|
3816
|
+
return { name, files: {
|
|
3817
|
+
"SKILL.md": read(`./skills/${name}/SKILL.md`, "../../release/docs/appendix/studio-pixie-hook-guide/SKILL.md"),
|
|
3818
|
+
"guide.md": read(`./skills/${name}/guide.md`, "../../release/docs/appendix/pixie-gate.md")
|
|
3819
|
+
} };
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3508
3822
|
// src/host/catalog-baseline.ts
|
|
3509
3823
|
import { randomUUID as randomUUID11 } from "node:crypto";
|
|
3510
|
-
import { mkdir as
|
|
3824
|
+
import { mkdir as mkdir9, readFile as readFile9, rename as rename9, unlink as unlink2, writeFile as writeFile9 } from "node:fs/promises";
|
|
3511
3825
|
import { dirname as dirname7, resolve as resolve5 } from "node:path";
|
|
3512
3826
|
var valid = (value) => {
|
|
3513
3827
|
if (!value || typeof value !== "object") return false;
|
|
@@ -3524,7 +3838,7 @@ var CatalogBaseline = class {
|
|
|
3524
3838
|
await this.tail;
|
|
3525
3839
|
let content;
|
|
3526
3840
|
try {
|
|
3527
|
-
content = await
|
|
3841
|
+
content = await readFile9(this.file, "utf8");
|
|
3528
3842
|
} catch (error) {
|
|
3529
3843
|
if (error.code === "ENOENT") return void 0;
|
|
3530
3844
|
throw error;
|
|
@@ -3536,11 +3850,11 @@ var CatalogBaseline = class {
|
|
|
3536
3850
|
save(snapshot) {
|
|
3537
3851
|
const content = JSON.stringify(snapshot, null, 2);
|
|
3538
3852
|
const operation = this.tail.then(async () => {
|
|
3539
|
-
await
|
|
3853
|
+
await mkdir9(dirname7(this.file), { recursive: true, mode: 448 });
|
|
3540
3854
|
const temp = `${this.file}.${randomUUID11()}.tmp`;
|
|
3541
3855
|
try {
|
|
3542
|
-
await
|
|
3543
|
-
await
|
|
3856
|
+
await writeFile9(temp, content, { mode: 384, flag: "wx" });
|
|
3857
|
+
await rename9(temp, this.file);
|
|
3544
3858
|
} finally {
|
|
3545
3859
|
await unlink2(temp).catch(() => {
|
|
3546
3860
|
});
|
|
@@ -3582,10 +3896,18 @@ var StudioCatalog = class extends (_a2 = TypertRemoteService2, _listPresets_dec
|
|
|
3582
3896
|
/** The newest complete collection and what it changes against the confirmed baseline. */
|
|
3583
3897
|
__publicField(this, "pending");
|
|
3584
3898
|
const storePath = studioStorePath(process.env, ctx.get("profileContext"));
|
|
3585
|
-
this.extensions = new MasterExtensions(
|
|
3586
|
-
this.baseline = new CatalogBaseline(
|
|
3899
|
+
this.extensions = new MasterExtensions(join8(dirname8(storePath), "master-extensions.json"));
|
|
3900
|
+
this.baseline = new CatalogBaseline(join8(dirname8(storePath), "catalog-snapshot.json"));
|
|
3587
3901
|
this.store = new PresetStore(ctx, storePath);
|
|
3588
3902
|
ctx.plugin(PixieService, dirname8(storePath));
|
|
3903
|
+
const home = ctx.get("profileContext")?.home;
|
|
3904
|
+
if (home) void Promise.resolve().then(() => {
|
|
3905
|
+
const skill = pixieHookSkill();
|
|
3906
|
+
return installSkill(join8(home, "skills"), skill).then((results) => {
|
|
3907
|
+
const kept = Object.entries(results).filter(([, result]) => result === "kept-user-edit").map(([file]) => file);
|
|
3908
|
+
if (kept.length) ctx.logger?.info?.(`studio: kept user-edited ${skill.name}/${kept.join(", ")}`);
|
|
3909
|
+
});
|
|
3910
|
+
}).catch((error) => ctx.logger?.warn?.(`studio: skill install skipped: ${String(error)}`));
|
|
3589
3911
|
this.snapshot = new Promise((resolve6) => {
|
|
3590
3912
|
setImmediate(resolve6);
|
|
3591
3913
|
}).then(() => this.capture());
|