odai-dsh-agent 0.2.1 → 0.2.3

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.
Files changed (51) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +138 -138
  3. package/bin/odai-dsh-agent.mjs +108 -108
  4. package/package.json +69 -69
  5. package/preset/odai/agent.cordis.yml +261 -260
  6. package/preset/odai/preset.yml +3 -3
  7. package/preset/odai/runtime/compaction-config.mjs +388 -388
  8. package/preset/odai/runtime/governance.mjs +128 -128
  9. package/preset/odai/runtime/index.mjs +2461 -2263
  10. package/preset/odai/runtime/output-config.mjs +360 -304
  11. package/preset/odai/runtime/research-packet.mjs +150 -150
  12. package/preset/odai/runtime/responsibility-scope.mjs +35 -2
  13. package/preset/odai/runtime/role-overlays.mjs +27 -27
  14. package/preset/odai/runtime/route-card.mjs +168 -152
  15. package/preset/odai/runtime/router.mjs +683 -651
  16. package/preset/odai/runtime/routing-config.mjs +480 -436
  17. package/preset/odai/runtime/routing-context.mjs +321 -321
  18. package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
  19. package/preset/odai/runtime/semantic-memory.mjs +842 -842
  20. package/preset/odai/runtime/session-compat.mjs +389 -389
  21. package/preset/odai/runtime/skill-bundle.mjs +318 -318
  22. package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
  23. package/preset/odai/skills/odai/agents/openai.yaml +4 -4
  24. package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
  25. package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
  26. package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
  27. package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
  28. package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
  29. package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
  30. package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
  31. package/preset/odai/skills/odai/assets/routing-roles/frontend.md +7 -7
  32. package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
  33. package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
  34. package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
  35. package/preset/odai/skills/odai/assets/task-state.md +14 -14
  36. package/preset/odai/skills/odai/manifest.json +37 -37
  37. package/preset/odai/skills/odai/references/craft.md +54 -54
  38. package/preset/odai/skills/odai/references/dao.md +30 -30
  39. package/preset/odai/skills/odai/references/leverage.md +53 -53
  40. package/preset/odai/skills/odai/references/planning.md +82 -82
  41. package/preset/odai/skills/odai/references/support.md +36 -36
  42. package/preset/odai/skills/odai/references/verification.md +23 -23
  43. package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
  44. package/preset/odai/skills/odai/scripts/build-routing.mjs +274 -274
  45. package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
  46. package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
  47. package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
  48. package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
  49. package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
  50. package/scripts/verify-agent-load.mjs +257 -257
  51. package/src/installer.mjs +398 -382
@@ -1,436 +1,480 @@
1
- import { randomUUID } from "node:crypto";
2
- import {
3
- copyFileSync,
4
- existsSync,
5
- mkdirSync,
6
- readFileSync,
7
- renameSync,
8
- rmSync,
9
- statSync,
10
- writeFileSync,
11
- } from "node:fs";
12
- import { homedir } from "node:os";
13
- import { dirname, resolve } from "node:path";
14
-
15
- import { requireModelRoute, sameModelRoute } from "./model-route.mjs";
16
-
17
- export const CONFIGURABLE_ROLES = Object.freeze(["researcher", "planner", "executor", "reviewer", "frontend"]);
18
- export const ROUTING_CONFIG_PROMPT = [
19
- "## Odai responsibility model configuration",
20
- "When the user naturally asks to inspect, set, change, or remove the research/investigation, planning/planner, execution/executor, review/acceptance/reviewer, or frontend design/implementation model, use odai_routing_config.",
21
- "Translate the user's natural responsibility wording into the tool's responsibility field. Do not require internal routing terms or a special prompt form.",
22
- "For set, call the tool only after the user explicitly supplies both provider and model. Pass reasoningEffort or maxTokens only when the user supplies them; otherwise omit them.",
23
- "Never infer, recommend as chosen, or silently select any provider, model, effort, token limit, or price. Ask a concise clarification when provider or model is ambiguous.",
24
- "Researcher routing is task-gated but not price-aware. A researcher mapping enables the narrow trigger but does not guarantee lower cost; compare actual provider prices and measured usage without inventing either.",
25
- "A generic subagent is not proof that a configured responsibility ran. When a real responsibility gap emerges after initial routing and manual delegation is necessary, begin the subagent label with `odai-<responsibility>` followed by a space or colon; otherwise keep it generic and do not claim the responsibility mapping was used.",
26
- "Do not ask the user to edit YAML, JSON, managed Agent files, or Plugin configuration. The tool owns persistence. A set/remove change applies from the next user turn.",
27
- "Base controller selection remains host-owned and is not changed by this tool.",
28
- ].join("\n");
29
- const ROLE_ROUTE_FIELDS = new Set(["provider", "model", "reasoningEffort", "maxTokens"]);
30
- const STORE_SCHEMA_VERSION = 1;
31
-
32
- function assertPlainObject(value, field) {
33
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
34
- throw new TypeError(`${field} must be an object`);
35
- }
36
- return value;
37
- }
38
-
39
- export function resolveRoleRoute(value, role) {
40
- if (value === undefined) return undefined;
41
- if (!CONFIGURABLE_ROLES.includes(role)) throw new TypeError(`unknown odai routing responsibility: ${role}`);
42
- const route = assertPlainObject(value, `routing role ${role}`);
43
- const unknown = Object.keys(route).filter((field) => !ROLE_ROUTE_FIELDS.has(field));
44
- if (unknown.length > 0) {
45
- throw new TypeError(`routing role ${role} has unknown fields: ${unknown.join(", ")}`);
46
- }
47
- if (typeof route.provider !== "string" || route.provider.trim() === "") {
48
- throw new TypeError(`routing role ${role}.provider must be a non-empty string`);
49
- }
50
- if (typeof route.model !== "string" || route.model.trim() === "") {
51
- throw new TypeError(`routing role ${role}.model must be a non-empty string`);
52
- }
53
- if (route.reasoningEffort !== undefined
54
- && (typeof route.reasoningEffort !== "string" || route.reasoningEffort.trim() === "")) {
55
- throw new TypeError(`routing role ${role}.reasoningEffort must be a non-empty string`);
56
- }
57
- if (route.maxTokens !== undefined
58
- && (!Number.isSafeInteger(route.maxTokens) || route.maxTokens <= 0)) {
59
- throw new TypeError(`routing role ${role}.maxTokens must be a positive integer`);
60
- }
61
-
62
- return Object.freeze({
63
- provider: route.provider.trim(),
64
- model: route.model.trim(),
65
- ...(route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort.trim() }),
66
- ...(route.maxTokens === undefined ? {} : { maxTokens: route.maxTokens }),
67
- });
68
- }
69
-
70
- export function resolveRoutingConfigPath(configuredPath, env = process.env) {
71
- if (configuredPath !== undefined) {
72
- if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
73
- throw new TypeError("config.routing.configPath must be a non-empty string");
74
- }
75
- return resolve(configuredPath.trim());
76
- }
77
- const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME.trim() !== ""
78
- ? resolve(env.DSH_HOME.trim())
79
- : resolve(homedir(), ".dsh");
80
- return resolve(dshHome, "odai", "routing.json");
81
- }
82
-
83
- export function readRoutingStore(configPath) {
84
- if (!existsSync(configPath)) {
85
- return Object.freeze({ schemaVersion: STORE_SCHEMA_VERSION, roles: Object.freeze({}) });
86
- }
87
-
88
- let parsed;
89
- try {
90
- parsed = JSON.parse(readFileSync(configPath, "utf8"));
91
- } catch (error) {
92
- throw new Error(`cannot read odai routing config ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
93
- }
94
- const store = assertPlainObject(parsed, `odai routing config ${configPath}`);
95
- const unknownStoreFields = Object.keys(store).filter((field) => !["schemaVersion", "roles"].includes(field));
96
- if (unknownStoreFields.length > 0) {
97
- throw new TypeError(`odai routing config ${configPath} has unknown fields: ${unknownStoreFields.join(", ")}`);
98
- }
99
- if (store.schemaVersion !== STORE_SCHEMA_VERSION) {
100
- throw new TypeError(`odai routing config ${configPath} has unsupported schemaVersion ${String(store.schemaVersion)}`);
101
- }
102
- const rawRoles = assertPlainObject(store.roles, `odai routing config ${configPath}.roles`);
103
- const unknown = Object.keys(rawRoles).filter((role) => !CONFIGURABLE_ROLES.includes(role));
104
- if (unknown.length > 0) {
105
- throw new TypeError(`odai routing config ${configPath} has unknown roles: ${unknown.join(", ")}`);
106
- }
107
- return Object.freeze({
108
- schemaVersion: STORE_SCHEMA_VERSION,
109
- roles: Object.freeze(Object.fromEntries(CONFIGURABLE_ROLES.flatMap((role) => {
110
- const route = resolveRoleRoute(rawRoles[role], role);
111
- return route ? [[role, route]] : [];
112
- }))),
113
- });
114
- }
115
-
116
- export function effectiveRoleRoute(configPath, configuredRoles, role) {
117
- return effectiveRoutingSnapshot(configPath, configuredRoles).roles[role];
118
- }
119
-
120
- export function effectiveRoutingSnapshot(configPath, configuredRoles = {}) {
121
- const persisted = readRoutingStore(configPath).roles;
122
- const roles = {};
123
- const sources = {};
124
- for (const role of CONFIGURABLE_ROLES) {
125
- const route = persisted[role] ?? configuredRoles[role];
126
- if (!route) continue;
127
- roles[role] = route;
128
- sources[role] = persisted[role] ? "persisted-mapping" : "deployment-config";
129
- }
130
- return Object.freeze({
131
- roles: Object.freeze(roles),
132
- sources: Object.freeze(sources),
133
- });
134
- }
135
-
136
- function writeRoutingStore(configPath, roles) {
137
- mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
138
- const temporary = `${configPath}.tmp-${process.pid}-${randomUUID()}`;
139
- const value = `${JSON.stringify({ schemaVersion: STORE_SCHEMA_VERSION, roles }, null, 2)}\n`;
140
- try {
141
- writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600, flag: "wx" });
142
- renameSync(temporary, configPath);
143
- } finally {
144
- rmSync(temporary, { force: true });
145
- }
146
- }
147
-
148
- function acquireRoutingStoreLock(configPath) {
149
- mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
150
- const lockPath = `${configPath}.lock`;
151
- const create = () => writeFileSync(lockPath, `${process.pid}\n`, {
152
- encoding: "utf8",
153
- mode: 0o600,
154
- flag: "wx",
155
- });
156
- try {
157
- create();
158
- } catch (error) {
159
- if (error?.code !== "EEXIST") throw error;
160
- const stale = (() => {
161
- try {
162
- return Date.now() - statSync(lockPath).mtimeMs > 30_000;
163
- } catch {
164
- return false;
165
- }
166
- })();
167
- if (!stale) throw new Error("Odai routing configuration is being updated; retry the tool call");
168
- rmSync(lockPath, { force: true });
169
- create();
170
- }
171
- return () => rmSync(lockPath, { force: true });
172
- }
173
-
174
- function preserveInvalidRoutingStore(configPath) {
175
- if (!existsSync(configPath)) return;
176
- renameSync(configPath, `${configPath}.invalid-${Date.now()}-${randomUUID()}`);
177
- }
178
-
179
- export function invalidatePersistedRoleRoute(configPath, role, expectedRoute) {
180
- if (!CONFIGURABLE_ROLES.includes(role)) throw new TypeError(`unknown odai routing responsibility: ${role}`);
181
- const releaseLock = acquireRoutingStoreLock(configPath);
182
- try {
183
- const current = readRoutingStore(configPath);
184
- const persisted = current.roles[role];
185
- if (!sameModelRoute(persisted, expectedRoute)) {
186
- return Object.freeze({ invalidated: false, reason: "mapping-changed-or-not-persisted" });
187
- }
188
- const backupPath = `${configPath}.invalidated-${Date.now()}-${randomUUID()}`;
189
- copyFileSync(configPath, backupPath);
190
- const roles = { ...current.roles };
191
- delete roles[role];
192
- writeRoutingStore(configPath, roles);
193
- return Object.freeze({ invalidated: true, backupPath, route: persisted });
194
- } finally {
195
- releaseLock();
196
- }
197
- }
198
-
199
- function routeSchema(required) {
200
- return {
201
- type: "object",
202
- additionalProperties: false,
203
- ...(required ? { required: ["provider", "model"] } : {}),
204
- properties: {
205
- provider: { type: "string" },
206
- model: { type: "string" },
207
- reasoningEffort: { type: "string" },
208
- maxTokens: { type: "integer" },
209
- },
210
- };
211
- }
212
-
213
- function resultFor(configPath, action, snapshot, responsibility, recoveredInvalidStore = false, latestRoute) {
214
- return {
215
- action,
216
- ...(responsibility ? { responsibility } : {}),
217
- configPath,
218
- roles: snapshot.roles,
219
- sources: snapshot.sources,
220
- requiresNextTurn: action !== "show",
221
- ...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
222
- ...(latestRoute ? { latestRoute } : {}),
223
- };
224
- }
225
-
226
- export function createRoutingConfigTool(configPath, options = {}) {
227
- const onConfigured = typeof options.onConfigured === "function" ? options.onConfigured : () => {};
228
- const configuredRoles = options.configuredRoles ?? {};
229
- const latestRouteFor = typeof options.latestRouteFor === "function" ? options.latestRouteFor : () => undefined;
230
- const resolveCallConfig = typeof options.resolveCallConfig === "function" ? options.resolveCallConfig : undefined;
231
- return {
232
- name: "odai_routing_config",
233
- description: [
234
- "Inspect effective Odai model mappings and the latest current-session route receipt, or set/remove persisted mappings for researcher, planner, executor, reviewer, and frontend responsibilities.",
235
- "Use this only when the user naturally and explicitly asks to inspect/remove a mapping or names the provider/model to set.",
236
- "Never choose a provider, model, reasoning effort, token limit, or price on the user's behalf.",
237
- "Researcher routing is task-gated but not price-aware; its mapping does not guarantee lower cost.",
238
- "For set/remove, handle one responsibility per call. Persisted mappings are shared by the Odai DSH Plugin and Agent and apply from the next user turn.",
239
- ].join(" "),
240
- parameters: {
241
- type: "object",
242
- additionalProperties: false,
243
- required: ["action"],
244
- properties: {
245
- action: {
246
- type: "string",
247
- enum: ["show", "set", "remove"],
248
- description: "Show all mappings, set one user-specified mapping, or remove one mapping.",
249
- },
250
- responsibility: {
251
- type: "string",
252
- enum: [...CONFIGURABLE_ROLES],
253
- description: "Required for set/remove; omitted for show.",
254
- },
255
- provider: {
256
- type: "string",
257
- description: "Provider id explicitly supplied by the user; required for set.",
258
- },
259
- model: {
260
- type: "string",
261
- description: "Model id explicitly supplied by the user; required for set.",
262
- },
263
- reasoningEffort: {
264
- type: "string",
265
- description: "Optional reasoning effort explicitly supplied by the user.",
266
- },
267
- maxTokens: {
268
- type: "integer",
269
- description: "Optional positive output limit explicitly supplied by the user. It limits routed child requests; inside a routed frontend controller responsibility scope it explicitly overrides the global controller ceiling only for that scope. Other in-place responsibility scopes remain governed by the controller policy."
270
- },
271
- },
272
- },
273
- output: {
274
- schema: {
275
- type: "object",
276
- additionalProperties: false,
277
- required: ["action", "configPath", "roles", "sources", "requiresNextTurn"],
278
- properties: {
279
- action: { type: "string", enum: ["show", "set", "remove"] },
280
- responsibility: { type: "string", enum: [...CONFIGURABLE_ROLES] },
281
- configPath: { type: "string" },
282
- roles: {
283
- type: "object",
284
- additionalProperties: false,
285
- properties: Object.fromEntries(CONFIGURABLE_ROLES.map((role) => [role, routeSchema(true)])),
286
- },
287
- sources: {
288
- type: "object",
289
- additionalProperties: false,
290
- properties: Object.fromEntries(CONFIGURABLE_ROLES.map((role) => [role, {
291
- type: "string",
292
- enum: ["persisted-mapping", "deployment-config"],
293
- }])),
294
- },
295
- requiresNextTurn: { type: "boolean" },
296
- recoveredInvalidStore: { type: "boolean" },
297
- latestRoute: {
298
- type: "object",
299
- additionalProperties: false,
300
- required: ["turn", "step", "responsibility", "status", "routeSource", "fallbackUsed"],
301
- properties: {
302
- turn: { type: "integer" },
303
- step: { type: "integer" },
304
- responsibility: { type: "string", enum: [...CONFIGURABLE_ROLES] },
305
- status: { type: "string", enum: ["applied", "mismatch", "unverified", "fallback"] },
306
- taskStatus: { type: "string", enum: ["completed", "fallback"] },
307
- routeMode: { type: "string", enum: ["inline", "same-turn", "child"] },
308
- routeSource: { type: "string", enum: ["persisted-mapping", "deployment-config"] },
309
- fallbackUsed: { type: "boolean" },
310
- requestedRoute: routeSchema(true),
311
- actualRoute: routeSchema(true),
312
- stopReason: { type: "string" },
313
- error: { type: "string" },
314
- taskStopReason: { type: "string" },
315
- taskError: { type: "string" },
316
- },
317
- },
318
- },
319
- },
320
- render(_args, value) {
321
- const mapping = Object.entries(value.roles)
322
- .map(([role, route]) => {
323
- const options = [
324
- ...(route.reasoningEffort ? [`reasoningEffort=${route.reasoningEffort}`] : []),
325
- ...(route.maxTokens ? [`maxTokens=${route.maxTokens}`] : []),
326
- ];
327
- return `${role}: ${route.provider}/${route.model}${options.length > 0 ? ` (${options.join(", ")})` : ""} [${value.sources[role]}]`;
328
- })
329
- .join("\n") || "No Odai responsibility models are configured.";
330
- const latestRoute = value.latestRoute
331
- ? [
332
- "\nLatest current-session responsibility route receipt:",
333
- `responsibility=${value.latestRoute.responsibility} status=${value.latestRoute.status} mode=${value.latestRoute.routeMode ?? "unknown"}`,
334
- `routeSource=${value.latestRoute.routeSource} fallbackUsed=${String(value.latestRoute.fallbackUsed)}`,
335
- ...(value.latestRoute.taskStatus ? [`taskStatus=${value.latestRoute.taskStatus}`] : []),
336
- ...(value.latestRoute.taskStopReason ? [`taskStopReason=${value.latestRoute.taskStopReason}`] : []),
337
- ...(value.latestRoute.error ? [`routeError=${value.latestRoute.error}`] : []),
338
- ...(value.latestRoute.taskError ? [`taskError=${value.latestRoute.taskError}`] : []),
339
- ...(value.latestRoute.actualRoute
340
- ? [`actual=${value.latestRoute.actualRoute.provider}/${value.latestRoute.actualRoute.model} (reasoningEffort=${value.latestRoute.actualRoute.reasoningEffort ?? "unspecified"})`]
341
- : ["actual=<unverified>"]),
342
- ].join("\n")
343
- : value.action === "show"
344
- ? "\nNo mapped responsibility route receipt is recorded in this session."
345
- : "";
346
- return [{
347
- type: "text",
348
- text: [
349
- `${value.action === "show" ? "Current" : "Updated"} Odai routing configuration:\n${mapping}`,
350
- ...(value.recoveredInvalidStore ? ["\nAn invalid prior store was preserved and replaced."] : []),
351
- ...(value.roles.researcher ? ["\nResearcher routing is task-gated but not price-aware. This mapping does not guarantee lower cost; compare actual provider prices and measured usage."] : []),
352
- latestRoute,
353
- ...(value.requiresNextTurn ? ["\nThe change applies from the next user turn."] : []),
354
- ].join(""),
355
- }];
356
- },
357
- },
358
- execute(args, execution) {
359
- if (!execution.agent) throw new Error("odai_routing_config requires an owning agent session");
360
- const header = execution.agent.session?.header;
361
- if (header?.origin === "subagent" || (Number.isSafeInteger(header?.delegationDepth) && header.delegationDepth > 0)) {
362
- throw new Error("child agents may not change Odai routing configuration");
363
- }
364
- if (!args || typeof args !== "object" || Array.isArray(args)) throw new TypeError("arguments must be an object");
365
- if (!["show", "set", "remove"].includes(args.action)) throw new TypeError("action must be show, set, or remove");
366
-
367
- if (args.action === "show") {
368
- return Promise.resolve(resultFor(
369
- configPath,
370
- "show",
371
- effectiveRoutingSnapshot(configPath, configuredRoles),
372
- undefined,
373
- false,
374
- latestRouteFor(execution.agent),
375
- ));
376
- }
377
- if (!CONFIGURABLE_ROLES.includes(args.responsibility)) {
378
- throw new TypeError("responsibility must be researcher, planner, executor, reviewer, or frontend for set/remove");
379
- }
380
- const proposedRoute = args.action === "set" ? resolveRoleRoute({
381
- provider: args.provider,
382
- model: args.model,
383
- ...(args.reasoningEffort === undefined ? {} : { reasoningEffort: args.reasoningEffort }),
384
- ...(args.maxTokens === undefined ? {} : { maxTokens: args.maxTokens }),
385
- }, args.responsibility) : undefined;
386
-
387
- const commit = (validationStatus) => {
388
- const releaseLock = acquireRoutingStoreLock(configPath);
389
- try {
390
- let current;
391
- let recoveredInvalidStore = false;
392
- try {
393
- current = readRoutingStore(configPath);
394
- } catch (error) {
395
- if (args.action !== "set") {
396
- throw new Error("Odai routing configuration is invalid; ask the user to set a responsibility mapping to repair it automatically", { cause: error });
397
- }
398
- preserveInvalidRoutingStore(configPath);
399
- current = { roles: {} };
400
- recoveredInvalidStore = true;
401
- }
402
-
403
- const roles = { ...current.roles };
404
- if (args.action === "remove") delete roles[args.responsibility];
405
- else roles[args.responsibility] = proposedRoute;
406
- writeRoutingStore(configPath, roles);
407
- onConfigured(execution.agent, {
408
- action: args.action,
409
- responsibility: args.responsibility,
410
- route: roles[args.responsibility],
411
- ...(validationStatus === "verified" ? { validationStatus } : {}),
412
- ...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
413
- });
414
- return resultFor(
415
- configPath,
416
- args.action,
417
- effectiveRoutingSnapshot(configPath, configuredRoles),
418
- args.responsibility,
419
- recoveredInvalidStore,
420
- );
421
- } finally {
422
- releaseLock();
423
- }
424
- };
425
- if (args.action === "set") {
426
- return requireModelRoute(
427
- resolveCallConfig,
428
- proposedRoute,
429
- execution.signal,
430
- `${args.responsibility} responsibility route`,
431
- ).then((validation) => commit(validation.status));
432
- }
433
- return Promise.resolve(commit());
434
- },
435
- };
436
- }
1
+ import { randomUUID } from "node:crypto";
2
+ import {
3
+ copyFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ renameSync,
8
+ rmSync,
9
+ statSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { homedir } from "node:os";
13
+ import { dirname, resolve } from "node:path";
14
+
15
+ import { requireModelRoute, sameModelRoute } from "./model-route.mjs";
16
+ import {
17
+ IN_PLACE_OUTPUT_RESPONSIBILITIES,
18
+ resolveInPlaceResponsibilityOutputBudgets,
19
+ } from "./output-config.mjs";
20
+
21
+ export const CONFIGURABLE_ROLES = Object.freeze(["researcher", "planner", "executor", "reviewer", "frontend"]);
22
+ export const ROUTING_CONFIG_PROMPT = [
23
+ "## Odai responsibility model configuration",
24
+ "When the user naturally asks to inspect, set, change, or remove the research/investigation, planning/planner, execution/executor, review/acceptance/reviewer, or frontend design/implementation model, use odai_routing_config.",
25
+ "Translate the user's natural responsibility wording into the tool's responsibility field. Do not require internal routing terms or a special prompt form.",
26
+ "For set, call the tool only after the user explicitly supplies both provider and model. Pass reasoningEffort or maxTokens only when the user supplies them; otherwise omit them.",
27
+ "Never infer, recommend as chosen, or silently select any provider, model, effort, token limit, or price. Ask a concise clarification when provider or model is ambiguous.",
28
+ "Researcher routing is task-gated but not price-aware. A researcher mapping enables the narrow trigger but does not guarantee lower cost; compare actual provider prices and measured usage without inventing either.",
29
+ "A generic subagent is not proof that a configured responsibility ran. When a real responsibility gap emerges after initial routing and manual delegation is necessary, begin the subagent label with `odai-<responsibility>` followed by a space or colon; otherwise keep it generic and do not claim the responsibility mapping was used.",
30
+ "Do not ask the user to edit YAML, JSON, managed Agent files, or Plugin configuration. The tool owns persistence. A set/remove change applies from the next user turn.",
31
+ "Base controller selection remains host-owned and is not changed by this tool.",
32
+ ].join("\n");
33
+ const ROLE_ROUTE_FIELDS = new Set(["provider", "model", "reasoningEffort", "maxTokens"]);
34
+ const STORE_SCHEMA_VERSION = 1;
35
+
36
+ function assertPlainObject(value, field) {
37
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
38
+ throw new TypeError(`${field} must be an object`);
39
+ }
40
+ return value;
41
+ }
42
+
43
+ export function resolveRoleRoute(value, role) {
44
+ if (value === undefined) return undefined;
45
+ if (!CONFIGURABLE_ROLES.includes(role)) throw new TypeError(`unknown odai routing responsibility: ${role}`);
46
+ const route = assertPlainObject(value, `routing role ${role}`);
47
+ const unknown = Object.keys(route).filter((field) => !ROLE_ROUTE_FIELDS.has(field));
48
+ if (unknown.length > 0) {
49
+ throw new TypeError(`routing role ${role} has unknown fields: ${unknown.join(", ")}`);
50
+ }
51
+ if (typeof route.provider !== "string" || route.provider.trim() === "") {
52
+ throw new TypeError(`routing role ${role}.provider must be a non-empty string`);
53
+ }
54
+ if (typeof route.model !== "string" || route.model.trim() === "") {
55
+ throw new TypeError(`routing role ${role}.model must be a non-empty string`);
56
+ }
57
+ if (route.reasoningEffort !== undefined
58
+ && (typeof route.reasoningEffort !== "string" || route.reasoningEffort.trim() === "")) {
59
+ throw new TypeError(`routing role ${role}.reasoningEffort must be a non-empty string`);
60
+ }
61
+ if (route.maxTokens !== undefined
62
+ && (!Number.isSafeInteger(route.maxTokens) || route.maxTokens <= 0)) {
63
+ throw new TypeError(`routing role ${role}.maxTokens must be a positive integer`);
64
+ }
65
+
66
+ return Object.freeze({
67
+ provider: route.provider.trim(),
68
+ model: route.model.trim(),
69
+ ...(route.reasoningEffort === undefined ? {} : { reasoningEffort: route.reasoningEffort.trim() }),
70
+ ...(route.maxTokens === undefined ? {} : { maxTokens: route.maxTokens }),
71
+ });
72
+ }
73
+
74
+ export function resolveRoutingConfigPath(configuredPath, env = process.env) {
75
+ if (configuredPath !== undefined) {
76
+ if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
77
+ throw new TypeError("config.routing.configPath must be a non-empty string");
78
+ }
79
+ return resolve(configuredPath.trim());
80
+ }
81
+ const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME.trim() !== ""
82
+ ? resolve(env.DSH_HOME.trim())
83
+ : resolve(homedir(), ".dsh");
84
+ return resolve(dshHome, "odai", "routing.json");
85
+ }
86
+
87
+ export function readRoutingStore(configPath) {
88
+ if (!existsSync(configPath)) {
89
+ return Object.freeze({ schemaVersion: STORE_SCHEMA_VERSION, roles: Object.freeze({}) });
90
+ }
91
+
92
+ let parsed;
93
+ try {
94
+ parsed = JSON.parse(readFileSync(configPath, "utf8"));
95
+ } catch (error) {
96
+ throw new Error(`cannot read odai routing config ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
97
+ }
98
+ const store = assertPlainObject(parsed, `odai routing config ${configPath}`);
99
+ const unknownStoreFields = Object.keys(store).filter((field) => !["schemaVersion", "roles"].includes(field));
100
+ if (unknownStoreFields.length > 0) {
101
+ throw new TypeError(`odai routing config ${configPath} has unknown fields: ${unknownStoreFields.join(", ")}`);
102
+ }
103
+ if (store.schemaVersion !== STORE_SCHEMA_VERSION) {
104
+ throw new TypeError(`odai routing config ${configPath} has unsupported schemaVersion ${String(store.schemaVersion)}`);
105
+ }
106
+ const rawRoles = assertPlainObject(store.roles, `odai routing config ${configPath}.roles`);
107
+ const unknown = Object.keys(rawRoles).filter((role) => !CONFIGURABLE_ROLES.includes(role));
108
+ if (unknown.length > 0) {
109
+ throw new TypeError(`odai routing config ${configPath} has unknown roles: ${unknown.join(", ")}`);
110
+ }
111
+ return Object.freeze({
112
+ schemaVersion: STORE_SCHEMA_VERSION,
113
+ roles: Object.freeze(Object.fromEntries(CONFIGURABLE_ROLES.flatMap((role) => {
114
+ const route = resolveRoleRoute(rawRoles[role], role);
115
+ return route ? [[role, route]] : [];
116
+ }))),
117
+ });
118
+ }
119
+
120
+ export function effectiveRoleRoute(configPath, configuredRoles, role) {
121
+ return effectiveRoutingSnapshot(configPath, configuredRoles).roles[role];
122
+ }
123
+
124
+ export function effectiveRoutingSnapshot(configPath, configuredRoles = {}) {
125
+ const persisted = readRoutingStore(configPath).roles;
126
+ const roles = {};
127
+ const sources = {};
128
+ for (const role of CONFIGURABLE_ROLES) {
129
+ const route = persisted[role] ?? configuredRoles[role];
130
+ if (!route) continue;
131
+ roles[role] = route;
132
+ sources[role] = persisted[role] ? "persisted-mapping" : "deployment-config";
133
+ }
134
+ return Object.freeze({
135
+ roles: Object.freeze(roles),
136
+ sources: Object.freeze(sources),
137
+ });
138
+ }
139
+
140
+ function writeRoutingStore(configPath, roles) {
141
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
142
+ const temporary = `${configPath}.tmp-${process.pid}-${randomUUID()}`;
143
+ const value = `${JSON.stringify({ schemaVersion: STORE_SCHEMA_VERSION, roles }, null, 2)}\n`;
144
+ try {
145
+ writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600, flag: "wx" });
146
+ renameSync(temporary, configPath);
147
+ } finally {
148
+ rmSync(temporary, { force: true });
149
+ }
150
+ }
151
+
152
+ function acquireRoutingStoreLock(configPath) {
153
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
154
+ const lockPath = `${configPath}.lock`;
155
+ const create = () => writeFileSync(lockPath, `${process.pid}\n`, {
156
+ encoding: "utf8",
157
+ mode: 0o600,
158
+ flag: "wx",
159
+ });
160
+ try {
161
+ create();
162
+ } catch (error) {
163
+ if (error?.code !== "EEXIST") throw error;
164
+ const stale = (() => {
165
+ try {
166
+ return Date.now() - statSync(lockPath).mtimeMs > 30_000;
167
+ } catch {
168
+ return false;
169
+ }
170
+ })();
171
+ if (!stale) throw new Error("Odai routing configuration is being updated; retry the tool call");
172
+ rmSync(lockPath, { force: true });
173
+ create();
174
+ }
175
+ return () => rmSync(lockPath, { force: true });
176
+ }
177
+
178
+ function preserveInvalidRoutingStore(configPath) {
179
+ if (!existsSync(configPath)) return;
180
+ renameSync(configPath, `${configPath}.invalid-${Date.now()}-${randomUUID()}`);
181
+ }
182
+
183
+ export function invalidatePersistedRoleRoute(configPath, role, expectedRoute) {
184
+ if (!CONFIGURABLE_ROLES.includes(role)) throw new TypeError(`unknown odai routing responsibility: ${role}`);
185
+ const releaseLock = acquireRoutingStoreLock(configPath);
186
+ try {
187
+ const current = readRoutingStore(configPath);
188
+ const persisted = current.roles[role];
189
+ if (!sameModelRoute(persisted, expectedRoute)) {
190
+ return Object.freeze({ invalidated: false, reason: "mapping-changed-or-not-persisted" });
191
+ }
192
+ const backupPath = `${configPath}.invalidated-${Date.now()}-${randomUUID()}`;
193
+ copyFileSync(configPath, backupPath);
194
+ const roles = { ...current.roles };
195
+ delete roles[role];
196
+ writeRoutingStore(configPath, roles);
197
+ return Object.freeze({ invalidated: true, backupPath, route: persisted });
198
+ } finally {
199
+ releaseLock();
200
+ }
201
+ }
202
+
203
+ function routeSchema(required) {
204
+ return {
205
+ type: "object",
206
+ additionalProperties: false,
207
+ ...(required ? { required: ["provider", "model"] } : {}),
208
+ properties: {
209
+ provider: { type: "string" },
210
+ model: { type: "string" },
211
+ reasoningEffort: { type: "string" },
212
+ maxTokens: { type: "integer" },
213
+ },
214
+ };
215
+ }
216
+
217
+ function resultFor(configPath, action, snapshot, responsibility, recoveredInvalidStore = false, latestRoute, outputPolicy) {
218
+ const responsibilityBudgets = resolveInPlaceResponsibilityOutputBudgets(outputPolicy, snapshot.roles);
219
+ return {
220
+ action,
221
+ ...(responsibility ? { responsibility } : {}),
222
+ configPath,
223
+ roles: snapshot.roles,
224
+ sources: snapshot.sources,
225
+ requiresNextTurn: action !== "show",
226
+ ...(responsibilityBudgets ? { responsibilityBudgets } : {}),
227
+ ...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
228
+ ...(latestRoute ? { latestRoute } : {}),
229
+ };
230
+ }
231
+
232
+ export function createRoutingConfigTool(configPath, options = {}) {
233
+ const onConfigured = typeof options.onConfigured === "function" ? options.onConfigured : () => {};
234
+ const configuredRoles = options.configuredRoles ?? {};
235
+ const latestRouteFor = typeof options.latestRouteFor === "function" ? options.latestRouteFor : () => undefined;
236
+ const outputPolicyFor = typeof options.outputPolicyFor === "function" ? options.outputPolicyFor : () => undefined;
237
+ const resolveCallConfig = typeof options.resolveCallConfig === "function" ? options.resolveCallConfig : undefined;
238
+ return {
239
+ name: "odai_routing_config",
240
+ description: [
241
+ "Inspect effective Odai model mappings and the latest current-session route receipt, or set/remove persisted mappings for researcher, planner, executor, reviewer, and frontend responsibilities.",
242
+ "Use this only when the user naturally and explicitly asks to inspect/remove a mapping or names the provider/model to set.",
243
+ "Never choose a provider, model, reasoning effort, token limit, or price on the user's behalf.",
244
+ "Researcher routing is task-gated but not price-aware; its mapping does not guarantee lower cost.",
245
+ "For set/remove, handle one responsibility per call. Persisted mappings are shared by the Odai DSH Plugin and Agent and apply from the next user turn.",
246
+ "Results expose configured in-place responsibility ceilings and warn when planner, executor, or frontend mappings without maxTokens inherit the controller ceiling; never invent an override.",
247
+ ].join(" "),
248
+ parameters: {
249
+ type: "object",
250
+ additionalProperties: false,
251
+ required: ["action"],
252
+ properties: {
253
+ action: {
254
+ type: "string",
255
+ enum: ["show", "set", "remove"],
256
+ description: "Show all mappings, set one user-specified mapping, or remove one mapping.",
257
+ },
258
+ responsibility: {
259
+ type: "string",
260
+ enum: [...CONFIGURABLE_ROLES],
261
+ description: "Required for set/remove; omitted for show.",
262
+ },
263
+ provider: {
264
+ type: "string",
265
+ description: "Provider id explicitly supplied by the user; required for set.",
266
+ },
267
+ model: {
268
+ type: "string",
269
+ description: "Model id explicitly supplied by the user; required for set.",
270
+ },
271
+ reasoningEffort: {
272
+ type: "string",
273
+ description: "Optional reasoning effort explicitly supplied by the user.",
274
+ },
275
+ maxTokens: {
276
+ type: "integer",
277
+ description: "Optional positive output limit explicitly supplied by the user. It limits routed child requests and explicitly overrides the global controller ceiling only inside the same planner, executor, or frontend responsibility scope when that responsibility runs in-place."
278
+ },
279
+ },
280
+ },
281
+ output: {
282
+ schema: {
283
+ type: "object",
284
+ additionalProperties: false,
285
+ required: ["action", "configPath", "roles", "sources", "requiresNextTurn"],
286
+ properties: {
287
+ action: { type: "string", enum: ["show", "set", "remove"] },
288
+ responsibility: { type: "string", enum: [...CONFIGURABLE_ROLES] },
289
+ configPath: { type: "string" },
290
+ roles: {
291
+ type: "object",
292
+ additionalProperties: false,
293
+ properties: Object.fromEntries(CONFIGURABLE_ROLES.map((role) => [role, routeSchema(true)])),
294
+ },
295
+ sources: {
296
+ type: "object",
297
+ additionalProperties: false,
298
+ properties: Object.fromEntries(CONFIGURABLE_ROLES.map((role) => [role, {
299
+ type: "string",
300
+ enum: ["persisted-mapping", "deployment-config"],
301
+ }])),
302
+ },
303
+ requiresNextTurn: { type: "boolean" },
304
+ recoveredInvalidStore: { type: "boolean" },
305
+ responsibilityBudgets: {
306
+ type: "object",
307
+ additionalProperties: false,
308
+ properties: Object.fromEntries(IN_PLACE_OUTPUT_RESPONSIBILITIES.map((responsibility) => [responsibility, {
309
+ type: "object",
310
+ additionalProperties: false,
311
+ required: ["source"],
312
+ properties: {
313
+ source: { type: "string", enum: ["responsibility-override", "controller-policy", "unbounded-by-odai"] },
314
+ maxTokens: { type: "integer" },
315
+ warning: { type: "string", enum: ["responsibility-inherits-controller-ceiling"] },
316
+ },
317
+ }])),
318
+ },
319
+ latestRoute: {
320
+ type: "object",
321
+ additionalProperties: false,
322
+ required: ["turn", "step", "responsibility", "status", "routeSource", "fallbackUsed"],
323
+ properties: {
324
+ turn: { type: "integer" },
325
+ step: { type: "integer" },
326
+ responsibility: { type: "string", enum: [...CONFIGURABLE_ROLES] },
327
+ responsibilityScopeId: { type: "string" },
328
+ routeCardId: { type: "string" },
329
+ status: { type: "string", enum: ["applied", "mismatch", "unverified", "fallback"] },
330
+ taskStatus: { type: "string", enum: ["completed", "fallback"] },
331
+ routeMode: { type: "string", enum: ["inline", "same-turn", "child"] },
332
+ routeSource: { type: "string", enum: ["persisted-mapping", "deployment-config"] },
333
+ fallbackUsed: { type: "boolean" },
334
+ requestedRoute: routeSchema(true),
335
+ actualRoute: routeSchema(true),
336
+ stopReason: { type: "string" },
337
+ error: { type: "string" },
338
+ taskStopReason: { type: "string" },
339
+ taskError: { type: "string" },
340
+ },
341
+ },
342
+ },
343
+ },
344
+ render(_args, value) {
345
+ const mapping = Object.entries(value.roles)
346
+ .map(([role, route]) => {
347
+ const options = [
348
+ ...(route.reasoningEffort ? [`reasoningEffort=${route.reasoningEffort}`] : []),
349
+ ...(route.maxTokens ? [`maxTokens=${route.maxTokens}`] : []),
350
+ ];
351
+ return `${role}: ${route.provider}/${route.model}${options.length > 0 ? ` (${options.join(", ")})` : ""} [${value.sources[role]}]`;
352
+ })
353
+ .join("\n") || "No Odai responsibility models are configured.";
354
+ const responsibilityBudgets = Object.entries(value.responsibilityBudgets ?? {});
355
+ const configuredResponsibilities = responsibilityBudgets.length > 0
356
+ ? `\nIn-place responsibility ceilings: ${responsibilityBudgets.map(([responsibility, budget]) => (
357
+ `${responsibility}=${budget.maxTokens === undefined ? "no Odai maxTokens" : `maxTokens=${budget.maxTokens}`} [${budget.source}]`
358
+ )).join("; ")}.`
359
+ : "";
360
+ const inheritedWarnings = responsibilityBudgets
361
+ .filter(([, budget]) => budget.warning === "responsibility-inherits-controller-ceiling")
362
+ .map(([responsibility]) => `\nWarning: ${responsibility} has no explicit maxTokens and inherits the controller ceiling when routed in-place; providers may count reasoning and truncate substantial work.`)
363
+ .join("");
364
+ const latestRoute = value.latestRoute
365
+ ? [
366
+ "\nLatest current-session responsibility route receipt:",
367
+ `responsibility=${value.latestRoute.responsibility} status=${value.latestRoute.status} mode=${value.latestRoute.routeMode ?? "unknown"}`,
368
+ `routeSource=${value.latestRoute.routeSource} fallbackUsed=${String(value.latestRoute.fallbackUsed)}`,
369
+ ...(value.latestRoute.responsibilityScopeId ? [`responsibilityScope=${value.latestRoute.responsibilityScopeId}`] : []),
370
+ ...(value.latestRoute.routeCardId ? [`routeCard=${value.latestRoute.routeCardId}`] : []),
371
+ ...(value.latestRoute.taskStatus ? [`taskStatus=${value.latestRoute.taskStatus}`] : []),
372
+ ...(value.latestRoute.taskStopReason ? [`taskStopReason=${value.latestRoute.taskStopReason}`] : []),
373
+ ...(value.latestRoute.error ? [`routeError=${value.latestRoute.error}`] : []),
374
+ ...(value.latestRoute.taskError ? [`taskError=${value.latestRoute.taskError}`] : []),
375
+ ...(value.latestRoute.actualRoute
376
+ ? [`actual=${value.latestRoute.actualRoute.provider}/${value.latestRoute.actualRoute.model} (${[
377
+ `reasoningEffort=${value.latestRoute.actualRoute.reasoningEffort ?? "unspecified"}`,
378
+ ...(value.latestRoute.actualRoute.maxTokens === undefined ? [] : [`maxTokens=${value.latestRoute.actualRoute.maxTokens}`]),
379
+ ].join(", ")})`]
380
+ : ["actual=<unverified>"]),
381
+ ].join("\n")
382
+ : value.action === "show"
383
+ ? "\nNo mapped responsibility route receipt is recorded in this session."
384
+ : "";
385
+ return [{
386
+ type: "text",
387
+ text: [
388
+ `${value.action === "show" ? "Current" : "Updated"} Odai routing configuration:\n${mapping}`,
389
+ ...(value.recoveredInvalidStore ? ["\nAn invalid prior store was preserved and replaced."] : []),
390
+ configuredResponsibilities,
391
+ inheritedWarnings,
392
+ ...(value.roles.researcher ? ["\nResearcher routing is task-gated but not price-aware. This mapping does not guarantee lower cost; compare actual provider prices and measured usage."] : []),
393
+ latestRoute,
394
+ ...(value.requiresNextTurn ? ["\nThe change applies from the next user turn."] : []),
395
+ ].join(""),
396
+ }];
397
+ },
398
+ },
399
+ execute(args, execution) {
400
+ if (!execution.agent) throw new Error("odai_routing_config requires an owning agent session");
401
+ const header = execution.agent.session?.header;
402
+ if (header?.origin === "subagent" || (Number.isSafeInteger(header?.delegationDepth) && header.delegationDepth > 0)) {
403
+ throw new Error("child agents may not change Odai routing configuration");
404
+ }
405
+ if (!args || typeof args !== "object" || Array.isArray(args)) throw new TypeError("arguments must be an object");
406
+ if (!["show", "set", "remove"].includes(args.action)) throw new TypeError("action must be show, set, or remove");
407
+
408
+ if (args.action === "show") {
409
+ return Promise.resolve(resultFor(
410
+ configPath,
411
+ "show",
412
+ effectiveRoutingSnapshot(configPath, configuredRoles),
413
+ undefined,
414
+ false,
415
+ latestRouteFor(execution.agent),
416
+ outputPolicyFor(),
417
+ ));
418
+ }
419
+ if (!CONFIGURABLE_ROLES.includes(args.responsibility)) {
420
+ throw new TypeError("responsibility must be researcher, planner, executor, reviewer, or frontend for set/remove");
421
+ }
422
+ const proposedRoute = args.action === "set" ? resolveRoleRoute({
423
+ provider: args.provider,
424
+ model: args.model,
425
+ ...(args.reasoningEffort === undefined ? {} : { reasoningEffort: args.reasoningEffort }),
426
+ ...(args.maxTokens === undefined ? {} : { maxTokens: args.maxTokens }),
427
+ }, args.responsibility) : undefined;
428
+
429
+ const commit = (validationStatus) => {
430
+ const releaseLock = acquireRoutingStoreLock(configPath);
431
+ try {
432
+ let current;
433
+ let recoveredInvalidStore = false;
434
+ try {
435
+ current = readRoutingStore(configPath);
436
+ } catch (error) {
437
+ if (args.action !== "set") {
438
+ throw new Error("Odai routing configuration is invalid; ask the user to set a responsibility mapping to repair it automatically", { cause: error });
439
+ }
440
+ preserveInvalidRoutingStore(configPath);
441
+ current = { roles: {} };
442
+ recoveredInvalidStore = true;
443
+ }
444
+
445
+ const roles = { ...current.roles };
446
+ if (args.action === "remove") delete roles[args.responsibility];
447
+ else roles[args.responsibility] = proposedRoute;
448
+ writeRoutingStore(configPath, roles);
449
+ onConfigured(execution.agent, {
450
+ action: args.action,
451
+ responsibility: args.responsibility,
452
+ route: roles[args.responsibility],
453
+ ...(validationStatus === "verified" ? { validationStatus } : {}),
454
+ ...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
455
+ });
456
+ return resultFor(
457
+ configPath,
458
+ args.action,
459
+ effectiveRoutingSnapshot(configPath, configuredRoles),
460
+ args.responsibility,
461
+ recoveredInvalidStore,
462
+ undefined,
463
+ outputPolicyFor(),
464
+ );
465
+ } finally {
466
+ releaseLock();
467
+ }
468
+ };
469
+ if (args.action === "set") {
470
+ return requireModelRoute(
471
+ resolveCallConfig,
472
+ proposedRoute,
473
+ execution.signal,
474
+ `${args.responsibility} responsibility route`,
475
+ ).then((validation) => commit(validation.status));
476
+ }
477
+ return Promise.resolve(commit());
478
+ },
479
+ };
480
+ }