odai-dsh-agent 0.2.5 → 0.2.7

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 (166) hide show
  1. package/README.md +3 -3
  2. package/build/bin/odai-dsh-agent.d.mts +3 -0
  3. package/build/bin/odai-dsh-agent.d.mts.map +1 -0
  4. package/build/bin/odai-dsh-agent.mjs +114 -0
  5. package/build/bin/odai-dsh-agent.mjs.map +1 -0
  6. package/build/src/dsh-version.d.mts +16 -0
  7. package/build/src/dsh-version.d.mts.map +1 -0
  8. package/build/src/dsh-version.mjs +20 -0
  9. package/build/src/dsh-version.mjs.map +1 -0
  10. package/build/src/installer.d.mts +46 -0
  11. package/build/src/installer.d.mts.map +1 -0
  12. package/build/src/installer.mjs +398 -0
  13. package/build/src/installer.mjs.map +1 -0
  14. package/package.json +13 -12
  15. package/preset/odai/runtime/compaction-config.d.mts +66 -0
  16. package/preset/odai/runtime/compaction-config.d.mts.map +1 -0
  17. package/preset/odai/runtime/compaction-config.mjs +320 -342
  18. package/preset/odai/runtime/compaction-config.mjs.map +1 -0
  19. package/preset/odai/runtime/compaction-runtime.d.mts +68 -0
  20. package/preset/odai/runtime/compaction-runtime.d.mts.map +1 -0
  21. package/preset/odai/runtime/compaction-runtime.mjs +157 -0
  22. package/preset/odai/runtime/compaction-runtime.mjs.map +1 -0
  23. package/preset/odai/runtime/context-activation.d.mts +31 -0
  24. package/preset/odai/runtime/context-activation.d.mts.map +1 -0
  25. package/preset/odai/runtime/context-activation.mjs +55 -49
  26. package/preset/odai/runtime/context-activation.mjs.map +1 -0
  27. package/preset/odai/runtime/context-capability.d.mts +16 -0
  28. package/preset/odai/runtime/context-capability.d.mts.map +1 -0
  29. package/preset/odai/runtime/context-capability.mjs +79 -74
  30. package/preset/odai/runtime/context-capability.mjs.map +1 -0
  31. package/preset/odai/runtime/governance.d.mts +30 -0
  32. package/preset/odai/runtime/governance.d.mts.map +1 -0
  33. package/preset/odai/runtime/governance.mjs +126 -113
  34. package/preset/odai/runtime/governance.mjs.map +1 -0
  35. package/preset/odai/runtime/human-care.d.mts +13 -0
  36. package/preset/odai/runtime/human-care.d.mts.map +1 -0
  37. package/preset/odai/runtime/human-care.mjs +45 -42
  38. package/preset/odai/runtime/human-care.mjs.map +1 -0
  39. package/preset/odai/runtime/human-safety-continuity-store.d.mts +43 -0
  40. package/preset/odai/runtime/human-safety-continuity-store.d.mts.map +1 -0
  41. package/preset/odai/runtime/human-safety-continuity-store.mjs +193 -215
  42. package/preset/odai/runtime/human-safety-continuity-store.mjs.map +1 -0
  43. package/preset/odai/runtime/human-safety-continuity.d.mts +27 -0
  44. package/preset/odai/runtime/human-safety-continuity.d.mts.map +1 -0
  45. package/preset/odai/runtime/human-safety-continuity.mjs +205 -216
  46. package/preset/odai/runtime/human-safety-continuity.mjs.map +1 -0
  47. package/preset/odai/runtime/human-safety.d.mts +14 -0
  48. package/preset/odai/runtime/human-safety.d.mts.map +1 -0
  49. package/preset/odai/runtime/human-safety.mjs +50 -47
  50. package/preset/odai/runtime/human-safety.mjs.map +1 -0
  51. package/preset/odai/runtime/index.d.mts +8 -0
  52. package/preset/odai/runtime/index.d.mts.map +1 -0
  53. package/preset/odai/runtime/index.mjs +190 -2473
  54. package/preset/odai/runtime/index.mjs.map +1 -0
  55. package/preset/odai/runtime/lifecycle-runtime.d.mts +83 -0
  56. package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -0
  57. package/preset/odai/runtime/lifecycle-runtime.mjs +1140 -0
  58. package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -0
  59. package/preset/odai/runtime/model-route.d.mts +22 -0
  60. package/preset/odai/runtime/model-route.d.mts.map +1 -0
  61. package/preset/odai/runtime/model-route.mjs +68 -60
  62. package/preset/odai/runtime/model-route.mjs.map +1 -0
  63. package/preset/odai/runtime/output-config.d.mts +54 -0
  64. package/preset/odai/runtime/output-config.d.mts.map +1 -0
  65. package/preset/odai/runtime/output-config.mjs +289 -315
  66. package/preset/odai/runtime/output-config.mjs.map +1 -0
  67. package/preset/odai/runtime/output-policy-state.d.mts +3 -0
  68. package/preset/odai/runtime/output-policy-state.d.mts.map +1 -0
  69. package/preset/odai/runtime/output-policy-state.mjs +41 -29
  70. package/preset/odai/runtime/output-policy-state.mjs.map +1 -0
  71. package/preset/odai/runtime/prompt-runtime.d.mts +55 -0
  72. package/preset/odai/runtime/prompt-runtime.d.mts.map +1 -0
  73. package/preset/odai/runtime/prompt-runtime.mjs +303 -0
  74. package/preset/odai/runtime/prompt-runtime.mjs.map +1 -0
  75. package/preset/odai/runtime/research-packet.d.mts +30 -0
  76. package/preset/odai/runtime/research-packet.d.mts.map +1 -0
  77. package/preset/odai/runtime/research-packet.mjs +116 -117
  78. package/preset/odai/runtime/research-packet.mjs.map +1 -0
  79. package/preset/odai/runtime/responsibility-gap.d.mts +26 -0
  80. package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -0
  81. package/preset/odai/runtime/responsibility-gap.mjs +93 -85
  82. package/preset/odai/runtime/responsibility-gap.mjs.map +1 -0
  83. package/preset/odai/runtime/responsibility-scope.d.mts +69 -0
  84. package/preset/odai/runtime/responsibility-scope.d.mts.map +1 -0
  85. package/preset/odai/runtime/responsibility-scope.mjs +199 -171
  86. package/preset/odai/runtime/responsibility-scope.mjs.map +1 -0
  87. package/preset/odai/runtime/role-overlays.d.mts +5 -0
  88. package/preset/odai/runtime/role-overlays.d.mts.map +1 -0
  89. package/preset/odai/runtime/role-overlays.mjs +15 -14
  90. package/preset/odai/runtime/role-overlays.mjs.map +1 -0
  91. package/preset/odai/runtime/route-card.d.mts +42 -0
  92. package/preset/odai/runtime/route-card.d.mts.map +1 -0
  93. package/preset/odai/runtime/route-card.mjs +158 -146
  94. package/preset/odai/runtime/route-card.mjs.map +1 -0
  95. package/preset/odai/runtime/router.d.mts +80 -0
  96. package/preset/odai/runtime/router.d.mts.map +1 -0
  97. package/preset/odai/runtime/router.mjs +509 -559
  98. package/preset/odai/runtime/router.mjs.map +1 -0
  99. package/preset/odai/runtime/routing-config.d.mts +76 -0
  100. package/preset/odai/runtime/routing-config.d.mts.map +1 -0
  101. package/preset/odai/runtime/routing-config.mjs +373 -438
  102. package/preset/odai/runtime/routing-config.mjs.map +1 -0
  103. package/preset/odai/runtime/routing-context.d.mts +64 -0
  104. package/preset/odai/runtime/routing-context.d.mts.map +1 -0
  105. package/preset/odai/runtime/routing-context.mjs +398 -277
  106. package/preset/odai/runtime/routing-context.mjs.map +1 -0
  107. package/preset/odai/runtime/runtime-config.d.mts +24 -0
  108. package/preset/odai/runtime/runtime-config.d.mts.map +1 -0
  109. package/preset/odai/runtime/runtime-config.mjs +181 -0
  110. package/preset/odai/runtime/runtime-config.mjs.map +1 -0
  111. package/preset/odai/runtime/runtime-support.d.mts +91 -0
  112. package/preset/odai/runtime/runtime-support.d.mts.map +1 -0
  113. package/preset/odai/runtime/runtime-support.mjs +371 -0
  114. package/preset/odai/runtime/runtime-support.mjs.map +1 -0
  115. package/preset/odai/runtime/runtime-types.d.mts +237 -0
  116. package/preset/odai/runtime/runtime-types.d.mts.map +1 -0
  117. package/preset/odai/runtime/runtime-types.mjs +4 -0
  118. package/preset/odai/runtime/runtime-types.mjs.map +1 -0
  119. package/preset/odai/runtime/semantic-memory-store.d.mts +110 -0
  120. package/preset/odai/runtime/semantic-memory-store.d.mts.map +1 -0
  121. package/preset/odai/runtime/semantic-memory-store.mjs +271 -385
  122. package/preset/odai/runtime/semantic-memory-store.mjs.map +1 -0
  123. package/preset/odai/runtime/semantic-memory.d.mts +88 -0
  124. package/preset/odai/runtime/semantic-memory.d.mts.map +1 -0
  125. package/preset/odai/runtime/semantic-memory.mjs +790 -750
  126. package/preset/odai/runtime/semantic-memory.mjs.map +1 -0
  127. package/preset/odai/runtime/session-compat.d.mts +31 -0
  128. package/preset/odai/runtime/session-compat.d.mts.map +1 -0
  129. package/preset/odai/runtime/session-compat.mjs +301 -328
  130. package/preset/odai/runtime/session-compat.mjs.map +1 -0
  131. package/preset/odai/runtime/session-evidence.d.mts +25 -0
  132. package/preset/odai/runtime/session-evidence.d.mts.map +1 -0
  133. package/preset/odai/runtime/session-evidence.mjs +309 -287
  134. package/preset/odai/runtime/session-evidence.mjs.map +1 -0
  135. package/preset/odai/runtime/skill-bundle.d.mts +53 -0
  136. package/preset/odai/runtime/skill-bundle.d.mts.map +1 -0
  137. package/preset/odai/runtime/skill-bundle.mjs +286 -276
  138. package/preset/odai/runtime/skill-bundle.mjs.map +1 -0
  139. package/preset/odai/runtime/skill-evolution.d.mts +112 -0
  140. package/preset/odai/runtime/skill-evolution.d.mts.map +1 -0
  141. package/preset/odai/runtime/skill-evolution.mjs +1287 -1261
  142. package/preset/odai/runtime/skill-evolution.mjs.map +1 -0
  143. package/preset/odai/runtime/skill-selection-state.d.mts +5 -0
  144. package/preset/odai/runtime/skill-selection-state.d.mts.map +1 -0
  145. package/preset/odai/runtime/skill-selection-state.mjs +59 -43
  146. package/preset/odai/runtime/skill-selection-state.mjs.map +1 -0
  147. package/preset/odai/runtime/skill-selector.d.mts +28 -0
  148. package/preset/odai/runtime/skill-selector.d.mts.map +1 -0
  149. package/preset/odai/runtime/skill-selector.mjs +100 -109
  150. package/preset/odai/runtime/skill-selector.mjs.map +1 -0
  151. package/preset/odai/runtime/skill-source-config.d.mts +32 -0
  152. package/preset/odai/runtime/skill-source-config.d.mts.map +1 -0
  153. package/preset/odai/runtime/skill-source-config.mjs +200 -210
  154. package/preset/odai/runtime/skill-source-config.mjs.map +1 -0
  155. package/preset/odai/runtime/store-lock.d.mts +2 -0
  156. package/preset/odai/runtime/store-lock.d.mts.map +1 -0
  157. package/preset/odai/runtime/store-lock.mjs +92 -87
  158. package/preset/odai/runtime/store-lock.mjs.map +1 -0
  159. package/preset/odai/runtime/tool-runtime.d.mts +43 -0
  160. package/preset/odai/runtime/tool-runtime.d.mts.map +1 -0
  161. package/preset/odai/runtime/tool-runtime.mjs +221 -0
  162. package/preset/odai/runtime/tool-runtime.mjs.map +1 -0
  163. package/bin/odai-dsh-agent.mjs +0 -108
  164. package/scripts/verify-agent-load.mjs +0 -260
  165. package/src/dsh-version.mjs +0 -24
  166. package/src/installer.mjs +0 -385
@@ -1,480 +1,415 @@
1
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";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
12
3
  import { homedir } from "node:os";
13
4
  import { dirname, resolve } from "node:path";
14
-
15
5
  import { requireModelRoute, sameModelRoute } from "./model-route.mjs";
16
- import {
17
- IN_PLACE_OUTPUT_RESPONSIBILITIES,
18
- resolveInPlaceResponsibilityOutputBudgets,
19
- } from "./output-config.mjs";
20
-
6
+ import { IN_PLACE_OUTPUT_RESPONSIBILITIES, resolveInPlaceResponsibilityOutputBudgets, } from "./output-config.mjs";
7
+ import { isUnknownRecord } from "./runtime-types.mjs";
21
8
  export const CONFIGURABLE_ROLES = Object.freeze(["researcher", "planner", "executor", "reviewer", "frontend"]);
22
9
  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.",
10
+ "## Odai responsibility model configuration",
11
+ "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.",
12
+ "Translate the user's natural responsibility wording into the tool's responsibility field. Do not require internal routing terms or a special prompt form.",
13
+ "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.",
14
+ "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.",
15
+ "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.",
16
+ "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.",
17
+ "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.",
18
+ "Base controller selection remains host-owned and is not changed by this tool.",
32
19
  ].join("\n");
33
20
  const ROLE_ROUTE_FIELDS = new Set(["provider", "model", "reasoningEffort", "maxTokens"]);
34
21
  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;
22
+ function isConfigurableRole(value) {
23
+ return typeof value === "string" && CONFIGURABLE_ROLES.includes(value);
24
+ }
25
+ function errorCode(error) {
26
+ if (error === null || typeof error !== "object" || !("code" in error))
27
+ return undefined;
28
+ return typeof error.code === "string" ? error.code : undefined;
41
29
  }
42
-
43
30
  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
- });
31
+ if (value === undefined)
32
+ return undefined;
33
+ if (!isConfigurableRole(role))
34
+ throw new TypeError(`unknown odai routing responsibility: ${String(role)}`);
35
+ if (!isUnknownRecord(value))
36
+ throw new TypeError(`routing role ${role} must be an object`);
37
+ const unknownFields = Object.keys(value).filter((field) => !ROLE_ROUTE_FIELDS.has(field));
38
+ if (unknownFields.length > 0) {
39
+ throw new TypeError(`routing role ${role} has unknown fields: ${unknownFields.join(", ")}`);
40
+ }
41
+ if (typeof value.provider !== "string" || value.provider.trim() === "") {
42
+ throw new TypeError(`routing role ${role}.provider must be a non-empty string`);
43
+ }
44
+ if (typeof value.model !== "string" || value.model.trim() === "") {
45
+ throw new TypeError(`routing role ${role}.model must be a non-empty string`);
46
+ }
47
+ if (value.reasoningEffort !== undefined
48
+ && (typeof value.reasoningEffort !== "string" || value.reasoningEffort.trim() === "")) {
49
+ throw new TypeError(`routing role ${role}.reasoningEffort must be a non-empty string`);
50
+ }
51
+ if (value.maxTokens !== undefined
52
+ && (typeof value.maxTokens !== "number" || !Number.isSafeInteger(value.maxTokens) || value.maxTokens <= 0)) {
53
+ throw new TypeError(`routing role ${role}.maxTokens must be a positive integer`);
54
+ }
55
+ return Object.freeze({
56
+ provider: value.provider.trim(),
57
+ model: value.model.trim(),
58
+ ...(typeof value.reasoningEffort === "string" ? { reasoningEffort: value.reasoningEffort.trim() } : {}),
59
+ ...(typeof value.maxTokens === "number" ? { maxTokens: value.maxTokens } : {}),
60
+ });
72
61
  }
73
-
74
62
  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");
63
+ if (configuredPath !== undefined) {
64
+ if (typeof configuredPath !== "string" || configuredPath.trim() === "") {
65
+ throw new TypeError("config.routing.configPath must be a non-empty string");
66
+ }
67
+ return resolve(configuredPath.trim());
78
68
  }
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");
69
+ const dshHome = typeof env.DSH_HOME === "string" && env.DSH_HOME.trim() !== ""
70
+ ? resolve(env.DSH_HOME.trim())
71
+ : resolve(homedir(), ".dsh");
72
+ return resolve(dshHome, "odai", "routing.json");
85
73
  }
86
-
87
74
  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
- });
75
+ if (!existsSync(configPath)) {
76
+ return Object.freeze({ schemaVersion: STORE_SCHEMA_VERSION, roles: Object.freeze({}) });
77
+ }
78
+ let parsed;
79
+ try {
80
+ parsed = JSON.parse(readFileSync(configPath, "utf8"));
81
+ }
82
+ catch (error) {
83
+ throw new Error(`cannot read odai routing config ${configPath}: ${error instanceof Error ? error.message : String(error)}`);
84
+ }
85
+ if (!isUnknownRecord(parsed))
86
+ throw new TypeError(`odai routing config ${configPath} must be an object`);
87
+ const unknownStoreFields = Object.keys(parsed).filter((field) => !["schemaVersion", "roles"].includes(field));
88
+ if (unknownStoreFields.length > 0) {
89
+ throw new TypeError(`odai routing config ${configPath} has unknown fields: ${unknownStoreFields.join(", ")}`);
90
+ }
91
+ if (parsed.schemaVersion !== STORE_SCHEMA_VERSION) {
92
+ throw new TypeError(`odai routing config ${configPath} has unsupported schemaVersion ${String(parsed.schemaVersion)}`);
93
+ }
94
+ if (!isUnknownRecord(parsed.roles))
95
+ throw new TypeError(`odai routing config ${configPath}.roles must be an object`);
96
+ const rawRoles = parsed.roles;
97
+ const unknownRoles = Object.keys(rawRoles).filter((role) => !isConfigurableRole(role));
98
+ if (unknownRoles.length > 0) {
99
+ throw new TypeError(`odai routing config ${configPath} has unknown roles: ${unknownRoles.join(", ")}`);
100
+ }
101
+ const entries = [];
102
+ for (const role of CONFIGURABLE_ROLES) {
103
+ const route = resolveRoleRoute(rawRoles[role], role);
104
+ if (route)
105
+ entries.push([role, route]);
106
+ }
107
+ return Object.freeze({
108
+ schemaVersion: STORE_SCHEMA_VERSION,
109
+ roles: Object.freeze(Object.fromEntries(entries)),
110
+ });
118
111
  }
119
-
120
112
  export function effectiveRoleRoute(configPath, configuredRoles, role) {
121
- return effectiveRoutingSnapshot(configPath, configuredRoles).roles[role];
113
+ return effectiveRoutingSnapshot(configPath, configuredRoles).roles[role];
122
114
  }
123
-
124
115
  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
- });
116
+ const persisted = readRoutingStore(configPath).roles;
117
+ const roles = {};
118
+ const sources = {};
119
+ for (const role of CONFIGURABLE_ROLES) {
120
+ const route = persisted[role] ?? configuredRoles[role];
121
+ if (!route)
122
+ continue;
123
+ roles[role] = route;
124
+ sources[role] = persisted[role] ? "persisted-mapping" : "deployment-config";
125
+ }
126
+ return Object.freeze({ roles: Object.freeze(roles), sources: Object.freeze(sources) });
138
127
  }
139
-
140
128
  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
- }
129
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
130
+ const temporary = `${configPath}.tmp-${process.pid}-${randomUUID()}`;
131
+ const value = `${JSON.stringify({ schemaVersion: STORE_SCHEMA_VERSION, roles }, null, 2)}\n`;
132
+ try {
133
+ writeFileSync(temporary, value, { encoding: "utf8", mode: 0o600, flag: "wx" });
134
+ renameSync(temporary, configPath);
135
+ }
136
+ finally {
137
+ rmSync(temporary, { force: true });
138
+ }
150
139
  }
151
-
152
140
  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 });
141
+ mkdirSync(dirname(configPath), { recursive: true, mode: 0o700 });
142
+ const lockPath = `${configPath}.lock`;
143
+ const create = () => writeFileSync(lockPath, `${process.pid}\n`, {
144
+ encoding: "utf8",
145
+ mode: 0o600,
146
+ flag: "wx",
147
+ });
148
+ try {
149
+ create();
150
+ }
151
+ catch (error) {
152
+ if (errorCode(error) !== "EEXIST")
153
+ throw error;
154
+ const stale = (() => {
155
+ try {
156
+ return Date.now() - statSync(lockPath).mtimeMs > 30_000;
157
+ }
158
+ catch {
159
+ return false;
160
+ }
161
+ })();
162
+ if (!stale)
163
+ throw new Error("Odai routing configuration is being updated; retry the tool call");
164
+ rmSync(lockPath, { force: true });
165
+ create();
166
+ }
167
+ return () => rmSync(lockPath, { force: true });
176
168
  }
177
-
178
169
  function preserveInvalidRoutingStore(configPath) {
179
- if (!existsSync(configPath)) return;
180
- renameSync(configPath, `${configPath}.invalid-${Date.now()}-${randomUUID()}`);
170
+ if (!existsSync(configPath))
171
+ return;
172
+ renameSync(configPath, `${configPath}.invalid-${Date.now()}-${randomUUID()}`);
181
173
  }
182
-
183
174
  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" });
175
+ if (!isConfigurableRole(role))
176
+ throw new TypeError(`unknown odai routing responsibility: ${String(role)}`);
177
+ const releaseLock = acquireRoutingStoreLock(configPath);
178
+ try {
179
+ const current = readRoutingStore(configPath);
180
+ const persisted = current.roles[role];
181
+ if (!sameModelRoute(persisted, expectedRoute) || !persisted) {
182
+ return Object.freeze({ invalidated: false, reason: "mapping-changed-or-not-persisted" });
183
+ }
184
+ const backupPath = `${configPath}.invalidated-${Date.now()}-${randomUUID()}`;
185
+ copyFileSync(configPath, backupPath);
186
+ const roles = { ...current.roles };
187
+ delete roles[role];
188
+ writeRoutingStore(configPath, roles);
189
+ return Object.freeze({ invalidated: true, backupPath, route: persisted });
190
+ }
191
+ finally {
192
+ releaseLock();
191
193
  }
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
194
  }
202
-
203
195
  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
- };
196
+ return {
197
+ type: "object",
198
+ additionalProperties: false,
199
+ ...(required ? { required: ["provider", "model"] } : {}),
200
+ properties: {
201
+ provider: { type: "string" },
202
+ model: { type: "string" },
203
+ reasoningEffort: { type: "string" },
204
+ maxTokens: { type: "integer" },
205
+ },
206
+ };
215
207
  }
216
-
217
208
  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
- };
209
+ const responsibilityBudgets = resolveInPlaceResponsibilityOutputBudgets(outputPolicy, snapshot.roles);
210
+ return {
211
+ action,
212
+ ...(responsibility ? { responsibility } : {}),
213
+ configPath,
214
+ roles: snapshot.roles,
215
+ sources: snapshot.sources,
216
+ requiresNextTurn: action !== "show",
217
+ ...(responsibilityBudgets ? { responsibilityBudgets } : {}),
218
+ ...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
219
+ ...(latestRoute ? { latestRoute } : {}),
220
+ };
230
221
  }
231
-
232
222
  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: {
223
+ const onConfigured = typeof options.onConfigured === "function" ? options.onConfigured : () => { };
224
+ const configuredRoles = options.configuredRoles ?? {};
225
+ const latestRouteFor = typeof options.latestRouteFor === "function" ? options.latestRouteFor : () => undefined;
226
+ const outputPolicyFor = typeof options.outputPolicyFor === "function" ? options.outputPolicyFor : () => undefined;
227
+ const resolveCallConfig = typeof options.resolveCallConfig === "function" ? options.resolveCallConfig : undefined;
228
+ return {
229
+ name: "odai_routing_config",
230
+ description: [
231
+ "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.",
232
+ "Use this only when the user naturally and explicitly asks to inspect/remove a mapping or names the provider/model to set.",
233
+ "Never choose a provider, model, reasoning effort, token limit, or price on the user's behalf.",
234
+ "Researcher routing is task-gated but not price-aware; its mapping does not guarantee lower cost.",
235
+ "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.",
236
+ "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.",
237
+ ].join(" "),
238
+ parameters: {
296
239
  type: "object",
297
240
  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"],
241
+ required: ["action"],
323
242
  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" },
243
+ action: { type: "string", enum: ["show", "set", "remove"], description: "Show all mappings, set one user-specified mapping, or remove one mapping." },
244
+ responsibility: { type: "string", enum: [...CONFIGURABLE_ROLES], description: "Required for set/remove; omitted for show." },
245
+ provider: { type: "string", description: "Provider id explicitly supplied by the user; required for set." },
246
+ model: { type: "string", description: "Model id explicitly supplied by the user; required for set." },
247
+ reasoningEffort: { type: "string", description: "Optional reasoning effort explicitly supplied by the user." },
248
+ maxTokens: { type: "integer", 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." },
249
+ },
250
+ },
251
+ output: {
252
+ schema: {
253
+ type: "object",
254
+ additionalProperties: false,
255
+ required: ["action", "configPath", "roles", "sources", "requiresNextTurn"],
256
+ properties: {
257
+ action: { type: "string", enum: ["show", "set", "remove"] },
258
+ responsibility: { type: "string", enum: [...CONFIGURABLE_ROLES] },
259
+ configPath: { type: "string" },
260
+ roles: { type: "object", additionalProperties: false, properties: Object.fromEntries(CONFIGURABLE_ROLES.map((role) => [role, routeSchema(true)])) },
261
+ sources: { type: "object", additionalProperties: false, properties: Object.fromEntries(CONFIGURABLE_ROLES.map((role) => [role, { type: "string", enum: ["persisted-mapping", "deployment-config"] }])) },
262
+ requiresNextTurn: { type: "boolean" },
263
+ recoveredInvalidStore: { type: "boolean" },
264
+ responsibilityBudgets: {
265
+ type: "object",
266
+ additionalProperties: false,
267
+ properties: Object.fromEntries(IN_PLACE_OUTPUT_RESPONSIBILITIES.map((responsibility) => [responsibility, {
268
+ type: "object",
269
+ additionalProperties: false,
270
+ required: ["source"],
271
+ properties: {
272
+ source: { type: "string", enum: ["responsibility-override", "controller-policy", "unbounded-by-odai"] },
273
+ maxTokens: { type: "integer" },
274
+ warning: { type: "string", enum: ["responsibility-inherits-controller-ceiling"] },
275
+ },
276
+ }])),
277
+ },
278
+ latestRoute: {
279
+ type: "object",
280
+ additionalProperties: false,
281
+ required: ["turn", "step", "responsibility", "status", "routeSource", "fallbackUsed"],
282
+ properties: {
283
+ turn: { type: "integer" }, step: { type: "integer" }, responsibility: { type: "string", enum: [...CONFIGURABLE_ROLES] },
284
+ responsibilityScopeId: { type: "string" }, routeCardId: { type: "string" },
285
+ status: { type: "string", enum: ["applied", "mismatch", "unverified", "fallback"] },
286
+ taskStatus: { type: "string", enum: ["completed", "fallback"] }, routeMode: { type: "string", enum: ["inline", "same-turn", "child"] },
287
+ routeSource: { type: "string", enum: ["persisted-mapping", "deployment-config"] }, fallbackUsed: { type: "boolean" },
288
+ requestedRoute: routeSchema(true), actualRoute: routeSchema(true), stopReason: { type: "string" }, error: { type: "string" },
289
+ taskStopReason: { type: "string" }, taskError: { type: "string" },
290
+ },
291
+ },
292
+ },
293
+ },
294
+ render(_arguments, value) {
295
+ const mapping = Object.entries(value.roles)
296
+ .map(([role, route]) => {
297
+ const options = [
298
+ ...(route.reasoningEffort ? [`reasoningEffort=${route.reasoningEffort}`] : []),
299
+ ...(route.maxTokens ? [`maxTokens=${route.maxTokens}`] : []),
300
+ ];
301
+ return `${role}: ${route.provider}/${route.model}${options.length > 0 ? ` (${options.join(", ")})` : ""} [${value.sources[role]}]`;
302
+ })
303
+ .join("\n") || "No Odai responsibility models are configured.";
304
+ const responsibilityBudgets = Object.entries(value.responsibilityBudgets ?? {});
305
+ const configuredResponsibilities = responsibilityBudgets.length > 0
306
+ ? `\nIn-place responsibility ceilings: ${responsibilityBudgets.map(([responsibility, budget]) => (`${responsibility}=${budget.maxTokens === undefined ? "no Odai maxTokens" : `maxTokens=${budget.maxTokens}`} [${budget.source}]`)).join("; ")}.`
307
+ : "";
308
+ const inheritedWarnings = responsibilityBudgets
309
+ .filter(([, budget]) => budget.warning === "responsibility-inherits-controller-ceiling")
310
+ .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.`)
311
+ .join("");
312
+ const latestRoute = value.latestRoute
313
+ ? [
314
+ "\nLatest current-session responsibility route receipt:",
315
+ `responsibility=${value.latestRoute.responsibility} status=${value.latestRoute.status} mode=${value.latestRoute.routeMode ?? "unknown"}`,
316
+ `routeSource=${value.latestRoute.routeSource} fallbackUsed=${String(value.latestRoute.fallbackUsed)}`,
317
+ ...(value.latestRoute.responsibilityScopeId ? [`responsibilityScope=${value.latestRoute.responsibilityScopeId}`] : []),
318
+ ...(value.latestRoute.routeCardId ? [`routeCard=${value.latestRoute.routeCardId}`] : []),
319
+ ...(value.latestRoute.taskStatus ? [`taskStatus=${value.latestRoute.taskStatus}`] : []),
320
+ ...(value.latestRoute.taskStopReason ? [`taskStopReason=${value.latestRoute.taskStopReason}`] : []),
321
+ ...(value.latestRoute.error ? [`routeError=${value.latestRoute.error}`] : []),
322
+ ...(value.latestRoute.taskError ? [`taskError=${value.latestRoute.taskError}`] : []),
323
+ ...(value.latestRoute.actualRoute
324
+ ? [`actual=${value.latestRoute.actualRoute.provider}/${value.latestRoute.actualRoute.model} (${[
325
+ `reasoningEffort=${value.latestRoute.actualRoute.reasoningEffort ?? "unspecified"}`,
326
+ ...(value.latestRoute.actualRoute.maxTokens === undefined ? [] : [`maxTokens=${value.latestRoute.actualRoute.maxTokens}`]),
327
+ ].join(", ")})`]
328
+ : ["actual=<unverified>"]),
329
+ ].join("\n")
330
+ : value.action === "show"
331
+ ? "\nNo mapped responsibility route receipt is recorded in this session."
332
+ : "";
333
+ return [{
334
+ type: "text",
335
+ text: [
336
+ `${value.action === "show" ? "Current" : "Updated"} Odai routing configuration:\n${mapping}`,
337
+ ...(value.recoveredInvalidStore ? ["\nAn invalid prior store was preserved and replaced."] : []),
338
+ configuredResponsibilities,
339
+ inheritedWarnings,
340
+ ...(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."] : []),
341
+ latestRoute,
342
+ ...(value.requiresNextTurn ? ["\nThe change applies from the next user turn."] : []),
343
+ ].join(""),
344
+ }];
340
345
  },
341
- },
342
346
  },
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 });
347
+ execute(arguments_, execution) {
348
+ if (!execution.agent)
349
+ throw new Error("odai_routing_config requires an owning agent session");
350
+ const agent = execution.agent;
351
+ const header = agent.session?.header;
352
+ if (header?.origin === "subagent" || (Number.isSafeInteger(header?.delegationDepth) && (header?.delegationDepth ?? 0) > 0)) {
353
+ throw new Error("child agents may not change Odai routing configuration");
439
354
  }
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
- };
355
+ if (!isUnknownRecord(arguments_))
356
+ throw new TypeError("arguments must be an object");
357
+ const action = arguments_.action;
358
+ if (action !== "show" && action !== "set" && action !== "remove")
359
+ throw new TypeError("action must be show, set, or remove");
360
+ if (action === "show") {
361
+ return Promise.resolve(resultFor(configPath, "show", effectiveRoutingSnapshot(configPath, configuredRoles), undefined, false, latestRouteFor(agent), outputPolicyFor()));
362
+ }
363
+ if (!isConfigurableRole(arguments_.responsibility)) {
364
+ throw new TypeError("responsibility must be researcher, planner, executor, reviewer, or frontend for set/remove");
365
+ }
366
+ const responsibility = arguments_.responsibility;
367
+ const proposedRoute = action === "set" ? resolveRoleRoute({
368
+ provider: arguments_.provider,
369
+ model: arguments_.model,
370
+ ...(arguments_.reasoningEffort === undefined ? {} : { reasoningEffort: arguments_.reasoningEffort }),
371
+ ...(arguments_.maxTokens === undefined ? {} : { maxTokens: arguments_.maxTokens }),
372
+ }, responsibility) : undefined;
373
+ const commit = (validationStatus) => {
374
+ const releaseLock = acquireRoutingStoreLock(configPath);
375
+ try {
376
+ let current;
377
+ let recoveredInvalidStore = false;
378
+ try {
379
+ current = readRoutingStore(configPath);
380
+ }
381
+ catch (error) {
382
+ if (action !== "set") {
383
+ throw new Error("Odai routing configuration is invalid; ask the user to set a responsibility mapping to repair it automatically", { cause: error });
384
+ }
385
+ preserveInvalidRoutingStore(configPath);
386
+ current = { roles: {} };
387
+ recoveredInvalidStore = true;
388
+ }
389
+ const roles = { ...current.roles };
390
+ if (action === "remove")
391
+ delete roles[responsibility];
392
+ else if (proposedRoute)
393
+ roles[responsibility] = proposedRoute;
394
+ writeRoutingStore(configPath, roles);
395
+ onConfigured(agent, {
396
+ action,
397
+ responsibility,
398
+ ...(roles[responsibility] ? { route: roles[responsibility] } : {}),
399
+ ...(validationStatus === "verified" ? { validationStatus } : {}),
400
+ ...(recoveredInvalidStore ? { recoveredInvalidStore: true } : {}),
401
+ });
402
+ return resultFor(configPath, action, effectiveRoutingSnapshot(configPath, configuredRoles), responsibility, recoveredInvalidStore, undefined, outputPolicyFor());
403
+ }
404
+ finally {
405
+ releaseLock();
406
+ }
407
+ };
408
+ if (action === "set" && proposedRoute) {
409
+ return requireModelRoute(resolveCallConfig, proposedRoute, execution.signal, `${responsibility} responsibility route`).then((validation) => commit(validation.status));
410
+ }
411
+ return Promise.resolve(commit());
412
+ },
413
+ };
480
414
  }
415
+ //# sourceMappingURL=routing-config.mjs.map