@carljia/omd-dsh 0.1.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/lib/index.js ADDED
@@ -0,0 +1,82 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { scopeOf } from "@deepseek-ai/dsh-scope";
3
+ /**
4
+ * @module @carljia/omd-dsh
5
+ *
6
+ * omd-mode: per-mode (agent preset) model routing row for DeepSeek Harness.
7
+ *
8
+ * Structurally identical to the harness built-in installModelSelection
9
+ * (dsh-agent): it listens on the system-prompt/assemble event to inject
10
+ * the provider/model prompt variables (so a persona can render the
11
+ * model and provider template variables), and overrides provider/model
12
+ * on the agent/request waterfall after next(), dropping any inherited
13
+ * reasoningEffort. Both listeners register with prepend: true so this
14
+ * row sits OUTSIDE the entry point per-session selection listener and
15
+ * its override wins deterministically -- that is the mechanism behind
16
+ * "each mode pins its own model".
17
+ *
18
+ * When provider/model are not configured the row passes everything
19
+ * through and only serves the persona banner variables (inheriting the
20
+ * entry/session selection), so it is safe to mount into any preset.
21
+ */
22
+ /** Cordis plugin name. */
23
+ const name = "omd-mode";
24
+ /** No service injection: this row only registers scoped event listeners. */
25
+ const inject = [];
26
+ /** Runtime schema for the omd-mode row. */
27
+ const Config = z.object({
28
+ mode: z.string().required(),
29
+ provider: z.string(),
30
+ model: z.string(),
31
+ reasoningEffort: z.string(),
32
+ });
33
+ function apply(ctx, config) {
34
+ if (scopeOf(ctx) === undefined) {
35
+ throw new Error("omd-mode: refusing to mount outside a scoped context (mode '" + config.mode + "'). " +
36
+ "Mount this row inside an agent preset; a global mount would pin the model for every agent in the process.");
37
+ }
38
+ const pinned = config.provider !== undefined && config.model !== undefined
39
+ ? {
40
+ provider: config.provider,
41
+ model: config.model,
42
+ }
43
+ : undefined;
44
+ // 子代理(subagentDepth > 0)透传:omd-task 的 tier 模型通过显式 agentOptions
45
+ // 落到子代理的 AgentOptions 上,本行若再覆盖会压回模式模型、破坏差异化委派。
46
+ // 无显式 agentOptions 的子代理按 DSH 原生语义继承父级入口选择。
47
+ const isSubagent = (agent) => agent !== undefined && agent !== null && agent.options !== undefined && agent.options !== null
48
+ && typeof agent.options.subagentDepth === "number" && agent.options.subagentDepth > 0;
49
+ if (config.reasoningEffort !== undefined && pinned !== undefined) {
50
+ pinned.reasoningEffort = config.reasoningEffort;
51
+ }
52
+ ctx.on("system-prompt/assemble", async (assembly, _context, next) => {
53
+ const assembled = await next();
54
+ if (pinned === undefined || isSubagent(_context && _context.agent))
55
+ return assembled;
56
+ return {
57
+ ...assembled,
58
+ variables: {
59
+ ...assembled.variables,
60
+ provider: pinned.provider,
61
+ model: pinned.model,
62
+ },
63
+ };
64
+ }, { prepend: true });
65
+ ctx.on("agent/request", async (_payload, next) => {
66
+ const resolved = await next();
67
+ if (pinned === undefined || isSubagent(_payload && _payload.agent))
68
+ return resolved;
69
+ const stripped = { ...resolved };
70
+ delete stripped.reasoningEffort;
71
+ const out = {
72
+ ...stripped,
73
+ provider: pinned.provider,
74
+ model: pinned.model,
75
+ };
76
+ if (pinned.reasoningEffort !== undefined) {
77
+ out.reasoningEffort = pinned.reasoningEffort;
78
+ }
79
+ return out;
80
+ }, { prepend: true });
81
+ }
82
+ export { Config, apply, inject, name };
package/lib/task.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module @carljia/omd-dsh/task
3
+ *
4
+ * omd-task: tiered subagent delegation tool (the DSH-native equivalent of
5
+ * OMD task(category=...)).
6
+ *
7
+ * The shipped dsh-tool-subagent pins ONE model/persona/toolFilter per tool
8
+ * instance, so a model cannot choose a model per call. DSH subagents
9
+ * service DOES accept per-request agentOptions/persona/toolFilter, and this
10
+ * row exposes that: the model passes a tier argument per call, and each
11
+ * tier maps to a fixed worker profile (provider/model/maxTokens/persona/
12
+ * toolFilter). Strong models stay at the top level while cheap tiers do
13
+ * repetitive investigation work -- differentiated invocation inside one
14
+ * mode, configured entirely in the preset YAML.
15
+ */
16
+ /** Cordis plugin name. */
17
+ declare const name = "omd-task";
18
+ /** Services this row needs. */
19
+ declare const inject: string[];
20
+ /** Runtime schema for the omd-task row. */
21
+ declare const Config: any;
22
+ declare function apply(ctx: any, config: any): void;
23
+ export { Config, apply, inject, name };
package/lib/task.js ADDED
@@ -0,0 +1,270 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { defineTool } from "@deepseek-ai/dsh-tools";
3
+ import { assertSubagentMaxDepth } from "@deepseek-ai/dsh-subagent";
4
+ import { scopeOf } from "@deepseek-ai/dsh-scope";
5
+ /**
6
+ * @module @carljia/omd-dsh/task
7
+ *
8
+ * omd-task: tiered subagent delegation tool (the DSH-native equivalent of
9
+ * OMD task(category=...)).
10
+ *
11
+ * The shipped dsh-tool-subagent pins ONE model/persona/toolFilter per tool
12
+ * instance, so a model cannot choose a model per call. DSH subagents
13
+ * service DOES accept per-request agentOptions/persona/toolFilter, and this
14
+ * row exposes that: the model passes a tier argument per call, and each
15
+ * tier maps to a fixed worker profile (provider/model/maxTokens/persona/
16
+ * toolFilter). Strong models stay at the top level while cheap tiers do
17
+ * repetitive investigation work -- differentiated invocation inside one
18
+ * mode, configured entirely in the preset YAML.
19
+ */
20
+ /** Cordis plugin name. */
21
+ const name = "omd-task";
22
+ /** Services this row needs. */
23
+ const inject = ["tools", "subagents", "systemPrompt"];
24
+ /** Prompt order after bounded delegation policy and before child reporting. */
25
+ const SUBAGENT_SECTION_ORDER = 116.5;
26
+ const TierSchema = z.object({
27
+ provider: z.string().required(),
28
+ model: z.string().required(),
29
+ hint: z.string(),
30
+ persona: z.string(),
31
+ toolFilter: z.object({
32
+ allow: z.array(z.string()),
33
+ deny: z.array(z.string()),
34
+ }),
35
+ maxTokens: z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER),
36
+ });
37
+ /** Runtime schema for the omd-task row. */
38
+ const Config = z.object({
39
+ provider: z.string().default("spawn"),
40
+ toolName: z.string().default("omd_task"),
41
+ backgroundMode: z.union(["continuable", "foreground"]).default("continuable"),
42
+ tiers: z.dict(TierSchema).required(),
43
+ defaultTier: z.string(),
44
+ maxDepth: z.union([z.natural().max(Number.MAX_SAFE_INTEGER), z.const("provider-managed")]).default(3),
45
+ });
46
+ const TIER_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]*$/;
47
+ /** Render text blocks from a canonical JSON block array. */
48
+ function outputValueText(values) {
49
+ return values
50
+ .filter((value) => typeof value === "object" && value !== null && !Array.isArray(value) && value.type === "text" && typeof value.text === "string")
51
+ .map((value) => value.text)
52
+ .join("");
53
+ }
54
+ /** A non-completed stop reason means the child did not finish cleanly. */
55
+ function stopReasonError(result) {
56
+ switch (result.stopReason) {
57
+ case "completed": return;
58
+ case "aborted": return "subagent run was cancelled";
59
+ case "error": return "subagent run failed";
60
+ case "max-tokens": return "subagent run hit its token limit before finishing";
61
+ case "refusal": return "subagent declined the task";
62
+ default: return "subagent run ended abnormally (" + String(result.stopReason) + ")";
63
+ }
64
+ }
65
+ /** Append failure detail and the child preserved partial answer. */
66
+ function withDiagnosticAndPartialText(error, result) {
67
+ const diagnostic = result.diagnostic === undefined ? "" : "\nDiagnostic: " + result.diagnostic;
68
+ const text = result.output.filter((block) => block.type === "text").map((block) => block.text).join("");
69
+ return error + diagnostic + (text.length === 0 ? "" : "\nPartial output before the run ended:\n" + text);
70
+ }
71
+ /** Collect and release one foreground run. */
72
+ async function settleForegroundRun(run) {
73
+ const [execution] = await Promise.allSettled([run.result.then((result) => {
74
+ const error = stopReasonError(result);
75
+ if (error !== undefined)
76
+ throw new Error(withDiagnosticAndPartialText(error, result));
77
+ return {
78
+ kind: "foreground",
79
+ runId: run.id,
80
+ output: result.output,
81
+ };
82
+ })]);
83
+ const [disposal] = await Promise.allSettled([Promise.resolve().then(() => run.dispose())]);
84
+ if (execution.status === "rejected") {
85
+ if (disposal.status === "rejected")
86
+ throw new AggregateError([execution.reason, disposal.reason], "subagent run failed: " + String(execution.reason) + "; dispose failed: " + String(disposal.reason));
87
+ throw execution.reason;
88
+ }
89
+ if (disposal.status === "rejected")
90
+ throw disposal.reason;
91
+ return execution.value;
92
+ }
93
+ /** Validate row config beyond the schema. Returns an error string or undefined. */
94
+ function configError(config) {
95
+ const tierNames = Object.keys(config.tiers);
96
+ if (tierNames.length === 0)
97
+ return "omd-task: `tiers` must define at least one tier";
98
+ for (const tierName of tierNames) {
99
+ if (!TIER_NAME_PATTERN.test(tierName)) {
100
+ return "omd-task: invalid tier name \"" + tierName + "\" (must match [a-z0-9][a-z0-9_-]*): " + tierNames.join(", ");
101
+ }
102
+ const filter = config.tiers[tierName].toolFilter;
103
+ if (filter !== undefined && filter.allow === undefined && filter.deny === undefined) {
104
+ return "omd-task: tier \"" + tierName + "\" configures toolFilter but names neither allow nor deny";
105
+ }
106
+ }
107
+ if (config.defaultTier !== undefined && config.tiers[config.defaultTier] === undefined) {
108
+ return "omd-task: defaultTier \"" + config.defaultTier + "\" is not one of the tiers: " + tierNames.join(", ");
109
+ }
110
+ return undefined;
111
+ }
112
+ /** Human-readable enumeration of the tiers for the tool description. */
113
+ function tierCatalog(tiers) {
114
+ const lines = [];
115
+ for (const tierName of Object.keys(tiers)) {
116
+ const tier = tiers[tierName];
117
+ const hint = tier.hint === undefined ? "no hint configured" : tier.hint;
118
+ lines.push("- tier \"" + tierName + "\": " + hint + " [model: " + tier.provider + "/" + tier.model + "]");
119
+ }
120
+ return lines.join("\n");
121
+ }
122
+ /** Resolve which tier one call uses. Throws with a helpful message when ambiguous. */
123
+ function resolveTier(config, requested) {
124
+ const tierNames = Object.keys(config.tiers);
125
+ if (requested !== undefined) {
126
+ if (config.tiers[requested] === undefined) {
127
+ throw new Error("omd_task: unknown tier \"" + requested + "\" -- valid tiers: " + tierNames.join(", "));
128
+ }
129
+ return requested;
130
+ }
131
+ if (config.defaultTier !== undefined)
132
+ return config.defaultTier;
133
+ if (tierNames.length === 1)
134
+ return tierNames[0];
135
+ throw new Error("omd_task: choose a tier for this task -- valid tiers: " + tierNames.join(", "));
136
+ }
137
+ function apply(ctx, config) {
138
+ if (scopeOf(ctx) === undefined) {
139
+ throw new Error("omd-task: refusing to mount outside a scoped context; mount this row inside an agent preset");
140
+ }
141
+ const invalid = configError(config);
142
+ if (invalid !== undefined)
143
+ throw new Error(invalid);
144
+ if (config.maxDepth !== "provider-managed")
145
+ assertSubagentMaxDepth(config.maxDepth);
146
+ const continuable = config.backgroundMode === "continuable";
147
+ const toolName = config.toolName;
148
+ let disposeTool;
149
+ const description = "Delegate a task to a tiered subagent. Every tier is a fixed worker profile (model + persona + tool boundary); choose the tier that fits the work and give the task a complete, standalone prompt. Available tiers:\n" +
150
+ tierCatalog(config.tiers) + "\n\n" +
151
+ "Use cheaper tiers for repeated investigation, searching, summarising and mechanical work; use stronger tiers for top-level planning, deep reasoning and hard problems. The subagent returns its result, not its intermediate steps." +
152
+ (continuable ? " This tool runs in the background by default and immediately returns a durable subagent id; the runtime sends you a notice when that run settles. Set run_in_background: false only when your next action depends on receiving the result." : " This call waits for the subagent and returns its result.");
153
+ const mount = (provider) => {
154
+ if (typeof config.maxDepth === "number" && !provider.capabilities.depthLimit) {
155
+ throw new Error("omd-task: provider \"" + provider.name + "\" cannot enforce maxDepth (no depthLimit capability) -- set maxDepth: provider-managed");
156
+ }
157
+ if (continuable && provider.prepareContinuable === undefined) {
158
+ throw new Error("omd-task: provider \"" + provider.name + "\" does not support backgroundMode: continuable");
159
+ }
160
+ disposeTool = ctx.tools.register(defineTool({
161
+ name: toolName,
162
+ description,
163
+ parameters: {
164
+ description: {
165
+ type: "string",
166
+ required: true,
167
+ description: "A short (3-5 word) description of the delegated task, for display.",
168
+ },
169
+ prompt: {
170
+ type: "string",
171
+ required: true,
172
+ description: "The complete, self-contained task for the subagent. It does not share this conversation\u2019s context, so include everything it needs.",
173
+ },
174
+ tier: {
175
+ type: "string",
176
+ description: "Which worker tier to use. " + tierCatalog(config.tiers) + (config.defaultTier !== undefined ? " Leave empty to use the default tier (\"" + config.defaultTier + "\")." : " Required when more than one tier exists."),
177
+ },
178
+ ...(continuable ? { run_in_background: {
179
+ type: "boolean",
180
+ description: "Whether to run in the background and return a durable subagent id immediately. Defaults to true. Set false to wait for the result when your next action depends on it.",
181
+ } } : {}),
182
+ },
183
+ output: {
184
+ schema: { oneOf: [
185
+ {
186
+ type: "object",
187
+ additionalProperties: false,
188
+ properties: {
189
+ kind: { type: "string", required: true, const: "continuable" },
190
+ subagentId: { type: "string", required: true },
191
+ },
192
+ },
193
+ {
194
+ type: "object",
195
+ additionalProperties: false,
196
+ properties: {
197
+ kind: { type: "string", required: true, const: "foreground" },
198
+ runId: { type: "string", required: true },
199
+ output: { type: "array", required: true, items: { type: "json" } },
200
+ },
201
+ },
202
+ ] },
203
+ render: (_args, value) => [{
204
+ type: "text",
205
+ text: value.kind === "continuable" ? "started subagent " + value.subagentId : outputValueText(value.output),
206
+ }],
207
+ },
208
+ isConcurrencySafe: () => true,
209
+ async execute(args, exec) {
210
+ const parent = exec.agent;
211
+ if (!parent)
212
+ throw new Error("omd_task requires a calling agent (exec.agent was undefined)");
213
+ const tierName = resolveTier(config, args.tier);
214
+ const tier = config.tiers[tierName];
215
+ const maxDepth = typeof config.maxDepth === "number" ? config.maxDepth : undefined;
216
+ const request = {
217
+ label: args.description,
218
+ prompt: [{ type: "text", text: args.prompt }],
219
+ parent,
220
+ agentOptions: {
221
+ provider: tier.provider,
222
+ model: tier.model,
223
+ ...(tier.maxTokens !== undefined ? { maxTokens: tier.maxTokens } : {}),
224
+ },
225
+ ...(tier.persona !== undefined ? { persona: tier.persona } : {}),
226
+ ...(tier.toolFilter !== undefined ? { toolFilter: tier.toolFilter } : {}),
227
+ ...(maxDepth !== undefined ? { maxDepth } : {}),
228
+ };
229
+ if (continuable && args.run_in_background !== false) {
230
+ return {
231
+ kind: "continuable",
232
+ subagentId: (await ctx.subagents.startContinuable({
233
+ provider: config.provider,
234
+ label: args.description,
235
+ request,
236
+ signal: exec.signal,
237
+ })).childId,
238
+ };
239
+ }
240
+ return settleForegroundRun(await ctx.subagents.start(config.provider, {
241
+ ...request,
242
+ signal: exec.signal,
243
+ }));
244
+ },
245
+ }));
246
+ };
247
+ ctx.on("subagent/provider-added", (provider) => {
248
+ if (provider.name === config.provider && disposeTool === undefined)
249
+ mount(provider);
250
+ });
251
+ ctx.on("subagent/provider-removed", (providerName) => {
252
+ if (providerName !== config.provider || disposeTool === undefined)
253
+ return;
254
+ disposeTool();
255
+ disposeTool = undefined;
256
+ });
257
+ const present = ctx.subagents.getProvider(config.provider);
258
+ if (present !== undefined)
259
+ mount(present);
260
+ else
261
+ ctx.logger.info("omd-task: subagent provider \"" + config.provider + "\" not registered yet; the \"" + toolName + "\" tool will register when it appears");
262
+ if (continuable)
263
+ ctx.systemPrompt.section({
264
+ name: "tool:" + toolName,
265
+ order: SUBAGENT_SECTION_ORDER,
266
+ text: (context) => disposeTool === undefined || ctx.tools.get(toolName, context.scope) === undefined ? "" :
267
+ "Use " + toolName + " in the background by default. Start independent delegations together in one assistant message and continue useful work while they run. Set run_in_background: false only when your next action depends on that subagent result. When a background run settles, the runtime sends you a notice containing its outcome and any final assistant message.",
268
+ });
269
+ }
270
+ export { Config, apply, inject, name };
@@ -0,0 +1,82 @@
1
+ import z from "@deepseek-ai/schemastery";
2
+ import { scopeOf } from "@deepseek-ai/dsh-scope";
3
+ /**
4
+ * @module @carljia/omd-dsh
5
+ *
6
+ * omd-mode: per-mode (agent preset) model routing row for DeepSeek Harness.
7
+ *
8
+ * Structurally identical to the harness built-in installModelSelection
9
+ * (dsh-agent): it listens on the system-prompt/assemble event to inject
10
+ * the provider/model prompt variables (so a persona can render the
11
+ * model and provider template variables), and overrides provider/model
12
+ * on the agent/request waterfall after next(), dropping any inherited
13
+ * reasoningEffort. Both listeners register with prepend: true so this
14
+ * row sits OUTSIDE the entry point per-session selection listener and
15
+ * its override wins deterministically -- that is the mechanism behind
16
+ * "each mode pins its own model".
17
+ *
18
+ * When provider/model are not configured the row passes everything
19
+ * through and only serves the persona banner variables (inheriting the
20
+ * entry/session selection), so it is safe to mount into any preset.
21
+ */
22
+ /** Cordis plugin name. */
23
+ const name = "omd-mode";
24
+ /** No service injection: this row only registers scoped event listeners. */
25
+ const inject = [];
26
+ /** Runtime schema for the omd-mode row. */
27
+ const Config = z.object({
28
+ mode: z.string().required(),
29
+ provider: z.string(),
30
+ model: z.string(),
31
+ reasoningEffort: z.string(),
32
+ });
33
+ function apply(ctx, config) {
34
+ if (scopeOf(ctx) === undefined) {
35
+ throw new Error("omd-mode: refusing to mount outside a scoped context (mode '" + config.mode + "'). " +
36
+ "Mount this row inside an agent preset; a global mount would pin the model for every agent in the process.");
37
+ }
38
+ const pinned = config.provider !== undefined && config.model !== undefined
39
+ ? {
40
+ provider: config.provider,
41
+ model: config.model,
42
+ }
43
+ : undefined;
44
+ // 子代理(subagentDepth > 0)透传:omd-task 的 tier 模型通过显式 agentOptions
45
+ // 落到子代理的 AgentOptions 上,本行若再覆盖会压回模式模型、破坏差异化委派。
46
+ // 无显式 agentOptions 的子代理按 DSH 原生语义继承父级入口选择。
47
+ const isSubagent = (agent) => agent !== undefined && agent !== null && agent.options !== undefined && agent.options !== null
48
+ && typeof agent.options.subagentDepth === "number" && agent.options.subagentDepth > 0;
49
+ if (config.reasoningEffort !== undefined && pinned !== undefined) {
50
+ pinned.reasoningEffort = config.reasoningEffort;
51
+ }
52
+ ctx.on("system-prompt/assemble", async (assembly, _context, next) => {
53
+ const assembled = await next();
54
+ if (pinned === undefined || isSubagent(_context && _context.agent))
55
+ return assembled;
56
+ return {
57
+ ...assembled,
58
+ variables: {
59
+ ...assembled.variables,
60
+ provider: pinned.provider,
61
+ model: pinned.model,
62
+ },
63
+ };
64
+ }, { prepend: true });
65
+ ctx.on("agent/request", async (_payload, next) => {
66
+ const resolved = await next();
67
+ if (pinned === undefined || isSubagent(_payload && _payload.agent))
68
+ return resolved;
69
+ const stripped = { ...resolved };
70
+ delete stripped.reasoningEffort;
71
+ const out = {
72
+ ...stripped,
73
+ provider: pinned.provider,
74
+ model: pinned.model,
75
+ };
76
+ if (pinned.reasoningEffort !== undefined) {
77
+ out.reasoningEffort = pinned.reasoningEffort;
78
+ }
79
+ return out;
80
+ }, { prepend: true });
81
+ }
82
+ export { Config, apply, inject, name };