@matamt-dbg/dsh-studio 0.4.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -3
- package/client.js +6021 -513
- package/index.js +1653 -136
- package/judge.js +1 -0
- package/loop.js +1655 -0
- package/package.json +12 -10
- package/policy.js +13 -0
- package/roles.js +222 -14
- package/skills/studio-pixie-hook-guide/SKILL.md +6 -4
- package/skills/studio-pixie-hook-guide/guide.md +31 -8
- package/skills/studio-pixie-loop-guide/SKILL.md +48 -0
- package/skills/studio-pixie-loop-guide/guide.md +364 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matamt-dbg/dsh-studio",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
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",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
"./policy": "./policy.js",
|
|
25
25
|
"./roles": "./roles.js",
|
|
26
26
|
"./batch": "./batch.js",
|
|
27
|
-
"./judge": "./judge.js"
|
|
27
|
+
"./judge": "./judge.js",
|
|
28
|
+
"./loop": "./loop.js"
|
|
28
29
|
},
|
|
29
30
|
"files": [
|
|
30
31
|
"index.js",
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
"roles.js",
|
|
34
35
|
"batch.js",
|
|
35
36
|
"judge.js",
|
|
37
|
+
"loop.js",
|
|
36
38
|
"skills",
|
|
37
39
|
"cordis.patch.yml"
|
|
38
40
|
],
|
|
@@ -40,14 +42,14 @@
|
|
|
40
42
|
"js-yaml": "^4.3.2"
|
|
41
43
|
},
|
|
42
44
|
"peerDependencies": {
|
|
43
|
-
"@deepseek-ai/dsh-app-boot": "0.2.0-rc.2 || 0.2.1-alpha.2",
|
|
44
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2 || 0.2.1-alpha.2",
|
|
45
|
-
"@deepseek-ai/dsh": "0.2.0-rc.2 || 0.2.1-alpha.2",
|
|
46
|
-
"@deepseek-ai/dsh-scope": "0.2.0-rc.2 || 0.2.1-alpha.2",
|
|
47
|
-
"@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2 || 0.2.1-alpha.2",
|
|
48
|
-
"@deepseek-ai/dsh-tools": "0.2.0-rc.2 || 0.2.1-alpha.2",
|
|
49
|
-
"@deepseek-ai/dsh-jobs": "0.2.0-rc.2 || 0.2.1-alpha.2",
|
|
50
|
-
"@deepseek-ai/dsh-llm": "0.2.0-rc.2 || 0.2.1-alpha.2",
|
|
45
|
+
"@deepseek-ai/dsh-app-boot": "0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-rc.1 || 0.2.1-rc.2",
|
|
46
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-rc.1 || 0.2.1-rc.2",
|
|
47
|
+
"@deepseek-ai/dsh": "0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-rc.1 || 0.2.1-rc.2",
|
|
48
|
+
"@deepseek-ai/dsh-scope": "0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-rc.1 || 0.2.1-rc.2",
|
|
49
|
+
"@deepseek-ai/dsh-system-prompt": "0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-rc.1 || 0.2.1-rc.2",
|
|
50
|
+
"@deepseek-ai/dsh-tools": "0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-rc.1 || 0.2.1-rc.2",
|
|
51
|
+
"@deepseek-ai/dsh-jobs": "0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-rc.1 || 0.2.1-rc.2",
|
|
52
|
+
"@deepseek-ai/dsh-llm": "0.2.0-rc.2 || 0.2.1-alpha.2 || 0.2.1-rc.1 || 0.2.1-rc.2",
|
|
51
53
|
"@deepseek-ai/cordis-plugin-include": "*"
|
|
52
54
|
},
|
|
53
55
|
"peerDependenciesMeta": {
|
package/policy.js
CHANGED
|
@@ -66,6 +66,17 @@ function sectionsOnlyFor(hidden, kept) {
|
|
|
66
66
|
return [...owned(hidden)].filter((section) => !still.has(section));
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// src/host/role-child-policy-contract.ts
|
|
70
|
+
function isStudioChild(id) {
|
|
71
|
+
return typeof id === "string" && id.startsWith("studio-child-");
|
|
72
|
+
}
|
|
73
|
+
function roleChildPolicy(ctx, agent) {
|
|
74
|
+
if (!isStudioChild(agent.id)) return void 0;
|
|
75
|
+
const owner = ctx.get("studioChildPolicies");
|
|
76
|
+
if (!owner) throw new Error("Studio child policy owner is unavailable");
|
|
77
|
+
return owner.forAgent(agent);
|
|
78
|
+
}
|
|
79
|
+
|
|
69
80
|
// src/host/preset-policy.ts
|
|
70
81
|
var POLICY_SECTION_ORDERS = Object.freeze({
|
|
71
82
|
"tool:jobs": "TOOL_JOBS",
|
|
@@ -198,6 +209,8 @@ function apply(ctx, config) {
|
|
|
198
209
|
const registry = ctx.get("agents");
|
|
199
210
|
const hideable = config.childToolFilter?.deny ?? [];
|
|
200
211
|
const policyFor = (agent) => {
|
|
212
|
+
const fixed = roleChildPolicy(ctx, agent);
|
|
213
|
+
if (fixed) return childPolicy(config, fixed.deny);
|
|
201
214
|
if (!hideable.length) return config;
|
|
202
215
|
const provided = (name2) => scopeOf(ctx) === void 0 || ctx.tools.get(name2, scopeOf(ctx)) !== void 0;
|
|
203
216
|
const hidden = hideable.filter((name2) => agent.ctx.tools.get(name2, agent) === void 0 && provided(name2));
|
package/roles.js
CHANGED
|
@@ -25,6 +25,7 @@ async function validateRoleModel(llm, role, signal) {
|
|
|
25
25
|
|
|
26
26
|
// src/host/role-tool.ts
|
|
27
27
|
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
28
|
+
import { dirname as dirname2 } from "node:path";
|
|
28
29
|
import { ReasoningEffortId as ReasoningEffortId2 } from "@deepseek-ai/dsh-llm";
|
|
29
30
|
|
|
30
31
|
// src/role-settings.ts
|
|
@@ -73,31 +74,217 @@ function roleToolFilter(role, presetDefault = []) {
|
|
|
73
74
|
return own.mode === "replace" ? [...own.deny] : [.../* @__PURE__ */ new Set([...presetDefault, ...own.deny])];
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// src/session-roles.ts
|
|
78
|
+
var PARENT_ROLE = { name: "Subagent", role: "subagent", model: "inherit", reasoning: "inherit", spawn: "fresh", persona: "" };
|
|
79
|
+
var emptySessionRoles = () => ({ version: 1, roles: [] });
|
|
80
|
+
var object2 = (value) => !!value && typeof value === "object" && !Array.isArray(value);
|
|
81
|
+
var validated = (roles) => executableRoles(parseRoles(roles));
|
|
82
|
+
function parseSessionRoles(input) {
|
|
83
|
+
if (!object2(input) || input.version !== 1) throw new Error("Invalid session roles record");
|
|
84
|
+
return { version: 1, roles: validated(input.roles) };
|
|
85
|
+
}
|
|
86
|
+
function mergeRoles(preset, record) {
|
|
87
|
+
const own = new Map(record.roles.map((role) => [role.role, role]));
|
|
88
|
+
const merged = preset.map((role) => {
|
|
89
|
+
const copy = own.get(role.role);
|
|
90
|
+
return copy ? { ...copy, source: "session" } : { ...role, source: "preset" };
|
|
91
|
+
});
|
|
92
|
+
const presetIds = new Set(preset.map((role) => role.role));
|
|
93
|
+
for (const role of record.roles) if (!presetIds.has(role.role)) merged.push({ ...role, source: "session" });
|
|
94
|
+
return merged;
|
|
95
|
+
}
|
|
96
|
+
function selectRole(merged, roleId) {
|
|
97
|
+
return merged.length ? merged.find((role) => role.role === roleId) : PARENT_ROLE;
|
|
98
|
+
}
|
|
99
|
+
|
|
76
100
|
// src/package-name.ts
|
|
77
101
|
var PACKAGE_NAME = "@matamt-dbg/dsh-studio";
|
|
78
102
|
var POLICY_PLUGIN = `${PACKAGE_NAME}/policy`;
|
|
79
103
|
var ROLE_PLUGIN = `${PACKAGE_NAME}/roles`;
|
|
80
104
|
var BATCH_PLUGIN = `${PACKAGE_NAME}/batch`;
|
|
81
105
|
var JUDGE_PLUGIN = `${PACKAGE_NAME}/judge`;
|
|
106
|
+
var LOOP_PLUGIN = `${PACKAGE_NAME}/loop`;
|
|
82
107
|
|
|
83
108
|
// src/delegation.ts
|
|
84
109
|
var ROLE_TOOL = "subagent_role";
|
|
110
|
+
var LEGACY_ROLE_TOOL = "studio_role";
|
|
111
|
+
var GENERIC_DELEGATION_TOOLS = ["subagent", "subagent_fork", "list_subagent_models"];
|
|
112
|
+
|
|
113
|
+
// src/optimized-preset.ts
|
|
114
|
+
var OPTIMIZED_TOOLS = [
|
|
115
|
+
"read_batch",
|
|
116
|
+
"read",
|
|
117
|
+
"write",
|
|
118
|
+
"edit",
|
|
119
|
+
"glob",
|
|
120
|
+
"grep",
|
|
121
|
+
"job_output",
|
|
122
|
+
"job_list",
|
|
123
|
+
"job_kill",
|
|
124
|
+
"skill",
|
|
125
|
+
"ask_user_question",
|
|
126
|
+
"web_search",
|
|
127
|
+
"web_fetch",
|
|
128
|
+
"present",
|
|
129
|
+
"bash",
|
|
130
|
+
"read_image",
|
|
131
|
+
"exit_plan_mode",
|
|
132
|
+
"send_message",
|
|
133
|
+
"interrupt_agent",
|
|
134
|
+
"list_agents",
|
|
135
|
+
"workflow",
|
|
136
|
+
"subagent_role"
|
|
137
|
+
];
|
|
138
|
+
var ALLOWED_TOOLS = /* @__PURE__ */ new Set([...OPTIMIZED_TOOLS, ...GENERIC_DELEGATION_TOOLS, LEGACY_ROLE_TOOL]);
|
|
139
|
+
|
|
140
|
+
// src/host/preset-store.ts
|
|
141
|
+
import { createScope } from "@deepseek-ai/dsh-scope";
|
|
142
|
+
|
|
143
|
+
// src/host/preset-policy.ts
|
|
144
|
+
import { scopeChainOf, scopeOf } from "@deepseek-ai/dsh-scope";
|
|
145
|
+
var POLICY_SECTION_ORDERS = Object.freeze({
|
|
146
|
+
"tool:jobs": "TOOL_JOBS",
|
|
147
|
+
"tool:bash": "TOOL_BASH",
|
|
148
|
+
"tool:pwsh": "TOOL_PWSH",
|
|
149
|
+
"tool:read": "TOOL_READ",
|
|
150
|
+
"tool:write": "TOOL_WRITE",
|
|
151
|
+
"tool:edit": "TOOL_EDIT",
|
|
152
|
+
"tool:glob": "TOOL_GLOB",
|
|
153
|
+
"tool:grep": "TOOL_GREP",
|
|
154
|
+
"tool:web_fetch": "TOOL_WEB_FETCH",
|
|
155
|
+
"tool:web_search": "TOOL_WEB_SEARCH",
|
|
156
|
+
"tool:goal": "TOOL_GOAL",
|
|
157
|
+
"tool:workflow": "TOOL_WORKFLOW"
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// src/config-overrides.ts
|
|
161
|
+
var LEGACY_DROPPED_PACKAGES = Object.freeze([
|
|
162
|
+
"@deepseek-ai/dsh-agent-instructions",
|
|
163
|
+
"@deepseek-ai/dsh-time-context",
|
|
164
|
+
"@deepseek-ai/dsh-skill-filesystem",
|
|
165
|
+
"@deepseek-ai/dsh-compaction-basic",
|
|
166
|
+
"@deepseek-ai/dsh-command-compact",
|
|
167
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner"
|
|
168
|
+
]);
|
|
169
|
+
var CONFIG_REQUIRES = Object.freeze({
|
|
170
|
+
"@deepseek-ai/dsh-command-compact": "@deepseek-ai/dsh-compaction-basic"
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// src/host/preset-store.ts
|
|
174
|
+
import { dirname, join } from "node:path";
|
|
175
|
+
import { dump } from "js-yaml";
|
|
176
|
+
import { entryListSchema as entryListSchema2 } from "@deepseek-ai/cordis-plugin-include";
|
|
177
|
+
|
|
178
|
+
// src/provider-fields.ts
|
|
179
|
+
var field = (key, type, value, minimum) => ({ key, type, ...value === void 0 ? {} : { value }, ...minimum === void 0 ? {} : { minimum } });
|
|
180
|
+
var providerFields = {
|
|
181
|
+
"@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)],
|
|
182
|
+
"@deepseek-ai/dsh-compaction-tool-result-pruner": [field("thresholdChars", "number", 8192, 1), field("headChars", "number", 4096, 0), field("tailChars", "number", 1024, 0)],
|
|
183
|
+
"@deepseek-ai/dsh-agent-tool-presentation": [{ key: "mode", type: "string", options: ["native", "ptc"] }],
|
|
184
|
+
"@deepseek-ai/dsh-persona": [field("prefix", "string"), field("suffix", "string", ""), field("includeRuntimeContext", "boolean", true)],
|
|
185
|
+
"@deepseek-ai/dsh-agent-instructions": [field("maxBytes", "number", 65536), field("maxSourceBytes", "number", void 0, 1), field("projectRootMarkers", "string[]"), field("instructionFileCandidates", "string[]"), field("localInstructionFileCandidates", "string[]")],
|
|
186
|
+
"@deepseek-ai/dsh-time-context": [field("timeZone", "string"), field("refreshIntervalMs", "number", void 0, 0)],
|
|
187
|
+
"@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)]
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/host/composition-plan.ts
|
|
191
|
+
import { load } from "js-yaml";
|
|
192
|
+
import { entryListSchema } from "@deepseek-ai/cordis-plugin-include";
|
|
193
|
+
|
|
194
|
+
// src/host/preset-store.ts
|
|
195
|
+
function studioStorePath(env = process.env, profile) {
|
|
196
|
+
const directory = profile?.dir ?? env.DSH_PROFILE_DIR;
|
|
197
|
+
if (!directory) throw new Error("Studio preset persistence requires a profile");
|
|
198
|
+
return join(directory, "studio-data", "presets.json");
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/host/session-role-store.ts
|
|
202
|
+
import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
203
|
+
import { join as join2 } from "node:path";
|
|
204
|
+
var SESSION_ID = /^[\w-]{1,200}$/;
|
|
205
|
+
var sessionRolesRoot = (dataDir) => join2(dataDir, "session-roles");
|
|
206
|
+
function sessionRolesFile(root, sessionId) {
|
|
207
|
+
if (typeof sessionId !== "string" || !SESSION_ID.test(sessionId)) throw new Error(`Invalid session id for session roles: ${JSON.stringify(sessionId)}`);
|
|
208
|
+
return join2(root, `${sessionId}.json`);
|
|
209
|
+
}
|
|
210
|
+
async function readSessionRoles(root, sessionId) {
|
|
211
|
+
const file = sessionRolesFile(root, sessionId);
|
|
212
|
+
let text;
|
|
213
|
+
try {
|
|
214
|
+
text = await readFile(file, "utf8");
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error.code === "ENOENT") return emptySessionRoles();
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
try {
|
|
220
|
+
return parseSessionRoles(JSON.parse(text));
|
|
221
|
+
} catch (error) {
|
|
222
|
+
throw new Error(`Invalid session roles for session ${sessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/host/session-root.ts
|
|
227
|
+
var MAX_HOPS = 64;
|
|
228
|
+
async function resolveRootSession(sessionId, lookup, known) {
|
|
229
|
+
let current = sessionId;
|
|
230
|
+
let header = known ?? await lookup(current);
|
|
231
|
+
const seen = /* @__PURE__ */ new Set([current]);
|
|
232
|
+
for (let hop = 0; hop < MAX_HOPS && header?.origin === "subagent" && header.parentSession; hop++) {
|
|
233
|
+
const parent = header.parentSession;
|
|
234
|
+
if (seen.has(parent)) break;
|
|
235
|
+
const next = await lookup(parent);
|
|
236
|
+
if (!next) break;
|
|
237
|
+
seen.add(parent);
|
|
238
|
+
current = parent;
|
|
239
|
+
header = next;
|
|
240
|
+
}
|
|
241
|
+
return current;
|
|
242
|
+
}
|
|
243
|
+
function headerLookup(ctx, signal) {
|
|
244
|
+
return async (sessionId) => {
|
|
245
|
+
const live = ctx.get("sessions")?.get(sessionId);
|
|
246
|
+
if (live) return live.header;
|
|
247
|
+
const controller = ctx.get("sessionController");
|
|
248
|
+
if (!controller) return void 0;
|
|
249
|
+
try {
|
|
250
|
+
return (await controller.inspect(sessionId, signal)).meta;
|
|
251
|
+
} catch {
|
|
252
|
+
signal?.throwIfAborted();
|
|
253
|
+
return void 0;
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
85
257
|
|
|
86
258
|
// src/host/role-tool.ts
|
|
87
259
|
var name = "studio-role";
|
|
88
260
|
var inject = ["tools", "subagents", "llm"];
|
|
89
|
-
var
|
|
261
|
+
var ROLE_TOOL_DESCRIPTION = "Delegate a self-contained task to a subagent. Use action=list to see the specialist roles; when none are listed, a call starts a child with the same model and settings as you. Calls return a native child agent id; the child reports back through normal subagent delivery.";
|
|
262
|
+
var ROLE_PARAMETER_DESCRIPTION = "Role identifier from action=list; required for call when roles are listed.";
|
|
263
|
+
function sessionRolesDirectory(ctx) {
|
|
264
|
+
try {
|
|
265
|
+
return sessionRolesRoot(dirname2(studioStorePath(process.env, ctx.get?.("profileContext"))));
|
|
266
|
+
} catch {
|
|
267
|
+
return void 0;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
90
270
|
function apply(ctx, config) {
|
|
91
271
|
const roles = executableRoles(parseRoles(config.roles));
|
|
92
272
|
const deny = config.toolFilter?.deny;
|
|
93
273
|
if (deny !== void 0 && (!Array.isArray(deny) || deny.some((name2) => typeof name2 !== "string" || !name2))) throw new Error("Invalid subagent tool filter");
|
|
94
274
|
const presetDefault = deny ?? [];
|
|
275
|
+
const directory = sessionRolesDirectory(ctx);
|
|
276
|
+
async function sessionRoles(agent, signal) {
|
|
277
|
+
if (!directory || !agent?.id) return emptySessionRoles();
|
|
278
|
+
const root = await resolveRootSession(agent.id, headerLookup(ctx, signal), agent.session?.header);
|
|
279
|
+
signal.throwIfAborted();
|
|
280
|
+
return readSessionRoles(directory, root);
|
|
281
|
+
}
|
|
95
282
|
ctx.tools.register(defineTool({
|
|
96
283
|
name: ROLE_TOOL,
|
|
97
|
-
description:
|
|
284
|
+
description: ROLE_TOOL_DESCRIPTION,
|
|
98
285
|
parameters: {
|
|
99
286
|
action: { type: "string", enum: ["list", "call"], required: true, description: "list discovers role identifiers and settings; call delegates a task." },
|
|
100
|
-
role: { type: "string", description:
|
|
287
|
+
role: { type: "string", description: ROLE_PARAMETER_DESCRIPTION },
|
|
101
288
|
task: { type: "string", description: "Self-contained task for the specialist, required for call. Fork roles also inherit the conversation." }
|
|
102
289
|
},
|
|
103
290
|
output: {
|
|
@@ -106,9 +293,10 @@ function apply(ctx, config) {
|
|
|
106
293
|
},
|
|
107
294
|
isConcurrencySafe: () => true,
|
|
108
295
|
async execute(args, exec) {
|
|
109
|
-
if (args.action
|
|
110
|
-
|
|
111
|
-
|
|
296
|
+
if (args.action !== "list" && args.action !== "call") throw new Error("Unknown Studio role action");
|
|
297
|
+
const merged = mergeRoles(roles, await sessionRoles(exec.agent, exec.signal));
|
|
298
|
+
if (args.action === "list") return { result: JSON.stringify({ roles: merged.map(({ persona: _persona, ...role2 }) => role2) }) };
|
|
299
|
+
const role = selectRole(merged, args.role);
|
|
112
300
|
if (!role) throw new Error("Unknown Studio role; use action=list to discover roles");
|
|
113
301
|
if (typeof args.task !== "string" || !args.task.trim()) throw new Error("Role call requires a task");
|
|
114
302
|
if (!exec.agent) throw new Error("Studio role call requires a calling agent");
|
|
@@ -122,30 +310,50 @@ function apply(ctx, config) {
|
|
|
122
310
|
}
|
|
123
311
|
if (role.reasoning !== "inherit") agentOptions.reasoningEffort = ReasoningEffortId2(role.reasoning);
|
|
124
312
|
const maxDepth = ctx.subagents.resolveMaxDepth();
|
|
125
|
-
const hidden = roleToolFilter(
|
|
313
|
+
const hidden = roleToolFilter(role, presetDefault);
|
|
126
314
|
const toolFilter = hidden.length ? { deny: hidden } : void 0;
|
|
315
|
+
const provider = role.spawn === "fork" ? "fork" : "spawn";
|
|
316
|
+
const backend = ctx.subagents.getProvider?.(provider);
|
|
317
|
+
const capabilities = backend?.capabilities;
|
|
318
|
+
const needsPolicyOwner = capabilities !== void 0 && "agentPreset" in capabilities && (capabilities.persona !== true || capabilities.toolFilter !== true);
|
|
319
|
+
let prepared;
|
|
320
|
+
if (needsPolicyOwner) {
|
|
321
|
+
const owner = ctx.get("studioChildPolicies");
|
|
322
|
+
if (!owner) throw new Error("Studio child policy owner is unavailable; no child was started");
|
|
323
|
+
prepared = await owner.prepare(exec.agent.id, hidden, role.persona || void 0);
|
|
324
|
+
}
|
|
127
325
|
const spec = {
|
|
128
|
-
provider
|
|
326
|
+
provider,
|
|
129
327
|
label: role.name,
|
|
328
|
+
...prepared ? { childId: prepared.childId } : {},
|
|
130
329
|
request: {
|
|
131
330
|
parent: exec.agent,
|
|
132
331
|
prompt: [{ type: "text", text: args.task }],
|
|
133
332
|
...Object.keys(agentOptions).length ? { agentOptions } : {},
|
|
134
|
-
|
|
333
|
+
...!prepared && role.persona ? { persona: role.persona } : {},
|
|
135
334
|
...maxDepth === void 0 ? {} : { maxDepth },
|
|
136
|
-
|
|
335
|
+
...!prepared && toolFilter ? { toolFilter } : {}
|
|
137
336
|
},
|
|
138
337
|
signal: exec.signal
|
|
139
338
|
};
|
|
140
339
|
const native = ctx.subagents;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
340
|
+
try {
|
|
341
|
+
exec.signal.throwIfAborted();
|
|
342
|
+
const started = typeof native.startActivation === "function" ? await native.startActivation({ ...spec, delivery: "parent" }) : native.startContinuable ? await native.startContinuable(spec) : (() => {
|
|
343
|
+
throw new Error("This DSH runtime lacks a native child start API");
|
|
344
|
+
})();
|
|
345
|
+
if (prepared && started.childId !== prepared.childId) throw new Error("DSH did not preserve the Studio child policy identity");
|
|
346
|
+
return { result: JSON.stringify({ kind: "activation", subagentId: started.childId, role: role.role }) };
|
|
347
|
+
} catch (error) {
|
|
348
|
+
await prepared?.abandon();
|
|
349
|
+
throw error;
|
|
350
|
+
}
|
|
145
351
|
}
|
|
146
352
|
}));
|
|
147
353
|
}
|
|
148
354
|
export {
|
|
355
|
+
ROLE_PARAMETER_DESCRIPTION,
|
|
356
|
+
ROLE_TOOL_DESCRIPTION,
|
|
149
357
|
apply,
|
|
150
358
|
inject,
|
|
151
359
|
name
|
|
@@ -20,8 +20,9 @@ Help the user build a Pixie Gate: a task the Pixie model runs at one point of an
|
|
|
20
20
|
| `turn-end` | The agent's final answer |
|
|
21
21
|
|
|
22
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.
|
|
24
|
-
4.
|
|
23
|
+
3. When the user already has a Pixie Loop recipe for the check, prefer the gate's **Pixie Loop recipe** advanced option (`advanced.loop: { recipe, mode: "check" | "repair" }`) with no script: check stops with Pixie's feedback, repair replaces the content once it passes (user-message, agent-message, tool-after only). It cannot be combined with `advanced.script`.
|
|
24
|
+
4. 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.
|
|
25
|
+
5. 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
|
|
|
26
27
|
## Write a script
|
|
27
28
|
|
|
@@ -29,10 +30,11 @@ Help the user build a Pixie Gate: a task the Pixie model runs at one point of an
|
|
|
29
30
|
- 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
31
|
- 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
32
|
- 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.
|
|
33
|
+
- Use `pixie loop --recipe <name> [--mode check|repair] [--json] [--input]` to run a saved Pixie Loop recipe on the input: exit 0 when it passes (stdout: the input, or the repaired text for `repair`), exit 1 with the reason when it does not, exit 2 on errors. Newlines in stdout become `\n`; use `--json` (`passed`, `output`, `reason`, `feedback`) to read them safely. One loop is one helper call. Recipes with `act`, episodes, or selection are refused.
|
|
32
34
|
- 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
35
|
- 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
36
|
- In the task, reference only `{{hooks.<name>.text}}` and `{{hooks.<name>.data}}` of the chosen script.
|
|
35
|
-
- At `
|
|
37
|
+
- At every point, the gate's **Show the result in the chat** setting (`advanced.showInChat`, on by default) decides whether the result shows right under what it checked (the message, the tool call, or the turn's answer), whoever concluded it: the note if any, otherwise the verdict and reason. It is never written to the session log. For a note, start it with a bracketed label line (e.g. `[한국어 번역]`) to title the block; a label containing `not verified`/`미확인` gets a warning badge.
|
|
36
38
|
|
|
37
39
|
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
40
|
|
|
@@ -42,7 +44,7 @@ Do not edit `definitions.json` unless the user asks you to; prefer guiding them
|
|
|
42
44
|
|
|
43
45
|
```json
|
|
44
46
|
{ "id": "<uuid>", "name": "…", "enabled": true, "when": "user-message", "instruction": "…",
|
|
45
|
-
"advanced": { "confirmReplace": false, "onError": "pass", "agents": "root", "timeoutMs": 60000, "maxSteers": 1, "script": "optional.sh" },
|
|
47
|
+
"advanced": { "confirmReplace": false, "onError": "pass", "agents": "root", "timeoutMs": 60000, "maxSteers": 1, "showInChat": true, "script": "optional.sh", "loop": { "recipe": "optional-recipe", "mode": "check" } },
|
|
46
48
|
"presetIds": ["<preset id>"], "disabledPresetIds": ["<attached preset where it is off>"] }
|
|
47
49
|
```
|
|
48
50
|
|
|
@@ -55,12 +55,14 @@ When the verdict is stop, the replacement and addition never reach the main mode
|
|
|
55
55
|
|
|
56
56
|
| Option | Default | Description |
|
|
57
57
|
| --- | --- | --- |
|
|
58
|
+
| Pixie Loop recipe | None | Judge with a saved [Pixie Loop recipe](pixie-loop.md#recipes) instead of the task. **Check** stops when the content does not pass, with Pixie's feedback as the reason. **Repair** reworks the content until it passes and replaces it (user message, agent message, and tool result points only). Cannot be combined with a hook script |
|
|
58
59
|
| Pixie hook script | None | A script run before the task. See [below](#pixie-hook-scripts) |
|
|
60
|
+
| Show the result in the chat | On | Shows the result right under what the gate checked. See [Chat notes](#chat-notes-and-per-session-switches) |
|
|
59
61
|
| Confirm before replacing | Off | Stops a real replacement and shows it as a suggestion (message points) |
|
|
60
|
-
|
|
|
61
|
-
| Target agents |
|
|
62
|
-
| Time limit | 60
|
|
63
|
-
| Redo limit | 1 | Corrections per turn at the answer-end point |
|
|
62
|
+
| If Pixie fails | Continue as usual | Continue as usual or stop it on model or script errors and timeouts |
|
|
63
|
+
| Target agents | Main agent (Subagents for agent messages) | Main agent, Subagents, or All |
|
|
64
|
+
| Time limit (seconds) | 60 | Script, helper, and model calls together |
|
|
65
|
+
| Redo limit per turn | 1 | Corrections per turn at the answer-end point |
|
|
64
66
|
|
|
65
67
|
## Pixie hook scripts
|
|
66
68
|
|
|
@@ -111,14 +113,16 @@ Inside a script, the `pixie` command calls the Pixie model. It is first on the s
|
|
|
111
113
|
```sh
|
|
112
114
|
pixie ask "<instruction>" [--input]
|
|
113
115
|
pixie judge [--type bool|select|score] --question "<question>" [type arguments] [--json] [--input]
|
|
116
|
+
pixie loop --recipe <name> [--mode check|repair] [--json] [--input]
|
|
114
117
|
```
|
|
115
118
|
|
|
116
119
|
- With `--input` the helper reads its input from stdin; otherwise it uses the gate target.
|
|
117
120
|
- `ask` prints the answer and exits 0, using the Pixie model.
|
|
118
121
|
- `judge` judges with the [decision engine](#decision-engine); types and output follow the table there.
|
|
122
|
+
- `loop` runs a saved [Pixie Loop recipe](pixie-loop.md#recipes) on one input. `check` (default) exits 0 and prints the input when it passes, otherwise exits 1 and prints the reason. `repair` reworks it until it passes and prints the result with exit 0, or exits 1 with the reason. Newlines become `\n`. `--json` prints `{"recipe","mode","passed","output"?,"feedback"?,"reason","runId"}`. One run counts as one helper call; the recipe's attempt limit and the gate time limit bound its model calls. Recipes that run commands (`act`), episodes, and selections are not allowed. Runs are recorded in Pixie tab → Loops.
|
|
119
123
|
- Usage, token, limit, and model errors exit 2 with a message on stderr. A failed helper call never decides the gate; the script's exit code does.
|
|
120
124
|
- Limits: 20 calls and 256 KB per request for each run. Calls follow the gate time limit and cancellation; their usage is added to the gate and listed under **Helper calls** in the run record.
|
|
121
|
-
- A token reaches only that run's ask and
|
|
125
|
+
- A token reaches only that run's ask, judge, and loop. The helper uses a Studio route on the DSH web server, so it is unavailable without one (for example, headless).
|
|
122
126
|
|
|
123
127
|
## Decision engine
|
|
124
128
|
|
|
@@ -236,12 +240,31 @@ On start, Studio installs a skill with this guide and the hook rules into `<DSH
|
|
|
236
240
|
|
|
237
241
|
## Chat notes and per-session switches
|
|
238
242
|
|
|
239
|
-
**Chat notes:**
|
|
243
|
+
**Chat notes:** a gate with **Show the result in the chat** on (Advanced; on by default) shows its result as a block right under what it checked, whether the script (exit 11) or Pixie concluded it. The setting decides; the output does not. The block is drawn from Studio's gate records and is never written to the session log.
|
|
240
244
|
|
|
245
|
+
| Point | Where the block appears |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| You send an instruction / agents message each other | Right under that message, outside the turn's folded steps. A subagent's message shows in the subagent's chat |
|
|
248
|
+
| Before a tool runs / a tool result arrives | Right under that tool call, inside the turn's steps (expand the turn to see it). Before and after results of one call show together |
|
|
249
|
+
| The agent finishes its answer | Right under that turn's answer |
|
|
250
|
+
|
|
251
|
+
A rejected instruction has no message in the chat, so its result stays in the Pixie tab only.
|
|
252
|
+
|
|
253
|
+

|
|
254
|
+
|
|
255
|
+
When the turn is folded, the instruction and answer results stay visible; tool results fold with the steps.
|
|
256
|
+
|
|
257
|
+

|
|
258
|
+
|
|
259
|
+
Turn the setting off in the gate editor under **Advanced → Show the result in the chat**. The gate still runs and its record stays in the Pixie tab.
|
|
260
|
+
|
|
261
|
+

|
|
262
|
+
|
|
263
|
+
- The block shows the note when there is one, for example a translation. Without a note it shows the verdict with its reason, and any replacement or addition in a collapsed section. A failed run shows one error line.
|
|
241
264
|
- If the note's first line starts with a bracketed label such as `[한국어 번역]`, the label becomes the block title. A label containing `not verified` (or `미확인`) gets a **not verified** badge.
|
|
242
265
|
- The body renders as Markdown. The block offers **Hide**, **Open in Pixie tab**, and **Turn off in this session**.
|
|
243
|
-
- It appears after the
|
|
244
|
-
- The
|
|
266
|
+
- It appears after the gate finishes; there is no progress indicator. Skipped turns (exit 10), cancelled runs, and runs whose gate had the setting off get no block. Each run keeps the setting it started with. If a gate ran more than once at the same place, only the last result shows.
|
|
267
|
+
- The result block does not change the checked message, tool call, or answer. **Hide** collapses only the gate result.
|
|
245
268
|
|
|
246
269
|
**Per-session switches:** at the top of **Pixie** tab → **Gates**, **Gates in this session** lists every gate that applies to this session's preset with a switch that turns it off for this session only. **Turn off in this session** on a chat block is the same switch.
|
|
247
270
|
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: studio-pixie-loop-guide
|
|
3
|
+
description: Write DSH Studio `pixie_loop` specs — many independent items judged by the same criteria (translation review, copy edits, per-file audits), experiments judged by running each output (selectors, regexes), step-by-step episodes, and picking one listed option per item or step (e.g. game actions), all outside your context with the Pixie model and the decision engine. Use when the user asks to batch, review, translate, try, or choose over many items with Pixie or Jev, or to write or debug a pixie_loop spec.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# DSH Studio Pixie Loop
|
|
7
|
+
|
|
8
|
+
`pixie_loop` runs work → code checks → decision-engine judgment → Pixie feedback → retry for each item, outside your context, and returns only counts, a results file, and the items that never passed. The full contract and worked examples are in `guide.md` next to this file. Read it before writing a spec.
|
|
9
|
+
|
|
10
|
+
## Decide whether it fits
|
|
11
|
+
|
|
12
|
+
Use `pixie_loop` only when all hold:
|
|
13
|
+
|
|
14
|
+
- There are several independent items (usually 5 or more).
|
|
15
|
+
- One fixed set of criteria judges every item.
|
|
16
|
+
- The worker needs no tools: everything it needs fits in the item input and `shared`. If an output must be run to be judged, one fixed `act` command can run it.
|
|
17
|
+
- The result can be judged from text (an act command's output counts).
|
|
18
|
+
|
|
19
|
+
Otherwise: one judgment → `judge`; multi-step work with files or tools → `subagent_role`; branching or many cooperating agents → `workflow` (only when the user asks for one); a check at a set moment of the agent's work → Pixie Gate (`studio-pixie-hook-guide`). Tell the user which and why.
|
|
20
|
+
|
|
21
|
+
## Write the spec
|
|
22
|
+
|
|
23
|
+
1. **Items.** Give each a stable unique `id` (a key, a file path). Under ~20 short items use `items.inline`; otherwise write a JSONL file with one `{"id","input"}` per line and pass `items.path`. Inputs up to 32,000 characters; a file item must fit on one line under the read tool's line limit. For file audits, read the files yourself and put their contents in the inputs.
|
|
24
|
+
2. **work.instruction.** What to do with one input, and the output format. Say "Reply with the output only."
|
|
25
|
+
3. **shared.** Glossary, style rules, and examples every item needs. Never put other items here.
|
|
26
|
+
4. **Criteria.** Plain English sentences, one property each, about `input` and `output`. Name failure types ("say whether it is a mistranslation, terminology, tone, or omission"). Give `checks` instead when the user already has exact questions; each question needs a pass rule (`noul` → `{min}`, `choice` → `{accept}` leaving a failing option, `score` → `{min}` level).
|
|
27
|
+
5. **codeChecks.** Put every exact requirement here, not in criteria: `placeholders` when inputs contain `{x}`, `{{x}}`, `%s`, or tags; `regex` with `expect` `match`/`no-match` for required or forbidden text; `json` with `keys` when the output must be JSON.
|
|
28
|
+
6. **Calibration.** At least one realistic pass example and one realistic fail example per criterion. Use real inputs. The run refuses to start if the checks do not separate them; then reword the failing question or switch to `checks`.
|
|
29
|
+
7. **Limits.** `limits.tokens` ≈ items × attempts × (input + output + shared tokens) × 3 for checks and feedback, plus headroom; `limits.timeMs` generous. Defaults: `maxAttempts` 3, `concurrency` 4.
|
|
30
|
+
8. **Act (optional).** When an output only proves itself by running it, add `act: { tool: "bash", command: "… {{output}} …", safeToRepeat: true }`. Values are quoted for you: write placeholders as separate words, never inside quotes or glued to text. Only use commands that are safe to run again; set `isolated: true` only when each item uses its own state. Write a small probe script that prints just what the judgment needs, and give each calibration example an `observation`. Add `extract` when observations are long.
|
|
31
|
+
9. **Episodes and selection (optional).** `mode: "episode"` makes each output one step that act runs, until the checks say the goal is reached. `select` lets the decision engine pick one option from JSON `{"options": [...]}` in the input or (episodes) the observation; the worker writes nothing. Never invent options yourself; they must come from the source. Set `minConfidence` when a wrong pick is costly.
|
|
32
|
+
10. **Route (optional).** `route: { question: "<choice id>" }` groups results by that answer.
|
|
33
|
+
11. **output** path for the results JSONL. Use `run_in_background: true` for long runs; keep working and collect with `job_output`.
|
|
34
|
+
|
|
35
|
+
## Recipes
|
|
36
|
+
|
|
37
|
+
If the user has a saved recipe for the job (Studio → Pixie → Gates → Loop recipes), prefer `{ "recipe": "<name>", "items": ..., "output": ... }`; its checks were calibrated when saved. Recipes are also what gates (Check/Repair) and hook scripts (`pixie loop --recipe`) run; for gate and hook work see `studio-pixie-hook-guide`. Recipes with `act`, episodes, or selection run only through the tool.
|
|
38
|
+
|
|
39
|
+
## After the run
|
|
40
|
+
|
|
41
|
+
- Report the counts and the results file. Do not re-run passed items.
|
|
42
|
+
- Handle each `escalated` item yourself (fix it, or ask the user), using its failed checks and feedback. Feedback is Pixie's guess, not the verdict.
|
|
43
|
+
- `errors` are engine or model failures, not fails. Rerun only those ids if the cause is transient.
|
|
44
|
+
- `stopped` means a limit was reached: rerun with only the ids missing from the results file.
|
|
45
|
+
- If the start was refused (`Calibration failed`, `could not compile`), fix the spec; do not loosen the criteria until bad examples pass.
|
|
46
|
+
- Merge results into the target files yourself; `pixie_loop` writes only the results file.
|
|
47
|
+
|
|
48
|
+
The user can inspect every attempt in **Pixie tab → Loops**. Item contents go to the Pixie model provider and the decision engine; tell the user before sending sensitive material.
|