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
@@ -0,0 +1,1081 @@
1
+ import { classifyResponsibilityInterruptionText, decideResearchPrefetch, decideRoute, extractLatestUserText, extractRoutingText, renderMissingRouteConfigNotice, renderRouteFailureNotice, renderRouteNotice, requiresFailClosedProtection, } from "./router.mjs";
2
+ import { classifyModelRouteFailure, probeModelRoute } from "./model-route.mjs";
3
+ import { selectSharedOutputPolicyForTurn } from "./output-policy-state.mjs";
4
+ import { isSubagentSession, outputText, pluginMessage, renderOutputLimitInterruptionNotice, renderResearchTaskContract, routeFromConfig, routeMismatchFor, routedRoleOf, runRoutedRole, sameRequestModelRoute, } from "./runtime-support.mjs";
5
+ import { claimResponsibilityScope, createResponsibilityScope, latestStoppedResponsibilityScope, pendingResponsibilityInterruption, pendingResponsibilityScopeRestoration, responsibilityScopeClaimedEvent, responsibilityScopeOwnsRequest, responsibilityScopeStartedEvent, responsibilityScopeStopReason, } from "./responsibility-scope.mjs";
6
+ import { activeRouteCard, routeCardById, } from "./route-card.mjs";
7
+ import { buildRoleContextPacket, renderRoleContextPacket } from "./routing-context.mjs";
8
+ import { parseResearchPacket, renderResearchPacket, verifyResearchPacketSources, } from "./research-packet.mjs";
9
+ import { dshRoleContract } from "./role-overlays.mjs";
10
+ import { sharedSkillSelection } from "./skill-selection-state.mjs";
11
+ import { captureAutomaticMemories, claimSemanticMemoryTurn, latestDirectUserMessage, memoryPacketMessage, renderSemanticMemoryPacket, retrieveSemanticMemories, } from "./semantic-memory.mjs";
12
+ function isInPlaceResponsibility(value) {
13
+ return value === "planner" || value === "reviewer" || value === "executor" || value === "frontend";
14
+ }
15
+ function isSubagentsService(value) {
16
+ return value !== null && typeof value === "object" && "start" in value && typeof value.start === "function";
17
+ }
18
+ export function installLifecycleRuntime(deps) {
19
+ const { appendEvent, bundled, config, configuredRole, ctx, evidence, hasSessionEvent, invalidateFailedRoleRoute, logger, memorySettingsFor, outputUsageBySession, pendingResponsibilityGap, pendingRouteReceipts, pendingScopeRestorations, responsibilityScopeOwners, responsibilityScopes, routeProtections, selectOutputForAgent, stopDanglingResponsibilityScope, stopResponsibilityScope } = deps;
20
+ ctx.on("agent/request", async ({ agent, turn, step, signal }, next) => {
21
+ let proposed = await next();
22
+ const childRole = routedRoleOf(agent);
23
+ const childRoleState = childRole ? configuredRole(agent, childRole, turn) : undefined;
24
+ if (childRole && !childRoleState?.route) {
25
+ throw new Error(childRoleState?.error
26
+ ? `Odai ${childRole} child route is unavailable: ${childRoleState.detail}`
27
+ : `Odai ${childRole} child route is not configured`);
28
+ }
29
+ let scope = responsibilityScopes.get(agent);
30
+ if (scope && !responsibilityScopeOwnsRequest(scope, turn, step)) {
31
+ stopResponsibilityScope(agent, "ownership-boundary", { step });
32
+ scope = undefined;
33
+ }
34
+ if (!childRole && !scope) {
35
+ const restoration = pendingResponsibilityScopeRestoration(evidence.events(agent));
36
+ if (restoration && sameRequestModelRoute(proposed, restoration.temporaryRoute)) {
37
+ const { reasoningEffort: _temporaryEffort, maxTokens: _temporaryMaxTokens, ...withoutTemporaryRoute } = proposed;
38
+ proposed = Object.freeze({ ...withoutTemporaryRoute, ...restoration.baseRoute });
39
+ appendEvent(agent, "odai/responsibility-scope-restoration-requested", {
40
+ scopeId: restoration.scopeId,
41
+ turn,
42
+ step,
43
+ role: restoration.role,
44
+ requestedRoute: restoration.baseRoute,
45
+ });
46
+ if (agent?.session) {
47
+ pendingScopeRestorations.set(agent.session, Object.freeze({
48
+ agent,
49
+ scopeId: restoration.scopeId,
50
+ turn,
51
+ step,
52
+ role: restoration.role,
53
+ expectedRoute: restoration.baseRoute,
54
+ }));
55
+ }
56
+ }
57
+ }
58
+ const upgradeRole = scope?.role;
59
+ let roleRoute = childRole
60
+ ? childRoleState?.route
61
+ : scope
62
+ ? scope.route
63
+ : undefined;
64
+ const routeSource = childRole ? childRoleState?.source : scope?.source;
65
+ let routeMode = childRole ? "child" : sameRequestModelRoute(proposed, roleRoute) ? "inline" : "same-turn";
66
+ let scopedResponsibilityMaxTokens = upgradeRole ? roleRoute?.maxTokens : undefined;
67
+ const finalize = (finalRequest) => {
68
+ if (roleRoute && agent?.session && Number.isSafeInteger(turn) && Number.isSafeInteger(step)) {
69
+ const expectedRoute = roleRoute;
70
+ const receiptScope = responsibilityScopeOwnsRequest(responsibilityScopes.get(agent), turn, step)
71
+ ? responsibilityScopes.get(agent)
72
+ : scope;
73
+ const responsibility = childRole ?? upgradeRole;
74
+ if (!responsibility)
75
+ throw new Error("routed request is missing its responsibility");
76
+ pendingRouteReceipts.set(agent.session, Object.freeze({
77
+ agent,
78
+ turn,
79
+ step,
80
+ responsibility,
81
+ routeMode,
82
+ routeSource,
83
+ ...(receiptScope?.id ? { responsibilityScopeId: receiptScope.id } : {}),
84
+ ...(receiptScope?.cardId ? { routeCardId: receiptScope.cardId } : {}),
85
+ ...(receiptScope?.resumeOfScopeId ? { resumeOfScopeId: receiptScope.resumeOfScopeId } : {}),
86
+ requestedRoute: expectedRoute,
87
+ expectedRoute,
88
+ }));
89
+ }
90
+ return finalRequest;
91
+ };
92
+ let request = proposed;
93
+ if (roleRoute) {
94
+ const { reasoningEffort: _inheritedEffort, ...withoutInheritedEffort } = proposed;
95
+ request = Object.freeze({
96
+ ...withoutInheritedEffort,
97
+ provider: roleRoute.provider,
98
+ model: roleRoute.model,
99
+ ...(roleRoute.reasoningEffort === undefined ? {} : { reasoningEffort: roleRoute.reasoningEffort }),
100
+ ...((childRole || scopedResponsibilityMaxTokens !== undefined) && roleRoute.maxTokens !== undefined
101
+ ? { maxTokens: roleRoute.maxTokens }
102
+ : {}),
103
+ });
104
+ if (!childRole && scope?.state === "pending") {
105
+ const baseRoute = routeFromConfig(proposed);
106
+ const temporaryRoute = routeFromConfig(request);
107
+ if (!baseRoute || !temporaryRoute)
108
+ throw new Error("responsibility scope claim requires complete request routes");
109
+ scope = claimResponsibilityScope(scope, {
110
+ step,
111
+ baseRoute,
112
+ temporaryRoute,
113
+ routeMode,
114
+ });
115
+ responsibilityScopes.set(agent, scope);
116
+ appendEvent(agent, "odai/responsibility-scope-claimed", responsibilityScopeClaimedEvent(scope));
117
+ }
118
+ const validation = scope?.routeValidated
119
+ ? Object.freeze({ status: "verified" })
120
+ : await probeModelRoute((candidate, candidateSignal) => ctx.llm.resolveCallConfig(candidate, candidateSignal), routeFromConfig(request) ?? roleRoute, signal);
121
+ if (validation.status === "rejected") {
122
+ const responsibility = childRole ?? upgradeRole;
123
+ if (!responsibility)
124
+ throw new Error("route validation failure is missing its responsibility");
125
+ const invalidation = invalidateFailedRoleRoute(agent, responsibility, roleRoute, routeSource, validation.failure, { turn, step });
126
+ appendEvent(agent, "odai/route-fallback", {
127
+ turn,
128
+ step,
129
+ responsibility,
130
+ ...(scope?.id ? { responsibilityScopeId: scope.id } : {}),
131
+ ...(scope?.cardId ? { routeCardId: scope.cardId } : {}),
132
+ routeMode,
133
+ routeSource,
134
+ requestedRoute: roleRoute,
135
+ fallbackUsed: true,
136
+ fallbackRoute: routeFromConfig(proposed),
137
+ failureKind: validation.failure.kind,
138
+ errorCode: validation.failure.code,
139
+ error: validation.failure.message,
140
+ invalidated: invalidation.invalidated,
141
+ });
142
+ if (scope?.cardId) {
143
+ appendEvent(agent, "odai/route-card-claim-released", {
144
+ cardId: scope.cardId,
145
+ turn,
146
+ step,
147
+ reason: "route-validation-failed",
148
+ });
149
+ }
150
+ if (childRole) {
151
+ const error = new Error(`Odai ${childRole} route failed validation: ${validation.failure.code}: ${validation.failure.message}`);
152
+ error.code = validation.failure.code;
153
+ error.routeFailureKind = validation.failure.kind;
154
+ throw error;
155
+ }
156
+ stopResponsibilityScope(agent, "route-validation-failed", { step });
157
+ if (scope?.decision && requiresFailClosedProtection(scope.decision)) {
158
+ protectController(agent, turn, step, scope.decision, "route-validation", validation.failure.message);
159
+ }
160
+ roleRoute = undefined;
161
+ routeMode = "same-turn";
162
+ scopedResponsibilityMaxTokens = undefined;
163
+ request = proposed;
164
+ }
165
+ }
166
+ if (childRole || isSubagentSession(agent))
167
+ return finalize(request);
168
+ const outputSelection = await selectSharedOutputPolicyForTurn(agent, turn, selectOutputForAgent);
169
+ const configuredMaxTokens = outputSelection.policy.maxTokens;
170
+ if (scopedResponsibilityMaxTokens !== undefined) {
171
+ if (!hasSessionEvent(agent, "odai/output-budget-overridden", (data) => data?.turn === turn && data?.step === step)) {
172
+ appendEvent(agent, "odai/output-budget-overridden", {
173
+ turn,
174
+ ...(step === undefined ? {} : { step }),
175
+ responsibility: upgradeRole,
176
+ responsibilityMaxTokens: scopedResponsibilityMaxTokens,
177
+ ...(configuredMaxTokens === undefined ? {} : { configuredControllerMaxTokens: configuredMaxTokens }),
178
+ effectiveMaxTokens: scopedResponsibilityMaxTokens,
179
+ budgetSource: "responsibility-override",
180
+ semantics: "explicit-responsibility-override",
181
+ });
182
+ }
183
+ return finalize(Object.freeze({ ...request, maxTokens: scopedResponsibilityMaxTokens }));
184
+ }
185
+ if (configuredMaxTokens === undefined)
186
+ return finalize(request);
187
+ const priorMaxTokens = request.maxTokens;
188
+ const effectiveMaxTokens = priorMaxTokens === undefined
189
+ ? configuredMaxTokens
190
+ : Math.min(priorMaxTokens, configuredMaxTokens);
191
+ const budgetSource = priorMaxTokens !== undefined && priorMaxTokens < configuredMaxTokens
192
+ ? "preexisting-request-ceiling"
193
+ : "controller-policy";
194
+ if (!hasSessionEvent(agent, "odai/output-budget-applied", (data) => data?.turn === turn && data?.step === step)) {
195
+ appendEvent(agent, "odai/output-budget-applied", {
196
+ turn,
197
+ ...(step === undefined ? {} : { step }),
198
+ ...(upgradeRole === undefined ? {} : { responsibility: upgradeRole }),
199
+ configuredMaxTokens,
200
+ ...(priorMaxTokens === undefined ? {} : { priorMaxTokens }),
201
+ effectiveMaxTokens,
202
+ budgetSource,
203
+ semantics: "provider-request-ceiling",
204
+ });
205
+ }
206
+ return finalize(Object.freeze({ ...request, maxTokens: effectiveMaxTokens }));
207
+ }, { prepend: true });
208
+ const routeFallbackAttempts = new WeakMap();
209
+ ctx.on("agent/request-error", async ({ agent, turn, step, provider, failure, signal }, next) => {
210
+ const childRole = routedRoleOf(agent);
211
+ const scope = responsibilityScopes.get(agent);
212
+ if (!childRole && scope && (signal.aborted || failure?.code === "CONTEXT_WINDOW_EXCEEDED")) {
213
+ if (agent?.session)
214
+ pendingRouteReceipts.delete(agent.session);
215
+ stopResponsibilityScope(agent, signal.aborted ? "request-aborted" : "context-window-exceeded", { step });
216
+ return next();
217
+ }
218
+ if (signal.aborted || failure?.code === "CONTEXT_WINDOW_EXCEEDED")
219
+ return next();
220
+ const active = childRole
221
+ ? { role: childRole, ...configuredRole(agent, childRole, turn) }
222
+ : responsibilityScopeOwnsRequest(scope, turn, step)
223
+ ? scope
224
+ : undefined;
225
+ if (!active?.route || provider !== active.route.provider)
226
+ return next();
227
+ const classified = classifyModelRouteFailure(failure);
228
+ const invalidation = invalidateFailedRoleRoute(agent, active.role, active.route, active.source, classified, { turn, step });
229
+ appendEvent(agent, "odai/route-fallback", {
230
+ turn,
231
+ step,
232
+ responsibility: active.role,
233
+ ...(active.id ? { responsibilityScopeId: active.id } : {}),
234
+ ...(active.cardId ? { routeCardId: active.cardId } : {}),
235
+ routeMode: childRole ? "child" : "same-turn",
236
+ routeSource: active.source,
237
+ requestedRoute: active.route,
238
+ fallbackUsed: !childRole,
239
+ fallbackRoute: childRole ? undefined : active.baseRoute ?? routeFromConfig(agent?.options),
240
+ failureKind: classified.kind,
241
+ errorCode: classified.code,
242
+ error: classified.message,
243
+ invalidated: invalidation.invalidated,
244
+ });
245
+ if (active.cardId) {
246
+ appendEvent(agent, "odai/route-card-claim-released", {
247
+ cardId: active.cardId,
248
+ turn,
249
+ step,
250
+ reason: "route-request-failed",
251
+ });
252
+ }
253
+ if (childRole)
254
+ return next();
255
+ if (agent?.session)
256
+ pendingRouteReceipts.delete(agent.session);
257
+ stopResponsibilityScope(agent, classified.kind === "cancelled" ? "request-cancelled" : "route-request-failed", { step });
258
+ if (scope?.decision && requiresFailClosedProtection(scope.decision)) {
259
+ protectController(agent, turn, step, scope.decision, "route-request-failure", classified.message);
260
+ }
261
+ if (classified.kind === "cancelled")
262
+ return next();
263
+ let attempts = routeFallbackAttempts.get(agent);
264
+ if (!attempts) {
265
+ attempts = new Set();
266
+ routeFallbackAttempts.set(agent, attempts);
267
+ }
268
+ const key = `${turn}:${step}`;
269
+ if (attempts.has(key))
270
+ return next();
271
+ attempts.add(key);
272
+ return { kind: "retry" };
273
+ });
274
+ const protectController = (agent, turn, step, decision, source, failure = undefined, scopeId = undefined) => {
275
+ const protection = Object.freeze({
276
+ turn,
277
+ step,
278
+ mode: "read-only",
279
+ reasonCode: decision.reasonCode,
280
+ source,
281
+ ...(failure ? { failure } : {}),
282
+ ...(scopeId ? { scopeId } : {}),
283
+ });
284
+ routeProtections.set(agent, protection);
285
+ appendEvent(agent, "odai/route-protection", protection);
286
+ };
287
+ ctx.on("session/event", (session, event) => {
288
+ const owner = responsibilityScopeOwners.get(session);
289
+ const activeScope = owner ? responsibilityScopes.get(owner) : undefined;
290
+ const eventMatchesRequestPosition = (position) => Number.isSafeInteger(event.data.turn)
291
+ && Number.isSafeInteger(event?.data?.step)
292
+ && event.data.turn === position?.turn
293
+ && event.data.step === position?.step;
294
+ const scopeStopReason = responsibilityScopeStopReason(activeScope, event);
295
+ if (scopeStopReason && owner && activeScope) {
296
+ stopResponsibilityScope(owner, scopeStopReason, {
297
+ scopeId: activeScope.id,
298
+ ...(Number.isSafeInteger(event.data?.step) ? { step: event.data.step } : {}),
299
+ });
300
+ }
301
+ if (event?.type === "assistant/chunk" && event.data?.chunk?.type === "usage") {
302
+ outputUsageBySession.set(session, {
303
+ turn: event.data.turn,
304
+ step: event.data.step,
305
+ usage: event.data.chunk.usage,
306
+ });
307
+ }
308
+ else if (event?.type === "assistant/message" && event.data?.usage) {
309
+ outputUsageBySession.set(session, {
310
+ turn: event.data.turn,
311
+ step: event.data.step,
312
+ usage: event.data.usage,
313
+ });
314
+ }
315
+ if (event.type === "turn/end") {
316
+ const usage = outputUsageBySession.get(session);
317
+ const finishReason = event.data.reason;
318
+ if (typeof finishReason === "object" && finishReason?.kind === "max-tokens" && owner) {
319
+ const events = evidence.events(owner);
320
+ const stopped = latestStoppedResponsibilityScope(events, event.data?.turn);
321
+ const receipt = stopped && events.findLast((candidate) => (candidate?.type === "odai/route-applied"
322
+ && candidate.data?.status === "applied"
323
+ && candidate.data?.responsibilityScopeId === stopped.scopeId))?.data;
324
+ const routeCard = stopped?.routeCardId ? routeCardById(events, stopped.routeCardId) : undefined;
325
+ const observedOutputTokens = usage?.turn === stopped?.turn
326
+ && usage?.step === stopped?.stopStep
327
+ && Number.isSafeInteger(usage?.usage?.outputTokens)
328
+ ? usage?.usage?.outputTokens
329
+ : undefined;
330
+ if (stopped?.reason === "terminal-response"
331
+ && Number.isSafeInteger(stopped.stopStep)
332
+ && receipt?.step === stopped.stopStep
333
+ && typeof observedOutputTokens === "number"
334
+ && Number.isSafeInteger(observedOutputTokens)
335
+ && isInPlaceResponsibility(receipt?.responsibility)
336
+ && receipt.responsibility !== "reviewer"
337
+ && receipt.requestedRoute
338
+ && (receipt.responsibility !== "executor" || routeCard)) {
339
+ const effectiveRoute = receipt.actualRoute ?? receipt.requestedRoute;
340
+ appendEvent(owner, "odai/responsibility-interrupted", {
341
+ scopeId: stopped.scopeId,
342
+ turn: stopped.turn,
343
+ step: stopped.stopStep ?? stopped.startStep,
344
+ responsibility: receipt.responsibility,
345
+ reason: "max-tokens",
346
+ routeMode: receipt.routeMode,
347
+ routeSource: receipt.routeSource,
348
+ requestedRoute: receipt.requestedRoute,
349
+ ...(effectiveRoute ? { effectiveRoute } : {}),
350
+ ...(effectiveRoute?.maxTokens === undefined ? {} : { effectiveMaxTokens: effectiveRoute.maxTokens }),
351
+ outputTokens: observedOutputTokens,
352
+ ...(stopped.routeCardId ? { routeCardId: stopped.routeCardId } : {}),
353
+ ...(routeCard ? { routeCard } : {}),
354
+ });
355
+ }
356
+ }
357
+ outputUsageBySession.delete(session);
358
+ }
359
+ const pendingRestoration = pendingScopeRestorations.get(session);
360
+ if (pendingRestoration && event?.type === "turn/end" && event.data?.turn === pendingRestoration.turn) {
361
+ appendEvent(pendingRestoration.agent, "odai/responsibility-scope-restored", {
362
+ scopeId: pendingRestoration.scopeId,
363
+ turn: pendingRestoration.turn,
364
+ step: pendingRestoration.step,
365
+ role: pendingRestoration.role,
366
+ status: "unverified",
367
+ requestedRoute: pendingRestoration.expectedRoute,
368
+ stopReason: "no-effective-request",
369
+ });
370
+ pendingScopeRestorations.delete(session);
371
+ }
372
+ if (pendingRestoration
373
+ && ["request/header", "assistant/chunk", "assistant/message"].includes(event?.type)
374
+ && eventMatchesRequestPosition(pendingRestoration)) {
375
+ let actualRoute;
376
+ try {
377
+ actualRoute = event.type === "request/header"
378
+ ? routeFromConfig(event.data?.header?.config)
379
+ : routeFromConfig(session.requestHeader?.()?.config);
380
+ }
381
+ catch { }
382
+ if (actualRoute || event.type !== "request/header") {
383
+ const mismatch = routeMismatchFor(pendingRestoration.expectedRoute, actualRoute, "base-route restoration");
384
+ appendEvent(pendingRestoration.agent, "odai/responsibility-scope-restored", {
385
+ scopeId: pendingRestoration.scopeId,
386
+ turn: pendingRestoration.turn,
387
+ step: pendingRestoration.step,
388
+ role: pendingRestoration.role,
389
+ status: mismatch ? "mismatch" : "applied",
390
+ requestedRoute: pendingRestoration.expectedRoute,
391
+ ...(actualRoute ? { actualRoute } : {}),
392
+ ...(mismatch ? { error: mismatch } : {}),
393
+ });
394
+ if (mismatch) {
395
+ protectController(pendingRestoration.agent, pendingRestoration.turn, pendingRestoration.step, { reasonCode: "RESPONSIBILITY_BASE_ROUTE_RESTORATION_MISMATCH" }, "scope-restoration-mismatch", mismatch, pendingRestoration.scopeId);
396
+ }
397
+ pendingScopeRestorations.delete(session);
398
+ }
399
+ }
400
+ const pending = pendingRouteReceipts.get(session);
401
+ if (!pending)
402
+ return;
403
+ if (event?.type === "turn/end" && event.data?.turn === pending.turn) {
404
+ appendEvent(pending.agent, "odai/route-applied", {
405
+ turn: pending.turn,
406
+ step: pending.step,
407
+ responsibility: pending.responsibility,
408
+ ...(pending.responsibilityScopeId ? { responsibilityScopeId: pending.responsibilityScopeId } : {}),
409
+ ...(pending.routeCardId ? { routeCardId: pending.routeCardId } : {}),
410
+ status: "unverified",
411
+ routeMode: pending.routeMode,
412
+ routeSource: pending.routeSource,
413
+ fallbackUsed: true,
414
+ requestedRoute: pending.requestedRoute,
415
+ stopReason: "no-effective-request",
416
+ });
417
+ if (pending.routeCardId) {
418
+ appendEvent(pending.agent, "odai/route-card-claim-released", {
419
+ cardId: pending.routeCardId,
420
+ turn: pending.turn,
421
+ step: pending.step,
422
+ reason: "no-effective-request",
423
+ });
424
+ }
425
+ stopResponsibilityScope(pending.agent, "no-effective-request", {
426
+ scopeId: pending.responsibilityScopeId,
427
+ step: pending.step,
428
+ });
429
+ pendingRouteReceipts.delete(session);
430
+ return;
431
+ }
432
+ if (!["request/header", "assistant/chunk", "assistant/message"].includes(event?.type))
433
+ return;
434
+ if (!eventMatchesRequestPosition(pending))
435
+ return;
436
+ let actualRoute;
437
+ try {
438
+ actualRoute = event.type === "request/header"
439
+ ? routeFromConfig(event.data?.header?.config)
440
+ : routeFromConfig(session.requestHeader?.()?.config);
441
+ }
442
+ catch { }
443
+ if (!actualRoute && event.type === "request/header")
444
+ return;
445
+ const mismatch = routeMismatchFor(pending.expectedRoute, actualRoute, pending.routeMode);
446
+ appendEvent(pending.agent, "odai/route-applied", {
447
+ turn: pending.turn,
448
+ step: pending.step,
449
+ responsibility: pending.responsibility,
450
+ ...(pending.responsibilityScopeId ? { responsibilityScopeId: pending.responsibilityScopeId } : {}),
451
+ ...(pending.routeCardId ? { routeCardId: pending.routeCardId } : {}),
452
+ status: mismatch ? "mismatch" : "applied",
453
+ routeMode: pending.routeMode,
454
+ routeSource: pending.routeSource,
455
+ fallbackUsed: Boolean(mismatch),
456
+ requestedRoute: pending.requestedRoute,
457
+ ...(actualRoute ? { actualRoute } : {}),
458
+ ...(mismatch ? { stopReason: "route-mismatch", error: mismatch } : {}),
459
+ });
460
+ if (mismatch && pending.routeMode === "same-turn") {
461
+ stopResponsibilityScope(pending.agent, "route-mismatch", {
462
+ scopeId: pending.responsibilityScopeId,
463
+ step: pending.step,
464
+ });
465
+ }
466
+ if (!mismatch && pending.resumeOfScopeId) {
467
+ appendEvent(pending.agent, "odai/responsibility-interruption-consumed", {
468
+ scopeId: pending.resumeOfScopeId,
469
+ turn: pending.turn,
470
+ step: pending.step,
471
+ responsibility: pending.responsibility,
472
+ resumedScopeId: pending.responsibilityScopeId,
473
+ });
474
+ }
475
+ if (pending.routeCardId) {
476
+ appendEvent(pending.agent, mismatch ? "odai/route-card-claim-released" : "odai/route-card-consumed", {
477
+ cardId: pending.routeCardId,
478
+ turn: pending.turn,
479
+ step: pending.step,
480
+ ...(mismatch ? { reason: "route-mismatch" } : { receiptStatus: "applied" }),
481
+ });
482
+ }
483
+ pendingRouteReceipts.delete(session);
484
+ if (mismatch && pending.routeMode === "same-turn") {
485
+ protectController(pending.agent, pending.turn, pending.step, { reasonCode: `${pending.responsibility.toUpperCase()}_ROUTE_MISMATCH` }, "route-mismatch", mismatch);
486
+ }
487
+ });
488
+ ctx.on("agent/turn-stopping", ({ agent, turn }) => {
489
+ stopResponsibilityScope(agent, "turn-stopping");
490
+ const role = routedRoleOf(agent);
491
+ if (!role)
492
+ return;
493
+ const receipts = evidence.events(agent)
494
+ .filter((event) => event.type === "odai/route-applied"
495
+ && event.data?.turn === turn
496
+ && event.data?.responsibility === role
497
+ && event.data?.routeMode === "child")
498
+ .map((event) => event.data);
499
+ const failed = receipts.find((receipt) => receipt.status !== "applied");
500
+ if (receipts.length > 0 && !failed)
501
+ return;
502
+ const detail = failed?.error ?? failed?.stopReason ?? "no verified child route receipt";
503
+ throw new Error(`Odai ${role} child route was not verified: ${detail}`);
504
+ });
505
+ {
506
+ const routedSteps = new WeakMap();
507
+ ctx.on("agent/pre-step", async ({ agent, turn, step, signal }, next) => {
508
+ const subagentSession = isSubagentSession(agent);
509
+ if (!subagentSession) {
510
+ if (step === 1) {
511
+ stopResponsibilityScope(agent, "new-turn");
512
+ routeProtections.delete(agent);
513
+ }
514
+ stopDanglingResponsibilityScope(agent, "runtime-resume");
515
+ }
516
+ let downstream = await next();
517
+ if (downstream.kind === "reject" || signal.aborted)
518
+ return downstream;
519
+ const responsibilityGap = subagentSession ? undefined : pendingResponsibilityGap(agent, turn, step);
520
+ const authenticatedDirectMessage = latestDirectUserMessage(agent, undefined, { turn });
521
+ const suppliedDirectMessages = Array.isArray(downstream.messages)
522
+ ? downstream.messages.filter((message) => message?.role === "user" && message?.source?.kind === "user")
523
+ : [];
524
+ const directMessage = latestDirectUserMessage(agent, suppliedDirectMessages, { turn });
525
+ const authenticatedDirectText = authenticatedDirectMessage
526
+ ? extractLatestUserText([authenticatedDirectMessage])
527
+ : "";
528
+ const responsibilityEvents = subagentSession ? [] : evidence.events(agent);
529
+ const interruption = !subagentSession && step === 1
530
+ ? pendingResponsibilityInterruption(responsibilityEvents)
531
+ : undefined;
532
+ let responsibilityContinuation;
533
+ let interruptionNotice;
534
+ if (interruption && authenticatedDirectMessage) {
535
+ const disposition = classifyResponsibilityInterruptionText(authenticatedDirectText);
536
+ if (disposition === "continue" && directMessage) {
537
+ responsibilityContinuation = Object.freeze({ ...interruption, continuationText: authenticatedDirectText });
538
+ appendEvent(agent, "odai/responsibility-interruption-resume-requested", {
539
+ scopeId: interruption.scopeId,
540
+ turn,
541
+ step,
542
+ responsibility: interruption.responsibility,
543
+ });
544
+ }
545
+ else if (disposition === "preserve") {
546
+ if (directMessage) {
547
+ interruptionNotice = pluginMessage(renderOutputLimitInterruptionNotice(interruption), `odai verified ${interruption.responsibility} output-limit interruption`);
548
+ }
549
+ appendEvent(agent, "odai/responsibility-interruption-preserved", {
550
+ scopeId: interruption.scopeId,
551
+ turn,
552
+ step,
553
+ responsibility: interruption.responsibility,
554
+ reason: "output-limit-diagnostic",
555
+ });
556
+ }
557
+ else if (disposition === "clear") {
558
+ appendEvent(agent, "odai/responsibility-interruption-cleared", {
559
+ scopeId: interruption.scopeId,
560
+ turn,
561
+ step,
562
+ responsibility: interruption.responsibility,
563
+ reason: "superseded-by-user-task",
564
+ });
565
+ }
566
+ }
567
+ if (interruptionNotice) {
568
+ downstream = { ...downstream, messages: [...downstream.messages, interruptionNotice] };
569
+ }
570
+ if (subagentSession)
571
+ return downstream;
572
+ if (step !== 1 && !responsibilityGap)
573
+ return downstream;
574
+ if (step === 1 && claimSemanticMemoryTurn(agent, turn, step)) {
575
+ const settings = memorySettingsFor(agent, turn);
576
+ const message = directMessage;
577
+ const query = extractRoutingText(downstream.messages, agent?.session?.events).slice(0, config.routing.maxInputChars);
578
+ let retrieved = [];
579
+ let captured = [];
580
+ let error;
581
+ if (settings.mode === "auto" && message) {
582
+ try {
583
+ retrieved = retrieveSemanticMemories({
584
+ storePath: config.memory.storePath,
585
+ query,
586
+ cwd: agent?.session?.header?.cwd,
587
+ limit: config.memory.maxRetrieved,
588
+ });
589
+ captured = captureAutomaticMemories({
590
+ storePath: config.memory.storePath,
591
+ mode: settings.mode,
592
+ agent,
593
+ message,
594
+ turn,
595
+ cwd: agent?.session?.header?.cwd,
596
+ });
597
+ }
598
+ catch (memoryError) {
599
+ error = memoryError instanceof Error ? memoryError.message : String(memoryError);
600
+ logger.warn(`Odai semantic memory processing failed closed for this turn: ${error}`);
601
+ retrieved = [];
602
+ captured = [];
603
+ }
604
+ }
605
+ const captureEvidence = captured.filter((result) => result.changed);
606
+ if (retrieved.length > 0 || captureEvidence.length > 0 || error) {
607
+ appendEvent(agent, "odai/memory-processed", {
608
+ turn,
609
+ step,
610
+ mode: settings.mode,
611
+ source: settings.source,
612
+ retrievedIds: retrieved.map((entry) => entry.id),
613
+ captures: captureEvidence.map((result) => ({
614
+ changed: true,
615
+ reasonCode: result.reasonCode,
616
+ ...(result.id ? { id: result.id } : {}),
617
+ ...(result.status ? { status: result.status } : {}),
618
+ ...(result.scope ? { scope: result.scope } : {}),
619
+ })),
620
+ ...(error ? { status: "fallback", error: "memory-store-unavailable" } : { status: "completed" }),
621
+ });
622
+ }
623
+ const packet = renderSemanticMemoryPacket(retrieved);
624
+ if (packet) {
625
+ downstream = {
626
+ ...downstream,
627
+ messages: [...downstream.messages, memoryPacketMessage(packet)],
628
+ };
629
+ }
630
+ }
631
+ if (config.routing.mode === "off")
632
+ return downstream;
633
+ if (hasSessionEvent(agent, "odai/route-decided", (data) => data?.turn === turn && data?.step === step)) {
634
+ return downstream;
635
+ }
636
+ let routed = routedSteps.get(agent);
637
+ if (!routed) {
638
+ routed = new Set();
639
+ routedSteps.set(agent, routed);
640
+ }
641
+ const routeKey = `${turn}:${step}`;
642
+ if (routed.has(routeKey))
643
+ return downstream;
644
+ routed.add(routeKey);
645
+ const taskText = extractRoutingText(downstream.messages, agent?.session?.events).slice(0, config.routing.maxInputChars);
646
+ let routedDownstream = downstream;
647
+ let researchPacketText = "";
648
+ const researchDecision = decideResearchPrefetch({ text: taskText, proposal: responsibilityGap });
649
+ if (researchDecision.action === "delegate") {
650
+ appendEvent(agent, "odai/research-decided", {
651
+ turn,
652
+ step,
653
+ role: "researcher",
654
+ action: "delegate",
655
+ mode: config.routing.mode,
656
+ reasonCode: researchDecision.reasonCode,
657
+ signals: researchDecision.signals,
658
+ });
659
+ if (config.routing.mode === "observe") {
660
+ appendEvent(agent, "odai/research-result", {
661
+ turn,
662
+ step,
663
+ role: "researcher",
664
+ status: "observed",
665
+ stopReason: "observe-mode",
666
+ });
667
+ }
668
+ else {
669
+ const researchState = configuredRole(agent, "researcher", turn);
670
+ const researchRoute = researchState.route;
671
+ if (!researchRoute) {
672
+ appendEvent(agent, "odai/research-result", {
673
+ turn,
674
+ step,
675
+ role: "researcher",
676
+ status: "fallback",
677
+ stopReason: researchState.error ? "route-config-invalid" : "route-config-missing",
678
+ ...(researchState.error ? { error: researchState.detail } : {}),
679
+ });
680
+ }
681
+ else {
682
+ const researchBundle = sharedSkillSelection(agent, turn)?.bundle ?? bundled;
683
+ const researchContract = dshRoleContract("researcher", researchBundle.roleContracts.researcher, researchBundle.referenceContracts);
684
+ const subagents = isSubagentsService(ctx.subagents) ? ctx.subagents : undefined;
685
+ const result = subagents
686
+ ? await runRoutedRole({
687
+ subagents,
688
+ provider: config.routing.provider,
689
+ decision: researchDecision,
690
+ taskText: renderResearchTaskContract(taskText),
691
+ roleContract: researchContract,
692
+ agent,
693
+ signal,
694
+ roleRoute: researchRoute,
695
+ })
696
+ : Object.freeze({
697
+ status: "fallback",
698
+ stopReason: "infrastructure-error",
699
+ output: [],
700
+ error: "dsh subagents service unavailable",
701
+ });
702
+ let packet;
703
+ let packetError;
704
+ if (result.status === "completed") {
705
+ try {
706
+ packet = verifyResearchPacketSources(parseResearchPacket(outputText(result.output)), agent?.session?.header?.cwd);
707
+ }
708
+ catch (error) {
709
+ packetError = error instanceof Error ? error.message : String(error);
710
+ }
711
+ }
712
+ const completed = result.status === "completed" && packet !== undefined;
713
+ appendEvent(agent, "odai/research-result", {
714
+ turn,
715
+ step,
716
+ role: "researcher",
717
+ status: completed ? "completed" : "fallback",
718
+ stopReason: completed ? result.stopReason : (packetError ? "packet-invalid" : result.stopReason),
719
+ routeSource: researchState.source,
720
+ fallbackUsed: !completed,
721
+ routeReceiptStatus: result.routeReceiptStatus,
722
+ requestedRoute: researchRoute,
723
+ ...(result.routeReceiptError ? { routeReceiptError: result.routeReceiptError } : {}),
724
+ ...(result.actualRoute ? { actualRoute: result.actualRoute } : {}),
725
+ ...(packet ? { packetDigest: packet.digest, sourceCount: packet.sourceCount } : {}),
726
+ ...(packetError ? { error: packetError } : result.taskError ? { error: result.taskError } : {}),
727
+ });
728
+ if (completed && packet) {
729
+ researchPacketText = renderResearchPacket(packet);
730
+ routedDownstream = {
731
+ ...downstream,
732
+ messages: [
733
+ ...routedDownstream.messages,
734
+ pluginMessage(researchPacketText, `odai completed researcher evidence compression (${packet.sourceCount} sources)`),
735
+ ],
736
+ };
737
+ }
738
+ }
739
+ }
740
+ }
741
+ const frozenCard = responsibilityContinuation?.routeCard ?? activeRouteCard(evidence.events(agent));
742
+ let decision = decideRoute({
743
+ text: taskText,
744
+ routeCard: frozenCard,
745
+ proposal: responsibilityGap,
746
+ interruption: responsibilityContinuation,
747
+ });
748
+ let routeRole = decision.targetRole ?? decision.role;
749
+ const roleTaskText = researchPacketText ? `${taskText}\n\n${researchPacketText}` : taskText;
750
+ let roleContext = decision.action === "direct"
751
+ ? undefined
752
+ : buildRoleContextPacket(agent, routeRole, roleTaskText);
753
+ let localReviewerCoverage;
754
+ if (config.routing.mode === "auto"
755
+ && routeRole === "reviewer"
756
+ && decision.action === "delegate"
757
+ && roleContext
758
+ && !roleContext.sufficient) {
759
+ localReviewerCoverage = roleContext.coverage;
760
+ decision = Object.freeze({
761
+ ...decision,
762
+ role: "controller",
763
+ mode: "direct",
764
+ action: "direct",
765
+ targetRole: "reviewer",
766
+ signals: Object.freeze([...decision.signals, "review-evidence-packet-missing", "controller-local-review"]),
767
+ });
768
+ routeRole = "reviewer";
769
+ }
770
+ appendEvent(agent, "odai/route-decided", {
771
+ turn,
772
+ step,
773
+ role: decision.role,
774
+ action: decision.action,
775
+ ...(decision.targetRole ? { targetRole: decision.targetRole } : {}),
776
+ mode: config.routing.mode,
777
+ reasonCode: decision.reasonCode,
778
+ signals: decision.signals,
779
+ ...(responsibilityGap ? {
780
+ stateDigest: responsibilityGap.stateDigest,
781
+ gap: responsibilityGap.gap,
782
+ evidenceRefs: responsibilityGap.evidenceRefs,
783
+ expectedChange: responsibilityGap.expectedChange,
784
+ } : {}),
785
+ ...(decision.considerations ? { considerations: decision.considerations } : {}),
786
+ });
787
+ if (responsibilityGap) {
788
+ appendEvent(agent, "odai/responsibility-gap-consumed", {
789
+ turn,
790
+ step,
791
+ responsibility: responsibilityGap.responsibility,
792
+ stateDigest: responsibilityGap.stateDigest,
793
+ routeAction: decision.action,
794
+ reasonCode: decision.reasonCode,
795
+ });
796
+ }
797
+ if (decision.action === "direct") {
798
+ if (!localReviewerCoverage)
799
+ return routedDownstream;
800
+ if (!roleContext)
801
+ throw new Error("reviewer fallback is missing its context packet");
802
+ appendEvent(agent, "odai/route-context", {
803
+ turn,
804
+ step,
805
+ role: "reviewer",
806
+ mode: "controller-local",
807
+ digest: roleContext.digest,
808
+ evidenceCount: roleContext.evidenceCount,
809
+ toolEvidenceCount: roleContext.toolEvidenceCount,
810
+ acceptanceCount: localReviewerCoverage.acceptanceCount,
811
+ diffCount: localReviewerCoverage.diffCount,
812
+ testCount: localReviewerCoverage.testCount,
813
+ truncated: roleContext.truncated,
814
+ sufficient: false,
815
+ });
816
+ appendEvent(agent, "odai/route-result", {
817
+ turn,
818
+ step,
819
+ role: "reviewer",
820
+ action: "direct",
821
+ status: "fallback",
822
+ stopReason: "evidence-packet-missing",
823
+ independent: false,
824
+ });
825
+ return {
826
+ kind: "enter",
827
+ messages: [
828
+ ...routedDownstream.messages,
829
+ pluginMessage([
830
+ `An independent reviewer was not started because the bounded packet is incomplete (${JSON.stringify(localReviewerCoverage)}).`,
831
+ "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.",
832
+ "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.",
833
+ ].join("\n"), "odai reviewer evidence is incomplete; controller continues locally"),
834
+ ],
835
+ };
836
+ }
837
+ if (config.routing.mode === "observe") {
838
+ if (requiresFailClosedProtection(decision)) {
839
+ protectController(agent, turn, step, decision, "observe");
840
+ }
841
+ return {
842
+ kind: "enter",
843
+ messages: [
844
+ ...routedDownstream.messages,
845
+ pluginMessage(renderRouteNotice(decision, "observe"), `odai observed ${routeRole} gap (${decision.reasonCode})`),
846
+ ],
847
+ };
848
+ }
849
+ const roleState = configuredRole(agent, routeRole, turn);
850
+ const roleRoute = roleState.route;
851
+ if (!roleRoute) {
852
+ const invalidConfig = Boolean(roleState.error);
853
+ appendEvent(agent, "odai/route-config-missing", {
854
+ turn,
855
+ step,
856
+ role: routeRole,
857
+ action: decision.action,
858
+ mode: config.routing.mode,
859
+ status: invalidConfig ? "invalid" : "unconfigured",
860
+ ...(invalidConfig ? { error: roleState.detail } : {}),
861
+ });
862
+ if (requiresFailClosedProtection(decision)) {
863
+ protectController(agent, turn, step, decision, invalidConfig ? "route-config-invalid" : "route-config-missing");
864
+ }
865
+ if (routeRole === "frontend")
866
+ return routedDownstream;
867
+ return {
868
+ kind: "enter",
869
+ messages: [
870
+ ...routedDownstream.messages,
871
+ pluginMessage(renderMissingRouteConfigNotice(decision, config.routing.mode, roleState.error), `odai ${routeRole} route is ${invalidConfig ? "invalid" : "not configured"}`),
872
+ ],
873
+ };
874
+ }
875
+ const roleBundle = sharedSkillSelection(agent, turn)?.bundle ?? bundled;
876
+ const canonicalRoleContract = roleBundle.roleContracts[routeRole];
877
+ const roleContract = dshRoleContract(routeRole, canonicalRoleContract, roleBundle.referenceContracts);
878
+ let rolePreflightVerified = false;
879
+ if (routeRole === "frontend" && decision.action === "upgrade") {
880
+ const health = await probeModelRoute((candidate, candidateSignal) => ctx.llm.resolveCallConfig(candidate, candidateSignal), roleRoute, signal);
881
+ if (health.status === "rejected") {
882
+ const invalidation = invalidateFailedRoleRoute(agent, routeRole, roleRoute, roleState.source, health.failure, { turn, step, position: "pre-step" });
883
+ appendEvent(agent, "odai/route-result", {
884
+ turn,
885
+ step,
886
+ role: routeRole,
887
+ action: "upgrade",
888
+ status: "fallback",
889
+ stopReason: "route-preflight-failed",
890
+ routeSource: roleState.source,
891
+ fallbackUsed: true,
892
+ requestedRoute: roleRoute,
893
+ failureKind: health.failure.kind,
894
+ error: health.failure.message,
895
+ invalidated: invalidation.invalidated,
896
+ });
897
+ return {
898
+ kind: "enter",
899
+ messages: [
900
+ ...routedDownstream.messages,
901
+ pluginMessage([
902
+ `The configured frontend route failed preflight (${health.failure.kind}: ${health.failure.message}).`,
903
+ "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.",
904
+ "",
905
+ "frontend local-fallback responsibility contract:",
906
+ roleContract,
907
+ ].join("\n"), "odai frontend route unavailable; explicit local fallback"),
908
+ ],
909
+ };
910
+ }
911
+ rolePreflightVerified = health.status === "verified";
912
+ }
913
+ roleContext ??= buildRoleContextPacket(agent, routeRole, taskText);
914
+ const inPlaceUpgrade = decision.action === "upgrade"
915
+ && (config.routing.mode === "auto" || ["executor", "frontend"].includes(routeRole));
916
+ const contextMode = inPlaceUpgrade ? "same-turn" : "bounded-packet";
917
+ appendEvent(agent, "odai/route-context", {
918
+ turn,
919
+ step,
920
+ role: routeRole,
921
+ mode: contextMode,
922
+ digest: roleContext.digest,
923
+ evidenceCount: roleContext.evidenceCount,
924
+ toolEvidenceCount: roleContext.toolEvidenceCount,
925
+ acceptanceCount: roleContext.coverage.acceptanceCount,
926
+ diffCount: roleContext.coverage.diffCount,
927
+ testCount: roleContext.coverage.testCount,
928
+ truncated: roleContext.truncated,
929
+ sufficient: roleContext.sufficient,
930
+ });
931
+ if (routeRole === "reviewer" && decision.action === "delegate" && !roleContext.sufficient) {
932
+ appendEvent(agent, "odai/route-result", {
933
+ turn,
934
+ step,
935
+ role: "reviewer",
936
+ action: "delegate",
937
+ status: "fallback",
938
+ stopReason: "evidence-packet-missing",
939
+ contextDigest: roleContext.digest,
940
+ });
941
+ return {
942
+ kind: "enter",
943
+ messages: [
944
+ ...routedDownstream.messages,
945
+ pluginMessage(`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.`, "odai reviewer evidence packet is incomplete"),
946
+ ],
947
+ };
948
+ }
949
+ if (inPlaceUpgrade) {
950
+ if (!isInPlaceResponsibility(routeRole))
951
+ throw new Error(`unsupported in-place responsibility: ${routeRole}`);
952
+ stopResponsibilityScope(agent, "superseded", { step });
953
+ const responsibilityScope = createResponsibilityScope({
954
+ turn,
955
+ startStep: step,
956
+ role: routeRole,
957
+ route: roleRoute,
958
+ source: roleState.source,
959
+ decision,
960
+ routeValidated: rolePreflightVerified,
961
+ ...(routeRole === "executor" && frozenCard ? { cardId: frozenCard.id } : {}),
962
+ ...(responsibilityContinuation ? { resumeOfScopeId: responsibilityContinuation.scopeId } : {}),
963
+ });
964
+ responsibilityScopes.set(agent, responsibilityScope);
965
+ if (agent?.session)
966
+ responsibilityScopeOwners.set(agent.session, agent);
967
+ appendEvent(agent, "odai/responsibility-scope-started", responsibilityScopeStartedEvent(responsibilityScope));
968
+ if (["planner", "reviewer"].includes(routeRole)) {
969
+ protectController(agent, turn, step, decision, `responsibility-scope-${routeRole}`, undefined, responsibilityScope.id);
970
+ }
971
+ if (routeRole === "executor" && frozenCard) {
972
+ appendEvent(agent, "odai/route-card-claimed", {
973
+ cardId: frozenCard.id,
974
+ turn,
975
+ step,
976
+ ...(responsibilityContinuation ? { reason: "output-limit-continuation" } : {}),
977
+ });
978
+ }
979
+ appendEvent(agent, "odai/route-upgrade", {
980
+ turn,
981
+ step,
982
+ role: decision.role,
983
+ targetRole: routeRole,
984
+ status: "requested",
985
+ responsibilityScopeId: responsibilityScope.id,
986
+ ...(responsibilityContinuation ? { resumeOfScopeId: responsibilityContinuation.scopeId } : {}),
987
+ continuationPolicy: responsibilityScope.continuationPolicy,
988
+ stopPolicy: responsibilityScope.stopPolicy,
989
+ routeSource: roleState.source,
990
+ requestedRoute: roleRoute,
991
+ contextDigest: roleContext.digest,
992
+ contextMode,
993
+ ...(routeRole === "reviewer" ? { independent: false } : {}),
994
+ });
995
+ const contextBoundary = routeRole === "reviewer"
996
+ ? `The bounded packet is not independently reviewable (${JSON.stringify(roleContext.coverage)}). Perform a same-turn read-only check and do not claim independent acceptance.`
997
+ : "Retain the current controller conversation and workspace context; do not reconstruct it through a child handoff.";
998
+ return {
999
+ kind: "enter",
1000
+ messages: [
1001
+ ...routedDownstream.messages,
1002
+ pluginMessage([
1003
+ renderRouteNotice(decision, config.routing.mode, roleRoute),
1004
+ "",
1005
+ `Context digest: sha256:${roleContext.digest}`,
1006
+ contextBoundary,
1007
+ "",
1008
+ `${routeRole} responsibility contract:`,
1009
+ roleContract,
1010
+ ...(routeRole === "executor" && frozenCard
1011
+ ? ["", "Frozen route card:", JSON.stringify(frozenCard, null, 2)]
1012
+ : []),
1013
+ ].join("\n"), `odai upgraded controller route (${decision.reasonCode})`),
1014
+ ],
1015
+ };
1016
+ }
1017
+ const delegationDecision = decision.role === routeRole
1018
+ ? decision
1019
+ : Object.freeze({ ...decision, role: routeRole, mode: "delegate", action: "delegate" });
1020
+ const subagents = isSubagentsService(ctx.subagents) ? ctx.subagents : undefined;
1021
+ const result = subagents
1022
+ ? await runRoutedRole({
1023
+ subagents,
1024
+ provider: config.routing.provider,
1025
+ decision: delegationDecision,
1026
+ taskText: renderRoleContextPacket(roleContext),
1027
+ roleContract,
1028
+ agent,
1029
+ signal,
1030
+ roleRoute,
1031
+ })
1032
+ : Object.freeze({
1033
+ status: "fallback",
1034
+ stopReason: "infrastructure-error",
1035
+ output: [],
1036
+ error: "dsh subagents service unavailable",
1037
+ });
1038
+ appendEvent(agent, "odai/route-result", {
1039
+ turn,
1040
+ step,
1041
+ role: routeRole,
1042
+ action: "delegate",
1043
+ status: result.status,
1044
+ stopReason: result.stopReason,
1045
+ routeSource: roleState.source,
1046
+ fallbackUsed: result.status !== "completed",
1047
+ routeReceiptStatus: result.routeReceiptStatus,
1048
+ requestedRoute: roleRoute,
1049
+ contextDigest: roleContext.digest,
1050
+ ...(result.routeReceiptError ? { routeReceiptError: result.routeReceiptError } : {}),
1051
+ ...(result.actualRoute ? { actualRoute: result.actualRoute } : {}),
1052
+ ...(result.taskError ? { error: result.taskError } : {}),
1053
+ });
1054
+ if (result.status === "completed") {
1055
+ const childText = outputText(result.output);
1056
+ const heading = renderRouteNotice(delegationDecision, config.routing.mode, result.actualRoute);
1057
+ return {
1058
+ kind: "enter",
1059
+ messages: [
1060
+ ...routedDownstream.messages,
1061
+ pluginMessage(childText ? `${heading}\ncontext digest: sha256:${roleContext.digest}\n\n${routeRole} output:\n${childText}` : heading, `odai completed ${routeRole} route`, result.output.filter((block) => block?.type !== "text")),
1062
+ ],
1063
+ };
1064
+ }
1065
+ const failure = result.error ?? result.stopReason;
1066
+ if (requiresFailClosedProtection(delegationDecision)) {
1067
+ protectController(agent, turn, step, delegationDecision, "route-failure", failure);
1068
+ }
1069
+ return {
1070
+ kind: "enter",
1071
+ messages: [
1072
+ ...routedDownstream.messages,
1073
+ pluginMessage(renderRouteFailureNotice(delegationDecision, failure), requiresFailClosedProtection(delegationDecision)
1074
+ ? `odai blocked high-impact ${routeRole} fallback`
1075
+ : `odai fell back from ${routeRole} route`),
1076
+ ],
1077
+ };
1078
+ }, { prepend: true });
1079
+ }
1080
+ }
1081
+ //# sourceMappingURL=lifecycle-runtime.mjs.map