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.
- package/README.md +3 -3
- package/build/bin/odai-dsh-agent.d.mts +3 -0
- package/build/bin/odai-dsh-agent.d.mts.map +1 -0
- package/build/bin/odai-dsh-agent.mjs +114 -0
- package/build/bin/odai-dsh-agent.mjs.map +1 -0
- package/build/src/dsh-version.d.mts +16 -0
- package/build/src/dsh-version.d.mts.map +1 -0
- package/build/src/dsh-version.mjs +20 -0
- package/build/src/dsh-version.mjs.map +1 -0
- package/build/src/installer.d.mts +46 -0
- package/build/src/installer.d.mts.map +1 -0
- package/build/src/installer.mjs +398 -0
- package/build/src/installer.mjs.map +1 -0
- package/package.json +13 -12
- package/preset/odai/runtime/compaction-config.d.mts +66 -0
- package/preset/odai/runtime/compaction-config.d.mts.map +1 -0
- package/preset/odai/runtime/compaction-config.mjs +320 -342
- package/preset/odai/runtime/compaction-config.mjs.map +1 -0
- package/preset/odai/runtime/compaction-runtime.d.mts +68 -0
- package/preset/odai/runtime/compaction-runtime.d.mts.map +1 -0
- package/preset/odai/runtime/compaction-runtime.mjs +157 -0
- package/preset/odai/runtime/compaction-runtime.mjs.map +1 -0
- package/preset/odai/runtime/context-activation.d.mts +31 -0
- package/preset/odai/runtime/context-activation.d.mts.map +1 -0
- package/preset/odai/runtime/context-activation.mjs +55 -49
- package/preset/odai/runtime/context-activation.mjs.map +1 -0
- package/preset/odai/runtime/context-capability.d.mts +16 -0
- package/preset/odai/runtime/context-capability.d.mts.map +1 -0
- package/preset/odai/runtime/context-capability.mjs +79 -74
- package/preset/odai/runtime/context-capability.mjs.map +1 -0
- package/preset/odai/runtime/governance.d.mts +30 -0
- package/preset/odai/runtime/governance.d.mts.map +1 -0
- package/preset/odai/runtime/governance.mjs +126 -113
- package/preset/odai/runtime/governance.mjs.map +1 -0
- package/preset/odai/runtime/human-care.d.mts +13 -0
- package/preset/odai/runtime/human-care.d.mts.map +1 -0
- package/preset/odai/runtime/human-care.mjs +45 -42
- package/preset/odai/runtime/human-care.mjs.map +1 -0
- package/preset/odai/runtime/human-safety-continuity-store.d.mts +43 -0
- package/preset/odai/runtime/human-safety-continuity-store.d.mts.map +1 -0
- package/preset/odai/runtime/human-safety-continuity-store.mjs +193 -215
- package/preset/odai/runtime/human-safety-continuity-store.mjs.map +1 -0
- package/preset/odai/runtime/human-safety-continuity.d.mts +27 -0
- package/preset/odai/runtime/human-safety-continuity.d.mts.map +1 -0
- package/preset/odai/runtime/human-safety-continuity.mjs +205 -216
- package/preset/odai/runtime/human-safety-continuity.mjs.map +1 -0
- package/preset/odai/runtime/human-safety.d.mts +14 -0
- package/preset/odai/runtime/human-safety.d.mts.map +1 -0
- package/preset/odai/runtime/human-safety.mjs +50 -47
- package/preset/odai/runtime/human-safety.mjs.map +1 -0
- package/preset/odai/runtime/index.d.mts +8 -0
- package/preset/odai/runtime/index.d.mts.map +1 -0
- package/preset/odai/runtime/index.mjs +190 -2473
- package/preset/odai/runtime/index.mjs.map +1 -0
- package/preset/odai/runtime/lifecycle-runtime.d.mts +83 -0
- package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -0
- package/preset/odai/runtime/lifecycle-runtime.mjs +1140 -0
- package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -0
- package/preset/odai/runtime/model-route.d.mts +22 -0
- package/preset/odai/runtime/model-route.d.mts.map +1 -0
- package/preset/odai/runtime/model-route.mjs +68 -60
- package/preset/odai/runtime/model-route.mjs.map +1 -0
- package/preset/odai/runtime/output-config.d.mts +54 -0
- package/preset/odai/runtime/output-config.d.mts.map +1 -0
- package/preset/odai/runtime/output-config.mjs +289 -315
- package/preset/odai/runtime/output-config.mjs.map +1 -0
- package/preset/odai/runtime/output-policy-state.d.mts +3 -0
- package/preset/odai/runtime/output-policy-state.d.mts.map +1 -0
- package/preset/odai/runtime/output-policy-state.mjs +41 -29
- package/preset/odai/runtime/output-policy-state.mjs.map +1 -0
- package/preset/odai/runtime/prompt-runtime.d.mts +55 -0
- package/preset/odai/runtime/prompt-runtime.d.mts.map +1 -0
- package/preset/odai/runtime/prompt-runtime.mjs +303 -0
- package/preset/odai/runtime/prompt-runtime.mjs.map +1 -0
- package/preset/odai/runtime/research-packet.d.mts +30 -0
- package/preset/odai/runtime/research-packet.d.mts.map +1 -0
- package/preset/odai/runtime/research-packet.mjs +116 -117
- package/preset/odai/runtime/research-packet.mjs.map +1 -0
- package/preset/odai/runtime/responsibility-gap.d.mts +26 -0
- package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -0
- package/preset/odai/runtime/responsibility-gap.mjs +93 -85
- package/preset/odai/runtime/responsibility-gap.mjs.map +1 -0
- package/preset/odai/runtime/responsibility-scope.d.mts +69 -0
- package/preset/odai/runtime/responsibility-scope.d.mts.map +1 -0
- package/preset/odai/runtime/responsibility-scope.mjs +199 -171
- package/preset/odai/runtime/responsibility-scope.mjs.map +1 -0
- package/preset/odai/runtime/role-overlays.d.mts +5 -0
- package/preset/odai/runtime/role-overlays.d.mts.map +1 -0
- package/preset/odai/runtime/role-overlays.mjs +15 -14
- package/preset/odai/runtime/role-overlays.mjs.map +1 -0
- package/preset/odai/runtime/route-card.d.mts +42 -0
- package/preset/odai/runtime/route-card.d.mts.map +1 -0
- package/preset/odai/runtime/route-card.mjs +158 -146
- package/preset/odai/runtime/route-card.mjs.map +1 -0
- package/preset/odai/runtime/router.d.mts +80 -0
- package/preset/odai/runtime/router.d.mts.map +1 -0
- package/preset/odai/runtime/router.mjs +509 -559
- package/preset/odai/runtime/router.mjs.map +1 -0
- package/preset/odai/runtime/routing-config.d.mts +76 -0
- package/preset/odai/runtime/routing-config.d.mts.map +1 -0
- package/preset/odai/runtime/routing-config.mjs +373 -438
- package/preset/odai/runtime/routing-config.mjs.map +1 -0
- package/preset/odai/runtime/routing-context.d.mts +64 -0
- package/preset/odai/runtime/routing-context.d.mts.map +1 -0
- package/preset/odai/runtime/routing-context.mjs +398 -277
- package/preset/odai/runtime/routing-context.mjs.map +1 -0
- package/preset/odai/runtime/runtime-config.d.mts +24 -0
- package/preset/odai/runtime/runtime-config.d.mts.map +1 -0
- package/preset/odai/runtime/runtime-config.mjs +181 -0
- package/preset/odai/runtime/runtime-config.mjs.map +1 -0
- package/preset/odai/runtime/runtime-support.d.mts +91 -0
- package/preset/odai/runtime/runtime-support.d.mts.map +1 -0
- package/preset/odai/runtime/runtime-support.mjs +371 -0
- package/preset/odai/runtime/runtime-support.mjs.map +1 -0
- package/preset/odai/runtime/runtime-types.d.mts +237 -0
- package/preset/odai/runtime/runtime-types.d.mts.map +1 -0
- package/preset/odai/runtime/runtime-types.mjs +4 -0
- package/preset/odai/runtime/runtime-types.mjs.map +1 -0
- package/preset/odai/runtime/semantic-memory-store.d.mts +110 -0
- package/preset/odai/runtime/semantic-memory-store.d.mts.map +1 -0
- package/preset/odai/runtime/semantic-memory-store.mjs +271 -385
- package/preset/odai/runtime/semantic-memory-store.mjs.map +1 -0
- package/preset/odai/runtime/semantic-memory.d.mts +88 -0
- package/preset/odai/runtime/semantic-memory.d.mts.map +1 -0
- package/preset/odai/runtime/semantic-memory.mjs +790 -750
- package/preset/odai/runtime/semantic-memory.mjs.map +1 -0
- package/preset/odai/runtime/session-compat.d.mts +31 -0
- package/preset/odai/runtime/session-compat.d.mts.map +1 -0
- package/preset/odai/runtime/session-compat.mjs +301 -328
- package/preset/odai/runtime/session-compat.mjs.map +1 -0
- package/preset/odai/runtime/session-evidence.d.mts +25 -0
- package/preset/odai/runtime/session-evidence.d.mts.map +1 -0
- package/preset/odai/runtime/session-evidence.mjs +309 -287
- package/preset/odai/runtime/session-evidence.mjs.map +1 -0
- package/preset/odai/runtime/skill-bundle.d.mts +53 -0
- package/preset/odai/runtime/skill-bundle.d.mts.map +1 -0
- package/preset/odai/runtime/skill-bundle.mjs +286 -276
- package/preset/odai/runtime/skill-bundle.mjs.map +1 -0
- package/preset/odai/runtime/skill-evolution.d.mts +112 -0
- package/preset/odai/runtime/skill-evolution.d.mts.map +1 -0
- package/preset/odai/runtime/skill-evolution.mjs +1287 -1261
- package/preset/odai/runtime/skill-evolution.mjs.map +1 -0
- package/preset/odai/runtime/skill-selection-state.d.mts +5 -0
- package/preset/odai/runtime/skill-selection-state.d.mts.map +1 -0
- package/preset/odai/runtime/skill-selection-state.mjs +59 -43
- package/preset/odai/runtime/skill-selection-state.mjs.map +1 -0
- package/preset/odai/runtime/skill-selector.d.mts +28 -0
- package/preset/odai/runtime/skill-selector.d.mts.map +1 -0
- package/preset/odai/runtime/skill-selector.mjs +100 -109
- package/preset/odai/runtime/skill-selector.mjs.map +1 -0
- package/preset/odai/runtime/skill-source-config.d.mts +32 -0
- package/preset/odai/runtime/skill-source-config.d.mts.map +1 -0
- package/preset/odai/runtime/skill-source-config.mjs +200 -210
- package/preset/odai/runtime/skill-source-config.mjs.map +1 -0
- package/preset/odai/runtime/store-lock.d.mts +2 -0
- package/preset/odai/runtime/store-lock.d.mts.map +1 -0
- package/preset/odai/runtime/store-lock.mjs +92 -87
- package/preset/odai/runtime/store-lock.mjs.map +1 -0
- package/preset/odai/runtime/tool-runtime.d.mts +43 -0
- package/preset/odai/runtime/tool-runtime.d.mts.map +1 -0
- package/preset/odai/runtime/tool-runtime.mjs +221 -0
- package/preset/odai/runtime/tool-runtime.mjs.map +1 -0
- package/bin/odai-dsh-agent.mjs +0 -108
- package/scripts/verify-agent-load.mjs +0 -260
- package/src/dsh-version.mjs +0 -24
- 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
|
-
|
|
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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
113
|
+
return effectiveRoutingSnapshot(configPath, configuredRoles).roles[role];
|
|
122
114
|
}
|
|
123
|
-
|
|
124
115
|
export function effectiveRoutingSnapshot(configPath, configuredRoles = {}) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
responsibility
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|