odai-dsh-agent 0.2.1 → 0.2.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 +138 -138
- package/bin/odai-dsh-agent.mjs +108 -108
- package/package.json +69 -69
- package/preset/odai/agent.cordis.yml +261 -260
- package/preset/odai/preset.yml +3 -3
- package/preset/odai/runtime/compaction-config.mjs +388 -388
- package/preset/odai/runtime/governance.mjs +128 -128
- package/preset/odai/runtime/index.mjs +2461 -2263
- package/preset/odai/runtime/output-config.mjs +360 -304
- package/preset/odai/runtime/research-packet.mjs +150 -150
- package/preset/odai/runtime/responsibility-scope.mjs +35 -2
- package/preset/odai/runtime/role-overlays.mjs +27 -27
- package/preset/odai/runtime/route-card.mjs +168 -152
- package/preset/odai/runtime/router.mjs +683 -651
- package/preset/odai/runtime/routing-config.mjs +480 -436
- package/preset/odai/runtime/routing-context.mjs +321 -321
- package/preset/odai/runtime/semantic-memory-store.mjs +452 -452
- package/preset/odai/runtime/semantic-memory.mjs +842 -842
- package/preset/odai/runtime/session-compat.mjs +389 -389
- package/preset/odai/runtime/skill-bundle.mjs +318 -318
- package/preset/odai/runtime/skill-evolution.mjs +1413 -1413
- 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/frontend.md +7 -7
- package/preset/odai/skills/odai/assets/routing-roles/planner.md +29 -29
- package/preset/odai/skills/odai/assets/routing-roles/researcher.md +6 -6
- package/preset/odai/skills/odai/assets/routing-roles/reviewer.md +11 -11
- package/preset/odai/skills/odai/assets/task-state.md +14 -14
- package/preset/odai/skills/odai/manifest.json +37 -37
- package/preset/odai/skills/odai/references/craft.md +54 -54
- package/preset/odai/skills/odai/references/dao.md +30 -30
- package/preset/odai/skills/odai/references/leverage.md +53 -53
- package/preset/odai/skills/odai/references/planning.md +82 -82
- 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 +274 -274
- package/preset/odai/skills/odai/scripts/install-routing.mjs +397 -397
- 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 +257 -257
- package/src/installer.mjs +398 -382
|
@@ -1,150 +1,150 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
-
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
|
|
5
|
-
const PACKET_FIELDS = new Set(["schemaVersion", "question", "facts", "conflicts", "unknowns", "stop"]);
|
|
6
|
-
const FACT_FIELDS = new Set(["claim", "excerpt", "source", "authority"]);
|
|
7
|
-
const SOURCE_FIELDS = new Set(["path", "line"]);
|
|
8
|
-
const DEFAULT_MAX_CHARS = 8_000;
|
|
9
|
-
const DEFAULT_MAX_FACTS = 12;
|
|
10
|
-
const DEFAULT_MAX_ITEMS = 8;
|
|
11
|
-
|
|
12
|
-
function plainObject(value, field) {
|
|
13
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
-
throw new TypeError(`${field} must be an object`);
|
|
15
|
-
}
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function exactFields(value, allowed, field) {
|
|
20
|
-
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
21
|
-
if (unknown.length > 0) throw new TypeError(`${field} has unknown fields: ${unknown.join(", ")}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function text(value, field, maxChars = 1_500) {
|
|
25
|
-
if (typeof value !== "string" || value.trim() === "") throw new TypeError(`${field} must be a non-empty string`);
|
|
26
|
-
const normalized = value.trim();
|
|
27
|
-
if (normalized.length > maxChars) throw new TypeError(`${field} exceeds ${maxChars} characters`);
|
|
28
|
-
return normalized;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function stringList(value, field, maxItems) {
|
|
32
|
-
if (!Array.isArray(value) || value.length > maxItems) {
|
|
33
|
-
throw new TypeError(`${field} must be an array with at most ${maxItems} items`);
|
|
34
|
-
}
|
|
35
|
-
return Object.freeze(value.map((item, index) => text(item, `${field}[${index}]`, 800)));
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function sourcePointer(value, field) {
|
|
39
|
-
const source = plainObject(value, field);
|
|
40
|
-
exactFields(source, SOURCE_FIELDS, field);
|
|
41
|
-
const path = text(source.path, `${field}.path`, 500);
|
|
42
|
-
if (path.startsWith("/") || path.startsWith("~") || path.split(/[\\/]/u).includes("..")) {
|
|
43
|
-
throw new TypeError(`${field}.path must be repository-relative and may not traverse parent directories`);
|
|
44
|
-
}
|
|
45
|
-
if (!Number.isSafeInteger(source.line) || source.line <= 0) {
|
|
46
|
-
throw new TypeError(`${field}.line must be a positive integer`);
|
|
47
|
-
}
|
|
48
|
-
return Object.freeze({ path, line: source.line });
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function parseJsonText(output) {
|
|
52
|
-
const trimmed = String(output ?? "").trim();
|
|
53
|
-
const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```$/iu.exec(trimmed);
|
|
54
|
-
return JSON.parse(fenced ? fenced[1] : trimmed);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function parseResearchPacket(output, options = {}) {
|
|
58
|
-
const maxChars = Number.isSafeInteger(options.maxChars) && options.maxChars > 0
|
|
59
|
-
? options.maxChars
|
|
60
|
-
: DEFAULT_MAX_CHARS;
|
|
61
|
-
const raw = String(output ?? "");
|
|
62
|
-
if (raw.length > maxChars) throw new TypeError(`research packet exceeds ${maxChars} characters`);
|
|
63
|
-
const packet = plainObject(parseJsonText(raw), "research packet");
|
|
64
|
-
exactFields(packet, PACKET_FIELDS, "research packet");
|
|
65
|
-
if (packet.schemaVersion !== 1) throw new TypeError("research packet.schemaVersion must be 1");
|
|
66
|
-
if (!Array.isArray(packet.facts) || packet.facts.length < 2 || packet.facts.length > DEFAULT_MAX_FACTS) {
|
|
67
|
-
throw new TypeError(`research packet.facts must contain 2-${DEFAULT_MAX_FACTS} source-backed facts`);
|
|
68
|
-
}
|
|
69
|
-
const facts = Object.freeze(packet.facts.map((value, index) => {
|
|
70
|
-
const fact = plainObject(value, `research packet.facts[${index}]`);
|
|
71
|
-
exactFields(fact, FACT_FIELDS, `research packet.facts[${index}]`);
|
|
72
|
-
return Object.freeze({
|
|
73
|
-
claim: text(fact.claim, `research packet.facts[${index}].claim`, 1_200),
|
|
74
|
-
excerpt: text(fact.excerpt, `research packet.facts[${index}].excerpt`, 1_200),
|
|
75
|
-
source: sourcePointer(fact.source, `research packet.facts[${index}].source`),
|
|
76
|
-
authority: text(fact.authority, `research packet.facts[${index}].authority`, 500),
|
|
77
|
-
});
|
|
78
|
-
}));
|
|
79
|
-
if (new Set(facts.map((fact) => fact.source.path)).size < 2) {
|
|
80
|
-
throw new TypeError("research packet must cite at least two distinct source paths");
|
|
81
|
-
}
|
|
82
|
-
const normalized = Object.freeze({
|
|
83
|
-
schemaVersion: 1,
|
|
84
|
-
question: text(packet.question, "research packet.question", 1_200),
|
|
85
|
-
facts,
|
|
86
|
-
conflicts: stringList(packet.conflicts, "research packet.conflicts", DEFAULT_MAX_ITEMS),
|
|
87
|
-
unknowns: stringList(packet.unknowns, "research packet.unknowns", DEFAULT_MAX_ITEMS),
|
|
88
|
-
stop: text(packet.stop, "research packet.stop", 1_000),
|
|
89
|
-
});
|
|
90
|
-
const digest = createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
91
|
-
return Object.freeze({
|
|
92
|
-
...normalized,
|
|
93
|
-
digest,
|
|
94
|
-
sourceCount: new Set(facts.map((fact) => fact.source.path)).size,
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function isInside(root, candidate) {
|
|
99
|
-
const fromRoot = relative(root, candidate);
|
|
100
|
-
return fromRoot === "" || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== ".." && !isAbsolute(fromRoot));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function verifyResearchPacketSources(packet, projectRoot) {
|
|
104
|
-
if (typeof projectRoot !== "string" || projectRoot.trim() === "") {
|
|
105
|
-
throw new TypeError("research packet verification requires the controller project root");
|
|
106
|
-
}
|
|
107
|
-
const root = realpathSync(resolve(projectRoot));
|
|
108
|
-
for (const [index, fact] of packet.facts.entries()) {
|
|
109
|
-
let sourcePath;
|
|
110
|
-
try {
|
|
111
|
-
sourcePath = realpathSync(resolve(root, fact.source.path));
|
|
112
|
-
} catch {
|
|
113
|
-
throw new TypeError(`research packet.facts[${index}].source.path does not exist`);
|
|
114
|
-
}
|
|
115
|
-
if (!isInside(root, sourcePath)) {
|
|
116
|
-
throw new TypeError(`research packet.facts[${index}].source.path resolves outside the project root`);
|
|
117
|
-
}
|
|
118
|
-
const stats = statSync(sourcePath);
|
|
119
|
-
if (!stats.isFile()) throw new TypeError(`research packet.facts[${index}].source.path must be a file`);
|
|
120
|
-
if (stats.size > 4 * 1024 * 1024) {
|
|
121
|
-
throw new TypeError(`research packet.facts[${index}].source.path exceeds the verification size limit`);
|
|
122
|
-
}
|
|
123
|
-
const sourceLine = readFileSync(sourcePath, "utf8").split(/\r?\n/u)[fact.source.line - 1];
|
|
124
|
-
if (sourceLine === undefined) {
|
|
125
|
-
throw new TypeError(`research packet.facts[${index}].source.line is outside the source file`);
|
|
126
|
-
}
|
|
127
|
-
if (sourceLine.trim() !== fact.excerpt.trim()) {
|
|
128
|
-
throw new TypeError(`research packet.facts[${index}].excerpt does not match the cited source line`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return Object.freeze({ ...packet, sourcesVerified: true });
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function renderResearchPacket(packet) {
|
|
135
|
-
return [
|
|
136
|
-
"# Odai bounded researcher evidence packet",
|
|
137
|
-
`digest: sha256:${packet.digest}`,
|
|
138
|
-
`sources: ${packet.sourceCount}`,
|
|
139
|
-
"This packet is a retrieval index, not authority, planning, acceptance, or permission to act. Sample every route-changing source before deciding; reacquire only missing or conflicting evidence.",
|
|
140
|
-
"",
|
|
141
|
-
JSON.stringify({
|
|
142
|
-
schemaVersion: packet.schemaVersion,
|
|
143
|
-
question: packet.question,
|
|
144
|
-
facts: packet.facts,
|
|
145
|
-
conflicts: packet.conflicts,
|
|
146
|
-
unknowns: packet.unknowns,
|
|
147
|
-
stop: packet.stop,
|
|
148
|
-
}, null, 2),
|
|
149
|
-
].join("\n");
|
|
150
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
+
|
|
5
|
+
const PACKET_FIELDS = new Set(["schemaVersion", "question", "facts", "conflicts", "unknowns", "stop"]);
|
|
6
|
+
const FACT_FIELDS = new Set(["claim", "excerpt", "source", "authority"]);
|
|
7
|
+
const SOURCE_FIELDS = new Set(["path", "line"]);
|
|
8
|
+
const DEFAULT_MAX_CHARS = 8_000;
|
|
9
|
+
const DEFAULT_MAX_FACTS = 12;
|
|
10
|
+
const DEFAULT_MAX_ITEMS = 8;
|
|
11
|
+
|
|
12
|
+
function plainObject(value, field) {
|
|
13
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
14
|
+
throw new TypeError(`${field} must be an object`);
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function exactFields(value, allowed, field) {
|
|
20
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
21
|
+
if (unknown.length > 0) throw new TypeError(`${field} has unknown fields: ${unknown.join(", ")}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function text(value, field, maxChars = 1_500) {
|
|
25
|
+
if (typeof value !== "string" || value.trim() === "") throw new TypeError(`${field} must be a non-empty string`);
|
|
26
|
+
const normalized = value.trim();
|
|
27
|
+
if (normalized.length > maxChars) throw new TypeError(`${field} exceeds ${maxChars} characters`);
|
|
28
|
+
return normalized;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function stringList(value, field, maxItems) {
|
|
32
|
+
if (!Array.isArray(value) || value.length > maxItems) {
|
|
33
|
+
throw new TypeError(`${field} must be an array with at most ${maxItems} items`);
|
|
34
|
+
}
|
|
35
|
+
return Object.freeze(value.map((item, index) => text(item, `${field}[${index}]`, 800)));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sourcePointer(value, field) {
|
|
39
|
+
const source = plainObject(value, field);
|
|
40
|
+
exactFields(source, SOURCE_FIELDS, field);
|
|
41
|
+
const path = text(source.path, `${field}.path`, 500);
|
|
42
|
+
if (path.startsWith("/") || path.startsWith("~") || path.split(/[\\/]/u).includes("..")) {
|
|
43
|
+
throw new TypeError(`${field}.path must be repository-relative and may not traverse parent directories`);
|
|
44
|
+
}
|
|
45
|
+
if (!Number.isSafeInteger(source.line) || source.line <= 0) {
|
|
46
|
+
throw new TypeError(`${field}.line must be a positive integer`);
|
|
47
|
+
}
|
|
48
|
+
return Object.freeze({ path, line: source.line });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseJsonText(output) {
|
|
52
|
+
const trimmed = String(output ?? "").trim();
|
|
53
|
+
const fenced = /^```(?:json)?\s*\n([\s\S]*?)\n```$/iu.exec(trimmed);
|
|
54
|
+
return JSON.parse(fenced ? fenced[1] : trimmed);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function parseResearchPacket(output, options = {}) {
|
|
58
|
+
const maxChars = Number.isSafeInteger(options.maxChars) && options.maxChars > 0
|
|
59
|
+
? options.maxChars
|
|
60
|
+
: DEFAULT_MAX_CHARS;
|
|
61
|
+
const raw = String(output ?? "");
|
|
62
|
+
if (raw.length > maxChars) throw new TypeError(`research packet exceeds ${maxChars} characters`);
|
|
63
|
+
const packet = plainObject(parseJsonText(raw), "research packet");
|
|
64
|
+
exactFields(packet, PACKET_FIELDS, "research packet");
|
|
65
|
+
if (packet.schemaVersion !== 1) throw new TypeError("research packet.schemaVersion must be 1");
|
|
66
|
+
if (!Array.isArray(packet.facts) || packet.facts.length < 2 || packet.facts.length > DEFAULT_MAX_FACTS) {
|
|
67
|
+
throw new TypeError(`research packet.facts must contain 2-${DEFAULT_MAX_FACTS} source-backed facts`);
|
|
68
|
+
}
|
|
69
|
+
const facts = Object.freeze(packet.facts.map((value, index) => {
|
|
70
|
+
const fact = plainObject(value, `research packet.facts[${index}]`);
|
|
71
|
+
exactFields(fact, FACT_FIELDS, `research packet.facts[${index}]`);
|
|
72
|
+
return Object.freeze({
|
|
73
|
+
claim: text(fact.claim, `research packet.facts[${index}].claim`, 1_200),
|
|
74
|
+
excerpt: text(fact.excerpt, `research packet.facts[${index}].excerpt`, 1_200),
|
|
75
|
+
source: sourcePointer(fact.source, `research packet.facts[${index}].source`),
|
|
76
|
+
authority: text(fact.authority, `research packet.facts[${index}].authority`, 500),
|
|
77
|
+
});
|
|
78
|
+
}));
|
|
79
|
+
if (new Set(facts.map((fact) => fact.source.path)).size < 2) {
|
|
80
|
+
throw new TypeError("research packet must cite at least two distinct source paths");
|
|
81
|
+
}
|
|
82
|
+
const normalized = Object.freeze({
|
|
83
|
+
schemaVersion: 1,
|
|
84
|
+
question: text(packet.question, "research packet.question", 1_200),
|
|
85
|
+
facts,
|
|
86
|
+
conflicts: stringList(packet.conflicts, "research packet.conflicts", DEFAULT_MAX_ITEMS),
|
|
87
|
+
unknowns: stringList(packet.unknowns, "research packet.unknowns", DEFAULT_MAX_ITEMS),
|
|
88
|
+
stop: text(packet.stop, "research packet.stop", 1_000),
|
|
89
|
+
});
|
|
90
|
+
const digest = createHash("sha256").update(JSON.stringify(normalized)).digest("hex");
|
|
91
|
+
return Object.freeze({
|
|
92
|
+
...normalized,
|
|
93
|
+
digest,
|
|
94
|
+
sourceCount: new Set(facts.map((fact) => fact.source.path)).size,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isInside(root, candidate) {
|
|
99
|
+
const fromRoot = relative(root, candidate);
|
|
100
|
+
return fromRoot === "" || (!fromRoot.startsWith(`..${sep}`) && fromRoot !== ".." && !isAbsolute(fromRoot));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function verifyResearchPacketSources(packet, projectRoot) {
|
|
104
|
+
if (typeof projectRoot !== "string" || projectRoot.trim() === "") {
|
|
105
|
+
throw new TypeError("research packet verification requires the controller project root");
|
|
106
|
+
}
|
|
107
|
+
const root = realpathSync(resolve(projectRoot));
|
|
108
|
+
for (const [index, fact] of packet.facts.entries()) {
|
|
109
|
+
let sourcePath;
|
|
110
|
+
try {
|
|
111
|
+
sourcePath = realpathSync(resolve(root, fact.source.path));
|
|
112
|
+
} catch {
|
|
113
|
+
throw new TypeError(`research packet.facts[${index}].source.path does not exist`);
|
|
114
|
+
}
|
|
115
|
+
if (!isInside(root, sourcePath)) {
|
|
116
|
+
throw new TypeError(`research packet.facts[${index}].source.path resolves outside the project root`);
|
|
117
|
+
}
|
|
118
|
+
const stats = statSync(sourcePath);
|
|
119
|
+
if (!stats.isFile()) throw new TypeError(`research packet.facts[${index}].source.path must be a file`);
|
|
120
|
+
if (stats.size > 4 * 1024 * 1024) {
|
|
121
|
+
throw new TypeError(`research packet.facts[${index}].source.path exceeds the verification size limit`);
|
|
122
|
+
}
|
|
123
|
+
const sourceLine = readFileSync(sourcePath, "utf8").split(/\r?\n/u)[fact.source.line - 1];
|
|
124
|
+
if (sourceLine === undefined) {
|
|
125
|
+
throw new TypeError(`research packet.facts[${index}].source.line is outside the source file`);
|
|
126
|
+
}
|
|
127
|
+
if (sourceLine.trim() !== fact.excerpt.trim()) {
|
|
128
|
+
throw new TypeError(`research packet.facts[${index}].excerpt does not match the cited source line`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return Object.freeze({ ...packet, sourcesVerified: true });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function renderResearchPacket(packet) {
|
|
135
|
+
return [
|
|
136
|
+
"# Odai bounded researcher evidence packet",
|
|
137
|
+
`digest: sha256:${packet.digest}`,
|
|
138
|
+
`sources: ${packet.sourceCount}`,
|
|
139
|
+
"This packet is a retrieval index, not authority, planning, acceptance, or permission to act. Sample every route-changing source before deciding; reacquire only missing or conflicting evidence.",
|
|
140
|
+
"",
|
|
141
|
+
JSON.stringify({
|
|
142
|
+
schemaVersion: packet.schemaVersion,
|
|
143
|
+
question: packet.question,
|
|
144
|
+
facts: packet.facts,
|
|
145
|
+
conflicts: packet.conflicts,
|
|
146
|
+
unknowns: packet.unknowns,
|
|
147
|
+
stop: packet.stop,
|
|
148
|
+
}, null, 2),
|
|
149
|
+
].join("\n");
|
|
150
|
+
}
|
|
@@ -28,11 +28,15 @@ export function createResponsibilityScope({
|
|
|
28
28
|
decision,
|
|
29
29
|
routeValidated = false,
|
|
30
30
|
cardId,
|
|
31
|
+
resumeOfScopeId,
|
|
31
32
|
}) {
|
|
32
33
|
if (!Number.isSafeInteger(turn) || turn < 1) throw new TypeError("scope turn must be a positive integer");
|
|
33
34
|
if (!Number.isSafeInteger(startStep) || startStep < 1) throw new TypeError("scope startStep must be a positive integer");
|
|
34
35
|
const requestedRoute = routeSnapshot(route);
|
|
35
36
|
if (!requestedRoute) throw new TypeError("scope route must contain provider and model");
|
|
37
|
+
if (resumeOfScopeId !== undefined && (typeof resumeOfScopeId !== "string" || resumeOfScopeId === "")) {
|
|
38
|
+
throw new TypeError("resumeOfScopeId must be a non-empty string");
|
|
39
|
+
}
|
|
36
40
|
return Object.freeze({
|
|
37
41
|
id: randomUUID(),
|
|
38
42
|
state: "pending",
|
|
@@ -46,6 +50,7 @@ export function createResponsibilityScope({
|
|
|
46
50
|
stopPolicy: "terminal-response-or-ownership-boundary",
|
|
47
51
|
routeValidated: routeValidated === true,
|
|
48
52
|
...(cardId ? { cardId } : {}),
|
|
53
|
+
...(resumeOfScopeId ? { resumeOfScopeId } : {}),
|
|
49
54
|
});
|
|
50
55
|
}
|
|
51
56
|
|
|
@@ -113,6 +118,7 @@ function scopeEventData(scope) {
|
|
|
113
118
|
stopPolicy: scope.stopPolicy,
|
|
114
119
|
...(scope.source ? { routeSource: scope.source } : {}),
|
|
115
120
|
...(scope.cardId ? { routeCardId: scope.cardId } : {}),
|
|
121
|
+
...(scope.resumeOfScopeId ? { resumeOfScopeId: scope.resumeOfScopeId } : {}),
|
|
116
122
|
...(scope.baseRoute ? { baseRoute: scope.baseRoute } : {}),
|
|
117
123
|
...(scope.temporaryRoute ? { temporaryRoute: scope.temporaryRoute } : {}),
|
|
118
124
|
...(scope.routeMode ? { routeMode: scope.routeMode } : {}),
|
|
@@ -148,6 +154,33 @@ export function latestDanglingResponsibilityScope(events) {
|
|
|
148
154
|
return [...states.values()].findLast((entry) => entry.state === "claimed")?.data;
|
|
149
155
|
}
|
|
150
156
|
|
|
157
|
+
export function latestStoppedResponsibilityScope(events, turn) {
|
|
158
|
+
return (Array.isArray(events) ? events : []).findLast((event) => (
|
|
159
|
+
event?.type === "odai/responsibility-scope-stopped"
|
|
160
|
+
&& event.data?.turn === turn
|
|
161
|
+
&& typeof event.data?.scopeId === "string"
|
|
162
|
+
))?.data;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function pendingResponsibilityInterruption(events) {
|
|
166
|
+
const settled = new Set();
|
|
167
|
+
for (let index = (Array.isArray(events) ? events.length : 0) - 1; index >= 0; index -= 1) {
|
|
168
|
+
const event = events[index];
|
|
169
|
+
const scopeId = event?.data?.scopeId;
|
|
170
|
+
if (typeof scopeId !== "string") continue;
|
|
171
|
+
if (["odai/responsibility-interruption-consumed", "odai/responsibility-interruption-cleared"].includes(event.type)) {
|
|
172
|
+
settled.add(scopeId);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (event.type === "odai/responsibility-interrupted") {
|
|
176
|
+
return event.data?.reason === "max-tokens" && !settled.has(scopeId)
|
|
177
|
+
? event.data
|
|
178
|
+
: undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
|
|
151
184
|
export function pendingResponsibilityScopeRestoration(events) {
|
|
152
185
|
let candidate;
|
|
153
186
|
for (const event of Array.isArray(events) ? events : []) {
|
|
@@ -157,10 +190,10 @@ export function pendingResponsibilityScopeRestoration(events) {
|
|
|
157
190
|
candidate = event.data;
|
|
158
191
|
continue;
|
|
159
192
|
}
|
|
160
|
-
if (candidate && event?.type === "request/header") candidate = undefined;
|
|
161
193
|
if (candidate
|
|
162
194
|
&& event?.type === "odai/responsibility-scope-restored"
|
|
163
|
-
&& event.data?.scopeId === candidate.scopeId
|
|
195
|
+
&& event.data?.scopeId === candidate.scopeId
|
|
196
|
+
&& event.data?.status === "applied") candidate = undefined;
|
|
164
197
|
}
|
|
165
198
|
return candidate;
|
|
166
199
|
}
|
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
const DSH_ROLE_OVERLAYS = Object.freeze({
|
|
2
|
-
researcher: `## DSH researcher execution boundary
|
|
3
|
-
|
|
4
|
-
This responsibility runs as one read-only child before the primary routing decision. Use only read, glob, grep, and other non-mutating source tools; do not run shell commands, edit, plan, recommend, approve, or delegate. Return JSON only, with no fence and no fields beyond this exact shape: {"schemaVersion":1,"question":"...","facts":[{"claim":"...","excerpt":"exact complete cited line","source":{"path":"repository/relative/path","line":1},"authority":"source role and freshness boundary"}],"conflicts":[],"unknowns":[],"stop":"..."}. Return the smallest useful 2-6 facts from at least two distinct files. DSH validates every cited file, line, and excerpt inside the project root before hashing and exposing this packet to the controller or planner.`,
|
|
5
|
-
planner: `## DSH planner execution boundary
|
|
6
|
-
|
|
7
|
-
In auto mode this responsibility runs by replacing the controller route only inside one explicit responsibility scope, not by starting a child. The scope may continue through read-only tool calls, but ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, or turn boundary. Retain the current conversation and project context, use only read-only evidence, and do not implement or edit. If the result is mode: planned, execute: executor, and separation has a concrete observable benefit, use odai_route_card to freeze exactly one structured card with non-empty evidence, scope, acceptance conditions, and stop condition. When the original current task already authorizes implementation, immediately submit one executor gap through odai_responsibility_gap so the same task continues without another user prompt. For plan-only work, a new task, expanded scope, or missing user-owned authorization, stop for the minimal user decision instead. Otherwise do not create a route card.`,
|
|
8
|
-
executor: `## DSH executor execution boundary
|
|
9
|
-
|
|
10
|
-
In auto mode this responsibility is reachable when an active frozen route card proves observable separation benefit and either the original current task already authorizes implementation or the user explicitly continues it. It runs in one bounded controller responsibility scope because DSH routed children are read-only. The scope may continue only through its own tool chain and ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, card release, or turn boundary. Follow the claimed card exactly; do not reinterpret the request, broaden scope, or claim final acceptance. The card is consumed after the effective executor request route is verified; that route receipt admits one executor attempt but does not keep the responsibility scope alive by itself.`,
|
|
11
|
-
reviewer: `## DSH reviewer execution boundary
|
|
12
|
-
|
|
13
|
-
Start an independent child only from a bounded, hash-addressed packet that contains verified tool evidence. The child's effective request header must match its configured route before its output is accepted. Without such a packet, auto mode keeps the current controller route and may perform only a controller-local read-only check while it gathers project-available acceptance evidence. That local check is not independent acceptance, must not claim reviewer approval, and must not stop the authorized task solely to ask the user for artifacts the project can produce.`,
|
|
14
|
-
frontend: `## DSH frontend execution boundary
|
|
15
|
-
|
|
16
|
-
This responsibility runs in one bounded controller responsibility scope so it retains the active conversation, workspace, dev-server, and browser context. It is not an independent child. The scope may continue only through its own tool chain and ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, or turn boundary. DSH may use a user-persisted frontend provider/model/reasoning route and, only when that mapping explicitly includes maxTokens, apply that responsibility ceiling inside this scope. The runtime compares the effective DSH request header with that mapping and records an applied, mismatch, or unverified receipt; configuration and self-report are not routing evidence.`,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
export function dshRoleContract(role, canonicalContract, referenceContracts = {}) {
|
|
20
|
-
const canonical = typeof canonicalContract === "string" ? canonicalContract.trim() : "";
|
|
21
|
-
const overlay = DSH_ROLE_OVERLAYS[role];
|
|
22
|
-
if (!canonical) throw new Error(`canonical ${role} responsibility contract is unavailable`);
|
|
23
|
-
const craft = role === "frontend" && typeof referenceContracts.craft === "string"
|
|
24
|
-
? `## Canonical craft reference\n\n${referenceContracts.craft.trim()}`
|
|
25
|
-
: "";
|
|
26
|
-
return [canonical, craft, overlay].filter(Boolean).join("\n\n");
|
|
27
|
-
}
|
|
1
|
+
const DSH_ROLE_OVERLAYS = Object.freeze({
|
|
2
|
+
researcher: `## DSH researcher execution boundary
|
|
3
|
+
|
|
4
|
+
This responsibility runs as one read-only child before the primary routing decision. Use only read, glob, grep, and other non-mutating source tools; do not run shell commands, edit, plan, recommend, approve, or delegate. Return JSON only, with no fence and no fields beyond this exact shape: {"schemaVersion":1,"question":"...","facts":[{"claim":"...","excerpt":"exact complete cited line","source":{"path":"repository/relative/path","line":1},"authority":"source role and freshness boundary"}],"conflicts":[],"unknowns":[],"stop":"..."}. Return the smallest useful 2-6 facts from at least two distinct files. DSH validates every cited file, line, and excerpt inside the project root before hashing and exposing this packet to the controller or planner.`,
|
|
5
|
+
planner: `## DSH planner execution boundary
|
|
6
|
+
|
|
7
|
+
In auto mode this responsibility runs by replacing the controller route only inside one explicit responsibility scope, not by starting a child. The scope may continue through read-only tool calls, but ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, or turn boundary. When the user-persisted planner mapping explicitly includes maxTokens, that responsibility ceiling overrides the controller ceiling only inside this scope. Retain the current conversation and project context, use only read-only evidence, and do not implement or edit. If the result is mode: planned, execute: executor, and separation has a concrete observable benefit, use odai_route_card to freeze exactly one structured card with non-empty evidence, scope, acceptance conditions, and stop condition. When the original current task already authorizes implementation, immediately submit one executor gap through odai_responsibility_gap so the same task continues without another user prompt. For plan-only work, a new task, expanded scope, or missing user-owned authorization, stop for the minimal user decision instead. Otherwise do not create a route card.`,
|
|
8
|
+
executor: `## DSH executor execution boundary
|
|
9
|
+
|
|
10
|
+
In auto mode this responsibility is reachable when an active frozen route card proves observable separation benefit and either the original current task already authorizes implementation or the user explicitly continues it. It runs in one bounded controller responsibility scope because DSH routed children are read-only. The scope may continue only through its own tool chain and ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, card release, or turn boundary. When the user-persisted executor mapping explicitly includes maxTokens, that responsibility ceiling overrides the controller ceiling only inside this scope. Follow the claimed card exactly; do not reinterpret the request, broaden scope, or claim final acceptance. The card is consumed after the effective executor request route is verified; that route receipt admits one executor attempt but does not keep the responsibility scope alive by itself.`,
|
|
11
|
+
reviewer: `## DSH reviewer execution boundary
|
|
12
|
+
|
|
13
|
+
Start an independent child only from a bounded, hash-addressed packet that contains verified tool evidence. The child's effective request header must match its configured route before its output is accepted. Without such a packet, auto mode keeps the current controller route and may perform only a controller-local read-only check while it gathers project-available acceptance evidence. That local check is not independent acceptance, must not claim reviewer approval, and must not stop the authorized task solely to ask the user for artifacts the project can produce.`,
|
|
14
|
+
frontend: `## DSH frontend execution boundary
|
|
15
|
+
|
|
16
|
+
This responsibility runs in one bounded controller responsibility scope so it retains the active conversation, workspace, dev-server, and browser context. It is not an independent child. The scope may continue only through its own tool chain and ends on a terminal response, new direct-human input, failure, cancellation, route mismatch, or turn boundary. DSH may use a user-persisted frontend provider/model/reasoning route and, only when that mapping explicitly includes maxTokens, apply that responsibility ceiling inside this scope. The runtime compares the effective DSH request header with that mapping and records an applied, mismatch, or unverified receipt; configuration and self-report are not routing evidence.`,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function dshRoleContract(role, canonicalContract, referenceContracts = {}) {
|
|
20
|
+
const canonical = typeof canonicalContract === "string" ? canonicalContract.trim() : "";
|
|
21
|
+
const overlay = DSH_ROLE_OVERLAYS[role];
|
|
22
|
+
if (!canonical) throw new Error(`canonical ${role} responsibility contract is unavailable`);
|
|
23
|
+
const craft = role === "frontend" && typeof referenceContracts.craft === "string"
|
|
24
|
+
? `## Canonical craft reference\n\n${referenceContracts.craft.trim()}`
|
|
25
|
+
: "";
|
|
26
|
+
return [canonical, craft, overlay].filter(Boolean).join("\n\n");
|
|
27
|
+
}
|