odai-dsh-agent 0.0.4 → 0.0.6
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 +12 -2
- package/bin/odai-dsh-agent.mjs +1 -1
- package/package.json +1 -1
- package/preset/odai/runtime/index.mjs +172 -27
- 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
|
@@ -13,7 +13,7 @@ $DSH_HOME/.agent-presets/odai/
|
|
|
13
13
|
.odai-agent.json
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
The composition preserves every capability row and setting from the `standard` agent surface in `@deepseek-ai/dsh@0.1.0-rc.6`, then adds the scoped Odai runtime in default `auto` mode. Release verification compares the complete derived composition against that exact DSH Standard source; a missing, added, reordered, or changed Standard row fails the probe. Host-owned persistence, sandbox, approval, registries, and base controller selection remain in the selected DSH profile. Ordinary requests stay on that controller. A complete contextual high-impact decision gap upgrades the same turn only when a planner mapping exists; the controller's durable `request/header` proves the actual route. Explicit independent planning/review requests still start a verified child when their mappings exist. `execute` remains an explicit all-delegation comparison mode. `observe` changes no model and starts no child; it injects a local evidence-and-decision protocol and makes an unresolved high-impact turn read-only.
|
|
16
|
+
The composition preserves every capability row and setting from the `standard` agent surface in `@deepseek-ai/dsh@0.1.0-rc.6`, then adds the scoped Odai runtime in default `auto` mode. Release verification compares the complete derived composition against that exact DSH Standard source; a missing, added, reordered, or changed Standard row fails the probe. Host-owned persistence, sandbox, approval, registries, and base controller selection remain in the selected DSH profile. Ordinary requests stay on that controller. A complete contextual high-impact decision gap upgrades the same turn only when a planner mapping exists; the controller's durable `request/header` proves the actual route. Explicit independent planning/review requests still start a verified child when their mappings exist. `execute` remains an explicit all-delegation comparison mode. `observe` changes no model and starts no child; it injects a local evidence-and-decision protocol and makes an unresolved high-impact turn read-only. For DSH compaction on the same provider/model, an omitted summarizer reasoning setting inherits the session's current user-selected reasoning effort so the auxiliary call remains cache-compatible; explicit and cross-model settings are not changed.
|
|
17
17
|
|
|
18
18
|
## Install
|
|
19
19
|
|
|
@@ -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/bin/odai-dsh-agent.mjs
CHANGED
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,9 +27,26 @@ 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
|
-
export const inject = ["systemPrompt", "tools", "subagents"];
|
|
49
|
+
export const inject = ["systemPrompt", "tools", "subagents", "sessions"];
|
|
33
50
|
|
|
34
51
|
const PLUGIN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
35
52
|
const ROUTING_MODES = new Set(["off", "observe", "auto", "execute"]);
|
|
@@ -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,51 @@ 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
|
|
121
|
+
export function inheritCompactionReasoning(options, sessions) {
|
|
122
|
+
if (options?.purpose !== "compaction"
|
|
123
|
+
|| options.reasoningEffort !== undefined
|
|
124
|
+
|| options.sessionId === undefined
|
|
125
|
+
|| !Object.isExtensible(options)) {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const route = sessions?.get?.(options.sessionId)?.requestHeader?.()?.config;
|
|
130
|
+
if (route?.provider !== options.provider
|
|
131
|
+
|| route?.model !== options.model
|
|
132
|
+
|| typeof route.reasoningEffort !== "string"
|
|
133
|
+
|| route.reasoningEffort.length === 0) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
options.reasoningEffort = route.reasoningEffort;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolveSkillPath(configuredPath, env = process.env) {
|
|
142
|
+
const explicit = configuredPath ?? env.ODAI_SKILL_PATH;
|
|
143
|
+
if (explicit !== undefined) {
|
|
144
|
+
const path = resolve(explicit);
|
|
145
|
+
if (!existsSync(path)) throw new Error(`explicit Odai canonical skill not found: ${path}`);
|
|
146
|
+
return path;
|
|
147
|
+
}
|
|
148
|
+
|
|
98
149
|
const candidates = [
|
|
99
|
-
configuredPath,
|
|
100
|
-
process.env.ODAI_SKILL_PATH,
|
|
101
150
|
resolve(PLUGIN_DIR, "skills/odai/SKILL.md"),
|
|
102
151
|
resolve(PLUGIN_DIR, "../../skills/odai/SKILL.md"),
|
|
103
|
-
]
|
|
104
|
-
|
|
152
|
+
];
|
|
105
153
|
const found = candidates.find((candidate) => existsSync(candidate));
|
|
106
154
|
if (found) return found;
|
|
107
|
-
|
|
108
|
-
throw new Error(`odai canonical skill not found; checked: ${candidates.join(", ")}`);
|
|
155
|
+
throw new Error(`Odai bundled canonical skill not found; checked: ${candidates.join(", ")}`);
|
|
109
156
|
}
|
|
110
157
|
|
|
111
158
|
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
|
-
})));
|
|
159
|
+
return loadSkillBundle(skillPath).roleContracts;
|
|
119
160
|
}
|
|
120
161
|
|
|
121
162
|
function deepFreeze(value) {
|
|
@@ -263,13 +304,20 @@ export async function runRoutedRole({ subagents, provider, decision, taskText, r
|
|
|
263
304
|
return outcome;
|
|
264
305
|
}
|
|
265
306
|
|
|
266
|
-
function canonicalPrompt(
|
|
307
|
+
function canonicalPrompt(selection) {
|
|
308
|
+
const { bundle } = selection;
|
|
309
|
+
const fallback = selection.status === "fallback"
|
|
310
|
+
? `Selection fallback: ${selection.reasonCode}${selection.detail ? ` (${selection.detail})` : ""}.`
|
|
311
|
+
: undefined;
|
|
267
312
|
return [
|
|
268
313
|
"## odai canonical governance",
|
|
269
|
-
`Canonical source: ${
|
|
314
|
+
`Canonical source: ${bundle.source} (${bundle.provider})`,
|
|
315
|
+
`Canonical skill: ${bundle.manifest.skillVersion}; runtime contract: ${bundle.manifest.runtimeContract}; digest: ${bundle.digest}.`,
|
|
316
|
+
...(fallback ? [fallback] : []),
|
|
270
317
|
"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.",
|
|
318
|
+
"Odai governance is already loaded by this runtime; do not call the skill tool to load odai again.",
|
|
271
319
|
"",
|
|
272
|
-
skillText,
|
|
320
|
+
bundle.skillText,
|
|
273
321
|
].join("\n");
|
|
274
322
|
}
|
|
275
323
|
|
|
@@ -302,21 +350,108 @@ export function apply(ctx, rawConfig) {
|
|
|
302
350
|
}
|
|
303
351
|
};
|
|
304
352
|
const hasSessionEvent = (agent, type, predicate) => evidence.has(agent, type, predicate);
|
|
353
|
+
ctx.on("llm/stream", (options, next) => {
|
|
354
|
+
inheritCompactionReasoning(options, ctx.sessions);
|
|
355
|
+
return next();
|
|
356
|
+
});
|
|
305
357
|
const skillPath = resolveSkillPath(config.skillPath);
|
|
306
|
-
const
|
|
307
|
-
|
|
308
|
-
const
|
|
358
|
+
const explicitSkillPath = config.skillPath !== undefined
|
|
359
|
+
|| (typeof process.env.ODAI_SKILL_PATH === "string" && process.env.ODAI_SKILL_PATH.trim() !== "");
|
|
360
|
+
const bundled = loadSkillBundle(skillPath, {
|
|
361
|
+
source: explicitSkillPath ? "path" : "bundled",
|
|
362
|
+
provider: explicitSkillPath ? "odai-explicit-path" : "odai-dsh-runtime",
|
|
363
|
+
});
|
|
364
|
+
if (bundled.manifest.runtimeContract !== ODAI_RUNTIME_CONTRACT) {
|
|
365
|
+
throw new Error(`Odai canonical runtimeContract ${bundled.manifest.runtimeContract} is incompatible with this runtime`);
|
|
366
|
+
}
|
|
367
|
+
const baseSelection = Object.freeze({
|
|
368
|
+
mode: explicitSkillPath ? "path" : "bundled",
|
|
369
|
+
status: "selected",
|
|
370
|
+
reasonCode: explicitSkillPath ? "explicit-path" : "bundled-configured",
|
|
371
|
+
bundle: bundled,
|
|
372
|
+
rejections: Object.freeze([]),
|
|
373
|
+
});
|
|
309
374
|
|
|
310
375
|
ctx.systemPrompt.section({
|
|
311
376
|
name: "odai:canonical-governance",
|
|
312
377
|
order: -20,
|
|
313
|
-
text: canonicalPrompt(
|
|
378
|
+
text: canonicalPrompt(baseSelection),
|
|
314
379
|
});
|
|
315
380
|
ctx.systemPrompt.section({
|
|
316
381
|
name: "odai:routing-configuration",
|
|
317
382
|
order: -19,
|
|
318
383
|
text: ROUTING_CONFIG_PROMPT,
|
|
319
384
|
});
|
|
385
|
+
ctx.systemPrompt.section({
|
|
386
|
+
name: "odai:skill-source-configuration",
|
|
387
|
+
order: -18,
|
|
388
|
+
text: SKILL_SOURCE_CONFIG_PROMPT,
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
const optionalSkillRegistry = () => {
|
|
392
|
+
try {
|
|
393
|
+
if (ctx.skills && typeof ctx.skills.get === "function") return ctx.skills;
|
|
394
|
+
} catch {}
|
|
395
|
+
try {
|
|
396
|
+
const service = typeof ctx.get === "function" ? ctx.get("skills") : undefined;
|
|
397
|
+
return service && typeof service.get === "function" ? service : undefined;
|
|
398
|
+
} catch {
|
|
399
|
+
return undefined;
|
|
400
|
+
}
|
|
401
|
+
};
|
|
402
|
+
const selectForAgent = async (agent, context) => {
|
|
403
|
+
if (explicitSkillPath) return baseSelection;
|
|
404
|
+
let mode;
|
|
405
|
+
try {
|
|
406
|
+
mode = effectiveSkillSource(config.governance.skillConfigPath, config.governance.skillSource);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
return Object.freeze({
|
|
409
|
+
...baseSelection,
|
|
410
|
+
mode: "bundled",
|
|
411
|
+
status: "fallback",
|
|
412
|
+
reasonCode: "source-config-invalid",
|
|
413
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return resolveSkillSelection({
|
|
417
|
+
mode,
|
|
418
|
+
bundled,
|
|
419
|
+
cwd: agent.session?.header?.cwd,
|
|
420
|
+
scope: context.scope,
|
|
421
|
+
signal: context.signal,
|
|
422
|
+
skills: optionalSkillRegistry(),
|
|
423
|
+
});
|
|
424
|
+
};
|
|
425
|
+
ctx.on("system-prompt/assemble", async (assembly, context, next) => {
|
|
426
|
+
const downstream = await next();
|
|
427
|
+
const agent = context.agent;
|
|
428
|
+
if (!agent) return downstream;
|
|
429
|
+
const selection = await selectSharedSkillForTurn(agent, () => selectForAgent(agent, context));
|
|
430
|
+
const turn = currentAgentTurn(agent);
|
|
431
|
+
const selectionEvidence = {
|
|
432
|
+
...(turn === undefined ? {} : { turn }),
|
|
433
|
+
requestedMode: selection.mode,
|
|
434
|
+
status: selection.status,
|
|
435
|
+
reasonCode: selection.reasonCode,
|
|
436
|
+
effectiveSource: selection.bundle.source,
|
|
437
|
+
skillVersion: selection.bundle.manifest.skillVersion,
|
|
438
|
+
runtimeContract: selection.bundle.manifest.runtimeContract,
|
|
439
|
+
digest: selection.bundle.digest,
|
|
440
|
+
rejections: selection.rejections.map(({ source, reasonCode }) => ({ source, reasonCode })),
|
|
441
|
+
};
|
|
442
|
+
if (!hasSessionEvent(agent, "odai/skill-selected", (data) => data?.turn === turn && data?.digest === selection.bundle.digest)) {
|
|
443
|
+
appendEvent(agent, "odai/skill-selected", selectionEvidence);
|
|
444
|
+
}
|
|
445
|
+
if (selection.status === "fallback") {
|
|
446
|
+
logger.warn(`Odai skill source ${selection.mode} fell back to bundled governance (${selection.reasonCode})`);
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
...downstream,
|
|
450
|
+
sections: downstream.sections.map((section) => section.name === "odai:canonical-governance"
|
|
451
|
+
? { ...section, text: canonicalPrompt(selection) }
|
|
452
|
+
: section),
|
|
453
|
+
};
|
|
454
|
+
});
|
|
320
455
|
|
|
321
456
|
const routeProtections = new WeakMap();
|
|
322
457
|
const controllerUpgrades = new WeakMap();
|
|
@@ -354,6 +489,16 @@ export function apply(ctx, rawConfig) {
|
|
|
354
489
|
appendEvent(agent, "odai/routing-configured", data);
|
|
355
490
|
},
|
|
356
491
|
}));
|
|
492
|
+
ctx.tools.register(createSkillSourceConfigTool(
|
|
493
|
+
config.governance.skillConfigPath,
|
|
494
|
+
config.governance.skillSource,
|
|
495
|
+
{
|
|
496
|
+
explicitPath: explicitSkillPath,
|
|
497
|
+
onConfigured(agent, data) {
|
|
498
|
+
appendEvent(agent, "odai/skill-source-configured", data);
|
|
499
|
+
},
|
|
500
|
+
},
|
|
501
|
+
));
|
|
357
502
|
ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
|
|
358
503
|
|
|
359
504
|
ctx.on("tools/result", (execution, result) => {
|
|
@@ -423,7 +568,7 @@ export function apply(ctx, rawConfig) {
|
|
|
423
568
|
if (routed.has(routeKey)) return downstream;
|
|
424
569
|
routed.add(routeKey);
|
|
425
570
|
|
|
426
|
-
const taskText =
|
|
571
|
+
const taskText = extractRoutingText(downstream.messages, agent?.session?.events).slice(0, config.routing.maxInputChars);
|
|
427
572
|
const decision = decideRoute({ text: taskText });
|
|
428
573
|
const routeRole = decision.targetRole ?? decision.role;
|
|
429
574
|
appendEvent(agent, "odai/route-decided", {
|
|
@@ -520,7 +665,7 @@ export function apply(ctx, rawConfig) {
|
|
|
520
665
|
provider: config.routing.provider,
|
|
521
666
|
decision: delegationDecision,
|
|
522
667
|
taskText,
|
|
523
|
-
roleContract: roleContracts[routeRole],
|
|
668
|
+
roleContract: (sharedSkillSelection(agent, turn)?.bundle ?? bundled).roleContracts[routeRole],
|
|
524
669
|
agent,
|
|
525
670
|
signal,
|
|
526
671
|
roleRoute,
|
|
@@ -577,7 +722,7 @@ export function apply(ctx, rawConfig) {
|
|
|
577
722
|
}, { prepend: true });
|
|
578
723
|
}
|
|
579
724
|
|
|
580
|
-
logger.info(`loaded canonical governance from ${skillPath}; routing=${config.routing.mode}`);
|
|
725
|
+
logger.info(`loaded canonical governance ${bundled.manifest.skillVersion} from ${skillPath}; skillSource=${explicitSkillPath ? "path" : config.governance.skillSource}; routing=${config.routing.mode}`);
|
|
581
726
|
}
|
|
582
727
|
|
|
583
728
|
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,
|