odai-dsh-agent 0.0.2 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +86 -86
- package/bin/odai-dsh-agent.mjs +86 -87
- package/package.json +68 -68
- package/preset/odai/agent.cordis.yml +260 -260
- package/preset/odai/preset.yml +3 -3
- package/preset/odai/runtime/governance.mjs +120 -120
- package/preset/odai/runtime/index.mjs +584 -584
- package/preset/odai/runtime/router.mjs +290 -290
- package/preset/odai/runtime/routing-config.mjs +322 -322
- package/preset/odai/skills/odai/SKILL.md +65 -65
- package/preset/odai/skills/odai/agents/openai.yaml +4 -4
- package/preset/odai/skills/odai/assets/claude-agents/agent.md +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/config.toml +11 -11
- package/preset/odai/skills/odai/assets/codex-agents/role.toml +5 -5
- package/preset/odai/skills/odai/assets/copilot-agents/agent.md +13 -13
- package/preset/odai/skills/odai/assets/hooks-policy.example.json +24 -24
- package/preset/odai/skills/odai/assets/routing-roles/controller.md +15 -15
- package/preset/odai/skills/odai/assets/routing-roles/executor.md +11 -11
- package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
- package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
- package/preset/odai/skills/odai/assets/task-state.md +12 -12
- package/preset/odai/skills/odai/references/craft.md +53 -53
- package/preset/odai/skills/odai/references/dao.md +30 -30
- package/preset/odai/skills/odai/references/leverage.md +45 -45
- package/preset/odai/skills/odai/references/support.md +36 -36
- package/preset/odai/skills/odai/references/verification.md +23 -23
- package/preset/odai/skills/odai/scripts/build-hooks.mjs +273 -273
- package/preset/odai/skills/odai/scripts/build-routing.mjs +239 -239
- package/preset/odai/skills/odai/scripts/install-routing.mjs +375 -375
- package/preset/odai/skills/odai/scripts/odai-hook.mjs +457 -457
- package/preset/odai/skills/odai/scripts/run-role.mjs +336 -336
- package/preset/odai/skills/odai/scripts/run-routing.mjs +1154 -1154
- package/preset/odai/skills/odai/scripts/verify-routing.mjs +103 -103
- package/scripts/verify-agent-load.mjs +218 -217
- package/src/dsh-version.mjs +20 -0
- package/src/installer.mjs +270 -270
|
@@ -1,584 +1,584 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
-
import { dirname, resolve } from "node:path";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
decideRoute,
|
|
8
|
-
extractLatestUserText,
|
|
9
|
-
renderDelegationPrompt,
|
|
10
|
-
renderMissingRouteConfigNotice,
|
|
11
|
-
renderRouteFailureNotice,
|
|
12
|
-
renderRouteNotice,
|
|
13
|
-
requiresFailClosedProtection,
|
|
14
|
-
} from "./router.mjs";
|
|
15
|
-
import {
|
|
16
|
-
activeRouteProtection,
|
|
17
|
-
createChildToolGuard,
|
|
18
|
-
createRouteProtectionGuard,
|
|
19
|
-
summarizeToolResult,
|
|
20
|
-
} from "./governance.mjs";
|
|
21
|
-
import {
|
|
22
|
-
CONFIGURABLE_ROLES,
|
|
23
|
-
ROUTING_CONFIG_PROMPT,
|
|
24
|
-
createRoutingConfigTool,
|
|
25
|
-
effectiveRoleRoute,
|
|
26
|
-
resolveRoleRoute,
|
|
27
|
-
resolveRoutingConfigPath,
|
|
28
|
-
} from "./routing-config.mjs";
|
|
29
|
-
|
|
30
|
-
export const name = "odai-dsh-runtime";
|
|
31
|
-
export const inject = ["systemPrompt", "tools", "subagents"];
|
|
32
|
-
|
|
33
|
-
const PLUGIN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
|
-
const ROUTING_MODES = new Set(["off", "observe", "auto", "execute"]);
|
|
35
|
-
const ROUTED_ROLES = CONFIGURABLE_ROLES;
|
|
36
|
-
const DEFAULT_ROUTING_MODE = "auto";
|
|
37
|
-
|
|
38
|
-
function assertPlainObject(value, field) {
|
|
39
|
-
if (value === undefined) return {};
|
|
40
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
41
|
-
throw new TypeError(`${field} must be an object`);
|
|
42
|
-
}
|
|
43
|
-
return value;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function resolveConfig(rawConfig = {}) {
|
|
47
|
-
const raw = assertPlainObject(rawConfig, "config");
|
|
48
|
-
const routing = assertPlainObject(raw.routing, "config.routing");
|
|
49
|
-
const roles = assertPlainObject(routing.roles, "config.routing.roles");
|
|
50
|
-
const governance = assertPlainObject(raw.governance, "config.governance");
|
|
51
|
-
const mode = routing.mode ?? DEFAULT_ROUTING_MODE;
|
|
52
|
-
const provider = routing.provider ?? "spawn";
|
|
53
|
-
const maxInputChars = routing.maxInputChars ?? 12_000;
|
|
54
|
-
const configPath = resolveRoutingConfigPath(routing.configPath);
|
|
55
|
-
const additionalDeniedTools = governance.additionalDeniedTools ?? [];
|
|
56
|
-
const unknownRoles = Object.keys(roles).filter((role) => !ROUTED_ROLES.includes(role));
|
|
57
|
-
|
|
58
|
-
if (!ROUTING_MODES.has(mode)) {
|
|
59
|
-
throw new TypeError("config.routing.mode must be off, observe, auto, or execute");
|
|
60
|
-
}
|
|
61
|
-
if (typeof provider !== "string" || provider.trim() === "") {
|
|
62
|
-
throw new TypeError("config.routing.provider must be a non-empty string");
|
|
63
|
-
}
|
|
64
|
-
if (!Number.isSafeInteger(maxInputChars) || maxInputChars < 256) {
|
|
65
|
-
throw new TypeError("config.routing.maxInputChars must be an integer of at least 256");
|
|
66
|
-
}
|
|
67
|
-
if (unknownRoles.length > 0) {
|
|
68
|
-
throw new TypeError(`config.routing.roles has unknown roles: ${unknownRoles.join(", ")}`);
|
|
69
|
-
}
|
|
70
|
-
if (!Array.isArray(additionalDeniedTools)
|
|
71
|
-
|| additionalDeniedTools.some((tool) => typeof tool !== "string" || tool.trim() === "")) {
|
|
72
|
-
throw new TypeError("config.governance.additionalDeniedTools must be an array of non-empty strings");
|
|
73
|
-
}
|
|
74
|
-
if (raw.skillPath !== undefined && (typeof raw.skillPath !== "string" || raw.skillPath.trim() === "")) {
|
|
75
|
-
throw new TypeError("config.skillPath must be a non-empty string");
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return Object.freeze({
|
|
79
|
-
skillPath: raw.skillPath,
|
|
80
|
-
routing: Object.freeze({
|
|
81
|
-
mode,
|
|
82
|
-
provider: provider.trim(),
|
|
83
|
-
maxInputChars,
|
|
84
|
-
configPath,
|
|
85
|
-
roles: Object.freeze(Object.fromEntries(ROUTED_ROLES.map((role) => [
|
|
86
|
-
role,
|
|
87
|
-
resolveRoleRoute(roles[role], role),
|
|
88
|
-
]))),
|
|
89
|
-
}),
|
|
90
|
-
governance: Object.freeze({
|
|
91
|
-
additionalDeniedTools: Object.freeze(additionalDeniedTools.map((tool) => tool.trim())),
|
|
92
|
-
}),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export function resolveSkillPath(configuredPath) {
|
|
97
|
-
const candidates = [
|
|
98
|
-
configuredPath,
|
|
99
|
-
process.env.ODAI_SKILL_PATH,
|
|
100
|
-
resolve(PLUGIN_DIR, "skills/odai/SKILL.md"),
|
|
101
|
-
resolve(PLUGIN_DIR, "../../skills/odai/SKILL.md"),
|
|
102
|
-
].filter(Boolean).map((candidate) => resolve(candidate));
|
|
103
|
-
|
|
104
|
-
const found = candidates.find((candidate) => existsSync(candidate));
|
|
105
|
-
if (found) return found;
|
|
106
|
-
|
|
107
|
-
throw new Error(`odai canonical skill not found; checked: ${candidates.join(", ")}`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function loadRoleContracts(skillPath) {
|
|
111
|
-
const roleRoot = resolve(dirname(skillPath), "assets/routing-roles");
|
|
112
|
-
return Object.freeze(Object.fromEntries(ROUTED_ROLES.map((role) => {
|
|
113
|
-
const path = resolve(roleRoot, `${role}.md`);
|
|
114
|
-
const text = existsSync(path) ? readFileSync(path, "utf8").trim() : "";
|
|
115
|
-
if (!text) throw new Error(`odai canonical ${role} role is unavailable: ${path}`);
|
|
116
|
-
return [role, text];
|
|
117
|
-
})));
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function deepFreeze(value) {
|
|
121
|
-
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
122
|
-
for (const child of Object.values(value)) deepFreeze(child);
|
|
123
|
-
return Object.freeze(value);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function pluginMessage(text, summary, extraBlocks = []) {
|
|
127
|
-
return deepFreeze({
|
|
128
|
-
id: randomUUID(),
|
|
129
|
-
role: "user",
|
|
130
|
-
content: [{ type: "text", text }, ...extraBlocks],
|
|
131
|
-
source: {
|
|
132
|
-
kind: "plugin",
|
|
133
|
-
plugin: name,
|
|
134
|
-
form: "notice",
|
|
135
|
-
summary: summary.length <= 120 ? summary : `${summary.slice(0, 119)}…`,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function loggerFor(ctx) {
|
|
141
|
-
if (typeof ctx.logger !== "function") return { info() {}, warn() {} };
|
|
142
|
-
return ctx.logger(name);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
function appendEvent(agent, type, data, logger) {
|
|
146
|
-
try {
|
|
147
|
-
agent?.session?.append(type, data);
|
|
148
|
-
} catch (error) {
|
|
149
|
-
logger.warn(`failed to append ${type}: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function hasSessionEvent(agent, type, predicate) {
|
|
154
|
-
const events = agent?.session?.events;
|
|
155
|
-
if (!Array.isArray(events)) return false;
|
|
156
|
-
return events.some((event) => event?.type === type && predicate(event.data));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function outputText(blocks) {
|
|
160
|
-
if (!Array.isArray(blocks)) return "";
|
|
161
|
-
return blocks
|
|
162
|
-
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
163
|
-
.map((block) => block.text)
|
|
164
|
-
.join("\n")
|
|
165
|
-
.trim();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function roleAgentOptions(roleRoute) {
|
|
169
|
-
if (!roleRoute) return undefined;
|
|
170
|
-
return {
|
|
171
|
-
provider: roleRoute.provider,
|
|
172
|
-
model: roleRoute.model,
|
|
173
|
-
...(roleRoute.maxTokens === undefined ? {} : { maxTokens: roleRoute.maxTokens }),
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function latestRequestRoute(localAgent) {
|
|
178
|
-
const events = localAgent?.session?.events;
|
|
179
|
-
if (!Array.isArray(events)) return undefined;
|
|
180
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
181
|
-
const event = events[index];
|
|
182
|
-
if (event?.type !== "request/header") continue;
|
|
183
|
-
const config = event.data?.header?.config;
|
|
184
|
-
if (typeof config?.provider !== "string" || typeof config?.model !== "string") return undefined;
|
|
185
|
-
return Object.freeze({
|
|
186
|
-
provider: config.provider,
|
|
187
|
-
model: config.model,
|
|
188
|
-
...(config.reasoningEffort === undefined ? {} : { reasoningEffort: config.reasoningEffort }),
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
return undefined;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function routeMismatch(expected, actual) {
|
|
195
|
-
if (!expected) return undefined;
|
|
196
|
-
if (!actual) return "child request/header did not expose an actual model route";
|
|
197
|
-
for (const field of ["provider", "model", "reasoningEffort"]) {
|
|
198
|
-
if (expected[field] !== undefined && expected[field] !== actual[field]) {
|
|
199
|
-
return `child ${field} mismatch: expected ${expected[field]}, got ${actual[field] ?? "<absent>"}`;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return undefined;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export async function runRoutedRole({ subagents, provider, decision, taskText, roleContract, agent, signal, roleRoute }) {
|
|
206
|
-
let run;
|
|
207
|
-
let outcome;
|
|
208
|
-
try {
|
|
209
|
-
run = await subagents.start(provider, {
|
|
210
|
-
label: `odai-${decision.role}`,
|
|
211
|
-
prompt: [{ type: "text", text: renderDelegationPrompt(decision, taskText, roleContract) }],
|
|
212
|
-
parent: agent,
|
|
213
|
-
signal,
|
|
214
|
-
maxDepth: 1,
|
|
215
|
-
...(roleRoute ? { agentOptions: roleAgentOptions(roleRoute) } : {}),
|
|
216
|
-
});
|
|
217
|
-
const result = await run.result;
|
|
218
|
-
const actualRoute = latestRequestRoute(run.localAgent);
|
|
219
|
-
const mismatch = routeMismatch(roleRoute, actualRoute);
|
|
220
|
-
if (result.stopReason !== "completed") {
|
|
221
|
-
outcome = Object.freeze({
|
|
222
|
-
status: "fallback",
|
|
223
|
-
stopReason: result.stopReason,
|
|
224
|
-
output: result.output ?? [],
|
|
225
|
-
...(actualRoute ? { actualRoute } : {}),
|
|
226
|
-
});
|
|
227
|
-
} else if (mismatch) {
|
|
228
|
-
outcome = Object.freeze({
|
|
229
|
-
status: "fallback",
|
|
230
|
-
stopReason: "route-unverified",
|
|
231
|
-
output: [],
|
|
232
|
-
error: mismatch,
|
|
233
|
-
...(actualRoute ? { actualRoute } : {}),
|
|
234
|
-
});
|
|
235
|
-
} else if (!outputText(result.output)) {
|
|
236
|
-
outcome = Object.freeze({
|
|
237
|
-
status: "fallback",
|
|
238
|
-
stopReason: "route-empty-output",
|
|
239
|
-
output: [],
|
|
240
|
-
error: "child completed without textual evidence",
|
|
241
|
-
...(actualRoute ? { actualRoute } : {}),
|
|
242
|
-
});
|
|
243
|
-
} else {
|
|
244
|
-
outcome = Object.freeze({
|
|
245
|
-
status: "completed",
|
|
246
|
-
stopReason: result.stopReason,
|
|
247
|
-
output: result.output ?? [],
|
|
248
|
-
...(actualRoute ? { actualRoute } : {}),
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
} catch (error) {
|
|
252
|
-
outcome = Object.freeze({
|
|
253
|
-
status: "fallback",
|
|
254
|
-
stopReason: "infrastructure-error",
|
|
255
|
-
output: [],
|
|
256
|
-
error: error instanceof Error ? error.message : String(error),
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
if (run) {
|
|
261
|
-
try {
|
|
262
|
-
await run.dispose();
|
|
263
|
-
} catch (error) {
|
|
264
|
-
const disposeError = error instanceof Error ? error.message : String(error);
|
|
265
|
-
return Object.freeze({
|
|
266
|
-
status: "fallback",
|
|
267
|
-
stopReason: "infrastructure-error",
|
|
268
|
-
output: [],
|
|
269
|
-
error: outcome?.error
|
|
270
|
-
? `${outcome.error}; provider cleanup failed: ${disposeError}`
|
|
271
|
-
: `provider cleanup failed: ${disposeError}`,
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return outcome;
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function canonicalPrompt(skillPath, skillText) {
|
|
280
|
-
return [
|
|
281
|
-
"## odai canonical governance",
|
|
282
|
-
`Canonical source: ${skillPath}`,
|
|
283
|
-
"Apply this governance to every request. Keep the controller as the final delivery owner; use another role only for a real independent gap with observable net benefit.",
|
|
284
|
-
"",
|
|
285
|
-
skillText,
|
|
286
|
-
].join("\n");
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function routedRoleOf(agent) {
|
|
290
|
-
const events = agent?.session?.events;
|
|
291
|
-
if (!Array.isArray(events)) return undefined;
|
|
292
|
-
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
293
|
-
const event = events[index];
|
|
294
|
-
if (event?.type !== "subagent/descriptor") continue;
|
|
295
|
-
const label = event.data?.label;
|
|
296
|
-
if (typeof label !== "string" || !label.startsWith("odai-")) return undefined;
|
|
297
|
-
const role = label.slice("odai-".length);
|
|
298
|
-
return ROUTED_ROLES.includes(role) ? role : undefined;
|
|
299
|
-
}
|
|
300
|
-
return undefined;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export function apply(ctx, rawConfig) {
|
|
304
|
-
const config = resolveConfig(rawConfig);
|
|
305
|
-
const logger = loggerFor(ctx);
|
|
306
|
-
const skillPath = resolveSkillPath(config.skillPath);
|
|
307
|
-
const skillText = readFileSync(skillPath, "utf8").trim();
|
|
308
|
-
if (!skillText) throw new Error(`odai canonical skill is empty: ${skillPath}`);
|
|
309
|
-
const roleContracts = loadRoleContracts(skillPath);
|
|
310
|
-
|
|
311
|
-
ctx.systemPrompt.section({
|
|
312
|
-
name: "odai:canonical-governance",
|
|
313
|
-
order: -20,
|
|
314
|
-
text: canonicalPrompt(skillPath, skillText),
|
|
315
|
-
});
|
|
316
|
-
ctx.systemPrompt.section({
|
|
317
|
-
name: "odai:routing-configuration",
|
|
318
|
-
order: -19,
|
|
319
|
-
text: ROUTING_CONFIG_PROMPT,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const routeProtections = new WeakMap();
|
|
323
|
-
const controllerUpgrades = new WeakMap();
|
|
324
|
-
const configuredRole = (role) => {
|
|
325
|
-
try {
|
|
326
|
-
return { route: effectiveRoleRoute(config.routing.configPath, config.routing.roles, role) };
|
|
327
|
-
} catch (error) {
|
|
328
|
-
return {
|
|
329
|
-
error: "persisted Odai routing configuration failed validation",
|
|
330
|
-
detail: error instanceof Error ? error.message : String(error),
|
|
331
|
-
};
|
|
332
|
-
}
|
|
333
|
-
};
|
|
334
|
-
const onDenied = (execution, reason) => {
|
|
335
|
-
appendEvent(execution.agent, "odai/governance-denied", {
|
|
336
|
-
callId: String(execution.callId),
|
|
337
|
-
tool: execution.name,
|
|
338
|
-
reason,
|
|
339
|
-
}, logger);
|
|
340
|
-
};
|
|
341
|
-
const childGuard = createChildToolGuard({
|
|
342
|
-
additionalDeniedTools: config.governance.additionalDeniedTools,
|
|
343
|
-
onDenied,
|
|
344
|
-
});
|
|
345
|
-
const routeProtectionGuard = createRouteProtectionGuard({
|
|
346
|
-
additionalDeniedTools: config.governance.additionalDeniedTools,
|
|
347
|
-
onDenied,
|
|
348
|
-
protectionFor(agent) {
|
|
349
|
-
if (config.routing.mode === "off") return undefined;
|
|
350
|
-
return routeProtections.get(agent) ?? activeRouteProtection(agent);
|
|
351
|
-
},
|
|
352
|
-
});
|
|
353
|
-
ctx.tools.register(createRoutingConfigTool(config.routing.configPath));
|
|
354
|
-
ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
|
|
355
|
-
|
|
356
|
-
ctx.on("tools/result", (execution, result) => {
|
|
357
|
-
if (!execution.agent) return;
|
|
358
|
-
const summary = summarizeToolResult(execution, result);
|
|
359
|
-
if (hasSessionEvent(execution.agent, "odai/tool-observed", (data) => data?.callId === summary.callId)) return;
|
|
360
|
-
appendEvent(execution.agent, "odai/tool-observed", summary, logger);
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
ctx.on("agent/request", async ({ agent, turn }, next) => {
|
|
364
|
-
const proposed = await next();
|
|
365
|
-
const childRole = routedRoleOf(agent);
|
|
366
|
-
const childRoleState = childRole ? configuredRole(childRole) : undefined;
|
|
367
|
-
const upgrade = controllerUpgrades.get(agent);
|
|
368
|
-
const roleRoute = childRole
|
|
369
|
-
? childRoleState.route
|
|
370
|
-
: upgrade && upgrade.turn === turn
|
|
371
|
-
? upgrade.route
|
|
372
|
-
: undefined;
|
|
373
|
-
if (!roleRoute) return proposed;
|
|
374
|
-
|
|
375
|
-
const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = proposed;
|
|
376
|
-
return Object.freeze({
|
|
377
|
-
...withoutInheritedEffort,
|
|
378
|
-
provider: roleRoute.provider,
|
|
379
|
-
model: roleRoute.model,
|
|
380
|
-
...(roleRoute.reasoningEffort === undefined ? {} : { reasoningEffort: roleRoute.reasoningEffort }),
|
|
381
|
-
...(childRole && roleRoute.maxTokens !== undefined ? { maxTokens: roleRoute.maxTokens } : {}),
|
|
382
|
-
});
|
|
383
|
-
}, { prepend: true });
|
|
384
|
-
|
|
385
|
-
const protectController = (agent, turn, step, decision, source, failure) => {
|
|
386
|
-
const protection = Object.freeze({
|
|
387
|
-
turn,
|
|
388
|
-
step,
|
|
389
|
-
mode: "read-only",
|
|
390
|
-
reasonCode: decision.reasonCode,
|
|
391
|
-
source,
|
|
392
|
-
...(failure ? { failure } : {}),
|
|
393
|
-
});
|
|
394
|
-
routeProtections.set(agent, protection);
|
|
395
|
-
appendEvent(agent, "odai/route-protection", protection, logger);
|
|
396
|
-
};
|
|
397
|
-
|
|
398
|
-
if (config.routing.mode !== "off") {
|
|
399
|
-
const routedSteps = new WeakMap();
|
|
400
|
-
ctx.on("agent/pre-step", async ({ agent, turn, step, signal }, next) => {
|
|
401
|
-
const subagentSession = isSubagentSession(agent);
|
|
402
|
-
if (step === 1 && !subagentSession) {
|
|
403
|
-
routeProtections.delete(agent);
|
|
404
|
-
controllerUpgrades.delete(agent);
|
|
405
|
-
}
|
|
406
|
-
const downstream = await next();
|
|
407
|
-
if (downstream.kind === "reject" || signal.aborted || step !== 1 || subagentSession) {
|
|
408
|
-
return downstream;
|
|
409
|
-
}
|
|
410
|
-
if (hasSessionEvent(agent, "odai/route-decided", (data) => data?.turn === turn && data?.step === step)) {
|
|
411
|
-
return downstream;
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
let routed = routedSteps.get(agent);
|
|
415
|
-
if (!routed) {
|
|
416
|
-
routed = new Set();
|
|
417
|
-
routedSteps.set(agent, routed);
|
|
418
|
-
}
|
|
419
|
-
const routeKey = `${turn}:${step}`;
|
|
420
|
-
if (routed.has(routeKey)) return downstream;
|
|
421
|
-
routed.add(routeKey);
|
|
422
|
-
|
|
423
|
-
const taskText = extractLatestUserText(downstream.messages).slice(0, config.routing.maxInputChars);
|
|
424
|
-
const decision = decideRoute({ text: taskText });
|
|
425
|
-
const routeRole = decision.targetRole ?? decision.role;
|
|
426
|
-
appendEvent(agent, "odai/route-decided", {
|
|
427
|
-
turn,
|
|
428
|
-
step,
|
|
429
|
-
role: decision.role,
|
|
430
|
-
action: decision.action,
|
|
431
|
-
...(decision.targetRole ? { targetRole: decision.targetRole } : {}),
|
|
432
|
-
mode: config.routing.mode,
|
|
433
|
-
reasonCode: decision.reasonCode,
|
|
434
|
-
signals: decision.signals,
|
|
435
|
-
}, logger);
|
|
436
|
-
|
|
437
|
-
if (decision.action === "direct") return downstream;
|
|
438
|
-
|
|
439
|
-
if (config.routing.mode === "observe") {
|
|
440
|
-
if (requiresFailClosedProtection(decision)) {
|
|
441
|
-
protectController(agent, turn, step, decision, "observe");
|
|
442
|
-
}
|
|
443
|
-
return {
|
|
444
|
-
kind: "enter",
|
|
445
|
-
messages: [
|
|
446
|
-
...downstream.messages,
|
|
447
|
-
pluginMessage(
|
|
448
|
-
renderRouteNotice(decision, "observe"),
|
|
449
|
-
`odai observed ${routeRole} gap (${decision.reasonCode})`,
|
|
450
|
-
),
|
|
451
|
-
],
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
const roleState = configuredRole(routeRole);
|
|
456
|
-
const roleRoute = roleState.route;
|
|
457
|
-
if (!roleRoute) {
|
|
458
|
-
const invalidConfig = Boolean(roleState.error);
|
|
459
|
-
appendEvent(agent, "odai/route-config-missing", {
|
|
460
|
-
turn,
|
|
461
|
-
step,
|
|
462
|
-
role: routeRole,
|
|
463
|
-
action: decision.action,
|
|
464
|
-
mode: config.routing.mode,
|
|
465
|
-
status: invalidConfig ? "invalid" : "unconfigured",
|
|
466
|
-
...(invalidConfig ? { error: roleState.detail } : {}),
|
|
467
|
-
}, logger);
|
|
468
|
-
if (requiresFailClosedProtection(decision)) {
|
|
469
|
-
protectController(
|
|
470
|
-
agent,
|
|
471
|
-
turn,
|
|
472
|
-
step,
|
|
473
|
-
decision,
|
|
474
|
-
invalidConfig ? "route-config-invalid" : "route-config-missing",
|
|
475
|
-
);
|
|
476
|
-
}
|
|
477
|
-
return {
|
|
478
|
-
kind: "enter",
|
|
479
|
-
messages: [
|
|
480
|
-
...downstream.messages,
|
|
481
|
-
pluginMessage(
|
|
482
|
-
renderMissingRouteConfigNotice(decision, config.routing.mode, roleState.error),
|
|
483
|
-
`odai ${routeRole} route is ${invalidConfig ? "invalid" : "not configured"}`,
|
|
484
|
-
),
|
|
485
|
-
],
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
if (config.routing.mode === "auto" && decision.action === "upgrade") {
|
|
490
|
-
controllerUpgrades.set(agent, Object.freeze({ turn, route: roleRoute }));
|
|
491
|
-
appendEvent(agent, "odai/route-upgrade", {
|
|
492
|
-
turn,
|
|
493
|
-
step,
|
|
494
|
-
role: decision.role,
|
|
495
|
-
targetRole: routeRole,
|
|
496
|
-
status: "requested",
|
|
497
|
-
requestedRoute: roleRoute,
|
|
498
|
-
}, logger);
|
|
499
|
-
return {
|
|
500
|
-
kind: "enter",
|
|
501
|
-
messages: [
|
|
502
|
-
...downstream.messages,
|
|
503
|
-
pluginMessage(
|
|
504
|
-
renderRouteNotice(decision, "auto", roleRoute),
|
|
505
|
-
`odai upgraded controller route (${decision.reasonCode})`,
|
|
506
|
-
),
|
|
507
|
-
],
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
const delegationDecision = decision.role === routeRole
|
|
512
|
-
? decision
|
|
513
|
-
: Object.freeze({ ...decision, role: routeRole, mode: "delegate", action: "delegate" });
|
|
514
|
-
const result = ctx.subagents?.start
|
|
515
|
-
? await runRoutedRole({
|
|
516
|
-
subagents: ctx.subagents,
|
|
517
|
-
provider: config.routing.provider,
|
|
518
|
-
decision: delegationDecision,
|
|
519
|
-
taskText,
|
|
520
|
-
roleContract: roleContracts[routeRole],
|
|
521
|
-
agent,
|
|
522
|
-
signal,
|
|
523
|
-
roleRoute,
|
|
524
|
-
})
|
|
525
|
-
: Object.freeze({
|
|
526
|
-
status: "fallback",
|
|
527
|
-
stopReason: "infrastructure-error",
|
|
528
|
-
output: [],
|
|
529
|
-
error: "dsh subagents service unavailable",
|
|
530
|
-
});
|
|
531
|
-
appendEvent(agent, "odai/route-result", {
|
|
532
|
-
turn,
|
|
533
|
-
step,
|
|
534
|
-
role: routeRole,
|
|
535
|
-
action: "delegate",
|
|
536
|
-
status: result.status,
|
|
537
|
-
stopReason: result.stopReason,
|
|
538
|
-
...(result.actualRoute ? { actualRoute: result.actualRoute } : {}),
|
|
539
|
-
...(result.error ? { error: result.error } : {}),
|
|
540
|
-
}, logger);
|
|
541
|
-
|
|
542
|
-
if (result.status === "completed") {
|
|
543
|
-
const childText = outputText(result.output);
|
|
544
|
-
const heading = renderRouteNotice(delegationDecision, config.routing.mode, result.actualRoute);
|
|
545
|
-
return {
|
|
546
|
-
kind: "enter",
|
|
547
|
-
messages: [
|
|
548
|
-
...downstream.messages,
|
|
549
|
-
pluginMessage(
|
|
550
|
-
childText ? `${heading}\n\n${routeRole} output:\n${childText}` : heading,
|
|
551
|
-
`odai completed ${routeRole} route`,
|
|
552
|
-
result.output.filter((block) => block?.type !== "text"),
|
|
553
|
-
),
|
|
554
|
-
],
|
|
555
|
-
};
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const failure = result.error ?? result.stopReason;
|
|
559
|
-
if (requiresFailClosedProtection(delegationDecision)) {
|
|
560
|
-
protectController(agent, turn, step, delegationDecision, "route-failure", failure);
|
|
561
|
-
}
|
|
562
|
-
return {
|
|
563
|
-
kind: "enter",
|
|
564
|
-
messages: [
|
|
565
|
-
...downstream.messages,
|
|
566
|
-
pluginMessage(
|
|
567
|
-
renderRouteFailureNotice(delegationDecision, failure),
|
|
568
|
-
requiresFailClosedProtection(delegationDecision)
|
|
569
|
-
? `odai blocked high-impact ${routeRole} fallback`
|
|
570
|
-
: `odai fell back from ${routeRole} route`,
|
|
571
|
-
),
|
|
572
|
-
],
|
|
573
|
-
};
|
|
574
|
-
}, { prepend: true });
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
logger.info(`loaded canonical governance from ${skillPath}; routing=${config.routing.mode}`);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
function isSubagentSession(agent) {
|
|
581
|
-
const header = agent?.session?.header;
|
|
582
|
-
return header?.origin === "subagent"
|
|
583
|
-
|| (Number.isSafeInteger(header?.delegationDepth) && header.delegationDepth > 0);
|
|
584
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
decideRoute,
|
|
8
|
+
extractLatestUserText,
|
|
9
|
+
renderDelegationPrompt,
|
|
10
|
+
renderMissingRouteConfigNotice,
|
|
11
|
+
renderRouteFailureNotice,
|
|
12
|
+
renderRouteNotice,
|
|
13
|
+
requiresFailClosedProtection,
|
|
14
|
+
} from "./router.mjs";
|
|
15
|
+
import {
|
|
16
|
+
activeRouteProtection,
|
|
17
|
+
createChildToolGuard,
|
|
18
|
+
createRouteProtectionGuard,
|
|
19
|
+
summarizeToolResult,
|
|
20
|
+
} from "./governance.mjs";
|
|
21
|
+
import {
|
|
22
|
+
CONFIGURABLE_ROLES,
|
|
23
|
+
ROUTING_CONFIG_PROMPT,
|
|
24
|
+
createRoutingConfigTool,
|
|
25
|
+
effectiveRoleRoute,
|
|
26
|
+
resolveRoleRoute,
|
|
27
|
+
resolveRoutingConfigPath,
|
|
28
|
+
} from "./routing-config.mjs";
|
|
29
|
+
|
|
30
|
+
export const name = "odai-dsh-runtime";
|
|
31
|
+
export const inject = ["systemPrompt", "tools", "subagents"];
|
|
32
|
+
|
|
33
|
+
const PLUGIN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
34
|
+
const ROUTING_MODES = new Set(["off", "observe", "auto", "execute"]);
|
|
35
|
+
const ROUTED_ROLES = CONFIGURABLE_ROLES;
|
|
36
|
+
const DEFAULT_ROUTING_MODE = "auto";
|
|
37
|
+
|
|
38
|
+
function assertPlainObject(value, field) {
|
|
39
|
+
if (value === undefined) return {};
|
|
40
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
41
|
+
throw new TypeError(`${field} must be an object`);
|
|
42
|
+
}
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveConfig(rawConfig = {}) {
|
|
47
|
+
const raw = assertPlainObject(rawConfig, "config");
|
|
48
|
+
const routing = assertPlainObject(raw.routing, "config.routing");
|
|
49
|
+
const roles = assertPlainObject(routing.roles, "config.routing.roles");
|
|
50
|
+
const governance = assertPlainObject(raw.governance, "config.governance");
|
|
51
|
+
const mode = routing.mode ?? DEFAULT_ROUTING_MODE;
|
|
52
|
+
const provider = routing.provider ?? "spawn";
|
|
53
|
+
const maxInputChars = routing.maxInputChars ?? 12_000;
|
|
54
|
+
const configPath = resolveRoutingConfigPath(routing.configPath);
|
|
55
|
+
const additionalDeniedTools = governance.additionalDeniedTools ?? [];
|
|
56
|
+
const unknownRoles = Object.keys(roles).filter((role) => !ROUTED_ROLES.includes(role));
|
|
57
|
+
|
|
58
|
+
if (!ROUTING_MODES.has(mode)) {
|
|
59
|
+
throw new TypeError("config.routing.mode must be off, observe, auto, or execute");
|
|
60
|
+
}
|
|
61
|
+
if (typeof provider !== "string" || provider.trim() === "") {
|
|
62
|
+
throw new TypeError("config.routing.provider must be a non-empty string");
|
|
63
|
+
}
|
|
64
|
+
if (!Number.isSafeInteger(maxInputChars) || maxInputChars < 256) {
|
|
65
|
+
throw new TypeError("config.routing.maxInputChars must be an integer of at least 256");
|
|
66
|
+
}
|
|
67
|
+
if (unknownRoles.length > 0) {
|
|
68
|
+
throw new TypeError(`config.routing.roles has unknown roles: ${unknownRoles.join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
if (!Array.isArray(additionalDeniedTools)
|
|
71
|
+
|| additionalDeniedTools.some((tool) => typeof tool !== "string" || tool.trim() === "")) {
|
|
72
|
+
throw new TypeError("config.governance.additionalDeniedTools must be an array of non-empty strings");
|
|
73
|
+
}
|
|
74
|
+
if (raw.skillPath !== undefined && (typeof raw.skillPath !== "string" || raw.skillPath.trim() === "")) {
|
|
75
|
+
throw new TypeError("config.skillPath must be a non-empty string");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return Object.freeze({
|
|
79
|
+
skillPath: raw.skillPath,
|
|
80
|
+
routing: Object.freeze({
|
|
81
|
+
mode,
|
|
82
|
+
provider: provider.trim(),
|
|
83
|
+
maxInputChars,
|
|
84
|
+
configPath,
|
|
85
|
+
roles: Object.freeze(Object.fromEntries(ROUTED_ROLES.map((role) => [
|
|
86
|
+
role,
|
|
87
|
+
resolveRoleRoute(roles[role], role),
|
|
88
|
+
]))),
|
|
89
|
+
}),
|
|
90
|
+
governance: Object.freeze({
|
|
91
|
+
additionalDeniedTools: Object.freeze(additionalDeniedTools.map((tool) => tool.trim())),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function resolveSkillPath(configuredPath) {
|
|
97
|
+
const candidates = [
|
|
98
|
+
configuredPath,
|
|
99
|
+
process.env.ODAI_SKILL_PATH,
|
|
100
|
+
resolve(PLUGIN_DIR, "skills/odai/SKILL.md"),
|
|
101
|
+
resolve(PLUGIN_DIR, "../../skills/odai/SKILL.md"),
|
|
102
|
+
].filter(Boolean).map((candidate) => resolve(candidate));
|
|
103
|
+
|
|
104
|
+
const found = candidates.find((candidate) => existsSync(candidate));
|
|
105
|
+
if (found) return found;
|
|
106
|
+
|
|
107
|
+
throw new Error(`odai canonical skill not found; checked: ${candidates.join(", ")}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function loadRoleContracts(skillPath) {
|
|
111
|
+
const roleRoot = resolve(dirname(skillPath), "assets/routing-roles");
|
|
112
|
+
return Object.freeze(Object.fromEntries(ROUTED_ROLES.map((role) => {
|
|
113
|
+
const path = resolve(roleRoot, `${role}.md`);
|
|
114
|
+
const text = existsSync(path) ? readFileSync(path, "utf8").trim() : "";
|
|
115
|
+
if (!text) throw new Error(`odai canonical ${role} role is unavailable: ${path}`);
|
|
116
|
+
return [role, text];
|
|
117
|
+
})));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function deepFreeze(value) {
|
|
121
|
+
if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
122
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
123
|
+
return Object.freeze(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function pluginMessage(text, summary, extraBlocks = []) {
|
|
127
|
+
return deepFreeze({
|
|
128
|
+
id: randomUUID(),
|
|
129
|
+
role: "user",
|
|
130
|
+
content: [{ type: "text", text }, ...extraBlocks],
|
|
131
|
+
source: {
|
|
132
|
+
kind: "plugin",
|
|
133
|
+
plugin: name,
|
|
134
|
+
form: "notice",
|
|
135
|
+
summary: summary.length <= 120 ? summary : `${summary.slice(0, 119)}…`,
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function loggerFor(ctx) {
|
|
141
|
+
if (typeof ctx.logger !== "function") return { info() {}, warn() {} };
|
|
142
|
+
return ctx.logger(name);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function appendEvent(agent, type, data, logger) {
|
|
146
|
+
try {
|
|
147
|
+
agent?.session?.append(type, data);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
logger.warn(`failed to append ${type}: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function hasSessionEvent(agent, type, predicate) {
|
|
154
|
+
const events = agent?.session?.events;
|
|
155
|
+
if (!Array.isArray(events)) return false;
|
|
156
|
+
return events.some((event) => event?.type === type && predicate(event.data));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function outputText(blocks) {
|
|
160
|
+
if (!Array.isArray(blocks)) return "";
|
|
161
|
+
return blocks
|
|
162
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
163
|
+
.map((block) => block.text)
|
|
164
|
+
.join("\n")
|
|
165
|
+
.trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function roleAgentOptions(roleRoute) {
|
|
169
|
+
if (!roleRoute) return undefined;
|
|
170
|
+
return {
|
|
171
|
+
provider: roleRoute.provider,
|
|
172
|
+
model: roleRoute.model,
|
|
173
|
+
...(roleRoute.maxTokens === undefined ? {} : { maxTokens: roleRoute.maxTokens }),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function latestRequestRoute(localAgent) {
|
|
178
|
+
const events = localAgent?.session?.events;
|
|
179
|
+
if (!Array.isArray(events)) return undefined;
|
|
180
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
181
|
+
const event = events[index];
|
|
182
|
+
if (event?.type !== "request/header") continue;
|
|
183
|
+
const config = event.data?.header?.config;
|
|
184
|
+
if (typeof config?.provider !== "string" || typeof config?.model !== "string") return undefined;
|
|
185
|
+
return Object.freeze({
|
|
186
|
+
provider: config.provider,
|
|
187
|
+
model: config.model,
|
|
188
|
+
...(config.reasoningEffort === undefined ? {} : { reasoningEffort: config.reasoningEffort }),
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function routeMismatch(expected, actual) {
|
|
195
|
+
if (!expected) return undefined;
|
|
196
|
+
if (!actual) return "child request/header did not expose an actual model route";
|
|
197
|
+
for (const field of ["provider", "model", "reasoningEffort"]) {
|
|
198
|
+
if (expected[field] !== undefined && expected[field] !== actual[field]) {
|
|
199
|
+
return `child ${field} mismatch: expected ${expected[field]}, got ${actual[field] ?? "<absent>"}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function runRoutedRole({ subagents, provider, decision, taskText, roleContract, agent, signal, roleRoute }) {
|
|
206
|
+
let run;
|
|
207
|
+
let outcome;
|
|
208
|
+
try {
|
|
209
|
+
run = await subagents.start(provider, {
|
|
210
|
+
label: `odai-${decision.role}`,
|
|
211
|
+
prompt: [{ type: "text", text: renderDelegationPrompt(decision, taskText, roleContract) }],
|
|
212
|
+
parent: agent,
|
|
213
|
+
signal,
|
|
214
|
+
maxDepth: 1,
|
|
215
|
+
...(roleRoute ? { agentOptions: roleAgentOptions(roleRoute) } : {}),
|
|
216
|
+
});
|
|
217
|
+
const result = await run.result;
|
|
218
|
+
const actualRoute = latestRequestRoute(run.localAgent);
|
|
219
|
+
const mismatch = routeMismatch(roleRoute, actualRoute);
|
|
220
|
+
if (result.stopReason !== "completed") {
|
|
221
|
+
outcome = Object.freeze({
|
|
222
|
+
status: "fallback",
|
|
223
|
+
stopReason: result.stopReason,
|
|
224
|
+
output: result.output ?? [],
|
|
225
|
+
...(actualRoute ? { actualRoute } : {}),
|
|
226
|
+
});
|
|
227
|
+
} else if (mismatch) {
|
|
228
|
+
outcome = Object.freeze({
|
|
229
|
+
status: "fallback",
|
|
230
|
+
stopReason: "route-unverified",
|
|
231
|
+
output: [],
|
|
232
|
+
error: mismatch,
|
|
233
|
+
...(actualRoute ? { actualRoute } : {}),
|
|
234
|
+
});
|
|
235
|
+
} else if (!outputText(result.output)) {
|
|
236
|
+
outcome = Object.freeze({
|
|
237
|
+
status: "fallback",
|
|
238
|
+
stopReason: "route-empty-output",
|
|
239
|
+
output: [],
|
|
240
|
+
error: "child completed without textual evidence",
|
|
241
|
+
...(actualRoute ? { actualRoute } : {}),
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
outcome = Object.freeze({
|
|
245
|
+
status: "completed",
|
|
246
|
+
stopReason: result.stopReason,
|
|
247
|
+
output: result.output ?? [],
|
|
248
|
+
...(actualRoute ? { actualRoute } : {}),
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
} catch (error) {
|
|
252
|
+
outcome = Object.freeze({
|
|
253
|
+
status: "fallback",
|
|
254
|
+
stopReason: "infrastructure-error",
|
|
255
|
+
output: [],
|
|
256
|
+
error: error instanceof Error ? error.message : String(error),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (run) {
|
|
261
|
+
try {
|
|
262
|
+
await run.dispose();
|
|
263
|
+
} catch (error) {
|
|
264
|
+
const disposeError = error instanceof Error ? error.message : String(error);
|
|
265
|
+
return Object.freeze({
|
|
266
|
+
status: "fallback",
|
|
267
|
+
stopReason: "infrastructure-error",
|
|
268
|
+
output: [],
|
|
269
|
+
error: outcome?.error
|
|
270
|
+
? `${outcome.error}; provider cleanup failed: ${disposeError}`
|
|
271
|
+
: `provider cleanup failed: ${disposeError}`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return outcome;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function canonicalPrompt(skillPath, skillText) {
|
|
280
|
+
return [
|
|
281
|
+
"## odai canonical governance",
|
|
282
|
+
`Canonical source: ${skillPath}`,
|
|
283
|
+
"Apply this governance to every request. Keep the controller as the final delivery owner; use another role only for a real independent gap with observable net benefit.",
|
|
284
|
+
"",
|
|
285
|
+
skillText,
|
|
286
|
+
].join("\n");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function routedRoleOf(agent) {
|
|
290
|
+
const events = agent?.session?.events;
|
|
291
|
+
if (!Array.isArray(events)) return undefined;
|
|
292
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
293
|
+
const event = events[index];
|
|
294
|
+
if (event?.type !== "subagent/descriptor") continue;
|
|
295
|
+
const label = event.data?.label;
|
|
296
|
+
if (typeof label !== "string" || !label.startsWith("odai-")) return undefined;
|
|
297
|
+
const role = label.slice("odai-".length);
|
|
298
|
+
return ROUTED_ROLES.includes(role) ? role : undefined;
|
|
299
|
+
}
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function apply(ctx, rawConfig) {
|
|
304
|
+
const config = resolveConfig(rawConfig);
|
|
305
|
+
const logger = loggerFor(ctx);
|
|
306
|
+
const skillPath = resolveSkillPath(config.skillPath);
|
|
307
|
+
const skillText = readFileSync(skillPath, "utf8").trim();
|
|
308
|
+
if (!skillText) throw new Error(`odai canonical skill is empty: ${skillPath}`);
|
|
309
|
+
const roleContracts = loadRoleContracts(skillPath);
|
|
310
|
+
|
|
311
|
+
ctx.systemPrompt.section({
|
|
312
|
+
name: "odai:canonical-governance",
|
|
313
|
+
order: -20,
|
|
314
|
+
text: canonicalPrompt(skillPath, skillText),
|
|
315
|
+
});
|
|
316
|
+
ctx.systemPrompt.section({
|
|
317
|
+
name: "odai:routing-configuration",
|
|
318
|
+
order: -19,
|
|
319
|
+
text: ROUTING_CONFIG_PROMPT,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
const routeProtections = new WeakMap();
|
|
323
|
+
const controllerUpgrades = new WeakMap();
|
|
324
|
+
const configuredRole = (role) => {
|
|
325
|
+
try {
|
|
326
|
+
return { route: effectiveRoleRoute(config.routing.configPath, config.routing.roles, role) };
|
|
327
|
+
} catch (error) {
|
|
328
|
+
return {
|
|
329
|
+
error: "persisted Odai routing configuration failed validation",
|
|
330
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const onDenied = (execution, reason) => {
|
|
335
|
+
appendEvent(execution.agent, "odai/governance-denied", {
|
|
336
|
+
callId: String(execution.callId),
|
|
337
|
+
tool: execution.name,
|
|
338
|
+
reason,
|
|
339
|
+
}, logger);
|
|
340
|
+
};
|
|
341
|
+
const childGuard = createChildToolGuard({
|
|
342
|
+
additionalDeniedTools: config.governance.additionalDeniedTools,
|
|
343
|
+
onDenied,
|
|
344
|
+
});
|
|
345
|
+
const routeProtectionGuard = createRouteProtectionGuard({
|
|
346
|
+
additionalDeniedTools: config.governance.additionalDeniedTools,
|
|
347
|
+
onDenied,
|
|
348
|
+
protectionFor(agent) {
|
|
349
|
+
if (config.routing.mode === "off") return undefined;
|
|
350
|
+
return routeProtections.get(agent) ?? activeRouteProtection(agent);
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
ctx.tools.register(createRoutingConfigTool(config.routing.configPath));
|
|
354
|
+
ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
|
|
355
|
+
|
|
356
|
+
ctx.on("tools/result", (execution, result) => {
|
|
357
|
+
if (!execution.agent) return;
|
|
358
|
+
const summary = summarizeToolResult(execution, result);
|
|
359
|
+
if (hasSessionEvent(execution.agent, "odai/tool-observed", (data) => data?.callId === summary.callId)) return;
|
|
360
|
+
appendEvent(execution.agent, "odai/tool-observed", summary, logger);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
ctx.on("agent/request", async ({ agent, turn }, next) => {
|
|
364
|
+
const proposed = await next();
|
|
365
|
+
const childRole = routedRoleOf(agent);
|
|
366
|
+
const childRoleState = childRole ? configuredRole(childRole) : undefined;
|
|
367
|
+
const upgrade = controllerUpgrades.get(agent);
|
|
368
|
+
const roleRoute = childRole
|
|
369
|
+
? childRoleState.route
|
|
370
|
+
: upgrade && upgrade.turn === turn
|
|
371
|
+
? upgrade.route
|
|
372
|
+
: undefined;
|
|
373
|
+
if (!roleRoute) return proposed;
|
|
374
|
+
|
|
375
|
+
const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = proposed;
|
|
376
|
+
return Object.freeze({
|
|
377
|
+
...withoutInheritedEffort,
|
|
378
|
+
provider: roleRoute.provider,
|
|
379
|
+
model: roleRoute.model,
|
|
380
|
+
...(roleRoute.reasoningEffort === undefined ? {} : { reasoningEffort: roleRoute.reasoningEffort }),
|
|
381
|
+
...(childRole && roleRoute.maxTokens !== undefined ? { maxTokens: roleRoute.maxTokens } : {}),
|
|
382
|
+
});
|
|
383
|
+
}, { prepend: true });
|
|
384
|
+
|
|
385
|
+
const protectController = (agent, turn, step, decision, source, failure) => {
|
|
386
|
+
const protection = Object.freeze({
|
|
387
|
+
turn,
|
|
388
|
+
step,
|
|
389
|
+
mode: "read-only",
|
|
390
|
+
reasonCode: decision.reasonCode,
|
|
391
|
+
source,
|
|
392
|
+
...(failure ? { failure } : {}),
|
|
393
|
+
});
|
|
394
|
+
routeProtections.set(agent, protection);
|
|
395
|
+
appendEvent(agent, "odai/route-protection", protection, logger);
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
if (config.routing.mode !== "off") {
|
|
399
|
+
const routedSteps = new WeakMap();
|
|
400
|
+
ctx.on("agent/pre-step", async ({ agent, turn, step, signal }, next) => {
|
|
401
|
+
const subagentSession = isSubagentSession(agent);
|
|
402
|
+
if (step === 1 && !subagentSession) {
|
|
403
|
+
routeProtections.delete(agent);
|
|
404
|
+
controllerUpgrades.delete(agent);
|
|
405
|
+
}
|
|
406
|
+
const downstream = await next();
|
|
407
|
+
if (downstream.kind === "reject" || signal.aborted || step !== 1 || subagentSession) {
|
|
408
|
+
return downstream;
|
|
409
|
+
}
|
|
410
|
+
if (hasSessionEvent(agent, "odai/route-decided", (data) => data?.turn === turn && data?.step === step)) {
|
|
411
|
+
return downstream;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
let routed = routedSteps.get(agent);
|
|
415
|
+
if (!routed) {
|
|
416
|
+
routed = new Set();
|
|
417
|
+
routedSteps.set(agent, routed);
|
|
418
|
+
}
|
|
419
|
+
const routeKey = `${turn}:${step}`;
|
|
420
|
+
if (routed.has(routeKey)) return downstream;
|
|
421
|
+
routed.add(routeKey);
|
|
422
|
+
|
|
423
|
+
const taskText = extractLatestUserText(downstream.messages).slice(0, config.routing.maxInputChars);
|
|
424
|
+
const decision = decideRoute({ text: taskText });
|
|
425
|
+
const routeRole = decision.targetRole ?? decision.role;
|
|
426
|
+
appendEvent(agent, "odai/route-decided", {
|
|
427
|
+
turn,
|
|
428
|
+
step,
|
|
429
|
+
role: decision.role,
|
|
430
|
+
action: decision.action,
|
|
431
|
+
...(decision.targetRole ? { targetRole: decision.targetRole } : {}),
|
|
432
|
+
mode: config.routing.mode,
|
|
433
|
+
reasonCode: decision.reasonCode,
|
|
434
|
+
signals: decision.signals,
|
|
435
|
+
}, logger);
|
|
436
|
+
|
|
437
|
+
if (decision.action === "direct") return downstream;
|
|
438
|
+
|
|
439
|
+
if (config.routing.mode === "observe") {
|
|
440
|
+
if (requiresFailClosedProtection(decision)) {
|
|
441
|
+
protectController(agent, turn, step, decision, "observe");
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
kind: "enter",
|
|
445
|
+
messages: [
|
|
446
|
+
...downstream.messages,
|
|
447
|
+
pluginMessage(
|
|
448
|
+
renderRouteNotice(decision, "observe"),
|
|
449
|
+
`odai observed ${routeRole} gap (${decision.reasonCode})`,
|
|
450
|
+
),
|
|
451
|
+
],
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
const roleState = configuredRole(routeRole);
|
|
456
|
+
const roleRoute = roleState.route;
|
|
457
|
+
if (!roleRoute) {
|
|
458
|
+
const invalidConfig = Boolean(roleState.error);
|
|
459
|
+
appendEvent(agent, "odai/route-config-missing", {
|
|
460
|
+
turn,
|
|
461
|
+
step,
|
|
462
|
+
role: routeRole,
|
|
463
|
+
action: decision.action,
|
|
464
|
+
mode: config.routing.mode,
|
|
465
|
+
status: invalidConfig ? "invalid" : "unconfigured",
|
|
466
|
+
...(invalidConfig ? { error: roleState.detail } : {}),
|
|
467
|
+
}, logger);
|
|
468
|
+
if (requiresFailClosedProtection(decision)) {
|
|
469
|
+
protectController(
|
|
470
|
+
agent,
|
|
471
|
+
turn,
|
|
472
|
+
step,
|
|
473
|
+
decision,
|
|
474
|
+
invalidConfig ? "route-config-invalid" : "route-config-missing",
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
kind: "enter",
|
|
479
|
+
messages: [
|
|
480
|
+
...downstream.messages,
|
|
481
|
+
pluginMessage(
|
|
482
|
+
renderMissingRouteConfigNotice(decision, config.routing.mode, roleState.error),
|
|
483
|
+
`odai ${routeRole} route is ${invalidConfig ? "invalid" : "not configured"}`,
|
|
484
|
+
),
|
|
485
|
+
],
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (config.routing.mode === "auto" && decision.action === "upgrade") {
|
|
490
|
+
controllerUpgrades.set(agent, Object.freeze({ turn, route: roleRoute }));
|
|
491
|
+
appendEvent(agent, "odai/route-upgrade", {
|
|
492
|
+
turn,
|
|
493
|
+
step,
|
|
494
|
+
role: decision.role,
|
|
495
|
+
targetRole: routeRole,
|
|
496
|
+
status: "requested",
|
|
497
|
+
requestedRoute: roleRoute,
|
|
498
|
+
}, logger);
|
|
499
|
+
return {
|
|
500
|
+
kind: "enter",
|
|
501
|
+
messages: [
|
|
502
|
+
...downstream.messages,
|
|
503
|
+
pluginMessage(
|
|
504
|
+
renderRouteNotice(decision, "auto", roleRoute),
|
|
505
|
+
`odai upgraded controller route (${decision.reasonCode})`,
|
|
506
|
+
),
|
|
507
|
+
],
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const delegationDecision = decision.role === routeRole
|
|
512
|
+
? decision
|
|
513
|
+
: Object.freeze({ ...decision, role: routeRole, mode: "delegate", action: "delegate" });
|
|
514
|
+
const result = ctx.subagents?.start
|
|
515
|
+
? await runRoutedRole({
|
|
516
|
+
subagents: ctx.subagents,
|
|
517
|
+
provider: config.routing.provider,
|
|
518
|
+
decision: delegationDecision,
|
|
519
|
+
taskText,
|
|
520
|
+
roleContract: roleContracts[routeRole],
|
|
521
|
+
agent,
|
|
522
|
+
signal,
|
|
523
|
+
roleRoute,
|
|
524
|
+
})
|
|
525
|
+
: Object.freeze({
|
|
526
|
+
status: "fallback",
|
|
527
|
+
stopReason: "infrastructure-error",
|
|
528
|
+
output: [],
|
|
529
|
+
error: "dsh subagents service unavailable",
|
|
530
|
+
});
|
|
531
|
+
appendEvent(agent, "odai/route-result", {
|
|
532
|
+
turn,
|
|
533
|
+
step,
|
|
534
|
+
role: routeRole,
|
|
535
|
+
action: "delegate",
|
|
536
|
+
status: result.status,
|
|
537
|
+
stopReason: result.stopReason,
|
|
538
|
+
...(result.actualRoute ? { actualRoute: result.actualRoute } : {}),
|
|
539
|
+
...(result.error ? { error: result.error } : {}),
|
|
540
|
+
}, logger);
|
|
541
|
+
|
|
542
|
+
if (result.status === "completed") {
|
|
543
|
+
const childText = outputText(result.output);
|
|
544
|
+
const heading = renderRouteNotice(delegationDecision, config.routing.mode, result.actualRoute);
|
|
545
|
+
return {
|
|
546
|
+
kind: "enter",
|
|
547
|
+
messages: [
|
|
548
|
+
...downstream.messages,
|
|
549
|
+
pluginMessage(
|
|
550
|
+
childText ? `${heading}\n\n${routeRole} output:\n${childText}` : heading,
|
|
551
|
+
`odai completed ${routeRole} route`,
|
|
552
|
+
result.output.filter((block) => block?.type !== "text"),
|
|
553
|
+
),
|
|
554
|
+
],
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const failure = result.error ?? result.stopReason;
|
|
559
|
+
if (requiresFailClosedProtection(delegationDecision)) {
|
|
560
|
+
protectController(agent, turn, step, delegationDecision, "route-failure", failure);
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
kind: "enter",
|
|
564
|
+
messages: [
|
|
565
|
+
...downstream.messages,
|
|
566
|
+
pluginMessage(
|
|
567
|
+
renderRouteFailureNotice(delegationDecision, failure),
|
|
568
|
+
requiresFailClosedProtection(delegationDecision)
|
|
569
|
+
? `odai blocked high-impact ${routeRole} fallback`
|
|
570
|
+
: `odai fell back from ${routeRole} route`,
|
|
571
|
+
),
|
|
572
|
+
],
|
|
573
|
+
};
|
|
574
|
+
}, { prepend: true });
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
logger.info(`loaded canonical governance from ${skillPath}; routing=${config.routing.mode}`);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function isSubagentSession(agent) {
|
|
581
|
+
const header = agent?.session?.header;
|
|
582
|
+
return header?.origin === "subagent"
|
|
583
|
+
|| (Number.isSafeInteger(header?.delegationDepth) && header.delegationDepth > 0);
|
|
584
|
+
}
|