odai-dsh-agent 0.0.4 → 0.0.5
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 +11 -1
- package/package.json +1 -1
- package/preset/odai/runtime/index.mjs +147 -26
- package/preset/odai/runtime/router.mjs +76 -17
- package/preset/odai/runtime/skill-bundle.mjs +295 -0
- package/preset/odai/runtime/skill-selection-state.mjs +53 -0
- package/preset/odai/runtime/skill-selector.mjs +134 -0
- package/preset/odai/runtime/skill-source-config.mjs +239 -0
- package/preset/odai/skills/odai/SKILL.md +2 -1
- package/preset/odai/skills/odai/assets/routing-roles/controller.md +1 -1
- package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +1 -1
- package/preset/odai/skills/odai/manifest.json +33 -0
- package/preset/odai/skills/odai/references/craft.md +1 -0
- package/preset/odai/skills/odai/references/leverage.md +1 -0
- package/preset/odai/skills/odai/references/planning.md +81 -0
- package/src/installer.mjs +5 -0
package/README.md
CHANGED
|
@@ -53,6 +53,16 @@ The controller calls `odai_routing_config` to persist that explicit choice. The
|
|
|
53
53
|
|
|
54
54
|
Planner, executor, and reviewer are independent optional responsibilities. If any needed one has no mapping, high-impact work fails closed and remains read-only; lower-impact work continues only where it does not depend on the missing independent responsibility. Odai never chooses a model on the user's behalf.
|
|
55
55
|
|
|
56
|
+
## Skill sources
|
|
57
|
+
|
|
58
|
+
The Agent keeps the managed preset's complete skill copy as its `bundled` default, so existing installations do not change behavior. When the user explicitly asks to show, set, or reset the Odai skill source, the controller uses `odai_skill_source_config` and stores the choice in `$DSH_HOME/odai/source.json`, outside the managed preset:
|
|
59
|
+
|
|
60
|
+
- `bundled`: use the skill shipped with this Agent release.
|
|
61
|
+
- `auto`: allow a compatible current-project `.dsh/skills/odai` or `.agents/skills/odai` bundle, then DSH custom roots and newer user installs under `$DSH_HOME/skills/odai` or `$DSH_AGENTS_HOME/skills/odai` (default `~/.agents/skills/odai`), with bundled fallback.
|
|
62
|
+
- `user`: ignore project roots and require a compatible custom or user-level bundle. An unusable source produces an explicit bundled fallback diagnostic so it can be repaired through the same tool.
|
|
63
|
+
|
|
64
|
+
An independent install must be a complete directory bundle containing `SKILL.md`, `manifest.json`, and every manifest-declared file. The runtime checks SemVer 2.0.0 `skillVersion`, an exact supported `runtimeContract`, complete-file SHA-256 integrity, and same-version content conflicts. Prompt governance and routing role contracts are selected atomically for one agent turn; project sources are scoped by that session's cwd, and changes are reconsidered on the next user turn. Explicit deployment `skillPath` or `ODAI_SKILL_PATH` remains highest priority and requires a DSH restart.
|
|
65
|
+
|
|
56
66
|
## Status and uninstall
|
|
57
67
|
|
|
58
68
|
```sh
|
|
@@ -71,7 +81,7 @@ Use only the surface that matches the desired scope:
|
|
|
71
81
|
- `odai-dsh-agent`: selectable Odai governance for only sessions using this preset.
|
|
72
82
|
- both: supported only for a deliberate combination of profile-wide and Agent-scoped behavior; normally redundant, so it is not the default recommendation.
|
|
73
83
|
|
|
74
|
-
When both are deliberate,
|
|
84
|
+
When both are deliberate, a process-shared per-agent/per-turn skill snapshot keeps prompt governance and role contracts identical, the compatibility-safe evidence store deduplicates tool and route records, and denials remain monotonic. Neither package installs or changes the provider-neutral `odai-cli`.
|
|
75
85
|
|
|
76
86
|
## Development
|
|
77
87
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
decideRoute,
|
|
8
|
-
|
|
8
|
+
extractRoutingText,
|
|
9
9
|
renderDelegationPrompt,
|
|
10
10
|
renderMissingRouteConfigNotice,
|
|
11
11
|
renderRouteFailureNotice,
|
|
@@ -27,6 +27,23 @@ import {
|
|
|
27
27
|
resolveRoutingConfigPath,
|
|
28
28
|
} from "./routing-config.mjs";
|
|
29
29
|
import { createSessionEvidence, resolveSessionEvidenceRoot } from "./session-evidence.mjs";
|
|
30
|
+
import {
|
|
31
|
+
SKILL_SOURCE_CONFIG_PROMPT,
|
|
32
|
+
createSkillSourceConfigTool,
|
|
33
|
+
effectiveSkillSource,
|
|
34
|
+
resolveSkillSourceConfigPath,
|
|
35
|
+
} from "./skill-source-config.mjs";
|
|
36
|
+
import {
|
|
37
|
+
ODAI_RUNTIME_CONTRACT,
|
|
38
|
+
SKILL_SOURCE_MODES,
|
|
39
|
+
loadSkillBundle,
|
|
40
|
+
} from "./skill-bundle.mjs";
|
|
41
|
+
import { resolveSkillSelection } from "./skill-selector.mjs";
|
|
42
|
+
import {
|
|
43
|
+
currentAgentTurn,
|
|
44
|
+
selectSharedSkillForTurn,
|
|
45
|
+
sharedSkillSelection,
|
|
46
|
+
} from "./skill-selection-state.mjs";
|
|
30
47
|
|
|
31
48
|
export const name = "odai-dsh-runtime";
|
|
32
49
|
export const inject = ["systemPrompt", "tools", "subagents"];
|
|
@@ -54,6 +71,8 @@ export function resolveConfig(rawConfig = {}) {
|
|
|
54
71
|
const maxInputChars = routing.maxInputChars ?? 12_000;
|
|
55
72
|
const configPath = resolveRoutingConfigPath(routing.configPath);
|
|
56
73
|
const additionalDeniedTools = governance.additionalDeniedTools ?? [];
|
|
74
|
+
const skillSource = governance.skillSource ?? "bundled";
|
|
75
|
+
const skillConfigPath = resolveSkillSourceConfigPath(governance.skillConfigPath);
|
|
57
76
|
const unknownRoles = Object.keys(roles).filter((role) => !ROUTED_ROLES.includes(role));
|
|
58
77
|
|
|
59
78
|
if (!ROUTING_MODES.has(mode)) {
|
|
@@ -72,6 +91,9 @@ export function resolveConfig(rawConfig = {}) {
|
|
|
72
91
|
|| additionalDeniedTools.some((tool) => typeof tool !== "string" || tool.trim() === "")) {
|
|
73
92
|
throw new TypeError("config.governance.additionalDeniedTools must be an array of non-empty strings");
|
|
74
93
|
}
|
|
94
|
+
if (!SKILL_SOURCE_MODES.includes(skillSource)) {
|
|
95
|
+
throw new TypeError("config.governance.skillSource must be bundled, auto, or user");
|
|
96
|
+
}
|
|
75
97
|
if (raw.skillPath !== undefined && (typeof raw.skillPath !== "string" || raw.skillPath.trim() === "")) {
|
|
76
98
|
throw new TypeError("config.skillPath must be a non-empty string");
|
|
77
99
|
}
|
|
@@ -90,32 +112,31 @@ export function resolveConfig(rawConfig = {}) {
|
|
|
90
112
|
}),
|
|
91
113
|
governance: Object.freeze({
|
|
92
114
|
additionalDeniedTools: Object.freeze(additionalDeniedTools.map((tool) => tool.trim())),
|
|
115
|
+
skillSource,
|
|
116
|
+
skillConfigPath,
|
|
93
117
|
}),
|
|
94
118
|
});
|
|
95
119
|
}
|
|
96
120
|
|
|
97
|
-
export function resolveSkillPath(configuredPath) {
|
|
121
|
+
export function resolveSkillPath(configuredPath, env = process.env) {
|
|
122
|
+
const explicit = configuredPath ?? env.ODAI_SKILL_PATH;
|
|
123
|
+
if (explicit !== undefined) {
|
|
124
|
+
const path = resolve(explicit);
|
|
125
|
+
if (!existsSync(path)) throw new Error(`explicit Odai canonical skill not found: ${path}`);
|
|
126
|
+
return path;
|
|
127
|
+
}
|
|
128
|
+
|
|
98
129
|
const candidates = [
|
|
99
|
-
configuredPath,
|
|
100
|
-
process.env.ODAI_SKILL_PATH,
|
|
101
130
|
resolve(PLUGIN_DIR, "skills/odai/SKILL.md"),
|
|
102
131
|
resolve(PLUGIN_DIR, "../../skills/odai/SKILL.md"),
|
|
103
|
-
]
|
|
104
|
-
|
|
132
|
+
];
|
|
105
133
|
const found = candidates.find((candidate) => existsSync(candidate));
|
|
106
134
|
if (found) return found;
|
|
107
|
-
|
|
108
|
-
throw new Error(`odai canonical skill not found; checked: ${candidates.join(", ")}`);
|
|
135
|
+
throw new Error(`Odai bundled canonical skill not found; checked: ${candidates.join(", ")}`);
|
|
109
136
|
}
|
|
110
137
|
|
|
111
138
|
export function loadRoleContracts(skillPath) {
|
|
112
|
-
|
|
113
|
-
return Object.freeze(Object.fromEntries(ROUTED_ROLES.map((role) => {
|
|
114
|
-
const path = resolve(roleRoot, `${role}.md`);
|
|
115
|
-
const text = existsSync(path) ? readFileSync(path, "utf8").trim() : "";
|
|
116
|
-
if (!text) throw new Error(`odai canonical ${role} role is unavailable: ${path}`);
|
|
117
|
-
return [role, text];
|
|
118
|
-
})));
|
|
139
|
+
return loadSkillBundle(skillPath).roleContracts;
|
|
119
140
|
}
|
|
120
141
|
|
|
121
142
|
function deepFreeze(value) {
|
|
@@ -263,13 +284,20 @@ export async function runRoutedRole({ subagents, provider, decision, taskText, r
|
|
|
263
284
|
return outcome;
|
|
264
285
|
}
|
|
265
286
|
|
|
266
|
-
function canonicalPrompt(
|
|
287
|
+
function canonicalPrompt(selection) {
|
|
288
|
+
const { bundle } = selection;
|
|
289
|
+
const fallback = selection.status === "fallback"
|
|
290
|
+
? `Selection fallback: ${selection.reasonCode}${selection.detail ? ` (${selection.detail})` : ""}.`
|
|
291
|
+
: undefined;
|
|
267
292
|
return [
|
|
268
293
|
"## odai canonical governance",
|
|
269
|
-
`Canonical source: ${
|
|
294
|
+
`Canonical source: ${bundle.source} (${bundle.provider})`,
|
|
295
|
+
`Canonical skill: ${bundle.manifest.skillVersion}; runtime contract: ${bundle.manifest.runtimeContract}; digest: ${bundle.digest}.`,
|
|
296
|
+
...(fallback ? [fallback] : []),
|
|
270
297
|
"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.",
|
|
298
|
+
"Odai governance is already loaded by this runtime; do not call the skill tool to load odai again.",
|
|
271
299
|
"",
|
|
272
|
-
skillText,
|
|
300
|
+
bundle.skillText,
|
|
273
301
|
].join("\n");
|
|
274
302
|
}
|
|
275
303
|
|
|
@@ -303,20 +331,103 @@ export function apply(ctx, rawConfig) {
|
|
|
303
331
|
};
|
|
304
332
|
const hasSessionEvent = (agent, type, predicate) => evidence.has(agent, type, predicate);
|
|
305
333
|
const skillPath = resolveSkillPath(config.skillPath);
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
const
|
|
334
|
+
const explicitSkillPath = config.skillPath !== undefined
|
|
335
|
+
|| (typeof process.env.ODAI_SKILL_PATH === "string" && process.env.ODAI_SKILL_PATH.trim() !== "");
|
|
336
|
+
const bundled = loadSkillBundle(skillPath, {
|
|
337
|
+
source: explicitSkillPath ? "path" : "bundled",
|
|
338
|
+
provider: explicitSkillPath ? "odai-explicit-path" : "odai-dsh-runtime",
|
|
339
|
+
});
|
|
340
|
+
if (bundled.manifest.runtimeContract !== ODAI_RUNTIME_CONTRACT) {
|
|
341
|
+
throw new Error(`Odai canonical runtimeContract ${bundled.manifest.runtimeContract} is incompatible with this runtime`);
|
|
342
|
+
}
|
|
343
|
+
const baseSelection = Object.freeze({
|
|
344
|
+
mode: explicitSkillPath ? "path" : "bundled",
|
|
345
|
+
status: "selected",
|
|
346
|
+
reasonCode: explicitSkillPath ? "explicit-path" : "bundled-configured",
|
|
347
|
+
bundle: bundled,
|
|
348
|
+
rejections: Object.freeze([]),
|
|
349
|
+
});
|
|
309
350
|
|
|
310
351
|
ctx.systemPrompt.section({
|
|
311
352
|
name: "odai:canonical-governance",
|
|
312
353
|
order: -20,
|
|
313
|
-
text: canonicalPrompt(
|
|
354
|
+
text: canonicalPrompt(baseSelection),
|
|
314
355
|
});
|
|
315
356
|
ctx.systemPrompt.section({
|
|
316
357
|
name: "odai:routing-configuration",
|
|
317
358
|
order: -19,
|
|
318
359
|
text: ROUTING_CONFIG_PROMPT,
|
|
319
360
|
});
|
|
361
|
+
ctx.systemPrompt.section({
|
|
362
|
+
name: "odai:skill-source-configuration",
|
|
363
|
+
order: -18,
|
|
364
|
+
text: SKILL_SOURCE_CONFIG_PROMPT,
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
const optionalSkillRegistry = () => {
|
|
368
|
+
try {
|
|
369
|
+
if (ctx.skills && typeof ctx.skills.get === "function") return ctx.skills;
|
|
370
|
+
} catch {}
|
|
371
|
+
try {
|
|
372
|
+
const service = typeof ctx.get === "function" ? ctx.get("skills") : undefined;
|
|
373
|
+
return service && typeof service.get === "function" ? service : undefined;
|
|
374
|
+
} catch {
|
|
375
|
+
return undefined;
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
const selectForAgent = async (agent, context) => {
|
|
379
|
+
if (explicitSkillPath) return baseSelection;
|
|
380
|
+
let mode;
|
|
381
|
+
try {
|
|
382
|
+
mode = effectiveSkillSource(config.governance.skillConfigPath, config.governance.skillSource);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
return Object.freeze({
|
|
385
|
+
...baseSelection,
|
|
386
|
+
mode: "bundled",
|
|
387
|
+
status: "fallback",
|
|
388
|
+
reasonCode: "source-config-invalid",
|
|
389
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
return resolveSkillSelection({
|
|
393
|
+
mode,
|
|
394
|
+
bundled,
|
|
395
|
+
cwd: agent.session?.header?.cwd,
|
|
396
|
+
scope: context.scope,
|
|
397
|
+
signal: context.signal,
|
|
398
|
+
skills: optionalSkillRegistry(),
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
ctx.on("system-prompt/assemble", async (assembly, context, next) => {
|
|
402
|
+
const downstream = await next();
|
|
403
|
+
const agent = context.agent;
|
|
404
|
+
if (!agent) return downstream;
|
|
405
|
+
const selection = await selectSharedSkillForTurn(agent, () => selectForAgent(agent, context));
|
|
406
|
+
const turn = currentAgentTurn(agent);
|
|
407
|
+
const selectionEvidence = {
|
|
408
|
+
...(turn === undefined ? {} : { turn }),
|
|
409
|
+
requestedMode: selection.mode,
|
|
410
|
+
status: selection.status,
|
|
411
|
+
reasonCode: selection.reasonCode,
|
|
412
|
+
effectiveSource: selection.bundle.source,
|
|
413
|
+
skillVersion: selection.bundle.manifest.skillVersion,
|
|
414
|
+
runtimeContract: selection.bundle.manifest.runtimeContract,
|
|
415
|
+
digest: selection.bundle.digest,
|
|
416
|
+
rejections: selection.rejections.map(({ source, reasonCode }) => ({ source, reasonCode })),
|
|
417
|
+
};
|
|
418
|
+
if (!hasSessionEvent(agent, "odai/skill-selected", (data) => data?.turn === turn && data?.digest === selection.bundle.digest)) {
|
|
419
|
+
appendEvent(agent, "odai/skill-selected", selectionEvidence);
|
|
420
|
+
}
|
|
421
|
+
if (selection.status === "fallback") {
|
|
422
|
+
logger.warn(`Odai skill source ${selection.mode} fell back to bundled governance (${selection.reasonCode})`);
|
|
423
|
+
}
|
|
424
|
+
return {
|
|
425
|
+
...downstream,
|
|
426
|
+
sections: downstream.sections.map((section) => section.name === "odai:canonical-governance"
|
|
427
|
+
? { ...section, text: canonicalPrompt(selection) }
|
|
428
|
+
: section),
|
|
429
|
+
};
|
|
430
|
+
});
|
|
320
431
|
|
|
321
432
|
const routeProtections = new WeakMap();
|
|
322
433
|
const controllerUpgrades = new WeakMap();
|
|
@@ -354,6 +465,16 @@ export function apply(ctx, rawConfig) {
|
|
|
354
465
|
appendEvent(agent, "odai/routing-configured", data);
|
|
355
466
|
},
|
|
356
467
|
}));
|
|
468
|
+
ctx.tools.register(createSkillSourceConfigTool(
|
|
469
|
+
config.governance.skillConfigPath,
|
|
470
|
+
config.governance.skillSource,
|
|
471
|
+
{
|
|
472
|
+
explicitPath: explicitSkillPath,
|
|
473
|
+
onConfigured(agent, data) {
|
|
474
|
+
appendEvent(agent, "odai/skill-source-configured", data);
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
));
|
|
357
478
|
ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
|
|
358
479
|
|
|
359
480
|
ctx.on("tools/result", (execution, result) => {
|
|
@@ -423,7 +544,7 @@ export function apply(ctx, rawConfig) {
|
|
|
423
544
|
if (routed.has(routeKey)) return downstream;
|
|
424
545
|
routed.add(routeKey);
|
|
425
546
|
|
|
426
|
-
const taskText =
|
|
547
|
+
const taskText = extractRoutingText(downstream.messages, agent?.session?.events).slice(0, config.routing.maxInputChars);
|
|
427
548
|
const decision = decideRoute({ text: taskText });
|
|
428
549
|
const routeRole = decision.targetRole ?? decision.role;
|
|
429
550
|
appendEvent(agent, "odai/route-decided", {
|
|
@@ -520,7 +641,7 @@ export function apply(ctx, rawConfig) {
|
|
|
520
641
|
provider: config.routing.provider,
|
|
521
642
|
decision: delegationDecision,
|
|
522
643
|
taskText,
|
|
523
|
-
roleContract: roleContracts[routeRole],
|
|
644
|
+
roleContract: (sharedSkillSelection(agent, turn)?.bundle ?? bundled).roleContracts[routeRole],
|
|
524
645
|
agent,
|
|
525
646
|
signal,
|
|
526
647
|
roleRoute,
|
|
@@ -577,7 +698,7 @@ export function apply(ctx, rawConfig) {
|
|
|
577
698
|
}, { prepend: true });
|
|
578
699
|
}
|
|
579
700
|
|
|
580
|
-
logger.info(`loaded canonical governance from ${skillPath}; routing=${config.routing.mode}`);
|
|
701
|
+
logger.info(`loaded canonical governance ${bundled.manifest.skillVersion} from ${skillPath}; skillSource=${explicitSkillPath ? "path" : config.governance.skillSource}; routing=${config.routing.mode}`);
|
|
581
702
|
}
|
|
582
703
|
|
|
583
704
|
function isSubagentSession(agent) {
|
|
@@ -60,6 +60,16 @@ const IRREVERSIBLE_ACTION_PATTERNS = [
|
|
|
60
60
|
/\b(?:delete|drop|clear|migrate|deploy|release|disable)\b/iu,
|
|
61
61
|
];
|
|
62
62
|
|
|
63
|
+
const CONTINUATION_PATTERNS = [
|
|
64
|
+
/(?:继续|接着|进一步|深入|再(?:判断|分析|评估|检查|处理)|按(?:照)?(?:刚才|上面|上述|前面|这个|该)|那就|就按)/iu,
|
|
65
|
+
/\b(?:continue|proceed|go ahead|follow up|dig deeper|based on (?:that|the previous|the above))\b/iu,
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
const LOW_RISK_TRANSFORM_PATTERNS = [
|
|
69
|
+
/(?:重述|总结|概括|压缩|翻译|改写|润色|格式化|解释(?:一下)?)/iu,
|
|
70
|
+
/\b(?:restate|summari[sz]e|translate|shorten|rewrite|format|explain)\b/iu,
|
|
71
|
+
];
|
|
72
|
+
|
|
63
73
|
function matchesAny(text, patterns) {
|
|
64
74
|
return patterns.some((pattern) => pattern.test(text));
|
|
65
75
|
}
|
|
@@ -72,6 +82,48 @@ function stripQuotedMaterial(text) {
|
|
|
72
82
|
.replace(/“[^”\n]*”|‘[^’\n]*’|「[^」\n]*」|『[^』\n]*』|《[^》\n]*》|"[^"\n]*"/gu, " ");
|
|
73
83
|
}
|
|
74
84
|
|
|
85
|
+
function hasContextualPlannerGap(text) {
|
|
86
|
+
return matchesAny(text, RISK_PATTERNS)
|
|
87
|
+
&& matchesAny(text, UNVERIFIED_CAUSAL_PATTERNS)
|
|
88
|
+
&& matchesAny(text, CONCRETE_CHANGE_PATTERNS)
|
|
89
|
+
&& (matchesAny(text, SPECIFIC_PARAMETER_PATTERNS)
|
|
90
|
+
|| matchesAny(text, URGENCY_PATTERNS)
|
|
91
|
+
|| matchesAny(text, IRREVERSIBLE_ACTION_PATTERNS));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isLowRiskTransform(text) {
|
|
95
|
+
return matchesAny(stripQuotedMaterial(text), LOW_RISK_TRANSFORM_PATTERNS);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function genuineUserText(message) {
|
|
99
|
+
if (message?.role !== "user" || message?.source?.kind !== "user" || !Array.isArray(message.content)) return "";
|
|
100
|
+
return message.content
|
|
101
|
+
.filter((block) => block?.type === "text" && typeof block.text === "string")
|
|
102
|
+
.map((block) => block.text)
|
|
103
|
+
.join("\n")
|
|
104
|
+
.trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function genuineUserTexts(messages, sessionEvents = []) {
|
|
108
|
+
const candidates = [
|
|
109
|
+
...(Array.isArray(messages) ? [...messages].reverse() : []),
|
|
110
|
+
...(Array.isArray(sessionEvents)
|
|
111
|
+
? [...sessionEvents].reverse()
|
|
112
|
+
.filter((event) => event?.type === "user/message")
|
|
113
|
+
.map((event) => event.data)
|
|
114
|
+
: []),
|
|
115
|
+
];
|
|
116
|
+
const seen = new Set();
|
|
117
|
+
const texts = [];
|
|
118
|
+
for (const message of candidates) {
|
|
119
|
+
const text = genuineUserText(message);
|
|
120
|
+
if (!text || seen.has(text)) continue;
|
|
121
|
+
seen.add(text);
|
|
122
|
+
texts.push(text);
|
|
123
|
+
}
|
|
124
|
+
return texts;
|
|
125
|
+
}
|
|
126
|
+
|
|
75
127
|
function route(role, reasonCode, reason, signals, action = role === "controller" ? "direct" : "delegate", targetRole) {
|
|
76
128
|
return Object.freeze({
|
|
77
129
|
role,
|
|
@@ -137,11 +189,11 @@ export function decideRoute(input = {}) {
|
|
|
137
189
|
);
|
|
138
190
|
}
|
|
139
191
|
|
|
140
|
-
const
|
|
192
|
+
const contextualPlannerGap = riskPresent
|
|
141
193
|
&& unverifiedCausalClaim
|
|
142
194
|
&& concreteChangeRequest
|
|
143
195
|
&& (specificOperationalParameter || urgencyPressure || irreversibleAction);
|
|
144
|
-
if (
|
|
196
|
+
if (contextualPlannerGap) {
|
|
145
197
|
return route(
|
|
146
198
|
"controller",
|
|
147
199
|
HIGH_IMPACT_PLANNER_REASON,
|
|
@@ -162,23 +214,29 @@ export function decideRoute(input = {}) {
|
|
|
162
214
|
}
|
|
163
215
|
|
|
164
216
|
export function extractLatestUserText(messages) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
.join("\n")
|
|
177
|
-
.trim();
|
|
178
|
-
if (text) return text;
|
|
217
|
+
return genuineUserTexts(messages)[0] ?? "";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function extractRoutingText(messages, sessionEvents) {
|
|
221
|
+
const texts = genuineUserTexts(messages, sessionEvents);
|
|
222
|
+
const latest = texts[0] ?? "";
|
|
223
|
+
if (!latest
|
|
224
|
+
|| hasContextualPlannerGap(latest)
|
|
225
|
+
|| !matchesAny(stripQuotedMaterial(latest), CONTINUATION_PATTERNS)
|
|
226
|
+
|| isLowRiskTransform(latest)) {
|
|
227
|
+
return latest;
|
|
179
228
|
}
|
|
180
229
|
|
|
181
|
-
|
|
230
|
+
let referencedHighImpact;
|
|
231
|
+
for (const text of texts.slice(1)) {
|
|
232
|
+
if (hasContextualPlannerGap(text)) {
|
|
233
|
+
referencedHighImpact = text;
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
if (!isLowRiskTransform(text)) break;
|
|
237
|
+
}
|
|
238
|
+
if (!referencedHighImpact) return latest;
|
|
239
|
+
return `${latest}\n\nReferenced earlier high-impact user context:\n${referencedHighImpact}`;
|
|
182
240
|
}
|
|
183
241
|
|
|
184
242
|
export function requiresFailClosedProtection(decision) {
|
|
@@ -283,6 +341,7 @@ export function renderDelegationPrompt(decision, taskText, roleContract) {
|
|
|
283
341
|
`You are the odai ${decision.role}.`,
|
|
284
342
|
roleContract.trim(),
|
|
285
343
|
"Runtime boundary: do not edit files, run shell commands, ask the user, or delegate further. The controller owns all final decisions and delivery.",
|
|
344
|
+
"Context boundary: this is a bounded task and evidence packet, not an inherited controller transcript. Use only the supplied contract, task, evidence, and source pointers; do not request or reconstruct the controller's full history.",
|
|
286
345
|
"",
|
|
287
346
|
"Task:",
|
|
288
347
|
taskText,
|