@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/judge.js
ADDED
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
// src/host/judge-tool.ts
|
|
2
|
+
import { dirname as dirname3, join as join3 } from "node:path";
|
|
3
|
+
|
|
4
|
+
// src/pixie-contracts.ts
|
|
5
|
+
var DEFAULT_PIXIE_SETTINGS = {
|
|
6
|
+
contextMode: "fresh",
|
|
7
|
+
systemPrompt: "You are Pixie, a concise side assistant. Answer the user directly. You are not the session agent and cannot act on its workspace.",
|
|
8
|
+
allowThreadModel: true
|
|
9
|
+
};
|
|
10
|
+
var DEFAULT_JUDGE_SYSTEM_PROMPT = [
|
|
11
|
+
"You are a judgment component. Evaluate the given state against each question independently.",
|
|
12
|
+
'Reply with exactly one JSON object and nothing else: {"answers": {<question id>: <answer>}}.',
|
|
13
|
+
"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.",
|
|
14
|
+
'choice answer: {"choice": <option name>, "probabilities": {<every option name>: number}} \u2014 probabilities sum to 1.',
|
|
15
|
+
'score answer: {"score": number from 0 to levels-1 (fractional allowed), "probabilities": [one number per level, in order]} \u2014 probabilities sum to 1.',
|
|
16
|
+
'noul answer: {"noul": probability from 0 to 1 that the answer is yes}.'
|
|
17
|
+
].join("\n");
|
|
18
|
+
var JudgeValidationError = class extends Error {
|
|
19
|
+
constructor(code, message) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
function validateJudgeRequest(value) {
|
|
25
|
+
if (!value || typeof value !== "object") throw new Error("Judge request must be an object");
|
|
26
|
+
const { state, questions } = value;
|
|
27
|
+
if (state === void 0) throw new Error("Judge request needs a state");
|
|
28
|
+
if (!questions || typeof questions !== "object" || Array.isArray(questions)) throw new Error("Judge request needs questions by id");
|
|
29
|
+
const entries = Object.entries(questions);
|
|
30
|
+
if (entries.length === 0) throw new JudgeValidationError("questions", "Judge request needs at least one question");
|
|
31
|
+
for (const [id, raw] of entries) {
|
|
32
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(id) || ["__proto__", "constructor", "prototype"].includes(id)) throw new JudgeValidationError("questionId", `Invalid judge question id: ${id}`);
|
|
33
|
+
const question = raw;
|
|
34
|
+
if (!question || typeof question !== "object" || Array.isArray(question)) throw new JudgeValidationError("questionType", `Question "${id}" must be choice, score, or noul`);
|
|
35
|
+
if (!["choice", "score", "noul"].includes(question.type)) throw new JudgeValidationError("questionType", `Question "${id}" must be choice, score, or noul`);
|
|
36
|
+
if (typeof question.instructions !== "string" || !question.instructions.trim()) throw new JudgeValidationError("instructions", `Question "${id}" needs instructions`);
|
|
37
|
+
const criteria = question.criteria;
|
|
38
|
+
if (question.type === "choice") {
|
|
39
|
+
const names = criteria && typeof criteria === "object" && !Array.isArray(criteria) ? Object.keys(criteria) : [];
|
|
40
|
+
if (names.length < 2 || names.length > 255 || names.some((name) => !name.trim() || typeof criteria[name] !== "string")) throw new JudgeValidationError("criteria", `Choice "${id}" needs 2\u2013255 named options with string descriptions`);
|
|
41
|
+
} else if (question.type === "score") {
|
|
42
|
+
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`);
|
|
43
|
+
} 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"))) {
|
|
44
|
+
throw new JudgeValidationError("criteria", `Invalid noul criteria: ${id}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { state, questions };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/host/pixie-runtime.ts
|
|
51
|
+
async function* untilAborted(stream, signal) {
|
|
52
|
+
const iterator = stream[Symbol.asyncIterator]();
|
|
53
|
+
let onAbort = () => {
|
|
54
|
+
};
|
|
55
|
+
const aborted = new Promise((_, reject) => {
|
|
56
|
+
onAbort = () => reject(signal.reason ?? new Error("aborted"));
|
|
57
|
+
if (signal.aborted) onAbort();
|
|
58
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
59
|
+
});
|
|
60
|
+
aborted.catch(() => {
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
while (true) {
|
|
64
|
+
const next = await Promise.race([iterator.next(), aborted]);
|
|
65
|
+
if (next.done) return;
|
|
66
|
+
yield next.value;
|
|
67
|
+
}
|
|
68
|
+
} finally {
|
|
69
|
+
signal.removeEventListener("abort", onAbort);
|
|
70
|
+
void Promise.resolve(iterator.return?.()).catch(() => {
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// src/host/pixie-judge.ts
|
|
76
|
+
var TOLERANCE = 0.02;
|
|
77
|
+
var isProbability = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0 && value <= 1;
|
|
78
|
+
var sumsToOne = (values) => Math.abs(values.reduce((sum, value) => sum + value, 0) - 1) <= TOLERANCE;
|
|
79
|
+
function checkAnswer(id, question, raw) {
|
|
80
|
+
const answer = raw;
|
|
81
|
+
if (!answer || typeof answer !== "object") throw new Error(`Missing answer for "${id}"`);
|
|
82
|
+
if (question.type === "choice") {
|
|
83
|
+
const names = Object.keys(question.criteria);
|
|
84
|
+
const probabilities = answer.probabilities;
|
|
85
|
+
if (typeof answer.choice !== "string" || !names.includes(answer.choice)) throw new Error(`Answer "${id}" chose an unknown option`);
|
|
86
|
+
if (!probabilities || names.some((name) => !isProbability(probabilities[name])) || Object.keys(probabilities).some((name) => !names.includes(name))) throw new Error(`Answer "${id}" needs a probability per option`);
|
|
87
|
+
const values = names.map((name) => probabilities[name]);
|
|
88
|
+
if (!sumsToOne(values)) throw new Error(`Answer "${id}" probabilities must sum to 1`);
|
|
89
|
+
return { type: "choice", choice: answer.choice, probabilities: Object.fromEntries(names.map((name) => [name, probabilities[name]])), confidence: isProbability(answer.confidence) ? answer.confidence : Math.max(...values) };
|
|
90
|
+
}
|
|
91
|
+
if (question.type === "score") {
|
|
92
|
+
const probabilities = answer.probabilities;
|
|
93
|
+
const top = question.criteria.length - 1;
|
|
94
|
+
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}`);
|
|
95
|
+
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`);
|
|
96
|
+
return { type: "score", score: answer.score, probabilities, confidence: isProbability(answer.confidence) ? answer.confidence : Math.max(...probabilities) };
|
|
97
|
+
}
|
|
98
|
+
if (!isProbability(answer.noul)) throw new Error(`Answer "${id}" noul must be a probability in 0..1`);
|
|
99
|
+
return { type: "noul", noul: answer.noul };
|
|
100
|
+
}
|
|
101
|
+
function parseJudgeOutput(text, questions) {
|
|
102
|
+
const fenced = /^```(?:json)?\s*([\s\S]*?)\s*```$/.exec(text.trim());
|
|
103
|
+
let parsed;
|
|
104
|
+
try {
|
|
105
|
+
parsed = JSON.parse(fenced ? fenced[1] : text.trim());
|
|
106
|
+
} catch {
|
|
107
|
+
throw new Error("Judge model did not return JSON");
|
|
108
|
+
}
|
|
109
|
+
const answers = parsed?.answers;
|
|
110
|
+
if (!answers || typeof answers !== "object") throw new Error("Judge model output has no answers object");
|
|
111
|
+
return Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, answers[id])]));
|
|
112
|
+
}
|
|
113
|
+
var LlmJudgeEngine = class {
|
|
114
|
+
constructor(llm, model, system = DEFAULT_JUDGE_SYSTEM_PROMPT) {
|
|
115
|
+
this.llm = llm;
|
|
116
|
+
this.model = model;
|
|
117
|
+
this.system = system;
|
|
118
|
+
}
|
|
119
|
+
kind = "judge";
|
|
120
|
+
label = "llm";
|
|
121
|
+
async judge(request, signal) {
|
|
122
|
+
let usage;
|
|
123
|
+
try {
|
|
124
|
+
const { state, questions } = validateJudgeRequest(request);
|
|
125
|
+
const prompt = `State:
|
|
126
|
+
${typeof state === "string" ? state : JSON.stringify(state, null, 2)}
|
|
127
|
+
|
|
128
|
+
Questions:
|
|
129
|
+
${JSON.stringify(questions, null, 2)}`;
|
|
130
|
+
let text = "";
|
|
131
|
+
let finished = false;
|
|
132
|
+
for await (const chunk of untilAborted(this.llm.stream({
|
|
133
|
+
provider: this.model.provider,
|
|
134
|
+
model: this.model.model,
|
|
135
|
+
...this.model.reasoningEffort ? { reasoningEffort: this.model.reasoningEffort } : {},
|
|
136
|
+
...this.model.maxTokens ? { maxTokens: this.model.maxTokens } : {},
|
|
137
|
+
system: this.system,
|
|
138
|
+
messages: [{ role: "user", content: [{ type: "text", text: prompt }] }],
|
|
139
|
+
signal
|
|
140
|
+
}), signal)) {
|
|
141
|
+
if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
142
|
+
if (chunk.type === "usage" && chunk.usage && typeof chunk.usage === "object") {
|
|
143
|
+
const raw = chunk.usage;
|
|
144
|
+
usage = { inputTokens: typeof raw.inputTokens === "number" ? raw.inputTokens : 0, outputTokens: typeof raw.outputTokens === "number" ? raw.outputTokens : 0 };
|
|
145
|
+
for (const key of ["cacheReadTokens", "cacheWriteTokens", "reasoningTokens"]) if (typeof raw[key] === "number") usage[key] = raw[key];
|
|
146
|
+
}
|
|
147
|
+
if (chunk.type === "finish") {
|
|
148
|
+
const reason = chunk.reason;
|
|
149
|
+
if (reason?.kind !== "stop") throw new Error(reason?.failure?.message ?? `Judge model ended with ${reason?.kind ?? "unknown"}`);
|
|
150
|
+
finished = true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
signal.throwIfAborted();
|
|
154
|
+
if (!finished) throw new Error("Judge model stream ended without a finish");
|
|
155
|
+
return { ok: true, answers: parseJudgeOutput(text, questions), ...usage ? { usage } : {} };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error), ...usage ? { usage } : {} };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// src/host/jev-judge.ts
|
|
163
|
+
var JEV_ENDPOINT = "https://api.typesafe.ai/v1/systemone";
|
|
164
|
+
var JEV_KEY_REF = "TYPESAFE_API_KEY";
|
|
165
|
+
var DEFAULT_JEV_MODEL = "jev-latest";
|
|
166
|
+
var RETRIES = 2;
|
|
167
|
+
var MAX_WAIT_MS = 4e3;
|
|
168
|
+
function fromWire(question, raw) {
|
|
169
|
+
if (question.type !== "score" || !raw || typeof raw !== "object") return raw;
|
|
170
|
+
const answer = raw;
|
|
171
|
+
const probabilities = answer.probabilities;
|
|
172
|
+
if (!probabilities || typeof probabilities !== "object" || Array.isArray(probabilities)) return raw;
|
|
173
|
+
const keyed = probabilities;
|
|
174
|
+
if (Object.keys(keyed).length !== question.criteria.length) return { ...answer, probabilities: void 0 };
|
|
175
|
+
return { ...answer, probabilities: question.criteria.map((_, index) => keyed[String(index)]) };
|
|
176
|
+
}
|
|
177
|
+
var wait = (ms, signal) => new Promise((resolve, reject) => {
|
|
178
|
+
if (signal.aborted) {
|
|
179
|
+
reject(signal.reason);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const timer = setTimeout(() => {
|
|
183
|
+
signal.removeEventListener("abort", abort);
|
|
184
|
+
resolve();
|
|
185
|
+
}, ms);
|
|
186
|
+
const abort = () => {
|
|
187
|
+
clearTimeout(timer);
|
|
188
|
+
reject(signal.reason);
|
|
189
|
+
};
|
|
190
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
191
|
+
});
|
|
192
|
+
function retryDelay(retryAfter, attempt) {
|
|
193
|
+
const seconds = retryAfter === null ? NaN : Number(retryAfter);
|
|
194
|
+
return Math.min(Number.isFinite(seconds) && seconds >= 0 ? seconds * 1e3 : 500 * 2 ** attempt, MAX_WAIT_MS);
|
|
195
|
+
}
|
|
196
|
+
var JevJudgeEngine = class {
|
|
197
|
+
constructor(deps) {
|
|
198
|
+
this.deps = deps;
|
|
199
|
+
}
|
|
200
|
+
kind = "judge";
|
|
201
|
+
label = "jev";
|
|
202
|
+
async judge(request, signal) {
|
|
203
|
+
let usage;
|
|
204
|
+
try {
|
|
205
|
+
const { state, questions } = validateJudgeRequest(request);
|
|
206
|
+
if (!this.deps.credentials) throw new Error("DSH credentials service is unavailable");
|
|
207
|
+
const key = (await this.deps.credentials.resolve(JEV_KEY_REF))?.value;
|
|
208
|
+
if (!key) throw new Error(`Jev API key is not configured (${JEV_KEY_REF})`);
|
|
209
|
+
signal.throwIfAborted();
|
|
210
|
+
const send = this.deps.fetch ?? globalThis.fetch;
|
|
211
|
+
const body = JSON.stringify({ model: this.deps.model, state, questions });
|
|
212
|
+
for (let attempt = 0; ; attempt++) {
|
|
213
|
+
const response = await send(JEV_ENDPOINT, {
|
|
214
|
+
method: "POST",
|
|
215
|
+
signal,
|
|
216
|
+
body,
|
|
217
|
+
headers: { authorization: `Bearer ${key}`, "content-type": "application/json" }
|
|
218
|
+
});
|
|
219
|
+
const text = await response.text();
|
|
220
|
+
if ((response.status === 429 || response.status === 529) && attempt < RETRIES) {
|
|
221
|
+
await wait(retryDelay(response.headers.get("retry-after"), attempt), signal);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (response.status !== 200) throw new Error(`Jev request failed: HTTP ${response.status}${text ? `: ${text.slice(0, 500)}` : ""}`);
|
|
225
|
+
let parsed;
|
|
226
|
+
try {
|
|
227
|
+
parsed = JSON.parse(text);
|
|
228
|
+
} catch {
|
|
229
|
+
throw new Error("Jev response is not JSON");
|
|
230
|
+
}
|
|
231
|
+
const raw = parsed.usage;
|
|
232
|
+
if (raw && typeof raw.input_tokens === "number" && typeof raw.output_tokens === "number") usage = { inputTokens: raw.input_tokens, outputTokens: raw.output_tokens };
|
|
233
|
+
const answers = parsed.answers;
|
|
234
|
+
if (!answers || typeof answers !== "object") throw new Error("Jev response has no answers object");
|
|
235
|
+
const typed = Object.fromEntries(Object.entries(questions).map(([id, question]) => [id, checkAnswer(id, question, fromWire(question, answers[id]))]));
|
|
236
|
+
return { ok: true, answers: typed, ...usage ? { usage } : {} };
|
|
237
|
+
}
|
|
238
|
+
} catch (error) {
|
|
239
|
+
const message = signal.aborted ? String(signal.reason instanceof Error ? signal.reason.message : signal.reason ?? "aborted") : error instanceof Error ? error.message : String(error);
|
|
240
|
+
return { ok: false, error: message, ...usage ? { usage } : {} };
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
};
|
|
244
|
+
function judgeEngineFor(settings, deps) {
|
|
245
|
+
if (settings.judgeEngine === "jev") return new JevJudgeEngine({ credentials: deps.credentials, model: settings.jevModel ?? DEFAULT_JEV_MODEL, ...deps.fetch ? { fetch: deps.fetch } : {} });
|
|
246
|
+
const model = settings.judgeModel ?? settings.model;
|
|
247
|
+
return model ? new LlmJudgeEngine(deps.llm, model, settings.judgeSystemPrompt || void 0) : void 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/host/pixie-store.ts
|
|
251
|
+
import { randomUUID } from "node:crypto";
|
|
252
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
253
|
+
import { dirname, join } from "node:path";
|
|
254
|
+
function parsePixieSettings(value) {
|
|
255
|
+
const raw = value;
|
|
256
|
+
if (!raw || typeof raw !== "object") throw new Error("Pixie settings must be an object");
|
|
257
|
+
if (raw.contextMode !== "fresh" && raw.contextMode !== "fork") throw new Error("Pixie settings contextMode must be fresh or fork");
|
|
258
|
+
if (typeof raw.systemPrompt !== "string") throw new Error("Pixie settings systemPrompt must be a string");
|
|
259
|
+
if (typeof raw.allowThreadModel !== "boolean") throw new Error("Pixie settings allowThreadModel must be a boolean");
|
|
260
|
+
const settings = { contextMode: raw.contextMode, systemPrompt: raw.systemPrompt, allowThreadModel: raw.allowThreadModel };
|
|
261
|
+
if (raw.model !== void 0) settings.model = parsePixieModel(raw.model);
|
|
262
|
+
if (raw.judgeModel !== void 0) settings.judgeModel = parsePixieModel(raw.judgeModel);
|
|
263
|
+
if (raw.judgeSystemPrompt !== void 0) {
|
|
264
|
+
if (typeof raw.judgeSystemPrompt !== "string") throw new Error("Pixie settings judgeSystemPrompt must be a string");
|
|
265
|
+
settings.judgeSystemPrompt = raw.judgeSystemPrompt;
|
|
266
|
+
}
|
|
267
|
+
if (raw.judgeEngine !== void 0) {
|
|
268
|
+
if (raw.judgeEngine !== "llm" && raw.judgeEngine !== "jev") throw new Error("Pixie settings judgeEngine must be llm or jev");
|
|
269
|
+
settings.judgeEngine = raw.judgeEngine;
|
|
270
|
+
}
|
|
271
|
+
if (raw.jevModel !== void 0) {
|
|
272
|
+
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");
|
|
273
|
+
settings.jevModel = raw.jevModel;
|
|
274
|
+
}
|
|
275
|
+
return settings;
|
|
276
|
+
}
|
|
277
|
+
function parsePixieModel(value) {
|
|
278
|
+
const raw = value;
|
|
279
|
+
if (!raw || typeof raw !== "object") throw new Error("Pixie model must be an object");
|
|
280
|
+
if (typeof raw.provider !== "string" || !raw.provider) throw new Error("Pixie model provider is required");
|
|
281
|
+
if (typeof raw.model !== "string" || !raw.model) throw new Error("Pixie model id is required");
|
|
282
|
+
const model = { provider: raw.provider, model: raw.model };
|
|
283
|
+
if (raw.reasoningEffort !== void 0) {
|
|
284
|
+
if (typeof raw.reasoningEffort !== "string" || !raw.reasoningEffort) throw new Error("Pixie model reasoningEffort must be a non-empty string");
|
|
285
|
+
model.reasoningEffort = raw.reasoningEffort;
|
|
286
|
+
}
|
|
287
|
+
if (raw.maxTokens !== void 0) {
|
|
288
|
+
if (!Number.isInteger(raw.maxTokens) || raw.maxTokens <= 0) throw new Error("Pixie model maxTokens must be a positive integer");
|
|
289
|
+
model.maxTokens = raw.maxTokens;
|
|
290
|
+
}
|
|
291
|
+
return model;
|
|
292
|
+
}
|
|
293
|
+
var PixieSettingsStore = class {
|
|
294
|
+
constructor(file) {
|
|
295
|
+
this.file = file;
|
|
296
|
+
}
|
|
297
|
+
tail = Promise.resolve();
|
|
298
|
+
async read() {
|
|
299
|
+
try {
|
|
300
|
+
return parsePixieSettings(JSON.parse(await readFile(this.file, "utf8")));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (error.code === "ENOENT") return structuredClone(DEFAULT_PIXIE_SETTINGS);
|
|
303
|
+
throw error;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
save(value) {
|
|
307
|
+
const run = this.tail.catch(() => {
|
|
308
|
+
}).then(async () => {
|
|
309
|
+
const settings = parsePixieSettings(value);
|
|
310
|
+
await mkdir(dirname(this.file), { recursive: true });
|
|
311
|
+
const temporary = `${this.file}.${randomUUID()}.tmp`;
|
|
312
|
+
await writeFile(temporary, JSON.stringify(settings, null, 2));
|
|
313
|
+
await rename(temporary, this.file);
|
|
314
|
+
return settings;
|
|
315
|
+
});
|
|
316
|
+
this.tail = run;
|
|
317
|
+
return run;
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// src/package-name.ts
|
|
322
|
+
var PACKAGE_NAME = "@matamt-dbg/dsh-studio";
|
|
323
|
+
var POLICY_PLUGIN = `${PACKAGE_NAME}/policy`;
|
|
324
|
+
var ROLE_PLUGIN = `${PACKAGE_NAME}/roles`;
|
|
325
|
+
var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
|
|
326
|
+
var JUDGE_PLUGIN = `${PACKAGE_NAME}/judge`;
|
|
327
|
+
|
|
328
|
+
// src/delegation.ts
|
|
329
|
+
var LEGACY_ROLE_TOOL = "studio_role";
|
|
330
|
+
var GENERIC_DELEGATION_TOOLS = ["subagent", "subagent_fork", "list_subagent_models"];
|
|
331
|
+
|
|
332
|
+
// src/optimized-preset.ts
|
|
333
|
+
var OPTIMIZED_TOOLS = [
|
|
334
|
+
"read_batch",
|
|
335
|
+
"read",
|
|
336
|
+
"write",
|
|
337
|
+
"edit",
|
|
338
|
+
"glob",
|
|
339
|
+
"grep",
|
|
340
|
+
"job_output",
|
|
341
|
+
"job_list",
|
|
342
|
+
"job_kill",
|
|
343
|
+
"skill",
|
|
344
|
+
"ask_user_question",
|
|
345
|
+
"web_search",
|
|
346
|
+
"web_fetch",
|
|
347
|
+
"present",
|
|
348
|
+
"bash",
|
|
349
|
+
"read_image",
|
|
350
|
+
"exit_plan_mode",
|
|
351
|
+
"send_message",
|
|
352
|
+
"interrupt_agent",
|
|
353
|
+
"list_agents",
|
|
354
|
+
"workflow",
|
|
355
|
+
"subagent_role"
|
|
356
|
+
];
|
|
357
|
+
var ALLOWED_TOOLS = /* @__PURE__ */ new Set([...OPTIMIZED_TOOLS, ...GENERIC_DELEGATION_TOOLS, LEGACY_ROLE_TOOL]);
|
|
358
|
+
|
|
359
|
+
// src/host/preset-store.ts
|
|
360
|
+
import { createScope } from "@deepseek-ai/dsh-scope";
|
|
361
|
+
|
|
362
|
+
// src/host/preset-policy.ts
|
|
363
|
+
import { scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
|
|
364
|
+
var POLICY_SECTION_ORDERS = Object.freeze({
|
|
365
|
+
"tool:jobs": "TOOL_JOBS",
|
|
366
|
+
"tool:bash": "TOOL_BASH",
|
|
367
|
+
"tool:pwsh": "TOOL_PWSH",
|
|
368
|
+
"tool:read": "TOOL_READ",
|
|
369
|
+
"tool:write": "TOOL_WRITE",
|
|
370
|
+
"tool:edit": "TOOL_EDIT",
|
|
371
|
+
"tool:glob": "TOOL_GLOB",
|
|
372
|
+
"tool:grep": "TOOL_GREP",
|
|
373
|
+
"tool:web_fetch": "TOOL_WEB_FETCH",
|
|
374
|
+
"tool:web_search": "TOOL_WEB_SEARCH",
|
|
375
|
+
"tool:goal": "TOOL_GOAL",
|
|
376
|
+
"tool:workflow": "TOOL_WORKFLOW"
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// src/config-overrides.ts
|
|
380
|
+
var LEGACY_DROPPED_PACKAGES = Object.freeze([
|
|
381
|
+
"@deepseek-ai/dsh-agent-instructions",
|
|
382
|
+
"@deepseek-ai/dsh-time-context",
|
|
383
|
+
"@deepseek-ai/dsh-skill-filesystem",
|
|
384
|
+
"@deepseek-ai/dsh-compaction-basic",
|
|
385
|
+
"@deepseek-ai/dsh-command-compact",
|
|
386
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner"
|
|
387
|
+
]);
|
|
388
|
+
var CONFIG_REQUIRES = Object.freeze({
|
|
389
|
+
"@deepseek-ai/dsh-command-compact": "@deepseek-ai/dsh-compaction-basic"
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
// src/host/preset-store.ts
|
|
393
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
394
|
+
import { dump } from "js-yaml";
|
|
395
|
+
import { entryListSchema as entryListSchema2 } from "@deepseek-ai/cordis-plugin-include";
|
|
396
|
+
|
|
397
|
+
// src/provider-fields.ts
|
|
398
|
+
var field = (key, type, value, minimum) => ({ key, type, ...value === void 0 ? {} : { value }, ...minimum === void 0 ? {} : { minimum } });
|
|
399
|
+
var providerFields = {
|
|
400
|
+
"@deepseek-ai/dsh-compaction-basic": [field("thresholdRatio", "number", 0.8, 0), field("headroomTokens", "number", 65536, 1), field("retainRatio", "number", 0.16, 0), field("retainTokens", "number", void 0, 0), field("summarizationProvider", "string", ""), field("summarizationModel", "string", ""), field("maxTokens", "number", void 0, 1), field("compactionRetries", "number", 1, 0), field("maxOverflowRetries", "number", 1, 0), field("auto", "boolean", true)],
|
|
401
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": [field("thresholdChars", "number", 8192, 1), field("headChars", "number", 4096, 0), field("tailChars", "number", 1024, 0)],
|
|
402
|
+
"@deepseek-ai/dsh-agent-tool-presentation": [{ key: "mode", type: "string", options: ["native", "ptc"] }],
|
|
403
|
+
"@deepseek-ai/dsh-persona": [field("prefix", "string"), field("suffix", "string", ""), field("includeRuntimeContext", "boolean", true)],
|
|
404
|
+
"@deepseek-ai/dsh-agent-instructions": [field("maxBytes", "number", 65536), field("maxSourceBytes", "number", void 0, 1), field("projectRootMarkers", "string[]"), field("instructionFileCandidates", "string[]"), field("localInstructionFileCandidates", "string[]")],
|
|
405
|
+
"@deepseek-ai/dsh-time-context": [field("timeZone", "string"), field("refreshIntervalMs", "number", void 0, 0)],
|
|
406
|
+
"@deepseek-ai/dsh-skill-filesystem": [field("providerName", "string", "filesystem"), field("includeDefaultRoots", "boolean", true), field("dshHome", "string"), field("agentsHome", "string"), field("customSkillDirs", "string[]", []), field("bundledSkillDir", "string"), field("watch", "boolean", true), field("watchUsePolling", "boolean", false), field("watchStabilityThresholdMs", "number", 200, 0), field("watchPollIntervalMs", "number", 100, 1), field("watchMaxProjects", "number", 128, 1), field("watchFollowSymlinks", "boolean", true)]
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// src/host/composition-plan.ts
|
|
410
|
+
import { load } from "js-yaml";
|
|
411
|
+
import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
|
|
412
|
+
|
|
413
|
+
// src/host/role-validation.ts
|
|
414
|
+
import { ReasoningEffortId } from "@deepseek-ai/dsh-llm";
|
|
415
|
+
|
|
416
|
+
// src/host/preset-store.ts
|
|
417
|
+
function studioStorePath(env = process.env, profile) {
|
|
418
|
+
const directory = profile?.dir ?? env.DSH_PROFILE_DIR;
|
|
419
|
+
if (!directory) throw new Error("Studio preset persistence requires a profile");
|
|
420
|
+
return join2(directory, "studio-data", "presets.json");
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/host/tool-lifetime.ts
|
|
424
|
+
function createLifetime() {
|
|
425
|
+
const controller = new AbortController();
|
|
426
|
+
const pending = /* @__PURE__ */ new Set();
|
|
427
|
+
let closing;
|
|
428
|
+
return {
|
|
429
|
+
signal: controller.signal,
|
|
430
|
+
run(body) {
|
|
431
|
+
controller.signal.throwIfAborted();
|
|
432
|
+
const promise = Promise.resolve().then(() => {
|
|
433
|
+
controller.signal.throwIfAborted();
|
|
434
|
+
return body(controller.signal);
|
|
435
|
+
});
|
|
436
|
+
pending.add(promise);
|
|
437
|
+
void promise.then(() => pending.delete(promise), () => pending.delete(promise));
|
|
438
|
+
return promise;
|
|
439
|
+
},
|
|
440
|
+
dispose() {
|
|
441
|
+
if (closing) return closing;
|
|
442
|
+
controller.abort(new Error("Studio tool scope disposed"));
|
|
443
|
+
closing = Promise.allSettled([...pending]).then(() => {
|
|
444
|
+
});
|
|
445
|
+
return closing;
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/host/judge-tool.ts
|
|
451
|
+
var JUDGE_TOOL = "judge";
|
|
452
|
+
var TEXT = { type: "string", minLength: 1 };
|
|
453
|
+
var JUDGE_PARAMETERS = {
|
|
454
|
+
type: "object",
|
|
455
|
+
additionalProperties: false,
|
|
456
|
+
required: ["state", "questions"],
|
|
457
|
+
properties: {
|
|
458
|
+
state: { description: "What to judge: text, or a JSON object/array. Name its fields in instructions with backticks, e.g. `source`.", anyOf: [{ type: "string" }, { type: "object" }, { type: "array" }] },
|
|
459
|
+
questions: {
|
|
460
|
+
type: "object",
|
|
461
|
+
minProperties: 1,
|
|
462
|
+
description: "Questions by id. The id is never shown to the judge, so instructions must carry the full meaning. Questions are judged independently and cannot see each other.",
|
|
463
|
+
additionalProperties: { oneOf: [
|
|
464
|
+
{ type: "object", additionalProperties: false, required: ["type", "instructions"], properties: {
|
|
465
|
+
type: { const: "noul" },
|
|
466
|
+
instructions: TEXT,
|
|
467
|
+
criteria: { type: "object", additionalProperties: false, properties: { true: TEXT, false: TEXT }, description: "What yes and no mean." }
|
|
468
|
+
}, description: "Yes/no: returns the probability of yes." },
|
|
469
|
+
{ type: "object", additionalProperties: false, required: ["type", "instructions", "criteria"], properties: {
|
|
470
|
+
type: { const: "choice" },
|
|
471
|
+
instructions: TEXT,
|
|
472
|
+
criteria: { type: "object", minProperties: 2, maxProperties: 255, additionalProperties: TEXT, description: "Option name \u2192 description. Include a no-match option when nothing may fit." }
|
|
473
|
+
}, description: "Pick one option: returns the choice, probabilities, and confidence." },
|
|
474
|
+
{ type: "object", additionalProperties: false, required: ["type", "instructions", "criteria"], properties: {
|
|
475
|
+
type: { const: "score" },
|
|
476
|
+
instructions: TEXT,
|
|
477
|
+
criteria: { type: "array", minItems: 2, maxItems: 10, items: TEXT, description: "Level descriptions, lowest first." }
|
|
478
|
+
}, description: "Rate on ordered levels: returns a fractional score from 0, probabilities, and confidence." }
|
|
479
|
+
] }
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
var DESCRIPTION = 'Judge text with the decision engine (TypeSafe Jev or an LLM, set in DSH Studio). Returns typed answers with probabilities instead of prose: noul for yes/no, choice to pick one option, score to rate on levels. Use it to rank, filter, or verify; it does not write or rewrite text. Ask several independent questions over the same state in one call. Probabilities are calibrated signals, not proof; a failed judgment is an error, never a "no".';
|
|
484
|
+
async function runJudge(engine, raw, signal) {
|
|
485
|
+
const request = validateJudgeRequest(raw);
|
|
486
|
+
if (!engine) throw new Error("No decision engine is configured: set a Pixie model or choose Jev in DSH Studio \u2192 Pixie \u2192 Gates \u2192 Decision engine");
|
|
487
|
+
const result = await engine.judge(request, signal);
|
|
488
|
+
if (!result.ok) throw new Error(result.error);
|
|
489
|
+
return { ...result, engine: engine.label };
|
|
490
|
+
}
|
|
491
|
+
function installJudge(ctx, life, settings) {
|
|
492
|
+
const tools = ctx.get("tools");
|
|
493
|
+
if (!tools) throw new Error("Studio judge tool requires tools");
|
|
494
|
+
return tools.register({
|
|
495
|
+
name: JUDGE_TOOL,
|
|
496
|
+
description: DESCRIPTION,
|
|
497
|
+
parameters: JUDGE_PARAMETERS,
|
|
498
|
+
output: { schema: { type: "object" }, render: (_args, value) => [{ type: "text", text: JSON.stringify(value) }] },
|
|
499
|
+
isConcurrencySafe: () => true,
|
|
500
|
+
async execute(raw, exec) {
|
|
501
|
+
return life.run(async (owned) => {
|
|
502
|
+
const signal = AbortSignal.any([owned, exec.signal]);
|
|
503
|
+
const llm = ctx.get("llm");
|
|
504
|
+
const engine = judgeEngineFor(await settings(), {
|
|
505
|
+
llm: llm ?? { stream: () => {
|
|
506
|
+
throw new Error("DSH llm service is unavailable");
|
|
507
|
+
} },
|
|
508
|
+
credentials: ctx.get("credentials")
|
|
509
|
+
});
|
|
510
|
+
const { answers, usage, engine: label } = await runJudge(engine, raw, signal);
|
|
511
|
+
return JSON.parse(JSON.stringify({ answers, engine: label, ...usage ? { usage } : {} }));
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
var inject = ["tools"];
|
|
517
|
+
function apply(ctx) {
|
|
518
|
+
const store = new PixieSettingsStore(join3(dirname3(studioStorePath(process.env, ctx.get("profileContext"))), "pixie-settings.json"));
|
|
519
|
+
const life = createLifetime();
|
|
520
|
+
ctx.effect(() => installJudge(ctx, life, () => store.read()));
|
|
521
|
+
ctx.effect(() => () => life.dispose());
|
|
522
|
+
}
|
|
523
|
+
export {
|
|
524
|
+
JUDGE_PARAMETERS,
|
|
525
|
+
JUDGE_TOOL,
|
|
526
|
+
apply,
|
|
527
|
+
inject,
|
|
528
|
+
installJudge,
|
|
529
|
+
runJudge
|
|
530
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matamt-dbg/dsh-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "DSH Studio: build, edit and apply DeepSeek Harness agent presets — tools, prompts, Configs and role-based subagents — from the web GUI.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"./client": "./client.js",
|
|
24
24
|
"./policy": "./policy.js",
|
|
25
25
|
"./roles": "./roles.js",
|
|
26
|
-
"./batch": "./batch.js"
|
|
26
|
+
"./batch": "./batch.js",
|
|
27
|
+
"./judge": "./judge.js"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
30
|
"index.js",
|
|
@@ -31,6 +32,8 @@
|
|
|
31
32
|
"policy.js",
|
|
32
33
|
"roles.js",
|
|
33
34
|
"batch.js",
|
|
35
|
+
"judge.js",
|
|
36
|
+
"skills",
|
|
34
37
|
"cordis.patch.yml"
|
|
35
38
|
],
|
|
36
39
|
"dependencies": {
|
package/roles.js
CHANGED
|
@@ -78,6 +78,7 @@ var PACKAGE_NAME = "@matamt-dbg/dsh-studio";
|
|
|
78
78
|
var POLICY_PLUGIN = `${PACKAGE_NAME}/policy`;
|
|
79
79
|
var ROLE_PLUGIN = `${PACKAGE_NAME}/roles`;
|
|
80
80
|
var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
|
|
81
|
+
var JUDGE_PLUGIN = `${PACKAGE_NAME}/judge`;
|
|
81
82
|
|
|
82
83
|
// src/delegation.ts
|
|
83
84
|
var ROLE_TOOL = "subagent_role";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: studio-pixie-hook-guide
|
|
3
|
+
description: Assemble DSH Studio Pixie Gate definitions and Pixie hook scripts for a user — choose the point, write the task, write and install a script, and explain how to save and verify it. Use when the user asks to build, write, debug, or explain a Pixie Gate or a script using `pixie ask`/`pixie judge`.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DSH Studio Pixie Gate
|
|
7
|
+
|
|
8
|
+
Help the user build a Pixie Gate: a task the Pixie model runs at one point of an agent's work. The full contract — output fields, per-point rules, exit codes, environment, helper, decision engine, and worked example scripts — is in `guide.md` next to this file. Read it before writing a gate or a script.
|
|
9
|
+
|
|
10
|
+
## Decide the gate
|
|
11
|
+
|
|
12
|
+
1. Ask what should happen and when, if the user has not said. Map "when" to exactly one point:
|
|
13
|
+
|
|
14
|
+
| Point (`when`) | Target text (stdin and what Pixie reads) |
|
|
15
|
+
| --- | --- |
|
|
16
|
+
| `user-message` | The user's instruction |
|
|
17
|
+
| `agent-message` | A message between agents |
|
|
18
|
+
| `tool-before` | Tool arguments JSON (`PIXIE_TOOL_NAME` holds the tool) |
|
|
19
|
+
| `tool-after` | The tool result text |
|
|
20
|
+
| `turn-end` | The agent's final answer |
|
|
21
|
+
|
|
22
|
+
2. Write the task (`instruction`) in plain words. Say which of the five output fields to use: verdict (`pass`/`stop`), reason, replacement, addition, note. Check the guide's application table: at `tool-before` and `turn-end` replacement and addition become notes; at `turn-end` stop asks the agent to redo with the reason.
|
|
23
|
+
3. For a judgment (yes/no threshold, picking one of fixed options, a graded score), write a hook script that calls `pixie judge` and decides the outcome itself: exit 10 to pass untouched, exit 11 to print the gate output, exit 0 only when Pixie should interpret the result. Gates have no judge option; the engine (LLM or TypeSafe Jev) is set in Studio → Pixie → Gates → Decision engine.
|
|
24
|
+
4. Use a hook script only when the two fields cannot express the flow: conditions (only some tools), loops, or external measurement. Prefer no script.
|
|
25
|
+
|
|
26
|
+
## Write a script
|
|
27
|
+
|
|
28
|
+
- Bash only (macOS and Linux). Save it as an executable file in the script folder (`chmod +x`): `<profile>/studio-data/pixie-gate/commands/` unless the user set another under Gates → Scope (`<profile>/studio-data/pixie-gate/settings.json`, `scriptDirectory`). Use letters, digits, `_`, `.`, `-`; no subfolders, no `..`.
|
|
29
|
+
- Read the target from stdin. Print what the task needs to stdout (256 KB max); print a JSON object to fill `{{hooks.<name>.data}}`. `<name>` is the file name without its last extension.
|
|
30
|
+
- Exit 0 to run the Pixie task, 10 to skip the gate, 11 to apply stdout as the gate output (a JSON object with optional `verdict`, `reason`, `replace`, `append`, `note`; no Pixie call). Anything else is an error. A gate whose script always exits 10 or 11 may have an empty task; exit 0 then fails.
|
|
31
|
+
- Use `pixie ask "<instruction>" [--input]` and `pixie judge [--type bool|select|score] --question "…" [--json] [--input]`. bool takes `--threshold` (default 0.5) and optional `--true`/`--false` meanings, prints the yes probability, exits 0 at or above the threshold and 1 below; select takes two or more `--option name="description"` and prints the chosen name; score takes 2–10 `--level "description"` (lowest first) and prints the score. `--json` prints `{"type","label","answer","engine"}`. Errors exit 2 and never become a "no". With `--input` the helper reads stdin; otherwise it uses the gate target. At most 20 helper calls per run.
|
|
32
|
+
- Write the whole judgment in `--question`: question IDs never reach the model. For several inputs (source and translation, answer and evidence), pipe a JSON object built with `jq` and name its fields in backticks in the question.
|
|
33
|
+
- The script runs in the session working directory under the session sandbox. Do not write files unless the session can write; never try to escape the sandbox.
|
|
34
|
+
- In the task, reference only `{{hooks.<name>.text}}` and `{{hooks.<name>.data}}` of the chosen script.
|
|
35
|
+
- At `turn-end`, a note concluded with exit 11 shows under that turn's answer in the chat. Start it with a bracketed label line (e.g. `[한국어 번역]`) to title the block; a label containing `not verified`/`미확인` gets a warning badge.
|
|
36
|
+
|
|
37
|
+
Test a script by hand before relying on it: `printf 'sample' | PIXIE_POINT=tool-after PIXIE_TOOL_NAME=grep ./script.sh; echo $?`. The helper only works inside a real gate run.
|
|
38
|
+
|
|
39
|
+
## Hand over
|
|
40
|
+
|
|
41
|
+
Do not edit `definitions.json` unless the user asks you to; prefer guiding them through **Studio → Pixie → Gates**. When you do write it, it is a JSON array of objects:
|
|
42
|
+
|
|
43
|
+
```json
|
|
44
|
+
{ "id": "<uuid>", "name": "…", "enabled": true, "when": "user-message", "instruction": "…",
|
|
45
|
+
"advanced": { "confirmReplace": false, "onError": "pass", "agents": "root", "timeoutMs": 60000, "maxSteers": 1, "script": "optional.sh" },
|
|
46
|
+
"presetIds": ["<preset id>"], "disabledPresetIds": ["<attached preset where it is off>"] }
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`enabled` switches the gate everywhere; `disabledPresetIds` (optional) switches it off for listed attached presets only. Only one gate per preset and `when` may be on for that preset. Studio validates the file on save and on read; invalid entries are ignored at runtime.
|
|
50
|
+
|
|
51
|
+
If a gate never runs in one session, check **Pixie tab → Gates → Gates in this session**: users can switch gates off per session.
|
|
52
|
+
|
|
53
|
+
Tell the user how to verify: run **Try it** in the editor (it skips scripts), then send a real message in a session using the chosen preset and open the gate record in the Pixie tab. Report what the record shows, not what should happen.
|