odai-dsh-agent 0.2.3 → 0.2.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.
Files changed (166) hide show
  1. package/README.md +3 -3
  2. package/build/bin/odai-dsh-agent.d.mts +3 -0
  3. package/build/bin/odai-dsh-agent.d.mts.map +1 -0
  4. package/build/bin/odai-dsh-agent.mjs +114 -0
  5. package/build/bin/odai-dsh-agent.mjs.map +1 -0
  6. package/build/src/dsh-version.d.mts +16 -0
  7. package/build/src/dsh-version.d.mts.map +1 -0
  8. package/build/src/dsh-version.mjs +20 -0
  9. package/build/src/dsh-version.mjs.map +1 -0
  10. package/build/src/installer.d.mts +46 -0
  11. package/build/src/installer.d.mts.map +1 -0
  12. package/build/src/installer.mjs +397 -0
  13. package/build/src/installer.mjs.map +1 -0
  14. package/package.json +13 -12
  15. package/preset/odai/runtime/compaction-config.d.mts +66 -0
  16. package/preset/odai/runtime/compaction-config.d.mts.map +1 -0
  17. package/preset/odai/runtime/compaction-config.mjs +320 -342
  18. package/preset/odai/runtime/compaction-config.mjs.map +1 -0
  19. package/preset/odai/runtime/compaction-runtime.d.mts +68 -0
  20. package/preset/odai/runtime/compaction-runtime.d.mts.map +1 -0
  21. package/preset/odai/runtime/compaction-runtime.mjs +157 -0
  22. package/preset/odai/runtime/compaction-runtime.mjs.map +1 -0
  23. package/preset/odai/runtime/context-activation.d.mts +31 -0
  24. package/preset/odai/runtime/context-activation.d.mts.map +1 -0
  25. package/preset/odai/runtime/context-activation.mjs +55 -49
  26. package/preset/odai/runtime/context-activation.mjs.map +1 -0
  27. package/preset/odai/runtime/context-capability.d.mts +16 -0
  28. package/preset/odai/runtime/context-capability.d.mts.map +1 -0
  29. package/preset/odai/runtime/context-capability.mjs +79 -74
  30. package/preset/odai/runtime/context-capability.mjs.map +1 -0
  31. package/preset/odai/runtime/governance.d.mts +30 -0
  32. package/preset/odai/runtime/governance.d.mts.map +1 -0
  33. package/preset/odai/runtime/governance.mjs +126 -113
  34. package/preset/odai/runtime/governance.mjs.map +1 -0
  35. package/preset/odai/runtime/human-care.d.mts +13 -0
  36. package/preset/odai/runtime/human-care.d.mts.map +1 -0
  37. package/preset/odai/runtime/human-care.mjs +45 -42
  38. package/preset/odai/runtime/human-care.mjs.map +1 -0
  39. package/preset/odai/runtime/human-safety-continuity-store.d.mts +43 -0
  40. package/preset/odai/runtime/human-safety-continuity-store.d.mts.map +1 -0
  41. package/preset/odai/runtime/human-safety-continuity-store.mjs +193 -215
  42. package/preset/odai/runtime/human-safety-continuity-store.mjs.map +1 -0
  43. package/preset/odai/runtime/human-safety-continuity.d.mts +27 -0
  44. package/preset/odai/runtime/human-safety-continuity.d.mts.map +1 -0
  45. package/preset/odai/runtime/human-safety-continuity.mjs +205 -216
  46. package/preset/odai/runtime/human-safety-continuity.mjs.map +1 -0
  47. package/preset/odai/runtime/human-safety.d.mts +14 -0
  48. package/preset/odai/runtime/human-safety.d.mts.map +1 -0
  49. package/preset/odai/runtime/human-safety.mjs +50 -47
  50. package/preset/odai/runtime/human-safety.mjs.map +1 -0
  51. package/preset/odai/runtime/index.d.mts +8 -0
  52. package/preset/odai/runtime/index.d.mts.map +1 -0
  53. package/preset/odai/runtime/index.mjs +161 -2441
  54. package/preset/odai/runtime/index.mjs.map +1 -0
  55. package/preset/odai/runtime/lifecycle-runtime.d.mts +83 -0
  56. package/preset/odai/runtime/lifecycle-runtime.d.mts.map +1 -0
  57. package/preset/odai/runtime/lifecycle-runtime.mjs +1081 -0
  58. package/preset/odai/runtime/lifecycle-runtime.mjs.map +1 -0
  59. package/preset/odai/runtime/model-route.d.mts +22 -0
  60. package/preset/odai/runtime/model-route.d.mts.map +1 -0
  61. package/preset/odai/runtime/model-route.mjs +68 -60
  62. package/preset/odai/runtime/model-route.mjs.map +1 -0
  63. package/preset/odai/runtime/output-config.d.mts +54 -0
  64. package/preset/odai/runtime/output-config.d.mts.map +1 -0
  65. package/preset/odai/runtime/output-config.mjs +289 -315
  66. package/preset/odai/runtime/output-config.mjs.map +1 -0
  67. package/preset/odai/runtime/output-policy-state.d.mts +3 -0
  68. package/preset/odai/runtime/output-policy-state.d.mts.map +1 -0
  69. package/preset/odai/runtime/output-policy-state.mjs +41 -29
  70. package/preset/odai/runtime/output-policy-state.mjs.map +1 -0
  71. package/preset/odai/runtime/prompt-runtime.d.mts +55 -0
  72. package/preset/odai/runtime/prompt-runtime.d.mts.map +1 -0
  73. package/preset/odai/runtime/prompt-runtime.mjs +303 -0
  74. package/preset/odai/runtime/prompt-runtime.mjs.map +1 -0
  75. package/preset/odai/runtime/research-packet.d.mts +30 -0
  76. package/preset/odai/runtime/research-packet.d.mts.map +1 -0
  77. package/preset/odai/runtime/research-packet.mjs +116 -117
  78. package/preset/odai/runtime/research-packet.mjs.map +1 -0
  79. package/preset/odai/runtime/responsibility-gap.d.mts +26 -0
  80. package/preset/odai/runtime/responsibility-gap.d.mts.map +1 -0
  81. package/preset/odai/runtime/responsibility-gap.mjs +91 -85
  82. package/preset/odai/runtime/responsibility-gap.mjs.map +1 -0
  83. package/preset/odai/runtime/responsibility-scope.d.mts +69 -0
  84. package/preset/odai/runtime/responsibility-scope.d.mts.map +1 -0
  85. package/preset/odai/runtime/responsibility-scope.mjs +199 -171
  86. package/preset/odai/runtime/responsibility-scope.mjs.map +1 -0
  87. package/preset/odai/runtime/role-overlays.d.mts +5 -0
  88. package/preset/odai/runtime/role-overlays.d.mts.map +1 -0
  89. package/preset/odai/runtime/role-overlays.mjs +14 -13
  90. package/preset/odai/runtime/role-overlays.mjs.map +1 -0
  91. package/preset/odai/runtime/route-card.d.mts +42 -0
  92. package/preset/odai/runtime/route-card.d.mts.map +1 -0
  93. package/preset/odai/runtime/route-card.mjs +158 -146
  94. package/preset/odai/runtime/route-card.mjs.map +1 -0
  95. package/preset/odai/runtime/router.d.mts +79 -0
  96. package/preset/odai/runtime/router.d.mts.map +1 -0
  97. package/preset/odai/runtime/router.mjs +491 -559
  98. package/preset/odai/runtime/router.mjs.map +1 -0
  99. package/preset/odai/runtime/routing-config.d.mts +76 -0
  100. package/preset/odai/runtime/routing-config.d.mts.map +1 -0
  101. package/preset/odai/runtime/routing-config.mjs +373 -438
  102. package/preset/odai/runtime/routing-config.mjs.map +1 -0
  103. package/preset/odai/runtime/routing-context.d.mts +47 -0
  104. package/preset/odai/runtime/routing-context.d.mts.map +1 -0
  105. package/preset/odai/runtime/routing-context.mjs +238 -273
  106. package/preset/odai/runtime/routing-context.mjs.map +1 -0
  107. package/preset/odai/runtime/runtime-config.d.mts +24 -0
  108. package/preset/odai/runtime/runtime-config.d.mts.map +1 -0
  109. package/preset/odai/runtime/runtime-config.mjs +181 -0
  110. package/preset/odai/runtime/runtime-config.mjs.map +1 -0
  111. package/preset/odai/runtime/runtime-support.d.mts +91 -0
  112. package/preset/odai/runtime/runtime-support.d.mts.map +1 -0
  113. package/preset/odai/runtime/runtime-support.mjs +371 -0
  114. package/preset/odai/runtime/runtime-support.mjs.map +1 -0
  115. package/preset/odai/runtime/runtime-types.d.mts +235 -0
  116. package/preset/odai/runtime/runtime-types.d.mts.map +1 -0
  117. package/preset/odai/runtime/runtime-types.mjs +4 -0
  118. package/preset/odai/runtime/runtime-types.mjs.map +1 -0
  119. package/preset/odai/runtime/semantic-memory-store.d.mts +110 -0
  120. package/preset/odai/runtime/semantic-memory-store.d.mts.map +1 -0
  121. package/preset/odai/runtime/semantic-memory-store.mjs +271 -385
  122. package/preset/odai/runtime/semantic-memory-store.mjs.map +1 -0
  123. package/preset/odai/runtime/semantic-memory.d.mts +88 -0
  124. package/preset/odai/runtime/semantic-memory.d.mts.map +1 -0
  125. package/preset/odai/runtime/semantic-memory.mjs +790 -750
  126. package/preset/odai/runtime/semantic-memory.mjs.map +1 -0
  127. package/preset/odai/runtime/session-compat.d.mts +31 -0
  128. package/preset/odai/runtime/session-compat.d.mts.map +1 -0
  129. package/preset/odai/runtime/session-compat.mjs +301 -328
  130. package/preset/odai/runtime/session-compat.mjs.map +1 -0
  131. package/preset/odai/runtime/session-evidence.d.mts +25 -0
  132. package/preset/odai/runtime/session-evidence.d.mts.map +1 -0
  133. package/preset/odai/runtime/session-evidence.mjs +309 -287
  134. package/preset/odai/runtime/session-evidence.mjs.map +1 -0
  135. package/preset/odai/runtime/skill-bundle.d.mts +53 -0
  136. package/preset/odai/runtime/skill-bundle.d.mts.map +1 -0
  137. package/preset/odai/runtime/skill-bundle.mjs +286 -276
  138. package/preset/odai/runtime/skill-bundle.mjs.map +1 -0
  139. package/preset/odai/runtime/skill-evolution.d.mts +112 -0
  140. package/preset/odai/runtime/skill-evolution.d.mts.map +1 -0
  141. package/preset/odai/runtime/skill-evolution.mjs +1287 -1261
  142. package/preset/odai/runtime/skill-evolution.mjs.map +1 -0
  143. package/preset/odai/runtime/skill-selection-state.d.mts +5 -0
  144. package/preset/odai/runtime/skill-selection-state.d.mts.map +1 -0
  145. package/preset/odai/runtime/skill-selection-state.mjs +59 -43
  146. package/preset/odai/runtime/skill-selection-state.mjs.map +1 -0
  147. package/preset/odai/runtime/skill-selector.d.mts +28 -0
  148. package/preset/odai/runtime/skill-selector.d.mts.map +1 -0
  149. package/preset/odai/runtime/skill-selector.mjs +100 -109
  150. package/preset/odai/runtime/skill-selector.mjs.map +1 -0
  151. package/preset/odai/runtime/skill-source-config.d.mts +32 -0
  152. package/preset/odai/runtime/skill-source-config.d.mts.map +1 -0
  153. package/preset/odai/runtime/skill-source-config.mjs +200 -210
  154. package/preset/odai/runtime/skill-source-config.mjs.map +1 -0
  155. package/preset/odai/runtime/store-lock.d.mts +2 -0
  156. package/preset/odai/runtime/store-lock.d.mts.map +1 -0
  157. package/preset/odai/runtime/store-lock.mjs +92 -87
  158. package/preset/odai/runtime/store-lock.mjs.map +1 -0
  159. package/preset/odai/runtime/tool-runtime.d.mts +43 -0
  160. package/preset/odai/runtime/tool-runtime.d.mts.map +1 -0
  161. package/preset/odai/runtime/tool-runtime.mjs +221 -0
  162. package/preset/odai/runtime/tool-runtime.mjs.map +1 -0
  163. package/bin/odai-dsh-agent.mjs +0 -108
  164. package/scripts/verify-agent-load.mjs +0 -257
  165. package/src/dsh-version.mjs +0 -24
  166. package/src/installer.mjs +0 -398
@@ -1,2461 +1,181 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync } from "node:fs";
3
- import { dirname, resolve } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
-
6
- import {
7
- classifyImplementationAuthorization,
8
- classifyResponsibilityInterruptionText,
9
- decideResearchPrefetch,
10
- decideRoute,
11
- extractLatestUserText,
12
- extractRoutingText,
13
- renderDelegationPrompt,
14
- renderMissingRouteConfigNotice,
15
- renderRouteFailureNotice,
16
- renderRouteNotice,
17
- requiresFailClosedProtection,
18
- } from "./router.mjs";
19
- import {
20
- activeRouteProtection,
21
- createChildToolGuard,
22
- createRouteProtectionGuard,
23
- isSubagent,
24
- summarizeToolResult,
25
- } from "./governance.mjs";
26
- import {
27
- CONFIGURABLE_ROLES,
28
- ROUTING_CONFIG_PROMPT,
29
- createRoutingConfigTool,
30
- effectiveRoutingSnapshot,
31
- invalidatePersistedRoleRoute,
32
- resolveRoleRoute,
33
- resolveRoutingConfigPath,
34
- } from "./routing-config.mjs";
35
- import {
36
- DEFAULT_OUTPUT_POLICY,
37
- createOutputConfigTool,
38
- effectiveOutputPolicy,
39
- renderOutputPolicyPrompt,
40
- resolveOutputConfigPath,
41
- } from "./output-config.mjs";
42
- import { selectSharedOutputPolicyForTurn } from "./output-policy-state.mjs";
43
- import {
44
- COMPACTION_CONFIG_PROMPT,
45
- applyCompactionStateProtocol,
46
- applyCompactionTarget,
47
- createCompactionConfigTool,
48
- effectiveCompactionTarget,
49
- invalidatePersistedCompactionTarget,
50
- resolveCompactionConfigPath,
51
- } from "./compaction-config.mjs";
52
- import { createSessionEvidence, resolveSessionEvidenceRoot } from "./session-evidence.mjs";
1
+ import { applyCompactionStateProtocol, applyCompactionTarget, effectiveCompactionTarget, invalidatePersistedCompactionTarget, } from "./compaction-config.mjs";
2
+ import { installCompactionRuntime } from "./compaction-runtime.mjs";
3
+ import { installLifecycleRuntime } from "./lifecycle-runtime.mjs";
53
4
  import { classifyModelRouteFailure, probeModelRoute } from "./model-route.mjs";
54
- import { RESPONSIBILITY_GAP_PROMPT, createResponsibilityGapTool } from "./responsibility-gap.mjs";
55
- import {
56
- ODAI_CORE_TOOL_NAMES,
57
- activeOdaiToolNames,
58
- classifyContextActivation,
59
- inactiveOdaiToolNames,
60
- } from "./context-activation.mjs";
61
- import {
62
- activateRequestedCapabilities,
63
- createContextCapabilityTool,
64
- requestedContextCapabilities,
65
- } from "./context-capability.mjs";
66
- import {
67
- claimResponsibilityScope,
68
- createResponsibilityScope,
69
- latestDanglingResponsibilityScope,
70
- latestStoppedResponsibilityScope,
71
- pendingResponsibilityInterruption,
72
- pendingResponsibilityScopeRestoration,
73
- responsibilityScopeClaimedEvent,
74
- responsibilityScopeOwnsRequest,
75
- responsibilityScopeStartedEvent,
76
- responsibilityScopeStopReason,
77
- responsibilityScopeStoppedEvent,
78
- } from "./responsibility-scope.mjs";
79
- import { HUMAN_CARE_REFERENCE_PATH, createHumanCareTool } from "./human-care.mjs";
80
- import { HUMAN_SAFETY_REFERENCE_PATH, createHumanSafetyTool } from "./human-safety.mjs";
81
- import {
82
- HUMAN_SAFETY_CONTINUITY_PROMPT,
83
- createHumanSafetyContinuityTool,
84
- renderHumanSafetyContinuitySection,
85
- } from "./human-safety-continuity.mjs";
86
- import {
87
- readHumanSafetyContinuityStore,
88
- resolveHumanSafetyContinuityStorePath,
89
- } from "./human-safety-continuity-store.mjs";
90
- import {
91
- ROUTE_CARD_PROMPT,
92
- activeRouteCard,
93
- createRouteCardTool,
94
- routeCardById,
95
- unsettledRouteCard,
96
- } from "./route-card.mjs";
97
- import { buildRoleContextPacket, renderRoleContextPacket } from "./routing-context.mjs";
98
- import {
99
- parseResearchPacket,
100
- renderResearchPacket,
101
- verifyResearchPacketSources,
102
- } from "./research-packet.mjs";
103
- import { dshRoleContract } from "./role-overlays.mjs";
104
- import {
105
- SKILL_SOURCE_CONFIG_PROMPT,
106
- createSkillSourceConfigTool,
107
- effectiveSkillSource,
108
- resolveSkillSourceConfigPath,
109
- } from "./skill-source-config.mjs";
110
- import {
111
- ODAI_RUNTIME_CONTRACT,
112
- SKILL_SOURCE_MODES,
113
- loadSkillBundle,
114
- readSkillBundleFile,
115
- } from "./skill-bundle.mjs";
116
- import { resolveSkillSelection } from "./skill-selector.mjs";
117
- import {
118
- currentAgentTurn,
119
- selectSharedSkillForTurn,
120
- sharedSkillSelection,
121
- } from "./skill-selection-state.mjs";
122
- import {
123
- applySkillEvolutionSelection,
124
- createSkillEvolutionTool,
125
- resolveSkillEvolutionRoot,
126
- skillEvolutionDisabled,
127
- } from "./skill-evolution.mjs";
128
- import {
129
- MEMORY_PROMPT,
130
- captureAutomaticMemories,
131
- claimSemanticMemoryTurn,
132
- createSemanticMemoryTool,
133
- latestDirectUserMessage,
134
- memoryPacketMessage,
135
- renderSemanticMemoryPacket,
136
- retrieveSemanticMemories,
137
- } from "./semantic-memory.mjs";
138
- import {
139
- MEMORY_MODES,
140
- effectiveMemorySettings,
141
- resolveMemoryStorePath,
142
- } from "./semantic-memory-store.mjs";
143
-
144
- export const name = "odai-dsh-runtime";
5
+ import { createPromptRuntime } from "./prompt-runtime.mjs";
6
+ import { latestDanglingResponsibilityScope, responsibilityScopeStoppedEvent, } from "./responsibility-scope.mjs";
7
+ import { invalidatePersistedRoleRoute } from "./routing-config.mjs";
8
+ import { inheritCompactionReasoning, resolveConfig, resolveSkillPath, } from "./runtime-config.mjs";
9
+ import { RUNTIME_NAME, loggerFor, routeFromConfig, runRoutedRole, sameRequestModelRoute, } from "./runtime-support.mjs";
10
+ import { createSessionEvidence, resolveSessionEvidenceRoot } from "./session-evidence.mjs";
11
+ import { currentAgentTurn } from "./skill-selection-state.mjs";
12
+ import { installToolRuntime } from "./tool-runtime.mjs";
13
+ import { resolveHumanSafetyContinuityStorePath } from "./human-safety-continuity-store.mjs";
14
+ export const name = RUNTIME_NAME;
15
+ export { inheritCompactionReasoning, resolveConfig, resolveSkillPath, runRoutedRole };
145
16
  export const inject = ["systemPrompt", "tools", "subagents", "sessions", "llm"];
146
-
147
- const PLUGIN_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
148
- const ROUTING_MODES = new Set(["off", "observe", "auto", "execute"]);
149
- const COMPACTION_CACHE_RETENTIONS = new Set(["provider-default", "short", "long", "none"]);
150
- const ROUTED_ROLES = CONFIGURABLE_ROLES;
151
- const DEFAULT_ROUTING_MODE = "auto";
152
-
153
- function assertPlainObject(value, field) {
154
- if (value === undefined) return {};
155
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
156
- throw new TypeError(`${field} must be an object`);
157
- }
158
- return value;
159
- }
160
-
161
- export function resolveConfig(rawConfig = {}) {
162
- const raw = assertPlainObject(rawConfig, "config");
163
- const routing = assertPlainObject(raw.routing, "config.routing");
164
- const roles = assertPlainObject(routing.roles, "config.routing.roles");
165
- const governance = assertPlainObject(raw.governance, "config.governance");
166
- const output = assertPlainObject(raw.output, "config.output");
167
- const compaction = assertPlainObject(raw.compaction, "config.compaction");
168
- const memory = assertPlainObject(raw.memory, "config.memory");
169
- const mode = routing.mode ?? DEFAULT_ROUTING_MODE;
170
- const provider = routing.provider ?? "spawn";
171
- const maxInputChars = routing.maxInputChars ?? 12_000;
172
- const configPath = resolveRoutingConfigPath(routing.configPath);
173
- const additionalDeniedTools = governance.additionalDeniedTools ?? [];
174
- const skillSource = governance.skillSource ?? "bundled";
175
- const skillConfigPath = resolveSkillSourceConfigPath(governance.skillConfigPath);
176
- const evolutionRoot = resolveSkillEvolutionRoot(governance.evolutionRoot);
177
- const outputConfigPath = resolveOutputConfigPath(output.configPath);
178
- const compactionConfigPath = resolveCompactionConfigPath(compaction.configPath);
179
- const memoryStorePath = resolveMemoryStorePath(memory.storePath);
180
- const memoryMode = memory.mode ?? "auto";
181
- const memoryMaxRetrieved = memory.maxRetrieved ?? 6;
182
- const compactionCacheRetention = compaction.cacheRetention
183
- ?? process.env.ODAI_COMPACTION_CACHE_RETENTION
184
- ?? "provider-default";
185
- const unknownRoles = Object.keys(roles).filter((role) => !ROUTED_ROLES.includes(role));
186
- const unknownOutputFields = Object.keys(output).filter((field) => field !== "configPath");
187
- const unknownCompactionFields = Object.keys(compaction).filter((field) => !["cacheRetention", "configPath"].includes(field));
188
- const unknownMemoryFields = Object.keys(memory).filter((field) => !["mode", "storePath", "maxRetrieved"].includes(field));
189
-
190
- if (!ROUTING_MODES.has(mode)) {
191
- throw new TypeError("config.routing.mode must be off, observe, auto, or execute");
192
- }
193
- if (typeof provider !== "string" || provider.trim() === "") {
194
- throw new TypeError("config.routing.provider must be a non-empty string");
195
- }
196
- if (!Number.isSafeInteger(maxInputChars) || maxInputChars < 256) {
197
- throw new TypeError("config.routing.maxInputChars must be an integer of at least 256");
198
- }
199
- if (unknownRoles.length > 0) {
200
- throw new TypeError(`config.routing.roles has unknown roles: ${unknownRoles.join(", ")}`);
201
- }
202
- if (unknownOutputFields.length > 0) {
203
- throw new TypeError(`config.output has unknown fields: ${unknownOutputFields.join(", ")}`);
204
- }
205
- if (unknownCompactionFields.length > 0) {
206
- throw new TypeError(`config.compaction has unknown fields: ${unknownCompactionFields.join(", ")}`);
207
- }
208
- if (unknownMemoryFields.length > 0) {
209
- throw new TypeError(`config.memory has unknown fields: ${unknownMemoryFields.join(", ")}`);
210
- }
211
- if (!MEMORY_MODES.includes(memoryMode)) {
212
- throw new TypeError("config.memory.mode must be auto or off");
213
- }
214
- if (!Number.isSafeInteger(memoryMaxRetrieved) || memoryMaxRetrieved < 1 || memoryMaxRetrieved > 12) {
215
- throw new TypeError("config.memory.maxRetrieved must be an integer from 1 to 12");
216
- }
217
- if (!COMPACTION_CACHE_RETENTIONS.has(compactionCacheRetention)) {
218
- throw new TypeError("config.compaction.cacheRetention must be provider-default, short, long, or none");
219
- }
220
- if (!Array.isArray(additionalDeniedTools)
221
- || additionalDeniedTools.some((tool) => typeof tool !== "string" || tool.trim() === "")) {
222
- throw new TypeError("config.governance.additionalDeniedTools must be an array of non-empty strings");
223
- }
224
- if (!SKILL_SOURCE_MODES.includes(skillSource)) {
225
- throw new TypeError("config.governance.skillSource must be bundled, auto, or user");
226
- }
227
- if (raw.skillPath !== undefined && (typeof raw.skillPath !== "string" || raw.skillPath.trim() === "")) {
228
- throw new TypeError("config.skillPath must be a non-empty string");
229
- }
230
-
231
- return Object.freeze({
232
- skillPath: raw.skillPath,
233
- routing: Object.freeze({
234
- mode,
235
- provider: provider.trim(),
236
- maxInputChars,
237
- configPath,
238
- roles: Object.freeze(Object.fromEntries(ROUTED_ROLES.map((role) => [
239
- role,
240
- resolveRoleRoute(roles[role], role),
241
- ]))),
242
- }),
243
- governance: Object.freeze({
244
- additionalDeniedTools: Object.freeze(additionalDeniedTools.map((tool) => tool.trim())),
245
- skillSource,
246
- skillConfigPath,
247
- evolutionRoot,
248
- }),
249
- output: Object.freeze({ configPath: outputConfigPath }),
250
- compaction: Object.freeze({
251
- cacheRetention: compactionCacheRetention,
252
- configPath: compactionConfigPath,
253
- }),
254
- memory: Object.freeze({
255
- mode: memoryMode,
256
- storePath: memoryStorePath,
257
- maxRetrieved: memoryMaxRetrieved,
258
- }),
259
- });
260
- }
261
-
262
- /** Mutate exact-route compaction options; returns true only when an option changed. */
263
- export function inheritCompactionReasoning(options, sessions, cacheRetention = "provider-default") {
264
- if (options?.purpose !== "compaction"
265
- || options.sessionId === undefined
266
- || !Object.isExtensible(options)) {
267
- return false;
268
- }
269
-
270
- const route = sessions?.get?.(options.sessionId)?.requestHeader?.()?.config;
271
- if (route?.provider !== options.provider
272
- || route?.model !== options.model
273
- || typeof route.reasoningEffort !== "string"
274
- || route.reasoningEffort.length === 0) {
275
- return false;
276
- }
277
-
278
- let changed = false;
279
- if (options.reasoningEffort === undefined) {
280
- options.reasoningEffort = route.reasoningEffort;
281
- changed = true;
282
- }
283
- if (options.cacheRetention === undefined && cacheRetention !== "provider-default") {
284
- options.cacheRetention = cacheRetention;
285
- changed = true;
286
- }
287
- return changed;
288
- }
289
-
290
- export function resolveSkillPath(configuredPath, env = process.env) {
291
- const explicit = configuredPath ?? env.ODAI_SKILL_PATH;
292
- if (explicit !== undefined) {
293
- const path = resolve(explicit);
294
- if (!existsSync(path)) throw new Error(`explicit Odai canonical skill not found: ${path}`);
295
- return path;
296
- }
297
-
298
- const candidates = [
299
- resolve(PLUGIN_DIR, "skills/odai/SKILL.md"),
300
- resolve(PLUGIN_DIR, "../../skills/odai/SKILL.md"),
301
- ];
302
- const found = candidates.find((candidate) => existsSync(candidate));
303
- if (found) return found;
304
- throw new Error(`Odai bundled canonical skill not found; checked: ${candidates.join(", ")}`);
305
- }
306
-
307
- export function loadRoleContracts(skillPath) {
308
- return loadSkillBundle(skillPath).roleContracts;
309
- }
310
-
311
- function deepFreeze(value) {
312
- if (value === null || typeof value !== "object" || Object.isFrozen(value)) return value;
313
- for (const child of Object.values(value)) deepFreeze(child);
314
- return Object.freeze(value);
315
- }
316
-
317
- function pluginMessage(text, summary, extraBlocks = []) {
318
- return deepFreeze({
319
- id: randomUUID(),
320
- role: "user",
321
- content: [{ type: "text", text }, ...extraBlocks],
322
- source: {
323
- kind: "plugin",
324
- plugin: name,
325
- form: "notice",
326
- summary: summary.length <= 120 ? summary : `${summary.slice(0, 119)}…`,
327
- },
328
- });
329
- }
330
-
331
- function renderOutputLimitInterruptionNotice(interruption) {
332
- const route = interruption.effectiveRoute ?? interruption.requestedRoute;
333
- return [
334
- "Odai verified output-limit interruption",
335
- `responsibility: ${interruption.responsibility}`,
336
- `interrupted scope: ${interruption.scopeId}`,
337
- `provider finish reason: ${interruption.reason}`,
338
- ...(route ? [`effective route: ${route.provider}/${route.model}`] : []),
339
- ...(interruption.effectiveMaxTokens === undefined ? [] : [`effective maxTokens: ${interruption.effectiveMaxTokens}`]),
340
- ...(interruption.outputTokens === undefined ? [] : [`observed outputTokens: ${interruption.outputTokens}`]),
341
- "This proves the responsibility output was interrupted, not that its task completed. Explain the verified cause without guessing. Resume only after an explicit user continuation.",
342
- ].join("\n");
343
- }
344
-
345
- function loggerFor(ctx) {
346
- if (typeof ctx.logger !== "function") return { info() {}, warn() {} };
347
- return ctx.logger(name);
348
- }
349
-
350
- function outputText(blocks) {
351
- if (!Array.isArray(blocks)) return "";
352
- return blocks
353
- .filter((block) => block?.type === "text" && typeof block.text === "string")
354
- .map((block) => block.text)
355
- .join("\n")
356
- .trim();
357
- }
358
-
359
- function renderResearchTaskContract(taskText) {
360
- return [
361
- "Decision-blocking factual question: Determine whether the user's causal claim is supported and which existing repository facts govern the safety of the requested high-impact change.",
362
- "Allowed source scope: the current project root only. Use repository-relative paths and read-only source tools; do not inspect parent, sibling, user, or unrelated directories.",
363
- "Authority and freshness: label each current-checkout source by its actual role (for example runtime configuration, implementation, test, incident record, or documentation). Do not invent an authority hierarchy; report unresolved conflicts and missing freshness evidence as unknowns.",
364
- "Stop condition: return the smallest packet with 2-6 source-backed facts from at least two files, or stop with the missing evidence boundary. Do not select a route or continue after additional reading cannot change this factual question.",
365
- "For every fact, excerpt must exactly equal the complete cited source line after trimming leading and trailing whitespace.",
366
- "",
367
- "Original user request:",
368
- taskText,
369
- ].join("\n");
370
- }
371
-
372
- function roleAgentOptions(roleRoute) {
373
- if (!roleRoute) return undefined;
374
- return {
375
- provider: roleRoute.provider,
376
- model: roleRoute.model,
377
- ...(roleRoute.maxTokens === undefined ? {} : { maxTokens: roleRoute.maxTokens }),
378
- };
379
- }
380
-
381
- function sameRequestModelRoute(left, right) {
382
- return Boolean(left && right
383
- && left.provider === right.provider
384
- && left.model === right.model
385
- && left.reasoningEffort === right.reasoningEffort);
386
- }
387
-
388
- function currentAgentStep(agent) {
389
- const events = agent?.session?.events;
390
- if (!Array.isArray(events)) return undefined;
391
- for (let index = events.length - 1; index >= 0; index -= 1) {
392
- if (events[index]?.type === "step/start" && Number.isSafeInteger(events[index].data?.step)) return events[index].data.step;
393
- }
394
- return undefined;
395
- }
396
-
397
- function routeFromConfig(config) {
398
- if (typeof config?.provider !== "string" || typeof config?.model !== "string") return undefined;
399
- return Object.freeze({
400
- provider: config.provider,
401
- model: config.model,
402
- ...(config.reasoningEffort === undefined ? {} : { reasoningEffort: config.reasoningEffort }),
403
- ...(config.maxTokens === undefined ? {} : { maxTokens: config.maxTokens }),
404
- });
405
- }
406
-
407
- function latestRequestRoute(localAgent) {
408
- const events = localAgent?.session?.events;
409
- if (!Array.isArray(events)) return undefined;
410
- for (let index = events.length - 1; index >= 0; index -= 1) {
411
- const event = events[index];
412
- if (event?.type !== "request/header") continue;
413
- return routeFromConfig(event.data?.header?.config);
414
- }
415
- return undefined;
416
- }
417
-
418
- function routeMismatchFor(expected, actual, subject) {
419
- if (!expected) return undefined;
420
- if (!actual) return `${subject} request/header did not expose an actual model route`;
421
- for (const field of ["provider", "model", "reasoningEffort", "maxTokens"]) {
422
- if (expected[field] !== undefined && expected[field] !== actual[field]) {
423
- return `${subject} ${field} mismatch: expected ${expected[field]}, got ${actual[field] ?? "<absent>"}`;
424
- }
425
- }
426
- return undefined;
427
- }
428
-
429
- function routeMismatch(expected, actual) {
430
- return routeMismatchFor(expected, actual, "child");
431
- }
432
-
433
- export async function runRoutedRole({ subagents, provider, decision, taskText, roleContract, agent, signal, roleRoute }) {
434
- let run;
435
- let outcome;
436
- try {
437
- run = await subagents.start(provider, {
438
- label: `odai-${decision.role}`,
439
- prompt: [{ type: "text", text: renderDelegationPrompt(decision, taskText, roleContract) }],
440
- parent: agent,
441
- signal,
442
- maxDepth: 1,
443
- ...(roleRoute ? { agentOptions: roleAgentOptions(roleRoute) } : {}),
444
- });
445
- const result = await run.result;
446
- const actualRoute = latestRequestRoute(run.localAgent);
447
- const mismatch = routeMismatch(roleRoute, actualRoute);
448
- const routeReceiptStatus = !actualRoute ? "unverified" : mismatch ? "mismatch" : "applied";
449
- const routeEvidence = Object.freeze({
450
- routeReceiptStatus,
451
- ...(mismatch ? { routeReceiptError: mismatch } : {}),
452
- ...(actualRoute ? { actualRoute } : {}),
453
- });
454
- if (result.stopReason !== "completed") {
455
- outcome = Object.freeze({
456
- status: "fallback",
457
- stopReason: result.stopReason,
458
- output: result.output ?? [],
459
- ...routeEvidence,
460
- });
461
- } else if (mismatch) {
462
- outcome = Object.freeze({
463
- status: "fallback",
464
- stopReason: "route-unverified",
465
- output: [],
466
- error: mismatch,
467
- ...routeEvidence,
468
- });
469
- } else if (!outputText(result.output)) {
470
- outcome = Object.freeze({
471
- status: "fallback",
472
- stopReason: "route-empty-output",
473
- output: [],
474
- error: "child completed without textual evidence",
475
- taskError: "child completed without textual evidence",
476
- ...routeEvidence,
477
- });
478
- } else {
479
- outcome = Object.freeze({
480
- status: "completed",
481
- stopReason: result.stopReason,
482
- output: result.output ?? [],
483
- ...routeEvidence,
484
- });
485
- }
486
- } catch (error) {
487
- const taskError = error instanceof Error ? error.message : String(error);
488
- outcome = Object.freeze({
489
- status: "fallback",
490
- stopReason: "infrastructure-error",
491
- output: [],
492
- routeReceiptStatus: "unverified",
493
- error: taskError,
494
- taskError,
495
- });
496
- }
497
-
498
- if (run) {
499
- try {
500
- await run.dispose();
501
- } catch (error) {
502
- const disposeError = error instanceof Error ? error.message : String(error);
503
- const cleanupTaskError = outcome?.taskError
504
- ? `${outcome.taskError}; provider cleanup failed: ${disposeError}`
505
- : `provider cleanup failed: ${disposeError}`;
506
- return Object.freeze({
507
- status: "fallback",
508
- stopReason: "infrastructure-error",
509
- output: [],
510
- routeReceiptStatus: outcome?.routeReceiptStatus ?? "unverified",
511
- ...(outcome?.routeReceiptError ? { routeReceiptError: outcome.routeReceiptError } : {}),
512
- ...(outcome?.actualRoute ? { actualRoute: outcome.actualRoute } : {}),
513
- error: outcome?.error
514
- ? `${outcome.error}; provider cleanup failed: ${disposeError}`
515
- : cleanupTaskError,
516
- taskError: cleanupTaskError,
517
- });
518
- }
519
- }
520
-
521
- return outcome;
17
+ function isResponsibilityGapProposal(data) {
18
+ return typeof data.responsibility === "string"
19
+ && ["researcher", "planner", "executor", "reviewer", "frontend", "user"].includes(data.responsibility)
20
+ && typeof data.gap === "string"
21
+ && Array.isArray(data.evidenceRefs)
22
+ && data.evidenceRefs.every((value) => typeof value === "string")
23
+ && typeof data.expectedChange === "string"
24
+ && typeof data.stateDigest === "string";
522
25
  }
523
-
524
- function canonicalPrompt(selection) {
525
- const { bundle } = selection;
526
- const fallback = selection.status === "fallback"
527
- ? `Selection fallback: ${selection.reasonCode}${selection.detail ? ` (${selection.detail})` : ""}.`
528
- : undefined;
529
- const evolution = selection.evolution?.status === "active"
530
- ? `User evolution: generation ${selection.evolution.generationId}; base digest ${selection.evolution.baseDigest}; current upstream digest ${selection.evolution.upstreamDigest}; rebase required: ${String(selection.evolution.rebaseRequired)}.`
531
- : undefined;
532
- return [
533
- "## odai canonical governance",
534
- `Canonical source: ${bundle.source} (${bundle.provider})`,
535
- `Canonical skill: ${bundle.manifest.skillVersion}; runtime contract: ${bundle.manifest.runtimeContract}; digest: ${bundle.digest}.`,
536
- ...(evolution ? [evolution] : []),
537
- ...(fallback ? [fallback] : []),
538
- "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.",
539
- "Odai governance is already loaded by this runtime; do not call the skill tool or read SKILL.md to load odai again.",
540
- "",
541
- bundle.skillText,
542
- ].join("\n");
543
- }
544
-
545
- function renderEffectiveRoutingContext(snapshotState) {
546
- if (snapshotState.error) {
547
- return [
548
- "Current effective responsibility mappings: unavailable because the saved routing configuration is invalid.",
549
- `Configuration error: ${snapshotState.detail}`,
550
- "Use odai_routing_config only when the user asks to inspect or repair it; do not infer any route.",
551
- ].join("\n");
552
- }
553
- const mappings = CONFIGURABLE_ROLES.flatMap((role) => {
554
- const route = snapshotState.snapshot.roles[role];
555
- if (!route) return [];
556
- const options = [
557
- ...(route.reasoningEffort ? [`reasoningEffort=${route.reasoningEffort}`] : []),
558
- ...(route.maxTokens ? [`maxTokens=${route.maxTokens}`] : []),
559
- ];
560
- return [`${role}=${route.provider}/${route.model}${options.length > 0 ? ` (${options.join(", ")})` : ""} [${snapshotState.snapshot.sources[role]}]`];
561
- });
562
- return [
563
- `Current effective responsibility mappings (runtime-owned; supersedes conversation summaries): ${mappings.join("; ") || "none"}.`,
564
- "These are route targets, not evidence that a responsibility ran. Only an actual route receipt proves use; a generic subagent does not.",
565
- ].join("\n");
566
- }
567
-
568
- function latestRouteReceipt(events) {
569
- for (let index = events.length - 1; index >= 0; index -= 1) {
570
- const event = events[index];
571
- const data = event?.data;
572
- if (!data?.routeSource || !Number.isSafeInteger(data.turn) || !Number.isSafeInteger(data.step)) continue;
573
- if (event.type === "odai/route-fallback") {
574
- return Object.freeze({
575
- turn: data.turn,
576
- step: data.step,
577
- responsibility: data.responsibility,
578
- ...(data.responsibilityScopeId ? { responsibilityScopeId: data.responsibilityScopeId } : {}),
579
- ...(data.routeCardId ? { routeCardId: data.routeCardId } : {}),
580
- status: "fallback",
581
- taskStatus: "fallback",
582
- routeMode: data.routeMode,
583
- routeSource: data.routeSource,
584
- fallbackUsed: true,
585
- requestedRoute: data.requestedRoute,
586
- ...(data.fallbackRoute ? { actualRoute: data.fallbackRoute } : {}),
587
- ...(data.error ? { error: data.error } : {}),
588
- });
589
- }
590
- if (event.type === "odai/route-applied") {
591
- return Object.freeze({
592
- turn: data.turn,
593
- step: data.step,
594
- responsibility: data.responsibility,
595
- ...(data.responsibilityScopeId ? { responsibilityScopeId: data.responsibilityScopeId } : {}),
596
- ...(data.routeCardId ? { routeCardId: data.routeCardId } : {}),
597
- status: data.status,
598
- routeMode: data.routeMode,
599
- routeSource: data.routeSource,
600
- fallbackUsed: data.fallbackUsed,
601
- requestedRoute: data.requestedRoute,
602
- ...(data.actualRoute ? { actualRoute: data.actualRoute } : {}),
603
- ...(data.stopReason ? { stopReason: data.stopReason } : {}),
604
- ...(data.error ? { error: data.error } : {}),
605
- });
606
- }
607
- if (["odai/route-result", "odai/research-result"].includes(event.type)) {
608
- const routeReceiptStatus = data.routeReceiptStatus ?? "unverified";
609
- return Object.freeze({
610
- turn: data.turn,
611
- step: data.step,
612
- responsibility: data.role,
613
- status: routeReceiptStatus,
614
- taskStatus: data.status,
615
- routeMode: "child",
616
- routeSource: data.routeSource,
617
- fallbackUsed: routeReceiptStatus !== "applied",
618
- requestedRoute: data.requestedRoute,
619
- ...(data.actualRoute ? { actualRoute: data.actualRoute } : {}),
620
- ...(data.routeReceiptError ? { error: data.routeReceiptError } : {}),
621
- ...(data.stopReason ? { taskStopReason: data.stopReason } : {}),
622
- ...(data.error ? { taskError: data.error } : {}),
623
- });
624
- }
625
- }
626
- return undefined;
627
- }
628
-
629
- function routedRoleOf(agent) {
630
- const events = agent?.session?.events;
631
- if (!Array.isArray(events)) return undefined;
632
- for (let index = events.length - 1; index >= 0; index -= 1) {
633
- const event = events[index];
634
- if (event?.type !== "subagent/descriptor") continue;
635
- const label = event.data?.label;
636
- if (typeof label !== "string") return undefined;
637
- const match = /^odai-(researcher|planner|executor|reviewer|frontend)(?:$|[\s:])/u.exec(label.trim());
638
- return match && ROUTED_ROLES.includes(match[1]) ? match[1] : undefined;
639
- }
640
- return undefined;
641
- }
642
-
643
26
  export function apply(ctx, rawConfig) {
644
- const config = resolveConfig(rawConfig);
645
- const logger = loggerFor(ctx);
646
- const humanSafetyContinuityStorePath = resolveHumanSafetyContinuityStorePath();
647
- const evidence = createSessionEvidence({
648
- root: resolveSessionEvidenceRoot(config.routing.configPath),
649
- logger,
650
- });
651
- const appendEvent = (agent, type, data) => {
652
- try {
653
- evidence.append(agent, type, data);
654
- } catch (error) {
655
- logger.warn(`failed to record ${type}: ${error instanceof Error ? error.message : String(error)}`);
656
- }
657
- };
658
- const hasSessionEvent = (agent, type, predicate) => evidence.has(agent, type, predicate);
659
- const selectCompactionTarget = () => {
660
- try {
661
- return effectiveCompactionTarget(config.compaction.configPath);
662
- } catch (error) {
663
- logger.warn(`Odai compaction model configuration is invalid; inheriting the conversation route: ${error instanceof Error ? error.message : String(error)}`);
664
- return Object.freeze({ source: "inherit", status: "fallback", reasonCode: "compaction-config-invalid" });
665
- }
666
- };
667
- const compactionFallbackRequests = new WeakSet();
668
- ctx.on("llm/stream", (options, next) => {
669
- if (options?.purpose !== "compaction" || compactionFallbackRequests.has(options)) return next();
670
- const selection = selectCompactionTarget();
671
- const downstream = next();
672
- const streamLike = downstream && typeof downstream[Symbol.asyncIterator] === "function";
673
- if (!selection.target || !streamLike) {
674
- applyCompactionTarget(options, selection.target, ctx.sessions);
675
- applyCompactionStateProtocol(options, selection.target);
676
- inheritCompactionReasoning(options, ctx.sessions, config.compaction.cacheRetention);
677
- return downstream;
678
- }
679
-
680
- const original = Object.freeze({ ...options, messages: options.messages });
681
- const session = options.sessionId === undefined ? undefined : ctx.sessions?.get?.(options.sessionId);
682
- const record = (data) => {
683
- if (session) appendEvent({ session }, "odai/compaction-route", data);
684
- };
685
- const restoreOriginal = () => {
686
- for (const key of Object.keys(options)) {
687
- if (!Object.hasOwn(original, key)) delete options[key];
688
- }
689
- Object.assign(options, original);
690
- };
691
- return (async function* configuredCompactionRoute() {
692
- const validation = await probeModelRoute(
693
- (candidate, signal) => ctx.llm.resolveCallConfig(candidate, signal),
694
- selection.target,
695
- options.signal,
696
- );
697
- if (validation.status === "rejected") {
698
- let invalidation = { invalidated: false };
699
- if (validation.failure.kind === "deterministic" && selection.source === "persisted") {
700
- try {
701
- invalidation = invalidatePersistedCompactionTarget(config.compaction.configPath, selection.target);
702
- } catch (error) {
703
- invalidation = { invalidated: false, error: error instanceof Error ? error.message : String(error) };
704
- }
705
- }
706
- record({
707
- status: "fallback",
708
- requestedRoute: selection.target,
709
- fallbackRoute: routeFromConfig(original),
710
- fallbackUsed: true,
711
- failureKind: validation.failure.kind,
712
- errorCode: validation.failure.code,
713
- error: validation.failure.message,
714
- invalidated: invalidation.invalidated,
715
- ...(invalidation.backupPath ? { backupPath: invalidation.backupPath } : {}),
716
- ...(invalidation.error ? { cleanupError: invalidation.error } : {}),
717
- });
718
- inheritCompactionReasoning(options, ctx.sessions, config.compaction.cacheRetention);
719
- for await (const chunk of downstream) yield chunk;
720
- return;
721
- }
722
-
723
- applyCompactionTarget(options, selection.target, ctx.sessions);
724
- applyCompactionStateProtocol(options, selection.target);
725
- inheritCompactionReasoning(options, ctx.sessions, config.compaction.cacheRetention);
726
- const buffered = [];
727
- let terminalFailure;
728
- for await (const chunk of downstream) {
729
- buffered.push(chunk);
730
- if (chunk?.type === "finish" && ["error", "aborted"].includes(chunk.reason?.kind)) {
731
- terminalFailure = chunk.reason.failure;
732
- }
733
- }
734
- if (!terminalFailure) {
735
- record({
736
- status: "applied",
737
- requestedRoute: selection.target,
738
- effectiveRoute: routeFromConfig(options),
739
- fallbackUsed: false,
740
- });
741
- for (const chunk of buffered) yield chunk;
742
- return;
743
- }
744
-
745
- const failure = classifyModelRouteFailure(terminalFailure);
746
- if (failure.kind === "cancelled" || options.signal?.aborted) {
747
- record({
748
- status: "failed",
749
- requestedRoute: selection.target,
750
- fallbackUsed: false,
751
- failureKind: failure.kind,
752
- errorCode: failure.code,
753
- error: failure.message,
754
- });
755
- for (const chunk of buffered) yield chunk;
756
- return;
757
- }
758
-
759
- if (sameRequestModelRoute(original, options)) {
760
- record({
761
- status: "failed",
762
- requestedRoute: selection.target,
763
- effectiveRoute: routeFromConfig(options),
764
- fallbackUsed: false,
765
- failureKind: failure.kind,
766
- errorCode: failure.code,
767
- error: failure.message,
768
- stopReason: "configured-and-inherited-routes-match",
769
- });
770
- for (const chunk of buffered) yield chunk;
771
- return;
772
- }
773
-
774
- let invalidation = { invalidated: false };
775
- if (failure.kind === "deterministic" && selection.source === "persisted") {
27
+ const config = resolveConfig(rawConfig);
28
+ const logger = loggerFor(ctx);
29
+ const humanSafetyContinuityStorePath = resolveHumanSafetyContinuityStorePath();
30
+ const evidence = createSessionEvidence({
31
+ root: resolveSessionEvidenceRoot(config.routing.configPath),
32
+ logger,
33
+ });
34
+ const appendEvent = (agent, type, data) => {
776
35
  try {
777
- invalidation = invalidatePersistedCompactionTarget(config.compaction.configPath, selection.target);
778
- } catch (error) {
779
- invalidation = { invalidated: false, error: error instanceof Error ? error.message : String(error) };
36
+ evidence.append(agent, type, data);
780
37
  }
781
- }
782
- restoreOriginal();
783
- const fallbackOptions = { ...original, messages: original.messages };
784
- inheritCompactionReasoning(fallbackOptions, ctx.sessions, config.compaction.cacheRetention);
785
- compactionFallbackRequests.add(fallbackOptions);
786
- record({
787
- status: "fallback",
788
- requestedRoute: selection.target,
789
- fallbackRoute: routeFromConfig(fallbackOptions),
790
- fallbackUsed: true,
791
- failureKind: failure.kind,
792
- errorCode: failure.code,
793
- error: failure.message,
794
- invalidated: invalidation.invalidated,
795
- ...(invalidation.backupPath ? { backupPath: invalidation.backupPath } : {}),
796
- ...(invalidation.error ? { cleanupError: invalidation.error } : {}),
797
- });
798
- for await (const chunk of ctx.llm.stream(fallbackOptions)) yield chunk;
799
- })();
800
- });
801
- const skillPath = resolveSkillPath(config.skillPath);
802
- const explicitSkillPath = config.skillPath !== undefined
803
- || (typeof process.env.ODAI_SKILL_PATH === "string" && process.env.ODAI_SKILL_PATH.trim() !== "");
804
- const bundled = loadSkillBundle(skillPath, {
805
- source: explicitSkillPath ? "path" : "bundled",
806
- provider: explicitSkillPath ? "odai-explicit-path" : "odai-dsh-runtime",
807
- });
808
- if (bundled.manifest.runtimeContract !== ODAI_RUNTIME_CONTRACT) {
809
- throw new Error(`Odai canonical runtimeContract ${bundled.manifest.runtimeContract} is incompatible with this runtime`);
810
- }
811
- const baseSelection = Object.freeze({
812
- mode: explicitSkillPath ? "path" : "bundled",
813
- status: "selected",
814
- reasonCode: explicitSkillPath ? "explicit-path" : "bundled-configured",
815
- bundle: bundled,
816
- rejections: Object.freeze([]),
817
- });
818
- const evolutionDisabled = explicitSkillPath || skillEvolutionDisabled();
819
-
820
- ctx.systemPrompt.section({
821
- name: "odai:canonical-governance",
822
- order: -20,
823
- text: canonicalPrompt(baseSelection),
824
- });
825
- ctx.systemPrompt.section({
826
- name: "odai:routing-configuration",
827
- order: -19,
828
- text: ROUTING_CONFIG_PROMPT,
829
- });
830
- ctx.systemPrompt.section({
831
- name: "odai:human-safety-continuity",
832
- order: -18.875,
833
- text: HUMAN_SAFETY_CONTINUITY_PROMPT,
834
- });
835
- ctx.systemPrompt.section({
836
- name: "odai:responsibility-gap",
837
- order: -18.75,
838
- text: RESPONSIBILITY_GAP_PROMPT,
839
- });
840
- ctx.systemPrompt.section({
841
- name: "odai:route-card",
842
- order: -18.5,
843
- text: ROUTE_CARD_PROMPT,
844
- });
845
- ctx.systemPrompt.section({
846
- name: "odai:skill-source-configuration",
847
- order: -18,
848
- text: SKILL_SOURCE_CONFIG_PROMPT,
849
- });
850
- ctx.systemPrompt.section({
851
- name: "odai:controller-output-policy",
852
- order: -17,
853
- text: "",
854
- });
855
- ctx.systemPrompt.section({
856
- name: "odai:compaction-model-configuration",
857
- order: -16,
858
- text: COMPACTION_CONFIG_PROMPT,
859
- });
860
- ctx.systemPrompt.section({
861
- name: "odai:semantic-memory",
862
- order: -15,
863
- text: MEMORY_PROMPT,
864
- });
865
-
866
- const optionalSkillRegistry = () => {
867
- try {
868
- if (ctx.skills && typeof ctx.skills.get === "function") return ctx.skills;
869
- } catch {}
870
- try {
871
- const service = typeof ctx.get === "function" ? ctx.get("skills") : undefined;
872
- return service && typeof service.get === "function" ? service : undefined;
873
- } catch {
874
- return undefined;
875
- }
876
- };
877
- const selectUpstreamForAgent = async (agent, context) => {
878
- if (explicitSkillPath) return baseSelection;
879
- let mode;
880
- try {
881
- mode = effectiveSkillSource(config.governance.skillConfigPath, config.governance.skillSource);
882
- } catch (error) {
883
- return Object.freeze({
884
- ...baseSelection,
885
- mode: "bundled",
886
- status: "fallback",
887
- reasonCode: "source-config-invalid",
888
- detail: error instanceof Error ? error.message : String(error),
889
- });
890
- }
891
- return resolveSkillSelection({
892
- mode,
893
- bundled,
894
- cwd: agent.session?.header?.cwd,
895
- scope: context.scope,
896
- signal: context.signal,
897
- skills: optionalSkillRegistry(),
898
- });
899
- };
900
- const selectForAgent = async (agent, context) => {
901
- const upstream = await selectUpstreamForAgent(agent, context);
902
- return applySkillEvolutionSelection(upstream, config.governance.evolutionRoot, { disabled: evolutionDisabled });
903
- };
904
- const selectOutputForAgent = () => {
905
- try {
906
- return effectiveOutputPolicy(config.output.configPath);
907
- } catch (error) {
908
- logger.warn(`Odai output configuration is invalid; using the default policy: ${error instanceof Error ? error.message : String(error)}`);
909
- return Object.freeze({
910
- policy: DEFAULT_OUTPUT_POLICY,
911
- source: "default",
912
- status: "fallback",
913
- reasonCode: "output-config-invalid",
914
- });
915
- }
916
- };
917
- const memorySettingsSnapshots = new WeakMap();
918
- const memorySettingsFor = (agent, turn = currentAgentTurn(agent)) => {
919
- const cached = memorySettingsSnapshots.get(agent);
920
- if (cached && cached.turn === turn) return cached.settings;
921
- let settings;
922
- try {
923
- settings = effectiveMemorySettings(config.memory.storePath, { mode: config.memory.mode });
924
- } catch (error) {
925
- logger.warn(`Odai semantic memory is unavailable; capture and retrieval are disabled for this turn: ${error instanceof Error ? error.message : String(error)}`);
926
- settings = Object.freeze({ mode: "off", source: "invalid-store" });
927
- }
928
- memorySettingsSnapshots.set(agent, { turn, settings });
929
- return settings;
930
- };
931
- const contextActivationFor = (agent, text, turn = currentAgentTurn(agent)) => activateRequestedCapabilities(
932
- classifyContextActivation(text),
933
- requestedContextCapabilities(evidence.events(agent), turn),
934
- );
935
- const routingSnapshots = new WeakMap();
936
- const routingSnapshotFor = (agent, turn = currentAgentTurn(agent)) => {
937
- const cached = routingSnapshots.get(agent);
938
- if (cached && cached.turn === turn) return cached.state;
939
- let state;
940
- try {
941
- state = Object.freeze({
942
- snapshot: effectiveRoutingSnapshot(config.routing.configPath, config.routing.roles),
943
- });
944
- } catch (error) {
945
- state = Object.freeze({
946
- error: "persisted Odai routing configuration failed validation",
947
- detail: error instanceof Error ? error.message : String(error),
948
- });
949
- }
950
- routingSnapshots.set(agent, { turn, state });
951
- return state;
952
- };
953
- ctx.on("system-prompt/assemble", async (assembly, context, next) => {
954
- const downstream = await next();
955
- const agent = context.agent;
956
- if (!agent) return downstream;
957
- const selection = await selectSharedSkillForTurn(agent, () => selectForAgent(agent, context));
958
- const turn = currentAgentTurn(agent);
959
- const outputSelection = isSubagentSession(agent)
960
- ? Object.freeze({ policy: DEFAULT_OUTPUT_POLICY, source: "default" })
961
- : await selectSharedOutputPolicyForTurn(agent, turn, selectOutputForAgent);
962
- const selectionEvidence = {
963
- ...(turn === undefined ? {} : { turn }),
964
- requestedMode: selection.mode,
965
- status: selection.status,
966
- reasonCode: selection.reasonCode,
967
- effectiveSource: selection.bundle.source,
968
- skillVersion: selection.bundle.manifest.skillVersion,
969
- runtimeContract: selection.bundle.manifest.runtimeContract,
970
- digest: selection.bundle.digest,
971
- rejections: selection.rejections.map(({ source, reasonCode }) => ({ source, reasonCode })),
972
- ...(selection.evolution?.generationId ? {
973
- evolution: {
974
- status: selection.evolution.status,
975
- generationId: selection.evolution.generationId,
976
- ...(selection.evolution.baseDigest ? { baseDigest: selection.evolution.baseDigest } : {}),
977
- ...(selection.evolution.upstreamDigest ? { upstreamDigest: selection.evolution.upstreamDigest } : {}),
978
- ...(selection.evolution.rebaseRequired === undefined ? {} : { rebaseRequired: selection.evolution.rebaseRequired }),
979
- },
980
- } : {}),
981
- };
982
- if (!hasSessionEvent(agent, "odai/skill-selected", (data) => data?.turn === turn && data?.digest === selection.bundle.digest)) {
983
- appendEvent(agent, "odai/skill-selected", selectionEvidence);
984
- }
985
- if (selection.status === "fallback") {
986
- logger.warn(`Odai skill source ${selection.mode} fell back to bundled governance (${selection.reasonCode})`);
987
- }
988
- const outputPrompt = renderOutputPolicyPrompt(outputSelection.policy);
989
- const childSession = isSubagentSession(agent);
990
- const directMessage = latestDirectUserMessage(agent);
991
- const activation = contextActivationFor(agent, directMessage ? extractLatestUserText([directMessage]) : "", turn);
992
- const proposedStep = (currentAgentStep(agent) ?? 0) + 1;
993
- const routeCardNeeded = !childSession && (Boolean(activeRouteCard(evidence.events(agent)))
994
- || ["planner", "executor"].includes(pendingResponsibilityGap(agent, turn, proposedStep)?.responsibility));
995
- const routingPrompt = !activation.routingConfig
996
- ? ""
997
- : childSession
998
- ? ROUTING_CONFIG_PROMPT
999
- : `${ROUTING_CONFIG_PROMPT}\n\n${renderEffectiveRoutingContext(routingSnapshotFor(agent, turn))}`;
1000
- let continuityRecordPrompt;
1001
- if (!childSession && (activation.care || activation.safety || activation.continuity)) {
1002
- try {
1003
- continuityRecordPrompt = renderHumanSafetyContinuitySection(
1004
- readHumanSafetyContinuityStore(humanSafetyContinuityStorePath),
1005
- );
1006
- } catch (error) {
1007
- logger.warn(`Odai human-safety continuity is unavailable for this turn: ${error instanceof Error ? error.message : String(error)}`);
1008
- }
1009
- }
1010
- const continuityPrompt = [activation.continuity ? HUMAN_SAFETY_CONTINUITY_PROMPT : undefined, continuityRecordPrompt]
1011
- .filter(Boolean)
1012
- .join("\n\n");
1013
- if (outputPrompt && !hasSessionEvent(agent, "odai/output-policy-selected", (data) => data?.turn === turn)) {
1014
- appendEvent(agent, "odai/output-policy-selected", {
1015
- ...(turn === undefined ? {} : { turn }),
1016
- source: outputSelection.source,
1017
- policy: outputSelection.policy,
1018
- });
1019
- }
1020
- return {
1021
- ...downstream,
1022
- sections: downstream.sections.map((section) => {
1023
- if (section.name === "odai:canonical-governance") return { ...section, text: canonicalPrompt(selection) };
1024
- if (section.name === "odai:routing-configuration") return { ...section, text: routingPrompt };
1025
- if (section.name === "odai:human-safety-continuity") return { ...section, text: continuityPrompt };
1026
- if (section.name === "odai:responsibility-gap") return { ...section, text: childSession ? "" : RESPONSIBILITY_GAP_PROMPT };
1027
- if (section.name === "odai:route-card") return { ...section, text: routeCardNeeded ? ROUTE_CARD_PROMPT : "" };
1028
- if (section.name === "odai:skill-source-configuration") return { ...section, text: activation.skillSource ? SKILL_SOURCE_CONFIG_PROMPT : "" };
1029
- if (section.name === "odai:controller-output-policy") return { ...section, text: outputPrompt };
1030
- if (section.name === "odai:compaction-model-configuration") return { ...section, text: activation.compactionConfig ? COMPACTION_CONFIG_PROMPT : "" };
1031
- if (section.name === "odai:semantic-memory") return { ...section, text: activation.memory ? MEMORY_PROMPT : "" };
1032
- return section;
1033
- }),
1034
- };
1035
- });
1036
-
1037
- const routeProtections = new WeakMap();
1038
- const responsibilityScopes = new WeakMap();
1039
- const responsibilityScopeOwners = new WeakMap();
1040
- const pendingRouteReceipts = new WeakMap();
1041
- const pendingScopeRestorations = new WeakMap();
1042
- const outputUsageBySession = new WeakMap();
1043
- const stopResponsibilityScope = (agent, reason, position = {}) => {
1044
- const scope = responsibilityScopes.get(agent);
1045
- if (!scope || (position.scopeId && position.scopeId !== scope.id)) return undefined;
1046
- responsibilityScopes.delete(agent);
1047
- const protection = routeProtections.get(agent);
1048
- if (protection?.scopeId === scope.id) routeProtections.delete(agent);
1049
- appendEvent(agent, "odai/route-protection-released", {
1050
- scopeId: scope.id,
1051
- turn: scope.turn,
1052
- reason,
1053
- });
1054
- appendEvent(agent, "odai/responsibility-scope-stopped", responsibilityScopeStoppedEvent(scope, reason, position));
1055
- return scope;
1056
- };
1057
- const stopDanglingResponsibilityScope = (agent, reason) => {
1058
- if (responsibilityScopes.has(agent)) return undefined;
1059
- const dangling = latestDanglingResponsibilityScope(evidence.events(agent));
1060
- if (!dangling) return undefined;
1061
- appendEvent(agent, "odai/route-protection-released", {
1062
- scopeId: dangling.scopeId,
1063
- turn: dangling.turn,
1064
- reason,
1065
- });
1066
- appendEvent(agent, "odai/responsibility-scope-stopped", {
1067
- ...dangling,
1068
- reason,
1069
- });
1070
- return dangling;
1071
- };
1072
- const configuredRole = (agent, role, turn = currentAgentTurn(agent)) => {
1073
- const state = routingSnapshotFor(agent, turn);
1074
- if (state.error) return state;
1075
- return {
1076
- route: state.snapshot.roles[role],
1077
- source: state.snapshot.sources[role],
1078
- };
1079
- };
1080
- const pendingResponsibilityGap = (agent, turn, step) => {
1081
- const consumed = new Set();
1082
- const events = evidence.events(agent);
1083
- for (let index = events.length - 1; index >= 0; index -= 1) {
1084
- const event = events[index];
1085
- if (event.data?.turn !== turn) continue;
1086
- if (event.type === "odai/responsibility-gap-consumed") {
1087
- consumed.add(event.data.stateDigest);
1088
- continue;
1089
- }
1090
- if (event.type !== "odai/responsibility-gap"
1091
- || !Number.isSafeInteger(event.data?.step)
1092
- || event.data.step >= step
1093
- || consumed.has(event.data.stateDigest)) continue;
1094
- return event.data;
1095
- }
1096
- return undefined;
1097
- };
1098
- const invalidateFailedRoleRoute = (agent, role, route, source, failure, position = {}) => {
1099
- let invalidation = Object.freeze({ invalidated: false, reason: "not-deterministic-or-not-persisted" });
1100
- if (failure.kind === "deterministic" && source === "persisted-mapping") {
1101
- try {
1102
- invalidation = invalidatePersistedRoleRoute(config.routing.configPath, role, route);
1103
- } catch (error) {
1104
- invalidation = Object.freeze({
1105
- invalidated: false,
1106
- reason: "cleanup-failed",
1107
- error: error instanceof Error ? error.message : String(error),
1108
- });
1109
- }
1110
- }
1111
- appendEvent(agent, "odai/route-health", {
1112
- ...position,
1113
- responsibility: role,
1114
- routeSource: source,
1115
- requestedRoute: route,
1116
- status: failure.kind === "deterministic" ? "invalid" : "unhealthy",
1117
- failureKind: failure.kind,
1118
- errorCode: failure.code,
1119
- error: failure.message,
1120
- invalidated: invalidation.invalidated,
1121
- ...(invalidation.backupPath ? { backupPath: invalidation.backupPath } : {}),
1122
- ...(invalidation.reason ? { cleanupReason: invalidation.reason } : {}),
1123
- ...(invalidation.error ? { cleanupError: invalidation.error } : {}),
1124
- });
1125
- return invalidation;
1126
- };
1127
- const onDenied = (execution, reason) => {
1128
- appendEvent(execution.agent, "odai/governance-denied", {
1129
- callId: String(execution.callId),
1130
- tool: execution.name,
1131
- reason,
1132
- }, logger);
1133
- };
1134
- const childGuard = createChildToolGuard({
1135
- additionalDeniedTools: config.governance.additionalDeniedTools,
1136
- onDenied,
1137
- });
1138
- const routeProtectionGuard = createRouteProtectionGuard({
1139
- additionalDeniedTools: config.governance.additionalDeniedTools,
1140
- onDenied,
1141
- protectionFor(agent) {
1142
- if (config.routing.mode === "off") return undefined;
1143
- return routeProtections.get(agent) ?? activeRouteProtection(agent, evidence.events(agent));
1144
- },
1145
- });
1146
- ctx.tools.register(createContextCapabilityTool({
1147
- isChild: isSubagent,
1148
- onRequested(agent, capability) {
1149
- const turn = currentAgentTurn(agent);
1150
- const step = currentAgentStep(agent);
1151
- appendEvent(agent, "odai/context-capability-requested", {
1152
- ...(turn === undefined ? {} : { turn }),
1153
- ...(step === undefined ? {} : { step }),
1154
- capability,
1155
- });
1156
- },
1157
- }));
1158
- ctx.tools.register(createRoutingConfigTool(config.routing.configPath, {
1159
- configuredRoles: config.routing.roles,
1160
- resolveCallConfig(route, signal) {
1161
- return ctx.llm.resolveCallConfig(route, signal);
1162
- },
1163
- latestRouteFor(agent) {
1164
- return latestRouteReceipt(evidence.events(agent));
1165
- },
1166
- outputPolicyFor() {
1167
- return selectOutputForAgent().policy;
1168
- },
1169
- onConfigured(agent, data) {
1170
- appendEvent(agent, "odai/routing-configured", data);
1171
- },
1172
- }));
1173
- ctx.tools.register(createHumanCareTool({
1174
- isChild: isSubagent,
1175
- contractFor(agent) {
1176
- const bundle = sharedSkillSelection(agent)?.bundle ?? bundled;
1177
- return readSkillBundleFile(bundle, HUMAN_CARE_REFERENCE_PATH).toString("utf8");
1178
- },
1179
- }));
1180
- ctx.tools.register(createHumanSafetyTool({
1181
- isChild: isSubagent,
1182
- contractFor(agent) {
1183
- const bundle = sharedSkillSelection(agent)?.bundle ?? bundled;
1184
- return readSkillBundleFile(bundle, HUMAN_SAFETY_REFERENCE_PATH).toString("utf8");
1185
- },
1186
- }));
1187
- ctx.tools.register(createResponsibilityGapTool({
1188
- isChild: isSubagent,
1189
- onProposed(agent, proposal) {
1190
- const turn = currentAgentTurn(agent);
1191
- const step = currentAgentStep(agent);
1192
- appendEvent(agent, "odai/responsibility-gap", {
1193
- ...(turn === undefined ? {} : { turn }),
1194
- ...(step === undefined ? {} : { step }),
1195
- ...proposal,
1196
- });
1197
- },
1198
- }));
1199
- ctx.tools.register(createRouteCardTool({
1200
- activeFor(agent) {
1201
- return activeRouteCard(evidence.events(agent));
1202
- },
1203
- unsettledFor(agent) {
1204
- return unsettledRouteCard(evidence.events(agent));
1205
- },
1206
- authorizationFor(agent) {
1207
- const message = latestDirectUserMessage(agent);
1208
- if (!message) return Object.freeze({ status: "unknown" });
1209
- return Object.freeze({
1210
- ...classifyImplementationAuthorization(extractLatestUserText([message])),
1211
- userMessageId: message.id,
1212
- });
1213
- },
1214
- onFrozen(agent, card) {
1215
- appendEvent(agent, "odai/route-card-frozen", { card });
1216
- },
1217
- onCleared(agent, cardId) {
1218
- appendEvent(agent, "odai/route-card-cleared", { cardId });
1219
- },
1220
- }));
1221
- ctx.tools.register(createSkillSourceConfigTool(
1222
- config.governance.skillConfigPath,
1223
- config.governance.skillSource,
1224
- {
1225
- explicitPath: explicitSkillPath,
1226
- onConfigured(agent, data) {
1227
- appendEvent(agent, "odai/skill-source-configured", data);
1228
- },
1229
- },
1230
- ));
1231
- ctx.tools.register(createSkillEvolutionTool(config.governance.evolutionRoot, {
1232
- disabled: evolutionDisabled,
1233
- currentSelectionFor(agent) {
1234
- return sharedSkillSelection(agent)
1235
- ?? applySkillEvolutionSelection(baseSelection, config.governance.evolutionRoot, { disabled: evolutionDisabled });
1236
- },
1237
- onChanged(agent, data) {
1238
- appendEvent(agent, `odai/evolution-${data.action}`, data);
1239
- },
1240
- }));
1241
- ctx.tools.register(createOutputConfigTool(config.output.configPath, {
1242
- isChild: isSubagent,
1243
- responsibilityRoutesFor() {
1244
- try {
1245
- return effectiveRoutingSnapshot(config.routing.configPath, config.routing.roles).roles;
1246
- } catch {
1247
- return undefined;
1248
- }
1249
- },
1250
- onConfigured(agent, data) {
1251
- appendEvent(agent, "odai/output-configured", data);
1252
- },
1253
- }));
1254
- ctx.tools.register(createCompactionConfigTool(config.compaction.configPath, {
1255
- isChild: isSubagent,
1256
- resolveCallConfig(route, signal) {
1257
- return ctx.llm.resolveCallConfig(route, signal);
1258
- },
1259
- onConfigured(agent, data) {
1260
- appendEvent(agent, "odai/compaction-configured", data);
1261
- },
1262
- }));
1263
- ctx.tools.register(createSemanticMemoryTool(config.memory.storePath, {
1264
- configuredMode: config.memory.mode,
1265
- onChanged(agent, data) {
1266
- appendEvent(agent, "odai/memory-changed", data);
1267
- },
1268
- }));
1269
- ctx.tools.register(createHumanSafetyContinuityTool({
1270
- storePath: humanSafetyContinuityStorePath,
1271
- directUserTextFor(agent) {
1272
- const message = latestDirectUserMessage(agent);
1273
- return message ? extractLatestUserText([message]) : "";
1274
- },
1275
- onChanged(agent, data) {
1276
- appendEvent(agent, "odai/human-safety-continuity-changed", data);
1277
- },
1278
- }));
1279
- ctx.tools.guard((execution) => childGuard(execution) ?? routeProtectionGuard(execution));
1280
-
1281
- const toolExposureStates = new WeakMap();
1282
- const syncToolExposure = (agent, activation, options = {}) => {
1283
- const child = isSubagentSession(agent);
1284
- const activeNames = activeOdaiToolNames(activation, { child, routeCard: options.routeCard === true });
1285
- const deniedNames = [
1286
- ...inactiveOdaiToolNames(activeNames),
1287
- ...(child ? ODAI_CORE_TOOL_NAMES : []),
1288
- ];
1289
- const key = deniedNames.join("\u0000");
1290
- const previous = toolExposureStates.get(agent);
1291
- if (previous?.key === key || ["unsupported", "fallback"].includes(previous?.key)) return activeNames;
1292
- previous?.dispose?.();
1293
- const restrict = agent?.ctx?.tools?.restrict;
1294
- if (typeof restrict !== "function") {
1295
- toolExposureStates.set(agent, Object.freeze({ key: "unsupported", dispose: undefined }));
1296
- return activeNames;
1297
- }
1298
- try {
1299
- const dispose = deniedNames.length > 0
1300
- ? restrict.call(agent.ctx.tools, { deny: deniedNames })
1301
- : undefined;
1302
- toolExposureStates.set(agent, Object.freeze({ key, dispose }));
1303
- appendEvent(agent, "odai/tool-exposure-selected", {
1304
- ...(options.turn === undefined ? {} : { turn: options.turn }),
1305
- ...(options.step === undefined ? {} : { step: options.step }),
1306
- mode: "adaptive",
1307
- activeTools: activeNames,
1308
- });
1309
- } catch (error) {
1310
- logger.warn(`Odai adaptive tool exposure is unavailable; retaining the complete tool catalog: ${error instanceof Error ? error.message : String(error)}`);
1311
- toolExposureStates.set(agent, Object.freeze({ key: "fallback", dispose: undefined }));
1312
- }
1313
- return activeNames;
1314
- };
1315
-
1316
- ctx.on("tools/result", (execution, result) => {
1317
- if (!execution.agent) return;
1318
- const summary = summarizeToolResult(execution, result);
1319
- if (hasSessionEvent(execution.agent, "odai/tool-observed", (data) => data?.callId === summary.callId)) return;
1320
- appendEvent(execution.agent, "odai/tool-observed", summary, logger);
1321
- });
1322
-
1323
- ctx.on("agent/request", async ({ agent, turn, step, signal }, next) => {
1324
- let proposed = await next();
1325
- const childRole = routedRoleOf(agent);
1326
- const childRoleState = childRole ? configuredRole(agent, childRole, turn) : undefined;
1327
- if (childRole && !childRoleState.route) {
1328
- throw new Error(childRoleState.error
1329
- ? `Odai ${childRole} child route is unavailable: ${childRoleState.detail}`
1330
- : `Odai ${childRole} child route is not configured`);
1331
- }
1332
- let scope = responsibilityScopes.get(agent);
1333
- if (scope && !responsibilityScopeOwnsRequest(scope, turn, step)) {
1334
- stopResponsibilityScope(agent, "ownership-boundary", { step });
1335
- scope = undefined;
1336
- }
1337
- if (!childRole && !scope) {
1338
- const restoration = pendingResponsibilityScopeRestoration(evidence.events(agent));
1339
- if (restoration && sameRequestModelRoute(proposed, restoration.temporaryRoute)) {
1340
- const { reasoningEffort: _temporaryEffort, maxTokens: _temporaryMaxTokens, ...withoutTemporaryRoute } = proposed;
1341
- proposed = Object.freeze({ ...withoutTemporaryRoute, ...restoration.baseRoute });
1342
- appendEvent(agent, "odai/responsibility-scope-restoration-requested", {
1343
- scopeId: restoration.scopeId,
1344
- turn,
1345
- step,
1346
- role: restoration.role,
1347
- requestedRoute: restoration.baseRoute,
1348
- });
1349
- if (agent?.session) {
1350
- pendingScopeRestorations.set(agent.session, Object.freeze({
1351
- agent,
1352
- scopeId: restoration.scopeId,
1353
- turn,
1354
- step,
1355
- role: restoration.role,
1356
- expectedRoute: restoration.baseRoute,
1357
- }));
38
+ catch (error) {
39
+ logger.warn(`failed to record ${type}: ${error instanceof Error ? error.message : String(error)}`);
1358
40
  }
1359
- }
1360
- }
1361
- const upgradeRole = scope?.role;
1362
- let roleRoute = childRole
1363
- ? childRoleState.route
1364
- : scope
1365
- ? scope.route
1366
- : undefined;
1367
- const routeSource = childRole ? childRoleState?.source : scope?.source;
1368
- let routeMode = childRole ? "child" : sameRequestModelRoute(proposed, roleRoute) ? "inline" : "same-turn";
1369
- let scopedResponsibilityMaxTokens = upgradeRole ? roleRoute?.maxTokens : undefined;
1370
- const finalize = (finalRequest) => {
1371
- if (roleRoute && agent?.session && Number.isSafeInteger(turn) && Number.isSafeInteger(step)) {
1372
- const expectedRoute = roleRoute;
1373
- const receiptScope = responsibilityScopeOwnsRequest(responsibilityScopes.get(agent), turn, step)
1374
- ? responsibilityScopes.get(agent)
1375
- : scope;
1376
- pendingRouteReceipts.set(agent.session, Object.freeze({
1377
- agent,
1378
- turn,
1379
- step,
1380
- responsibility: childRole ?? upgradeRole,
1381
- routeMode,
1382
- routeSource,
1383
- ...(receiptScope?.id ? { responsibilityScopeId: receiptScope.id } : {}),
1384
- ...(receiptScope?.cardId ? { routeCardId: receiptScope.cardId } : {}),
1385
- ...(receiptScope?.resumeOfScopeId ? { resumeOfScopeId: receiptScope.resumeOfScopeId } : {}),
1386
- requestedRoute: expectedRoute,
1387
- expectedRoute,
1388
- }));
1389
- }
1390
- return finalRequest;
1391
41
  };
1392
- let request = proposed;
1393
- if (roleRoute) {
1394
- const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = proposed;
1395
- request = Object.freeze({
1396
- ...withoutInheritedEffort,
1397
- provider: roleRoute.provider,
1398
- model: roleRoute.model,
1399
- ...(roleRoute.reasoningEffort === undefined ? {} : { reasoningEffort: roleRoute.reasoningEffort }),
1400
- ...((childRole || scopedResponsibilityMaxTokens !== undefined) && roleRoute.maxTokens !== undefined
1401
- ? { maxTokens: roleRoute.maxTokens }
1402
- : {}),
1403
- });
1404
- if (!childRole && scope?.state === "pending") {
1405
- scope = claimResponsibilityScope(scope, {
1406
- step,
1407
- baseRoute: proposed,
1408
- temporaryRoute: request,
1409
- routeMode,
1410
- });
1411
- responsibilityScopes.set(agent, scope);
1412
- appendEvent(agent, "odai/responsibility-scope-claimed", responsibilityScopeClaimedEvent(scope));
1413
- }
1414
- const validation = scope?.routeValidated
1415
- ? Object.freeze({ status: "verified" })
1416
- : await probeModelRoute(
1417
- (candidate, candidateSignal) => ctx.llm.resolveCallConfig(candidate, candidateSignal),
1418
- request,
1419
- signal,
1420
- );
1421
- if (validation.status === "rejected") {
1422
- const responsibility = childRole ?? upgradeRole;
1423
- const invalidation = invalidateFailedRoleRoute(
1424
- agent,
1425
- responsibility,
1426
- roleRoute,
1427
- routeSource,
1428
- validation.failure,
1429
- { turn, step },
1430
- );
1431
- appendEvent(agent, "odai/route-fallback", {
1432
- turn,
1433
- step,
1434
- responsibility,
1435
- ...(scope?.id ? { responsibilityScopeId: scope.id } : {}),
1436
- ...(scope?.cardId ? { routeCardId: scope.cardId } : {}),
1437
- routeMode,
1438
- routeSource,
1439
- requestedRoute: roleRoute,
1440
- fallbackUsed: true,
1441
- fallbackRoute: routeFromConfig(proposed),
1442
- failureKind: validation.failure.kind,
1443
- errorCode: validation.failure.code,
1444
- error: validation.failure.message,
1445
- invalidated: invalidation.invalidated,
1446
- });
1447
- if (scope?.cardId) {
1448
- appendEvent(agent, "odai/route-card-claim-released", {
1449
- cardId: scope.cardId,
1450
- turn,
1451
- step,
1452
- reason: "route-validation-failed",
1453
- });
1454
- }
1455
- if (childRole) {
1456
- const error = new Error(`Odai ${childRole} route failed validation: ${validation.failure.code}: ${validation.failure.message}`);
1457
- error.code = validation.failure.code;
1458
- error.routeFailureKind = validation.failure.kind;
1459
- throw error;
1460
- }
1461
- stopResponsibilityScope(agent, "route-validation-failed", { step });
1462
- if (scope?.decision && requiresFailClosedProtection(scope.decision)) {
1463
- protectController(agent, turn, step, scope.decision, "route-validation", validation.failure.message);
1464
- }
1465
- roleRoute = undefined;
1466
- routeMode = "same-turn";
1467
- scopedResponsibilityMaxTokens = undefined;
1468
- request = proposed;
1469
- }
1470
- }
1471
- if (childRole || isSubagentSession(agent)) return finalize(request);
1472
-
1473
- const outputSelection = await selectSharedOutputPolicyForTurn(agent, turn, selectOutputForAgent);
1474
- const configuredMaxTokens = outputSelection.policy.maxTokens;
1475
- if (scopedResponsibilityMaxTokens !== undefined) {
1476
- if (!hasSessionEvent(agent, "odai/output-budget-overridden", (data) => data?.turn === turn && data?.step === step)) {
1477
- appendEvent(agent, "odai/output-budget-overridden", {
1478
- turn,
1479
- ...(step === undefined ? {} : { step }),
1480
- responsibility: upgradeRole,
1481
- responsibilityMaxTokens: scopedResponsibilityMaxTokens,
1482
- ...(configuredMaxTokens === undefined ? {} : { configuredControllerMaxTokens: configuredMaxTokens }),
1483
- effectiveMaxTokens: scopedResponsibilityMaxTokens,
1484
- budgetSource: "responsibility-override",
1485
- semantics: "explicit-responsibility-override",
1486
- });
1487
- }
1488
- return finalize(Object.freeze({ ...request, maxTokens: scopedResponsibilityMaxTokens }));
1489
- }
1490
- if (configuredMaxTokens === undefined) return finalize(request);
1491
- const priorMaxTokens = request.maxTokens;
1492
- const effectiveMaxTokens = priorMaxTokens === undefined
1493
- ? configuredMaxTokens
1494
- : Math.min(priorMaxTokens, configuredMaxTokens);
1495
- const budgetSource = priorMaxTokens !== undefined && priorMaxTokens < configuredMaxTokens
1496
- ? "preexisting-request-ceiling"
1497
- : "controller-policy";
1498
- if (!hasSessionEvent(agent, "odai/output-budget-applied", (data) => data?.turn === turn && data?.step === step)) {
1499
- appendEvent(agent, "odai/output-budget-applied", {
1500
- turn,
1501
- ...(step === undefined ? {} : { step }),
1502
- ...(upgradeRole === undefined ? {} : { responsibility: upgradeRole }),
1503
- configuredMaxTokens,
1504
- ...(priorMaxTokens === undefined ? {} : { priorMaxTokens }),
1505
- effectiveMaxTokens,
1506
- budgetSource,
1507
- semantics: "provider-request-ceiling",
1508
- });
1509
- }
1510
- return finalize(Object.freeze({ ...request, maxTokens: effectiveMaxTokens }));
1511
- }, { prepend: true });
1512
-
1513
- const routeFallbackAttempts = new WeakMap();
1514
- ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure, signal }, next) => {
1515
- const childRole = routedRoleOf(agent);
1516
- const scope = responsibilityScopes.get(agent);
1517
- if (!childRole && scope && (signal.aborted || failure?.code === "CONTEXT_WINDOW_EXCEEDED")) {
1518
- if (agent?.session) pendingRouteReceipts.delete(agent.session);
1519
- stopResponsibilityScope(agent, signal.aborted ? "request-aborted" : "context-window-exceeded", { step });
1520
- return next();
1521
- }
1522
- if (signal.aborted || failure?.code === "CONTEXT_WINDOW_EXCEEDED") return next();
1523
- const active = childRole
1524
- ? { role: childRole, ...configuredRole(agent, childRole, turn) }
1525
- : responsibilityScopeOwnsRequest(scope, turn, step)
1526
- ? scope
1527
- : undefined;
1528
- if (!active?.route || provider !== active.route.provider) return next();
1529
-
1530
- const classified = classifyModelRouteFailure(failure);
1531
- const invalidation = invalidateFailedRoleRoute(
1532
- agent,
1533
- active.role,
1534
- active.route,
1535
- active.source,
1536
- classified,
1537
- { turn, step },
1538
- );
1539
- appendEvent(agent, "odai/route-fallback", {
1540
- turn,
1541
- step,
1542
- responsibility: active.role,
1543
- ...(active.id ? { responsibilityScopeId: active.id } : {}),
1544
- ...(active.cardId ? { routeCardId: active.cardId } : {}),
1545
- routeMode: childRole ? "child" : "same-turn",
1546
- routeSource: active.source,
1547
- requestedRoute: active.route,
1548
- fallbackUsed: !childRole,
1549
- fallbackRoute: childRole ? undefined : active.baseRoute ?? routeFromConfig(agent?.options),
1550
- failureKind: classified.kind,
1551
- errorCode: classified.code,
1552
- error: classified.message,
1553
- invalidated: invalidation.invalidated,
42
+ const hasSessionEvent = (agent, type, predicate) => evidence.has(agent, type, predicate);
43
+ installCompactionRuntime({
44
+ appendEvent,
45
+ applyCompactionStateProtocol,
46
+ applyCompactionTarget,
47
+ classifyModelRouteFailure,
48
+ config,
49
+ ctx,
50
+ effectiveCompactionTarget,
51
+ inheritCompactionReasoning,
52
+ invalidatePersistedCompactionTarget,
53
+ logger,
54
+ probeModelRoute,
55
+ routeFromConfig,
56
+ sameRequestModelRoute,
1554
57
  });
1555
- if (active.cardId) {
1556
- appendEvent(agent, "odai/route-card-claim-released", {
1557
- cardId: active.cardId,
1558
- turn,
1559
- step,
1560
- reason: "route-request-failed",
1561
- });
1562
- }
1563
- if (childRole) return next();
1564
- if (agent?.session) pendingRouteReceipts.delete(agent.session);
1565
- stopResponsibilityScope(agent, classified.kind === "cancelled" ? "request-cancelled" : "route-request-failed", { step });
1566
- if (scope?.decision && requiresFailClosedProtection(scope.decision)) {
1567
- protectController(agent, turn, step, scope.decision, "route-request-failure", classified.message);
1568
- }
1569
- if (classified.kind === "cancelled") return next();
1570
-
1571
- let attempts = routeFallbackAttempts.get(agent);
1572
- if (!attempts) {
1573
- attempts = new Set();
1574
- routeFallbackAttempts.set(agent, attempts);
1575
- }
1576
- const key = `${turn}:${step}`;
1577
- if (attempts.has(key)) return next();
1578
- attempts.add(key);
1579
- return { kind: "retry" };
1580
- });
1581
-
1582
- const protectController = (agent, turn, step, decision, source, failure, scopeId) => {
1583
- const protection = Object.freeze({
1584
- turn,
1585
- step,
1586
- mode: "read-only",
1587
- reasonCode: decision.reasonCode,
1588
- source,
1589
- ...(failure ? { failure } : {}),
1590
- ...(scopeId ? { scopeId } : {}),
58
+ const promptRuntime = createPromptRuntime({
59
+ appendEvent,
60
+ config,
61
+ ctx,
62
+ evidence,
63
+ hasSessionEvent,
64
+ humanSafetyContinuityStorePath,
65
+ logger,
1591
66
  });
1592
- routeProtections.set(agent, protection);
1593
- appendEvent(agent, "odai/route-protection", protection, logger);
1594
- };
1595
-
1596
- ctx.on("session/event", (session, event) => {
1597
- const owner = responsibilityScopeOwners.get(session);
1598
- const activeScope = owner ? responsibilityScopes.get(owner) : undefined;
1599
- const eventMatchesRequestPosition = (position) => Number.isSafeInteger(event?.data?.turn)
1600
- && Number.isSafeInteger(event?.data?.step)
1601
- && event.data.turn === position?.turn
1602
- && event.data.step === position?.step;
1603
- const scopeStopReason = responsibilityScopeStopReason(activeScope, event);
1604
- if (scopeStopReason) {
1605
- stopResponsibilityScope(owner, scopeStopReason, {
1606
- scopeId: activeScope.id,
1607
- ...(Number.isSafeInteger(event.data?.step) ? { step: event.data.step } : {}),
1608
- });
1609
- }
1610
-
1611
- if (event?.type === "assistant/chunk" && event.data?.chunk?.type === "usage") {
1612
- outputUsageBySession.set(session, {
1613
- turn: event.data.turn,
1614
- step: event.data.step,
1615
- usage: event.data.chunk.usage,
1616
- });
1617
- } else if (event?.type === "assistant/message" && event.data?.usage) {
1618
- outputUsageBySession.set(session, {
1619
- turn: event.data.turn,
1620
- step: event.data.step,
1621
- usage: event.data.usage,
1622
- });
1623
- }
1624
-
1625
- if (event?.type === "turn/end") {
1626
- const usage = outputUsageBySession.get(session);
1627
- if (event.data?.reason?.kind === "max-tokens" && owner) {
1628
- const events = evidence.events(owner);
1629
- const stopped = latestStoppedResponsibilityScope(events, event.data?.turn);
1630
- const receipt = stopped && events.findLast((candidate) => (
1631
- candidate?.type === "odai/route-applied"
1632
- && candidate.data?.status === "applied"
1633
- && candidate.data?.responsibilityScopeId === stopped.scopeId
1634
- ))?.data;
1635
- const routeCard = stopped?.routeCardId ? routeCardById(events, stopped.routeCardId) : undefined;
1636
- const observedOutputTokens = usage?.turn === stopped?.turn
1637
- && usage?.step === stopped?.stopStep
1638
- && Number.isSafeInteger(usage.usage?.outputTokens)
1639
- ? usage.usage.outputTokens
1640
- : undefined;
1641
- if (stopped?.reason === "terminal-response"
1642
- && Number.isSafeInteger(stopped.stopStep)
1643
- && receipt?.step === stopped.stopStep
1644
- && Number.isSafeInteger(observedOutputTokens)
1645
- && ["planner", "executor", "frontend"].includes(receipt.responsibility)
1646
- && (receipt.responsibility !== "executor" || routeCard)) {
1647
- const effectiveRoute = receipt.actualRoute ?? receipt.requestedRoute;
1648
- appendEvent(owner, "odai/responsibility-interrupted", {
1649
- scopeId: stopped.scopeId,
1650
- turn: stopped.turn,
1651
- step: stopped.stopStep ?? stopped.startStep,
1652
- responsibility: receipt.responsibility,
1653
- reason: "max-tokens",
1654
- routeMode: receipt.routeMode,
1655
- routeSource: receipt.routeSource,
1656
- requestedRoute: receipt.requestedRoute,
1657
- ...(effectiveRoute ? { effectiveRoute } : {}),
1658
- ...(effectiveRoute?.maxTokens === undefined ? {} : { effectiveMaxTokens: effectiveRoute.maxTokens }),
1659
- outputTokens: observedOutputTokens,
1660
- ...(stopped.routeCardId ? { routeCardId: stopped.routeCardId } : {}),
1661
- ...(routeCard ? { routeCard } : {}),
1662
- }, logger);
1663
- }
1664
- }
1665
- outputUsageBySession.delete(session);
1666
- }
1667
-
1668
- const pendingRestoration = pendingScopeRestorations.get(session);
1669
- if (pendingRestoration && event?.type === "turn/end" && event.data?.turn === pendingRestoration.turn) {
1670
- appendEvent(pendingRestoration.agent, "odai/responsibility-scope-restored", {
1671
- scopeId: pendingRestoration.scopeId,
1672
- turn: pendingRestoration.turn,
1673
- step: pendingRestoration.step,
1674
- role: pendingRestoration.role,
1675
- status: "unverified",
1676
- requestedRoute: pendingRestoration.expectedRoute,
1677
- stopReason: "no-effective-request",
1678
- });
1679
- pendingScopeRestorations.delete(session);
1680
- }
1681
- if (pendingRestoration
1682
- && ["request/header", "assistant/chunk", "assistant/message"].includes(event?.type)
1683
- && eventMatchesRequestPosition(pendingRestoration)) {
1684
- let actualRoute;
1685
- try {
1686
- actualRoute = event.type === "request/header"
1687
- ? routeFromConfig(event.data?.header?.config)
1688
- : routeFromConfig(session.requestHeader?.()?.config);
1689
- } catch {}
1690
- if (actualRoute || event.type !== "request/header") {
1691
- const mismatch = routeMismatchFor(pendingRestoration.expectedRoute, actualRoute, "base-route restoration");
1692
- appendEvent(pendingRestoration.agent, "odai/responsibility-scope-restored", {
1693
- scopeId: pendingRestoration.scopeId,
1694
- turn: pendingRestoration.turn,
1695
- step: pendingRestoration.step,
1696
- role: pendingRestoration.role,
1697
- status: mismatch ? "mismatch" : "applied",
1698
- requestedRoute: pendingRestoration.expectedRoute,
1699
- ...(actualRoute ? { actualRoute } : {}),
1700
- ...(mismatch ? { error: mismatch } : {}),
67
+ const { baseSelection, bundled, evolutionDisabled, explicitSkillPath, memorySettingsFor, routingSnapshotFor, selectOutputForAgent, skillPath, } = promptRuntime;
68
+ const routeProtections = new WeakMap();
69
+ const responsibilityScopes = new WeakMap();
70
+ const responsibilityScopeOwners = new WeakMap();
71
+ const pendingRouteReceipts = new WeakMap();
72
+ const pendingScopeRestorations = new WeakMap();
73
+ const outputUsageBySession = new WeakMap();
74
+ const stopResponsibilityScope = (agent, reason, position = {}) => {
75
+ const scope = responsibilityScopes.get(agent);
76
+ if (!scope || (position.scopeId && position.scopeId !== scope.id))
77
+ return undefined;
78
+ responsibilityScopes.delete(agent);
79
+ const protection = routeProtections.get(agent);
80
+ if (protection?.scopeId === scope.id)
81
+ routeProtections.delete(agent);
82
+ appendEvent(agent, "odai/route-protection-released", {
83
+ scopeId: scope.id,
84
+ turn: scope.turn,
85
+ reason,
1701
86
  });
1702
- if (mismatch) {
1703
- protectController(
1704
- pendingRestoration.agent,
1705
- pendingRestoration.turn,
1706
- pendingRestoration.step,
1707
- { reasonCode: "RESPONSIBILITY_BASE_ROUTE_RESTORATION_MISMATCH" },
1708
- "scope-restoration-mismatch",
1709
- mismatch,
1710
- pendingRestoration.scopeId,
1711
- );
1712
- }
1713
- pendingScopeRestorations.delete(session);
1714
- }
1715
- }
1716
-
1717
- const pending = pendingRouteReceipts.get(session);
1718
- if (!pending) return;
1719
- if (event?.type === "turn/end" && event.data?.turn === pending.turn) {
1720
- appendEvent(pending.agent, "odai/route-applied", {
1721
- turn: pending.turn,
1722
- step: pending.step,
1723
- responsibility: pending.responsibility,
1724
- ...(pending.responsibilityScopeId ? { responsibilityScopeId: pending.responsibilityScopeId } : {}),
1725
- ...(pending.routeCardId ? { routeCardId: pending.routeCardId } : {}),
1726
- status: "unverified",
1727
- routeMode: pending.routeMode,
1728
- routeSource: pending.routeSource,
1729
- fallbackUsed: true,
1730
- requestedRoute: pending.requestedRoute,
1731
- stopReason: "no-effective-request",
1732
- });
1733
- if (pending.routeCardId) {
1734
- appendEvent(pending.agent, "odai/route-card-claim-released", {
1735
- cardId: pending.routeCardId,
1736
- turn: pending.turn,
1737
- step: pending.step,
1738
- reason: "no-effective-request",
87
+ appendEvent(agent, "odai/responsibility-scope-stopped", responsibilityScopeStoppedEvent(scope, reason, position));
88
+ return scope;
89
+ };
90
+ const stopDanglingResponsibilityScope = (agent, reason) => {
91
+ if (responsibilityScopes.has(agent))
92
+ return undefined;
93
+ const dangling = latestDanglingResponsibilityScope(evidence.events(agent));
94
+ if (!dangling)
95
+ return undefined;
96
+ appendEvent(agent, "odai/route-protection-released", {
97
+ scopeId: dangling.scopeId,
98
+ turn: dangling.turn,
99
+ reason,
1739
100
  });
1740
- }
1741
- stopResponsibilityScope(pending.agent, "no-effective-request", {
1742
- scopeId: pending.responsibilityScopeId,
1743
- step: pending.step,
1744
- });
1745
- pendingRouteReceipts.delete(session);
1746
- return;
1747
- }
1748
- if (!["request/header", "assistant/chunk", "assistant/message"].includes(event?.type)) return;
1749
- if (!eventMatchesRequestPosition(pending)) return;
1750
- let actualRoute;
1751
- try {
1752
- actualRoute = event.type === "request/header"
1753
- ? routeFromConfig(event.data?.header?.config)
1754
- : routeFromConfig(session.requestHeader?.()?.config);
1755
- } catch {}
1756
- if (!actualRoute && event.type === "request/header") return;
1757
- const mismatch = routeMismatchFor(pending.expectedRoute, actualRoute, pending.routeMode);
1758
- appendEvent(pending.agent, "odai/route-applied", {
1759
- turn: pending.turn,
1760
- step: pending.step,
1761
- responsibility: pending.responsibility,
1762
- ...(pending.responsibilityScopeId ? { responsibilityScopeId: pending.responsibilityScopeId } : {}),
1763
- ...(pending.routeCardId ? { routeCardId: pending.routeCardId } : {}),
1764
- status: mismatch ? "mismatch" : "applied",
1765
- routeMode: pending.routeMode,
1766
- routeSource: pending.routeSource,
1767
- fallbackUsed: Boolean(mismatch),
1768
- requestedRoute: pending.requestedRoute,
1769
- ...(actualRoute ? { actualRoute } : {}),
1770
- ...(mismatch ? { stopReason: "route-mismatch", error: mismatch } : {}),
1771
- });
1772
- if (mismatch && pending.routeMode === "same-turn") {
1773
- stopResponsibilityScope(pending.agent, "route-mismatch", {
1774
- scopeId: pending.responsibilityScopeId,
1775
- step: pending.step,
1776
- });
1777
- }
1778
- if (!mismatch && pending.resumeOfScopeId) {
1779
- appendEvent(pending.agent, "odai/responsibility-interruption-consumed", {
1780
- scopeId: pending.resumeOfScopeId,
1781
- turn: pending.turn,
1782
- step: pending.step,
1783
- responsibility: pending.responsibility,
1784
- resumedScopeId: pending.responsibilityScopeId,
1785
- });
1786
- }
1787
- if (pending.routeCardId) {
1788
- appendEvent(pending.agent, mismatch ? "odai/route-card-claim-released" : "odai/route-card-consumed", {
1789
- cardId: pending.routeCardId,
1790
- turn: pending.turn,
1791
- step: pending.step,
1792
- ...(mismatch ? { reason: "route-mismatch" } : { receiptStatus: "applied" }),
1793
- });
1794
- }
1795
- pendingRouteReceipts.delete(session);
1796
- if (mismatch && pending.routeMode === "same-turn") {
1797
- protectController(
1798
- pending.agent,
1799
- pending.turn,
1800
- pending.step,
1801
- { reasonCode: `${pending.responsibility.toUpperCase()}_ROUTE_MISMATCH` },
1802
- "route-mismatch",
1803
- mismatch,
1804
- );
1805
- }
1806
- });
1807
-
1808
- ctx.on("agent/turn-stopping", ({ agent, turn }) => {
1809
- stopResponsibilityScope(agent, "turn-stopping");
1810
- const role = routedRoleOf(agent);
1811
- if (!role) return;
1812
- const receipts = evidence.events(agent)
1813
- .filter((event) => event.type === "odai/route-applied"
1814
- && event.data?.turn === turn
1815
- && event.data?.responsibility === role
1816
- && event.data?.routeMode === "child")
1817
- .map((event) => event.data);
1818
- const failed = receipts.find((receipt) => receipt.status !== "applied");
1819
- if (receipts.length > 0 && !failed) return;
1820
- const detail = failed?.error ?? failed?.stopReason ?? "no verified child route receipt";
1821
- throw new Error(`Odai ${role} child route was not verified: ${detail}`);
1822
- });
1823
-
1824
- {
1825
- const routedSteps = new WeakMap();
1826
- ctx.on("agent/pre-step", async ({ agent, turn, step, signal }, next) => {
1827
- const subagentSession = isSubagentSession(agent);
1828
- if (!subagentSession) {
1829
- if (step === 1) {
1830
- stopResponsibilityScope(agent, "new-turn");
1831
- routeProtections.delete(agent);
1832
- }
1833
- stopDanglingResponsibilityScope(agent, "runtime-resume");
1834
- }
1835
- let downstream = await next();
1836
- if (downstream.kind === "reject" || signal.aborted) return downstream;
1837
- const responsibilityGap = subagentSession ? undefined : pendingResponsibilityGap(agent, turn, step);
1838
- const authenticatedDirectMessage = latestDirectUserMessage(agent, undefined, { turn });
1839
- const suppliedDirectMessages = Array.isArray(downstream.messages)
1840
- ? downstream.messages.filter((message) => message?.role === "user" && message?.source?.kind === "user")
1841
- : [];
1842
- const directMessage = latestDirectUserMessage(agent, suppliedDirectMessages, { turn });
1843
- const directText = directMessage ? extractLatestUserText([directMessage]) : "";
1844
- const authenticatedDirectText = authenticatedDirectMessage
1845
- ? extractLatestUserText([authenticatedDirectMessage])
1846
- : "";
1847
- const responsibilityEvents = subagentSession ? [] : evidence.events(agent);
1848
- const interruption = !subagentSession && step === 1
1849
- ? pendingResponsibilityInterruption(responsibilityEvents)
1850
- : undefined;
1851
- let responsibilityContinuation;
1852
- let interruptionNotice;
1853
- if (interruption && authenticatedDirectMessage) {
1854
- const disposition = classifyResponsibilityInterruptionText(authenticatedDirectText);
1855
- if (disposition === "continue" && directMessage) {
1856
- responsibilityContinuation = Object.freeze({ ...interruption, continuationText: authenticatedDirectText });
1857
- appendEvent(agent, "odai/responsibility-interruption-resume-requested", {
1858
- scopeId: interruption.scopeId,
1859
- turn,
1860
- step,
1861
- responsibility: interruption.responsibility,
1862
- });
1863
- } else if (disposition === "preserve") {
1864
- if (directMessage) {
1865
- interruptionNotice = pluginMessage(
1866
- renderOutputLimitInterruptionNotice(interruption),
1867
- `odai verified ${interruption.responsibility} output-limit interruption`,
1868
- );
1869
- }
1870
- appendEvent(agent, "odai/responsibility-interruption-preserved", {
1871
- scopeId: interruption.scopeId,
1872
- turn,
1873
- step,
1874
- responsibility: interruption.responsibility,
1875
- reason: "output-limit-diagnostic",
1876
- });
1877
- } else if (disposition === "clear") {
1878
- appendEvent(agent, "odai/responsibility-interruption-cleared", {
1879
- scopeId: interruption.scopeId,
1880
- turn,
1881
- step,
1882
- responsibility: interruption.responsibility,
1883
- reason: "superseded-by-user-task",
1884
- });
1885
- }
1886
- }
1887
- const activation = contextActivationFor(agent, directText, turn);
1888
- const routeCardNeeded = !subagentSession && (Boolean(activeRouteCard(responsibilityEvents))
1889
- || ["planner", "executor"].includes(responsibilityGap?.responsibility)
1890
- || responsibilityContinuation?.responsibility === "executor");
1891
- syncToolExposure(agent, activation, { turn, step, routeCard: routeCardNeeded });
1892
- if (interruptionNotice) {
1893
- downstream = { ...downstream, messages: [...downstream.messages, interruptionNotice] };
1894
- }
1895
- if (subagentSession) return downstream;
1896
- if (step !== 1 && !responsibilityGap) return downstream;
1897
-
1898
- if (step === 1 && claimSemanticMemoryTurn(agent, turn, step)) {
1899
- const settings = memorySettingsFor(agent, turn);
1900
- const message = directMessage;
1901
- const query = extractRoutingText(downstream.messages, agent?.session?.events).slice(0, config.routing.maxInputChars);
1902
- let retrieved = [];
1903
- let captured = [];
1904
- let error;
1905
- if (settings.mode === "auto" && message) {
1906
- try {
1907
- retrieved = retrieveSemanticMemories({
1908
- storePath: config.memory.storePath,
1909
- query,
1910
- cwd: agent?.session?.header?.cwd,
1911
- limit: config.memory.maxRetrieved,
1912
- });
1913
- captured = captureAutomaticMemories({
1914
- storePath: config.memory.storePath,
1915
- mode: settings.mode,
1916
- agent,
1917
- message,
1918
- turn,
1919
- cwd: agent?.session?.header?.cwd,
1920
- });
1921
- } catch (memoryError) {
1922
- error = memoryError instanceof Error ? memoryError.message : String(memoryError);
1923
- logger.warn(`Odai semantic memory processing failed closed for this turn: ${error}`);
1924
- retrieved = [];
1925
- captured = [];
1926
- }
1927
- }
1928
- const captureEvidence = captured.filter((result) => result.changed);
1929
- if (retrieved.length > 0 || captureEvidence.length > 0 || error) {
1930
- appendEvent(agent, "odai/memory-processed", {
1931
- turn,
1932
- step,
1933
- mode: settings.mode,
1934
- source: settings.source,
1935
- retrievedIds: retrieved.map((entry) => entry.id),
1936
- captures: captureEvidence.map((result) => ({
1937
- changed: true,
1938
- reasonCode: result.reasonCode,
1939
- ...(result.id ? { id: result.id } : {}),
1940
- ...(result.status ? { status: result.status } : {}),
1941
- ...(result.scope ? { scope: result.scope } : {}),
1942
- })),
1943
- ...(error ? { status: "fallback", error: "memory-store-unavailable" } : { status: "completed" }),
1944
- }, logger);
1945
- }
1946
- const packet = renderSemanticMemoryPacket(retrieved);
1947
- if (packet) {
1948
- downstream = {
1949
- ...downstream,
1950
- messages: [...downstream.messages, memoryPacketMessage(packet)],
1951
- };
1952
- }
1953
- }
1954
-
1955
- if (config.routing.mode === "off") return downstream;
1956
- if (hasSessionEvent(agent, "odai/route-decided", (data) => data?.turn === turn && data?.step === step)) {
1957
- return downstream;
1958
- }
1959
-
1960
- let routed = routedSteps.get(agent);
1961
- if (!routed) {
1962
- routed = new Set();
1963
- routedSteps.set(agent, routed);
1964
- }
1965
- const routeKey = `${turn}:${step}`;
1966
- if (routed.has(routeKey)) return downstream;
1967
- routed.add(routeKey);
1968
-
1969
- const taskText = extractRoutingText(downstream.messages, agent?.session?.events).slice(0, config.routing.maxInputChars);
1970
- let routedDownstream = downstream;
1971
- let researchPacketText = "";
1972
- const researchDecision = decideResearchPrefetch({ text: taskText, proposal: responsibilityGap });
1973
- if (researchDecision.action === "delegate") {
1974
- appendEvent(agent, "odai/research-decided", {
1975
- turn,
1976
- step,
1977
- role: "researcher",
1978
- action: "delegate",
1979
- mode: config.routing.mode,
1980
- reasonCode: researchDecision.reasonCode,
1981
- signals: researchDecision.signals,
1982
- }, logger);
1983
- if (config.routing.mode === "observe") {
1984
- appendEvent(agent, "odai/research-result", {
1985
- turn,
1986
- step,
1987
- role: "researcher",
1988
- status: "observed",
1989
- stopReason: "observe-mode",
1990
- }, logger);
1991
- } else {
1992
- const researchState = configuredRole(agent, "researcher", turn);
1993
- const researchRoute = researchState.route;
1994
- if (!researchRoute) {
1995
- appendEvent(agent, "odai/research-result", {
1996
- turn,
1997
- step,
1998
- role: "researcher",
1999
- status: "fallback",
2000
- stopReason: researchState.error ? "route-config-invalid" : "route-config-missing",
2001
- ...(researchState.error ? { error: researchState.detail } : {}),
2002
- }, logger);
2003
- } else {
2004
- const researchBundle = sharedSkillSelection(agent, turn)?.bundle ?? bundled;
2005
- const researchContract = dshRoleContract(
2006
- "researcher",
2007
- researchBundle.roleContracts.researcher,
2008
- researchBundle.referenceContracts,
2009
- );
2010
- const result = ctx.subagents?.start
2011
- ? await runRoutedRole({
2012
- subagents: ctx.subagents,
2013
- provider: config.routing.provider,
2014
- decision: researchDecision,
2015
- taskText: renderResearchTaskContract(taskText),
2016
- roleContract: researchContract,
2017
- agent,
2018
- signal,
2019
- roleRoute: researchRoute,
2020
- })
2021
- : Object.freeze({
2022
- status: "fallback",
2023
- stopReason: "infrastructure-error",
2024
- output: [],
2025
- error: "dsh subagents service unavailable",
2026
- });
2027
- let packet;
2028
- let packetError;
2029
- if (result.status === "completed") {
2030
- try {
2031
- packet = verifyResearchPacketSources(
2032
- parseResearchPacket(outputText(result.output)),
2033
- agent?.session?.header?.cwd,
2034
- );
2035
- } catch (error) {
2036
- packetError = error instanceof Error ? error.message : String(error);
2037
- }
2038
- }
2039
- const completed = result.status === "completed" && packet !== undefined;
2040
- appendEvent(agent, "odai/research-result", {
2041
- turn,
2042
- step,
2043
- role: "researcher",
2044
- status: completed ? "completed" : "fallback",
2045
- stopReason: completed ? result.stopReason : (packetError ? "packet-invalid" : result.stopReason),
2046
- routeSource: researchState.source,
2047
- fallbackUsed: !completed,
2048
- routeReceiptStatus: result.routeReceiptStatus,
2049
- requestedRoute: researchRoute,
2050
- ...(result.routeReceiptError ? { routeReceiptError: result.routeReceiptError } : {}),
2051
- ...(result.actualRoute ? { actualRoute: result.actualRoute } : {}),
2052
- ...(packet ? { packetDigest: packet.digest, sourceCount: packet.sourceCount } : {}),
2053
- ...(packetError ? { error: packetError } : result.taskError ? { error: result.taskError } : {}),
2054
- }, logger);
2055
- if (completed) {
2056
- researchPacketText = renderResearchPacket(packet);
2057
- routedDownstream = {
2058
- ...downstream,
2059
- messages: [
2060
- ...routedDownstream.messages,
2061
- pluginMessage(
2062
- researchPacketText,
2063
- `odai completed researcher evidence compression (${packet.sourceCount} sources)`,
2064
- ),
2065
- ],
2066
- };
2067
- }
2068
- }
2069
- }
2070
- }
2071
-
2072
- const frozenCard = responsibilityContinuation?.routeCard ?? activeRouteCard(evidence.events(agent));
2073
- let decision = decideRoute({
2074
- text: taskText,
2075
- routeCard: frozenCard,
2076
- proposal: responsibilityGap,
2077
- interruption: responsibilityContinuation,
2078
- });
2079
- let routeRole = decision.targetRole ?? decision.role;
2080
- const roleTaskText = researchPacketText ? `${taskText}\n\n${researchPacketText}` : taskText;
2081
- let roleContext = decision.action === "direct"
2082
- ? undefined
2083
- : buildRoleContextPacket(agent, routeRole, roleTaskText);
2084
- let localReviewerCoverage;
2085
- if (config.routing.mode === "auto"
2086
- && routeRole === "reviewer"
2087
- && decision.action === "delegate"
2088
- && !roleContext.sufficient) {
2089
- localReviewerCoverage = roleContext.coverage;
2090
- decision = Object.freeze({
2091
- ...decision,
2092
- role: "controller",
2093
- mode: "direct",
2094
- action: "direct",
2095
- targetRole: "reviewer",
2096
- signals: Object.freeze([...decision.signals, "review-evidence-packet-missing", "controller-local-review"]),
101
+ appendEvent(agent, "odai/responsibility-scope-stopped", {
102
+ ...dangling,
103
+ reason,
2097
104
  });
2098
- routeRole = "reviewer";
2099
- }
2100
- appendEvent(agent, "odai/route-decided", {
2101
- turn,
2102
- step,
2103
- role: decision.role,
2104
- action: decision.action,
2105
- ...(decision.targetRole ? { targetRole: decision.targetRole } : {}),
2106
- mode: config.routing.mode,
2107
- reasonCode: decision.reasonCode,
2108
- signals: decision.signals,
2109
- ...(responsibilityGap ? {
2110
- stateDigest: responsibilityGap.stateDigest,
2111
- gap: responsibilityGap.gap,
2112
- evidenceRefs: responsibilityGap.evidenceRefs,
2113
- expectedChange: responsibilityGap.expectedChange,
2114
- } : {}),
2115
- ...(decision.considerations ? { considerations: decision.considerations } : {}),
2116
- }, logger);
2117
- if (responsibilityGap) {
2118
- appendEvent(agent, "odai/responsibility-gap-consumed", {
2119
- turn,
2120
- step,
2121
- responsibility: responsibilityGap.responsibility,
2122
- stateDigest: responsibilityGap.stateDigest,
2123
- routeAction: decision.action,
2124
- reasonCode: decision.reasonCode,
2125
- }, logger);
2126
- }
2127
-
2128
- if (decision.action === "direct") {
2129
- if (!localReviewerCoverage) return routedDownstream;
2130
- appendEvent(agent, "odai/route-context", {
2131
- turn,
2132
- step,
2133
- role: "reviewer",
2134
- mode: "controller-local",
2135
- digest: roleContext.digest,
2136
- evidenceCount: roleContext.evidenceCount,
2137
- toolEvidenceCount: roleContext.toolEvidenceCount,
2138
- acceptanceCount: localReviewerCoverage.acceptanceCount,
2139
- diffCount: localReviewerCoverage.diffCount,
2140
- testCount: localReviewerCoverage.testCount,
2141
- truncated: roleContext.truncated,
2142
- sufficient: false,
2143
- }, logger);
2144
- appendEvent(agent, "odai/route-result", {
2145
- turn,
2146
- step,
2147
- role: "reviewer",
2148
- action: "direct",
2149
- status: "fallback",
2150
- stopReason: "evidence-packet-missing",
2151
- independent: false,
2152
- }, logger);
105
+ return dangling;
106
+ };
107
+ const configuredRole = (agent, role, turn = currentAgentTurn(agent)) => {
108
+ const state = routingSnapshotFor(agent, turn);
109
+ if (state.error || !state.snapshot)
110
+ return state;
2153
111
  return {
2154
- kind: "enter",
2155
- messages: [
2156
- ...routedDownstream.messages,
2157
- pluginMessage(
2158
- [
2159
- `An independent reviewer was not started because the bounded packet is incomplete (${JSON.stringify(localReviewerCoverage)}).`,
2160
- "Remain on the current controller route and continue the authorized task. Gather project-available requirements, acceptance conditions, diff, tests, and matching tool evidence before resubmitting a changed reviewer gap.",
2161
- "A controller-local read-only check may guide the work, but it is not independent acceptance. Do not stop solely to ask the user for review artifacts the project can produce, and do not claim the reviewer approved release.",
2162
- ].join("\n"),
2163
- "odai reviewer evidence is incomplete; controller continues locally",
2164
- ),
2165
- ],
112
+ route: state.snapshot.roles[role],
113
+ source: state.snapshot.sources[role],
2166
114
  };
2167
- }
2168
-
2169
- if (config.routing.mode === "observe") {
2170
- if (requiresFailClosedProtection(decision)) {
2171
- protectController(agent, turn, step, decision, "observe");
115
+ };
116
+ const pendingResponsibilityGap = (agent, turn, step) => {
117
+ const consumed = new Set();
118
+ const events = evidence.events(agent);
119
+ for (let index = events.length - 1; index >= 0; index -= 1) {
120
+ const event = events[index];
121
+ if (event.data?.turn !== turn)
122
+ continue;
123
+ if (event.type === "odai/responsibility-gap-consumed") {
124
+ consumed.add(event.data.stateDigest);
125
+ continue;
126
+ }
127
+ if (event.type !== "odai/responsibility-gap"
128
+ || typeof event.data?.step !== "number"
129
+ || !Number.isSafeInteger(event.data.step)
130
+ || event.data.step >= step
131
+ || consumed.has(event.data.stateDigest)
132
+ || !isResponsibilityGapProposal(event.data))
133
+ continue;
134
+ return event.data;
2172
135
  }
2173
- return {
2174
- kind: "enter",
2175
- messages: [
2176
- ...routedDownstream.messages,
2177
- pluginMessage(
2178
- renderRouteNotice(decision, "observe"),
2179
- `odai observed ${routeRole} gap (${decision.reasonCode})`,
2180
- ),
2181
- ],
2182
- };
2183
- }
2184
-
2185
- const roleState = configuredRole(agent, routeRole, turn);
2186
- const roleRoute = roleState.route;
2187
- if (!roleRoute) {
2188
- const invalidConfig = Boolean(roleState.error);
2189
- appendEvent(agent, "odai/route-config-missing", {
2190
- turn,
2191
- step,
2192
- role: routeRole,
2193
- action: decision.action,
2194
- mode: config.routing.mode,
2195
- status: invalidConfig ? "invalid" : "unconfigured",
2196
- ...(invalidConfig ? { error: roleState.detail } : {}),
2197
- }, logger);
2198
- if (requiresFailClosedProtection(decision)) {
2199
- protectController(
2200
- agent,
2201
- turn,
2202
- step,
2203
- decision,
2204
- invalidConfig ? "route-config-invalid" : "route-config-missing",
2205
- );
136
+ return undefined;
137
+ };
138
+ const invalidateFailedRoleRoute = (agent, role, route, source, failure, position = {}) => {
139
+ let invalidation = Object.freeze({ invalidated: false, reason: "not-deterministic-or-not-persisted" });
140
+ if (failure.kind === "deterministic" && source === "persisted-mapping") {
141
+ try {
142
+ invalidation = invalidatePersistedRoleRoute(config.routing.configPath, role, route);
143
+ }
144
+ catch (error) {
145
+ invalidation = Object.freeze({
146
+ invalidated: false,
147
+ reason: "cleanup-failed",
148
+ error: error instanceof Error ? error.message : String(error),
149
+ });
150
+ }
2206
151
  }
2207
- if (routeRole === "frontend") return routedDownstream;
2208
- return {
2209
- kind: "enter",
2210
- messages: [
2211
- ...routedDownstream.messages,
2212
- pluginMessage(
2213
- renderMissingRouteConfigNotice(decision, config.routing.mode, roleState.error),
2214
- `odai ${routeRole} route is ${invalidConfig ? "invalid" : "not configured"}`,
2215
- ),
2216
- ],
2217
- };
2218
- }
2219
-
2220
- const roleBundle = sharedSkillSelection(agent, turn)?.bundle ?? bundled;
2221
- const canonicalRoleContract = roleBundle.roleContracts[routeRole];
2222
- const roleContract = dshRoleContract(routeRole, canonicalRoleContract, roleBundle.referenceContracts);
2223
- let rolePreflightVerified = false;
2224
- if (routeRole === "frontend" && decision.action === "upgrade") {
2225
- const health = await probeModelRoute(
2226
- (candidate, candidateSignal) => ctx.llm.resolveCallConfig(candidate, candidateSignal),
2227
- roleRoute,
2228
- signal,
2229
- );
2230
- if (health.status === "rejected") {
2231
- const invalidation = invalidateFailedRoleRoute(
2232
- agent,
2233
- routeRole,
2234
- roleRoute,
2235
- roleState.source,
2236
- health.failure,
2237
- { turn, step, position: "pre-step" },
2238
- );
2239
- appendEvent(agent, "odai/route-result", {
2240
- turn,
2241
- step,
2242
- role: routeRole,
2243
- action: "upgrade",
2244
- status: "fallback",
2245
- stopReason: "route-preflight-failed",
2246
- routeSource: roleState.source,
2247
- fallbackUsed: true,
2248
- requestedRoute: roleRoute,
2249
- failureKind: health.failure.kind,
2250
- error: health.failure.message,
152
+ appendEvent(agent, "odai/route-health", {
153
+ ...position,
154
+ responsibility: role,
155
+ routeSource: source,
156
+ requestedRoute: route,
157
+ status: failure.kind === "deterministic" ? "invalid" : "unhealthy",
158
+ failureKind: failure.kind,
159
+ errorCode: failure.code,
160
+ error: failure.message,
2251
161
  invalidated: invalidation.invalidated,
2252
- }, logger);
2253
- return {
2254
- kind: "enter",
2255
- messages: [
2256
- ...routedDownstream.messages,
2257
- pluginMessage(
2258
- [
2259
- `The configured frontend route failed preflight (${health.failure.kind}: ${health.failure.message}).`,
2260
- "Continue locally as the current controller for this turn using the canonical frontend and craft contract below. Do not claim the configured frontend responsibility ran; no routed receipt exists.",
2261
- "",
2262
- "frontend local-fallback responsibility contract:",
2263
- roleContract,
2264
- ].join("\n"),
2265
- "odai frontend route unavailable; explicit local fallback",
2266
- ),
2267
- ],
2268
- };
2269
- }
2270
- rolePreflightVerified = health.status === "verified";
2271
- }
2272
- roleContext ??= buildRoleContextPacket(agent, routeRole, taskText);
2273
- const inPlaceUpgrade = decision.action === "upgrade"
2274
- && (config.routing.mode === "auto" || ["executor", "frontend"].includes(routeRole));
2275
- const contextMode = inPlaceUpgrade ? "same-turn" : "bounded-packet";
2276
- appendEvent(agent, "odai/route-context", {
2277
- turn,
2278
- step,
2279
- role: routeRole,
2280
- mode: contextMode,
2281
- digest: roleContext.digest,
2282
- evidenceCount: roleContext.evidenceCount,
2283
- toolEvidenceCount: roleContext.toolEvidenceCount,
2284
- acceptanceCount: roleContext.coverage.acceptanceCount,
2285
- diffCount: roleContext.coverage.diffCount,
2286
- testCount: roleContext.coverage.testCount,
2287
- truncated: roleContext.truncated,
2288
- sufficient: roleContext.sufficient,
2289
- }, logger);
2290
-
2291
- if (routeRole === "reviewer" && decision.action === "delegate" && !roleContext.sufficient) {
2292
- appendEvent(agent, "odai/route-result", {
2293
- turn,
2294
- step,
2295
- role: "reviewer",
2296
- action: "delegate",
2297
- status: "fallback",
2298
- stopReason: "evidence-packet-missing",
2299
- contextDigest: roleContext.digest,
2300
- }, logger);
2301
- return {
2302
- kind: "enter",
2303
- messages: [
2304
- ...routedDownstream.messages,
2305
- pluginMessage(
2306
- `odai reviewer child was not started because the bounded packet is incomplete (${JSON.stringify(roleContext.coverage)}). Gather requirements, acceptance conditions, actual diff, tests, and matching tool evidence first; do not claim independent acceptance.`,
2307
- "odai reviewer evidence packet is incomplete",
2308
- ),
2309
- ],
2310
- };
2311
- }
2312
-
2313
- if (inPlaceUpgrade) {
2314
- stopResponsibilityScope(agent, "superseded", { step });
2315
- const responsibilityScope = createResponsibilityScope({
2316
- turn,
2317
- startStep: step,
2318
- role: routeRole,
2319
- route: roleRoute,
2320
- source: roleState.source,
2321
- decision,
2322
- routeValidated: rolePreflightVerified,
2323
- ...(routeRole === "executor" && frozenCard ? { cardId: frozenCard.id } : {}),
2324
- ...(responsibilityContinuation ? { resumeOfScopeId: responsibilityContinuation.scopeId } : {}),
162
+ ...(invalidation.backupPath ? { backupPath: invalidation.backupPath } : {}),
163
+ ...(invalidation.reason ? { cleanupReason: invalidation.reason } : {}),
164
+ ...(invalidation.error ? { cleanupError: invalidation.error } : {}),
2325
165
  });
2326
- responsibilityScopes.set(agent, responsibilityScope);
2327
- if (agent?.session) responsibilityScopeOwners.set(agent.session, agent);
2328
- appendEvent(agent, "odai/responsibility-scope-started", responsibilityScopeStartedEvent(responsibilityScope));
2329
- if (["planner", "reviewer"].includes(routeRole)) {
2330
- protectController(agent, turn, step, decision, `responsibility-scope-${routeRole}`, undefined, responsibilityScope.id);
2331
- }
2332
- if (routeRole === "executor" && frozenCard) {
2333
- appendEvent(agent, "odai/route-card-claimed", {
2334
- cardId: frozenCard.id,
2335
- turn,
2336
- step,
2337
- ...(responsibilityContinuation ? { reason: "output-limit-continuation" } : {}),
2338
- });
2339
- }
2340
- appendEvent(agent, "odai/route-upgrade", {
2341
- turn,
2342
- step,
2343
- role: decision.role,
2344
- targetRole: routeRole,
2345
- status: "requested",
2346
- responsibilityScopeId: responsibilityScope.id,
2347
- ...(responsibilityContinuation ? { resumeOfScopeId: responsibilityContinuation.scopeId } : {}),
2348
- continuationPolicy: responsibilityScope.continuationPolicy,
2349
- stopPolicy: responsibilityScope.stopPolicy,
2350
- routeSource: roleState.source,
2351
- requestedRoute: roleRoute,
2352
- contextDigest: roleContext.digest,
2353
- contextMode,
2354
- ...(routeRole === "reviewer" ? { independent: false } : {}),
2355
- }, logger);
2356
- const contextBoundary = routeRole === "reviewer"
2357
- ? `The bounded packet is not independently reviewable (${JSON.stringify(roleContext.coverage)}). Perform a same-turn read-only check and do not claim independent acceptance.`
2358
- : "Retain the current controller conversation and workspace context; do not reconstruct it through a child handoff.";
2359
- return {
2360
- kind: "enter",
2361
- messages: [
2362
- ...routedDownstream.messages,
2363
- pluginMessage(
2364
- [
2365
- renderRouteNotice(decision, config.routing.mode, roleRoute),
2366
- "",
2367
- `Context digest: sha256:${roleContext.digest}`,
2368
- contextBoundary,
2369
- "",
2370
- `${routeRole} responsibility contract:`,
2371
- roleContract,
2372
- ...(routeRole === "executor" && frozenCard
2373
- ? ["", "Frozen route card:", JSON.stringify(frozenCard, null, 2)]
2374
- : []),
2375
- ].join("\n"),
2376
- `odai upgraded controller route (${decision.reasonCode})`,
2377
- ),
2378
- ],
2379
- };
2380
- }
2381
-
2382
- const delegationDecision = decision.role === routeRole
2383
- ? decision
2384
- : Object.freeze({ ...decision, role: routeRole, mode: "delegate", action: "delegate" });
2385
- const result = ctx.subagents?.start
2386
- ? await runRoutedRole({
2387
- subagents: ctx.subagents,
2388
- provider: config.routing.provider,
2389
- decision: delegationDecision,
2390
- taskText: renderRoleContextPacket(roleContext),
2391
- roleContract,
2392
- agent,
2393
- signal,
2394
- roleRoute,
2395
- })
2396
- : Object.freeze({
2397
- status: "fallback",
2398
- stopReason: "infrastructure-error",
2399
- output: [],
2400
- error: "dsh subagents service unavailable",
2401
- });
2402
- appendEvent(agent, "odai/route-result", {
2403
- turn,
2404
- step,
2405
- role: routeRole,
2406
- action: "delegate",
2407
- status: result.status,
2408
- stopReason: result.stopReason,
2409
- routeSource: roleState.source,
2410
- fallbackUsed: result.status !== "completed",
2411
- routeReceiptStatus: result.routeReceiptStatus,
2412
- requestedRoute: roleRoute,
2413
- contextDigest: roleContext.digest,
2414
- ...(result.routeReceiptError ? { routeReceiptError: result.routeReceiptError } : {}),
2415
- ...(result.actualRoute ? { actualRoute: result.actualRoute } : {}),
2416
- ...(result.taskError ? { error: result.taskError } : {}),
2417
- }, logger);
2418
-
2419
- if (result.status === "completed") {
2420
- const childText = outputText(result.output);
2421
- const heading = renderRouteNotice(delegationDecision, config.routing.mode, result.actualRoute);
2422
- return {
2423
- kind: "enter",
2424
- messages: [
2425
- ...routedDownstream.messages,
2426
- pluginMessage(
2427
- childText ? `${heading}\ncontext digest: sha256:${roleContext.digest}\n\n${routeRole} output:\n${childText}` : heading,
2428
- `odai completed ${routeRole} route`,
2429
- result.output.filter((block) => block?.type !== "text"),
2430
- ),
2431
- ],
2432
- };
2433
- }
2434
-
2435
- const failure = result.error ?? result.stopReason;
2436
- if (requiresFailClosedProtection(delegationDecision)) {
2437
- protectController(agent, turn, step, delegationDecision, "route-failure", failure);
2438
- }
2439
- return {
2440
- kind: "enter",
2441
- messages: [
2442
- ...routedDownstream.messages,
2443
- pluginMessage(
2444
- renderRouteFailureNotice(delegationDecision, failure),
2445
- requiresFailClosedProtection(delegationDecision)
2446
- ? `odai blocked high-impact ${routeRole} fallback`
2447
- : `odai fell back from ${routeRole} route`,
2448
- ),
2449
- ],
2450
- };
2451
- }, { prepend: true });
2452
- }
2453
-
2454
- logger.info(`loaded canonical governance ${bundled.manifest.skillVersion} from ${skillPath}; skillSource=${explicitSkillPath ? "path" : config.governance.skillSource}; routing=${config.routing.mode}`);
2455
- }
2456
-
2457
- function isSubagentSession(agent) {
2458
- const header = agent?.session?.header;
2459
- return header?.origin === "subagent"
2460
- || (Number.isSafeInteger(header?.delegationDepth) && header.delegationDepth > 0);
166
+ return invalidation;
167
+ };
168
+ installToolRuntime({
169
+ appendEvent, baseSelection, bundled, config, ctx, evidence, evolutionDisabled, explicitSkillPath,
170
+ hasSessionEvent, humanSafetyContinuityStorePath, logger, pendingResponsibilityGap, promptRuntime,
171
+ routeProtections, selectOutputForAgent,
172
+ });
173
+ installLifecycleRuntime({
174
+ appendEvent, bundled, config, configuredRole, ctx, evidence, hasSessionEvent, invalidateFailedRoleRoute,
175
+ logger, memorySettingsFor, outputUsageBySession, pendingResponsibilityGap, pendingRouteReceipts,
176
+ pendingScopeRestorations, responsibilityScopeOwners, responsibilityScopes, routeProtections,
177
+ selectOutputForAgent, stopDanglingResponsibilityScope, stopResponsibilityScope,
178
+ });
179
+ logger.info(`loaded canonical governance ${bundled.manifest.skillVersion} from ${skillPath}; skillSource=${explicitSkillPath ? "path" : config.governance.skillSource}; routing=${config.routing.mode}`);
2461
180
  }
181
+ //# sourceMappingURL=index.mjs.map