odai-dsh-agent 0.2.5 → 0.2.7

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