@matamt-dbg/dsh-studio 0.2.9 → 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/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.2.9",
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/policy.js CHANGED
@@ -127,23 +127,27 @@ function patchParameter(parameters, path, description) {
127
127
  }
128
128
  node.description = description;
129
129
  }
130
- function installPolicy(ctx, scopeKey, config) {
130
+ function installPolicy(ctx, scopeKey, config, warn = () => {
131
+ }) {
131
132
  validatePolicy(config);
132
133
  if (scopeOf(ctx) !== scopeKey) throw new Error("Studio policy requires its owning scoped context");
133
134
  if (config.tools.includes("run_code") || ctx.tools.get("run_code", scopeKey) !== void 0) {
134
135
  throw new Error("Studio policy does not support PTC presentation or the reserved run_code tool; configure this preset with native tool presentation");
135
136
  }
136
137
  const missing = config.tools.filter((name2) => ctx.tools.get(name2, scopeKey) === void 0);
137
- if (missing.length) throw new Error(`Studio policy selected tools unavailable: ${missing.join(", ")}`);
138
+ if (missing.length) warn(`Studio policy selected tools unavailable: ${missing.join(", ")}`);
138
139
  const restoreProbe = ctx.tools.restrict({ allow: [] });
140
+ let local;
139
141
  try {
140
- const local = ctx.tools.schemas(scopeKey);
141
- if (local.length) throw new Error(`Studio policy does not support own-scope tools: ${local.map((tool) => tool.name).join(", ")}; register providers in an ancestor preset scope. Public API cannot hide late own-scope schemas; dynamic own-scope providers are unsupported`);
142
+ local = new Set(ctx.tools.schemas(scopeKey).map((tool) => tool.name));
142
143
  } finally {
143
144
  restoreProbe();
144
145
  }
145
146
  const selected = new Set(config.tools);
146
- const definitions = config.tools.map((name2) => {
147
+ const deny = ctx.tools.schemas(scopeKey).map((tool) => tool.name).filter((name2) => !selected.has(name2) && !local.has(name2));
148
+ const unpatchable = config.tools.filter((name2) => local.has(name2) && (config.toolOverrides[name2] || config.execution?.defaults[name2] || config.execution?.batch?.tool === name2));
149
+ if (unpatchable.length) warn(`Studio policy cannot apply overrides to agent-scope tools: ${unpatchable.join(", ")}`);
150
+ const definitions = config.tools.filter((name2) => !local.has(name2) && !missing.includes(name2)).map((name2) => {
147
151
  const definition = ctx.tools.get(name2, scopeKey);
148
152
  const override = config.toolOverrides[name2];
149
153
  const defaults = config.execution?.defaults[name2];
@@ -177,8 +181,7 @@ function installPolicy(ctx, scopeKey, config) {
177
181
  for (const release of disposers.splice(0).reverse()) release();
178
182
  };
179
183
  try {
180
- disposers.push(ctx.tools.restrict({ allow: [...selected] }));
181
- disposers.push(ctx.tools.guard((execution) => selected.has(execution.name) ? void 0 : "Tool excluded by Studio preset policy"));
184
+ if (deny.length) disposers.push(ctx.tools.restrict({ deny }));
182
185
  for (const definition of definitions) disposers.push(ctx.tools.register(definition));
183
186
  for (const [name2, text] of Object.entries(config.sections)) disposers.push(ctx.systemPrompt.section({ name: name2, order: ctx.systemPrompt.getSectionOrder(POLICY_SECTION_ORDERS[name2]), text, interpolate: false }));
184
187
  disposers.push(ctx.systemPrompt.section({ name: "studio:additional", order: Number.MAX_SAFE_INTEGER, text: config.prompt, interpolate: false }));
@@ -200,13 +203,16 @@ function apply(ctx, config) {
200
203
  const hidden = hideable.filter((name2) => agent.ctx.tools.get(name2, agent) === void 0 && provided(name2));
201
204
  return hidden.length ? childPolicy(config, hidden) : config;
202
205
  };
206
+ const warn = (message) => {
207
+ ctx.logger?.warn?.(message);
208
+ };
203
209
  const installed = /* @__PURE__ */ new Map();
204
210
  const installing = /* @__PURE__ */ new Set();
205
211
  const install = (agent) => {
206
212
  if (installed.has(agent) || installing.has(agent)) return;
207
213
  installing.add(agent);
208
214
  try {
209
- installed.set(agent, installPolicy(agent.ctx, agent, policyFor(agent)));
215
+ installed.set(agent, installPolicy(agent.ctx, agent, policyFor(agent), warn));
210
216
  } finally {
211
217
  installing.delete(agent);
212
218
  }
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";