@ljwei-stak/dsh-model-route-bot 0.66.3 → 0.67.0

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 (272) hide show
  1. package/.dsh-plugin/bot-product.mjs +81 -5
  2. package/.dsh-plugin/client.js +17490 -1791
  3. package/README.i18n.yaml +4 -4
  4. package/README.md +409 -364
  5. package/README.zh.md +409 -364
  6. package/apps/integration-client/README.md +31 -7
  7. package/apps/integration-client/api.mjs +14 -3
  8. package/apps/integration-client/binding-editor.jsx +308 -109
  9. package/apps/integration-client/binding-model.mjs +262 -0
  10. package/apps/integration-client/binding.css +144 -0
  11. package/apps/integration-client/bot-model-status.mjs +18 -0
  12. package/apps/integration-client/bot-profile-identity.jsx +197 -0
  13. package/apps/integration-client/bot-profile.css +57 -0
  14. package/apps/integration-client/bot-profile.mjs +97 -0
  15. package/apps/integration-client/bot-settings-dialog.css +18 -0
  16. package/apps/integration-client/bot-settings-dialog.jsx +111 -0
  17. package/apps/integration-client/bot-thread-header-bridge.css +20 -0
  18. package/apps/integration-client/bot-thread-header.jsx +278 -0
  19. package/apps/integration-client/bot-threads-panel.jsx +378 -0
  20. package/apps/integration-client/bot-threads.css +169 -0
  21. package/apps/integration-client/chat-design.mjs +250 -0
  22. package/apps/integration-client/chat-ui.jsx +175 -0
  23. package/apps/integration-client/chat.css +477 -0
  24. package/apps/integration-client/composer-draft-binding.mjs +50 -0
  25. package/apps/integration-client/composer-draft-bound.jsx +14 -0
  26. package/apps/integration-client/composer-draft-host.jsx +142 -0
  27. package/apps/integration-client/composer-draft-session.jsx +287 -0
  28. package/apps/integration-client/composer-draft-witness.mjs +115 -0
  29. package/apps/integration-client/composer-draft.css +4 -0
  30. package/apps/integration-client/composer-draft.mjs +322 -0
  31. package/apps/integration-client/contact-attention.jsx +54 -0
  32. package/apps/integration-client/data-panel.jsx +439 -0
  33. package/apps/integration-client/data.css +69 -0
  34. package/apps/integration-client/desktop-bridge.mjs +1 -0
  35. package/apps/integration-client/desktop-http-socket.mjs +49 -0
  36. package/apps/integration-client/desktop-keyboard.mjs +32 -0
  37. package/apps/integration-client/desktop-pane.jsx +99 -30
  38. package/apps/integration-client/desktop-stream.mjs +13 -3
  39. package/apps/integration-client/desktop-tools.jsx +19 -0
  40. package/apps/integration-client/desktop-touch.mjs +27 -0
  41. package/apps/integration-client/desktop.css +41 -0
  42. package/apps/integration-client/independent-chat-controller.mjs +224 -0
  43. package/apps/integration-client/independent-chat-entry.jsx +61 -0
  44. package/apps/integration-client/independent-chat-navigation.mjs +21 -0
  45. package/apps/integration-client/independent-chat-page.jsx +148 -0
  46. package/apps/integration-client/independent-chat-rich.jsx +64 -0
  47. package/apps/integration-client/independent-chat-rich.mjs +30 -0
  48. package/apps/integration-client/independent-chat.css +73 -0
  49. package/apps/integration-client/index.jsx +24 -16
  50. package/apps/integration-client/library-panel.jsx +4 -1
  51. package/apps/integration-client/licenses/jsqr-1.4.0-LICENSE.txt +202 -0
  52. package/apps/integration-client/main-route-editor.jsx +94 -0
  53. package/apps/integration-client/main-route-model.mjs +100 -0
  54. package/apps/integration-client/model-picker.css +37 -0
  55. package/apps/integration-client/model-picker.jsx +83 -0
  56. package/apps/integration-client/native-activity.css +141 -0
  57. package/apps/integration-client/native-activity.jsx +276 -0
  58. package/apps/integration-client/native-activity.mjs +384 -0
  59. package/apps/integration-client/notifications-panel.jsx +186 -0
  60. package/apps/integration-client/notifications.css +126 -0
  61. package/apps/integration-client/pairing-qr.jsx +79 -0
  62. package/apps/integration-client/panels.jsx +126 -115
  63. package/apps/integration-client/product-slots.jsx +46 -0
  64. package/apps/integration-client/routine-recorder.mjs +23 -2
  65. package/apps/integration-client/secure-pairing.jsx +84 -0
  66. package/apps/integration-client/tasks-panel.jsx +32 -10
  67. package/apps/integration-client/ui-icons.jsx +17 -0
  68. package/apps/integration-client/usage-explorer.jsx +43 -0
  69. package/apps/integration-client/usage-report.mjs +71 -0
  70. package/apps/integration-client/usage.css +34 -0
  71. package/apps/integration-client/web-panel-model.mjs +140 -0
  72. package/apps/integration-client/web-panel-page.jsx +221 -0
  73. package/apps/integration-client/web-panel-pins-live.jsx +28 -0
  74. package/apps/integration-client/web-panel.css +139 -0
  75. package/apps/integration-client/workspace-model.mjs +67 -0
  76. package/apps/integration-client/workspace-navigation.mjs +56 -0
  77. package/apps/integration-client/workspace-ui.jsx +173 -0
  78. package/apps/integration-client/workspace.css +332 -0
  79. package/modules/bot-runtime/SOURCE.json +176 -50
  80. package/modules/bot-runtime/index.mjs +169 -13
  81. package/modules/bot-runtime/product-overlay/OVERLAY.json +128 -2
  82. package/modules/bot-runtime/product-overlay/base/src/client/header.js +244 -0
  83. package/modules/bot-runtime/product-overlay/base/src/client/learning-card.js +202 -0
  84. package/modules/bot-runtime/product-overlay/base/src/client/model-menu.js +371 -0
  85. package/modules/bot-runtime/product-overlay/base/src/client/secret-card.js +363 -0
  86. package/modules/bot-runtime/product-overlay/base/src/client/sources.js +68 -0
  87. package/modules/bot-runtime/product-overlay/base/src/client/turn-error.js +260 -0
  88. package/modules/bot-runtime/product-overlay/base/src/host/channel.js +647 -0
  89. package/modules/bot-runtime/product-overlay/base/src/host/memory.js +2050 -0
  90. package/modules/bot-runtime/product-overlay/base/src/host/models.js +314 -0
  91. package/modules/bot-runtime/product-overlay/base/src/host/parts.js +1473 -0
  92. package/modules/bot-runtime/product-overlay/base/src/host/privacy.js +368 -0
  93. package/modules/bot-runtime/product-overlay/base/src/host/roster.js +776 -0
  94. package/modules/bot-runtime/product-overlay/base/src/host/store.js +512 -0
  95. package/modules/bot-runtime/product-overlay/base/src/host/usage.js +653 -0
  96. package/modules/bot-runtime/product-overlay/patches/src/client/details-drawer.js.patch +48 -2
  97. package/modules/bot-runtime/product-overlay/patches/src/client/header.js.patch +41 -0
  98. package/modules/bot-runtime/product-overlay/patches/src/client/index.js.patch +33 -1
  99. package/modules/bot-runtime/product-overlay/patches/src/client/learning-card.js.patch +20 -0
  100. package/modules/bot-runtime/product-overlay/patches/src/client/model-menu.js.patch +24 -0
  101. package/modules/bot-runtime/product-overlay/patches/src/client/secret-card.js.patch +25 -0
  102. package/modules/bot-runtime/product-overlay/patches/src/client/sources.js.patch +52 -0
  103. package/modules/bot-runtime/product-overlay/patches/src/client/turn-error.js.patch +23 -0
  104. package/modules/bot-runtime/product-overlay/patches/src/host/channel.js.patch +46 -0
  105. package/modules/bot-runtime/product-overlay/patches/src/host/memory.js.patch +11 -0
  106. package/modules/bot-runtime/product-overlay/patches/src/host/models.js.patch +16 -0
  107. package/modules/bot-runtime/product-overlay/patches/src/host/parts.js.patch +95 -0
  108. package/modules/bot-runtime/product-overlay/patches/src/host/privacy.js.patch +11 -0
  109. package/modules/bot-runtime/product-overlay/patches/src/host/roster.js.patch +145 -0
  110. package/modules/bot-runtime/product-overlay/patches/src/host/store.js.patch +68 -0
  111. package/modules/bot-runtime/product-overlay/patches/src/host/usage.js.patch +11 -0
  112. package/modules/bot-runtime/read-model.mjs +19 -4
  113. package/modules/bot-runtime/vendor/ds-bot/src/client/details-drawer.js +15 -5
  114. package/modules/bot-runtime/vendor/ds-bot/src/client/header.js +9 -2
  115. package/modules/bot-runtime/vendor/ds-bot/src/client/index.js +9 -2
  116. package/modules/bot-runtime/vendor/ds-bot/src/client/learning-card.js +2 -2
  117. package/modules/bot-runtime/vendor/ds-bot/src/client/model-menu.js +5 -3
  118. package/modules/bot-runtime/vendor/ds-bot/src/client/secret-card.js +3 -3
  119. package/modules/bot-runtime/vendor/ds-bot/src/client/sources.js +25 -2
  120. package/modules/bot-runtime/vendor/ds-bot/src/client/turn-error.js +3 -2
  121. package/modules/bot-runtime/vendor/ds-bot/src/host/channel.js +14 -2
  122. package/modules/bot-runtime/vendor/ds-bot/src/host/memory.js +1 -1
  123. package/modules/bot-runtime/vendor/ds-bot/src/host/models.js +7 -0
  124. package/modules/bot-runtime/vendor/ds-bot/src/host/parts.js +26 -9
  125. package/modules/bot-runtime/vendor/ds-bot/src/host/privacy.js +1 -1
  126. package/modules/bot-runtime/vendor/ds-bot/src/host/roster.js +112 -3
  127. package/modules/bot-runtime/vendor/ds-bot/src/host/store.js +34 -1
  128. package/modules/bot-runtime/vendor/ds-bot/src/host/usage.js +1 -1
  129. package/modules/independent-runtime/index.mjs +152 -62
  130. package/modules/independent-runtime/remote-tools.mjs +8 -5
  131. package/modules/openmaus-protocol/NOTICE +10 -0
  132. package/modules/openmaus-protocol/README.md +15 -0
  133. package/modules/openmaus-protocol/SOURCE.json +68 -0
  134. package/modules/openmaus-protocol/dist/protocol.mjs +1203 -0
  135. package/modules/openmaus-protocol/index.mjs +1 -0
  136. package/modules/openmaus-protocol/transport.mjs +77 -0
  137. package/modules/openmaus-protocol/vendor/LICENSE +202 -0
  138. package/modules/openmaus-protocol/vendor/companion/src/e2e/README.md +405 -0
  139. package/modules/openmaus-protocol/vendor/companion/src/e2e/credentials.ts +128 -0
  140. package/modules/openmaus-protocol/vendor/companion/src/e2e/descriptor.ts +84 -0
  141. package/modules/openmaus-protocol/vendor/companion/src/e2e/noise.ts +456 -0
  142. package/modules/openmaus-protocol/vendor/companion/src/e2e/phone.ts +104 -0
  143. package/modules/openmaus-protocol/vendor/companion/src/e2e/session.ts +417 -0
  144. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.json +2565 -0
  145. package/modules/openmaus-protocol/vendor/companion/src/e2e/vectors/v1.sha256 +1 -0
  146. package/modules/openmaus-protocol/vendor/companion/src/e2e/yamux.ts +335 -0
  147. package/package.json +20 -6
  148. package/packages/bot-acp/README.md +156 -0
  149. package/packages/bot-acp/licenses/LICENSE-OpenMausBot +202 -0
  150. package/packages/bot-acp/licenses/NOTICE.md +27 -0
  151. package/packages/bot-acp/src/acp-adapter.mjs +795 -0
  152. package/packages/bot-acp/src/acp-host.mjs +148 -0
  153. package/packages/bot-acp/src/native-launch.mjs +121 -0
  154. package/packages/bot-acp/src/permission-policy.mjs +283 -0
  155. package/packages/bot-acp/src/rpc-codec.mjs +265 -0
  156. package/packages/bot-acp/src/session-core.mjs +581 -0
  157. package/packages/bot-acp/src/stdio-rpc.mjs +329 -0
  158. package/packages/bot-engines/README.md +281 -0
  159. package/packages/bot-engines/licenses/LICENSE-OpenMausBot +202 -0
  160. package/packages/bot-engines/licenses/NOTICE.md +32 -0
  161. package/packages/bot-engines/src/claude-engine.mjs +772 -0
  162. package/packages/bot-engines/src/claude-llm-adapter.mjs +797 -0
  163. package/packages/bot-engines/src/claude-llm-turn.mjs +292 -0
  164. package/packages/bot-engines/src/claude-permission-broker.mjs +130 -0
  165. package/packages/bot-engines/src/claude-permission-proxy.mjs +87 -0
  166. package/packages/bot-engines/src/claude-protocol.mjs +177 -0
  167. package/packages/bot-engines/src/driver-catalog.mjs +334 -0
  168. package/packages/bot-engines/src/host-loop-request.mjs +95 -0
  169. package/packages/bot-engines/src/host-registrations.mjs +105 -0
  170. package/packages/capability-broker/src/cli-bootstrap.mjs +86 -16
  171. package/packages/capability-broker/src/cli-bridge-response.mjs +80 -0
  172. package/packages/capability-broker/src/cli-stdio-bridge.mjs +21 -8
  173. package/packages/cli-runtime/src/codex-session-core.mjs +4 -0
  174. package/packages/cli-runtime/src/native-cli-output.mjs +57 -0
  175. package/packages/cli-runtime/src/native-peer.ps1 +51 -3
  176. package/packages/cli-runtime/src/native-process.mjs +77 -14
  177. package/packages/model-router-plugin/README.md +3 -3
  178. package/packages/model-router-plugin/package.json +1 -1
  179. package/packages/orchestration/src/binding-product.mjs +19 -0
  180. package/packages/orchestration/src/bot-data-actions.mjs +45 -0
  181. package/packages/orchestration/src/bot-data-chart.mjs +109 -0
  182. package/packages/orchestration/src/bot-data-context.mjs +44 -0
  183. package/packages/orchestration/src/bot-data-engine.mjs +809 -0
  184. package/packages/orchestration/src/bot-data-io.mjs +186 -0
  185. package/packages/orchestration/src/bot-data-sql.mjs +195 -0
  186. package/packages/orchestration/src/bot-data-surface.mjs +144 -0
  187. package/packages/orchestration/src/bot-data-tools.mjs +68 -0
  188. package/packages/orchestration/src/bot-data-worker.mjs +228 -0
  189. package/packages/orchestration/src/bot-independent-execution.mjs +195 -0
  190. package/packages/orchestration/src/bot-independent-stream.mjs +74 -0
  191. package/packages/orchestration/src/bot-loop.mjs +152 -19
  192. package/packages/orchestration/src/bot-notifications.mjs +159 -0
  193. package/packages/orchestration/src/bot-policy.mjs +43 -4
  194. package/packages/orchestration/src/bot-profile.mjs +171 -0
  195. package/packages/orchestration/src/bot-routine.mjs +4 -4
  196. package/packages/orchestration/src/bot-runtime-assembly.mjs +255 -8
  197. package/packages/orchestration/src/bot-runtime-switch.mjs +13 -2
  198. package/packages/orchestration/src/bot-serving.mjs +164 -0
  199. package/packages/orchestration/src/bot-session-store.mjs +105 -9
  200. package/packages/orchestration/src/bot-settings.mjs +50 -11
  201. package/packages/orchestration/src/bot-thread-admission.mjs +84 -0
  202. package/packages/orchestration/src/bot-thread-host.mjs +187 -0
  203. package/packages/orchestration/src/bot-threads.mjs +790 -0
  204. package/packages/orchestration/src/data-product-access.mjs +37 -0
  205. package/packages/orchestration/src/data-tool-contract.mjs +291 -0
  206. package/packages/orchestration/src/desktop-http-carrier.mjs +120 -0
  207. package/packages/orchestration/src/desktop-proxy.mjs +311 -0
  208. package/packages/orchestration/src/desktop-sessions.mjs +43 -9
  209. package/packages/orchestration/src/ds-lifecycle.mjs +31 -4
  210. package/packages/orchestration/src/grant-refresh-queue.mjs +17 -0
  211. package/packages/orchestration/src/independent-chat-page.mjs +39 -0
  212. package/packages/orchestration/src/independent-chat-service.mjs +308 -0
  213. package/packages/orchestration/src/independent-context-state.mjs +44 -0
  214. package/packages/orchestration/src/independent-message-wire.mjs +84 -0
  215. package/packages/orchestration/src/independent-tool-catalog.mjs +175 -0
  216. package/packages/orchestration/src/independent-tool-families.mjs +73 -0
  217. package/packages/orchestration/src/independent-tool-host.mjs +344 -0
  218. package/packages/orchestration/src/main-bot-grant.mjs +15 -0
  219. package/packages/orchestration/src/main-model-routes.mjs +208 -0
  220. package/packages/orchestration/src/model-accounting.mjs +99 -35
  221. package/packages/orchestration/src/model-auxiliary-dispatch.mjs +187 -0
  222. package/packages/orchestration/src/model-loop-frame.mjs +179 -0
  223. package/packages/orchestration/src/model-prepared-generation.mjs +175 -0
  224. package/packages/orchestration/src/model-request-bounds.mjs +111 -0
  225. package/packages/orchestration/src/native-activity-contract.mjs +491 -0
  226. package/packages/orchestration/src/native-activity-service.mjs +102 -0
  227. package/packages/orchestration/src/native-activity-store.mjs +249 -0
  228. package/packages/orchestration/src/native-conversation-tools.mjs +78 -0
  229. package/packages/orchestration/src/native-data-tools.mjs +100 -0
  230. package/packages/orchestration/src/native-remote-actions.mjs +742 -0
  231. package/packages/orchestration/src/native-team-tools.mjs +73 -0
  232. package/packages/orchestration/src/pairing-invite.mjs +37 -0
  233. package/packages/orchestration/src/product-composition.mjs +1559 -1001
  234. package/packages/orchestration/src/remote-machines.mjs +82 -14
  235. package/packages/orchestration/src/routine-execution.mjs +5 -3
  236. package/packages/orchestration/src/routine-session.mjs +19 -13
  237. package/packages/orchestration/src/secure-pairing-host.mjs +112 -0
  238. package/packages/orchestration/src/task-journal.mjs +29 -0
  239. package/packages/orchestration/src/task-product.mjs +17 -0
  240. package/packages/orchestration/src/team-assignment-store.mjs +120 -0
  241. package/packages/orchestration/src/team-bridge.mjs +282 -0
  242. package/packages/orchestration/src/team-scheduler.mjs +83 -0
  243. package/packages/orchestration/src/team-sdk-tools.mjs +84 -0
  244. package/packages/orchestration/src/team-tool-contract.mjs +284 -0
  245. package/packages/orchestration/src/ui-host-namespace.mjs +23 -0
  246. package/packages/orchestration/src/web-panel-actions.mjs +103 -0
  247. package/packages/orchestration/src/web-panel-framing.mjs +227 -0
  248. package/packages/orchestration/src/web-panel-store.mjs +206 -0
  249. package/remote-computer-agent/README.md +39 -29
  250. package/remote-computer-agent/build.mjs +3 -0
  251. package/remote-computer-agent/dist/OPENMAUSBOT_LICENSE +202 -0
  252. package/remote-computer-agent/dist/OPENMAUSBOT_NOTICE +10 -0
  253. package/remote-computer-agent/dist/OPENMAUSBOT_SOURCE.json +68 -0
  254. package/remote-computer-agent/dist/cli-stdio-bridge.mjs +215 -25
  255. package/remote-computer-agent/dist/cli-stdio-bridge.mjs.map +4 -4
  256. package/remote-computer-agent/dist/cli.mjs +1688 -168
  257. package/remote-computer-agent/dist/cli.mjs.map +4 -4
  258. package/remote-computer-agent/dist/index.mjs +1579 -122
  259. package/remote-computer-agent/dist/index.mjs.map +4 -4
  260. package/remote-computer-agent/dist/native-peer.ps1 +51 -3
  261. package/remote-computer-agent/package-lock.json +308 -2
  262. package/remote-computer-agent/package.json +2 -1
  263. package/remote-computer-agent/pair-device.ps1 +20 -0
  264. package/remote-computer-agent/setup.ps1 +38 -16
  265. package/remote-computer-agent/src/cli.ts +24 -3
  266. package/remote-computer-agent/src/desktop-session.ts +11 -3
  267. package/remote-computer-agent/src/e2e-server.ts +81 -0
  268. package/remote-computer-agent/src/pairing-page.ts +35 -0
  269. package/remote-computer-agent/src/protocol.ts +25 -25
  270. package/remote-computer-agent/src/secure-pairing.ts +105 -0
  271. package/remote-computer-agent/src/store.ts +20 -5
  272. package/remote-computer-agent/src/transport.ts +39 -15
@@ -4,6 +4,7 @@ import {actualCost} from '../../../.dsh-plugin/shared/run-ledger.mjs'
4
4
  import {managedModelRequest,taskModelRequest} from './model-receipt.mjs'
5
5
  import {assessFundingAdmission} from './funding-admission.mjs'
6
6
  import {taskHash} from './task-contract.mjs'
7
+ import {planOutputBound,requestFieldAccess} from './model-request-bounds.mjs'
7
8
 
8
9
  const fail=code=>{throw Object.assign(new Error(code),{code})}
9
10
  const deferred=()=>{let resolve;const promise=new Promise(r=>{resolve=r});return {promise,resolve}}
@@ -25,7 +26,7 @@ function usageOf(value){
25
26
  }
26
27
 
27
28
  /** Every observed model call has one source receipt; only the linked run owns money. */
28
- export function createModelAccounting({store,authority,pricingFor,inputBoundFor,machineId,workspaceId,now=Date.now,ttlMs=120000,limits={dailyLimitUsd:1,monthlyLimitUsd:0},maxActive=16}){
29
+ export function createModelAccounting({store,authority,pricingFor,fundingRevisionFor,inputBoundFor,modelBoundsFor,preparedFrameFor,machineId,workspaceId,now=Date.now,ttlMs=120000,limits={dailyLimitUsd:1,monthlyLimitUsd:0},maxActive=16}){
29
30
  if(store?.kind!=='sqlite-router-state'||typeof store.recordModelReceipt!=='function'||typeof pricingFor!=='function')fail('MODEL_ACCOUNTING_STORAGE_REQUIRED')
30
31
  const active=new Set();let closed=false
31
32
  const controllerSet=new Set()
@@ -37,7 +38,8 @@ export function createModelAccounting({store,authority,pricingFor,inputBoundFor,
37
38
  ...(policy.scopeId?{fundingScopeId:policy.scopeId}:{}),...(policy.projectId?{fundingProjectId:policy.projectId}:{}),
38
39
  requestHash:requestHash(options),createdAt,ownerPid:process.pid,state:'admitted',finishedAt:null,usage:null,usageComplete:false,actualModel:null,reportedCostUsd:null,finishKind:null,outcome:null}
39
40
  }
40
- const callStore=(receipt,config,estimate)=>({read:()=>store.read(),update:(change,options)=>store.update(current=>{
41
+ const callStore=(receipt,config,estimate,assertFunding)=>({read:()=>store.read(),update:(change,options)=>store.update(current=>{
42
+ if (receipt.state === 'admitted' || receipt.state === 'running') assertFunding()
41
43
  if(receipt.state==='admitted'&&config.funding==='api'){
42
44
  const checked=assessFundingAdmission({store,state:current,botId:receipt.botId,scopeId:config.scopeId,projectId:config.projectId,policy:config,estimatedCostUsd:estimate,excludeRunId:receipt.runId,now:now()})
43
45
  if(!checked.accepted)return checked
@@ -65,30 +67,92 @@ export function createModelAccounting({store,authority,pricingFor,inputBoundFor,
65
67
  authority.assertTool(identity.subject,'model/infer')
66
68
  if(authority.describe(identity.subject).botId!==identity.botId)fail('MODEL_OWNER_MISMATCH')
67
69
  if(task&&(task.botId!==identity.botId||task.provider!==options.provider||task.model!==options.model))fail('MODEL_TASK_OWNER_MISMATCH')
68
- const config=await pricingFor({botId:identity.botId,provider:options.provider,model:options.model,source:identity.source})
70
+ const fundingQuery={botId:identity.botId,provider:options.provider,model:options.model,source:identity.source}
71
+ const config=await pricingFor(fundingQuery)
69
72
  if(active.size>=maxActive||active.has(options))fail('MODEL_CONCURRENCY_LIMIT')
70
73
  if(!config||!['api','subscription'].includes(config.funding))fail('MODEL_FUNDING_REQUIRED')
71
74
  if(config.funding==='api'&&(!config.pricing||!['input','output'].every(k=>Number.isFinite(config.pricing[k])&&config.pricing[k]>=0)))fail('MODEL_PRICE_REQUIRED')
72
- if(options.maxTokens!==undefined&&options.maxTokens!==null&&(!Number.isSafeInteger(options.maxTokens)||options.maxTokens<1))fail('MODEL_OUTPUT_BOUNDARY_REQUIRED')
75
+ if(options.maxTokens!==undefined&&(!Number.isSafeInteger(options.maxTokens)||options.maxTokens<1))fail('MODEL_OUTPUT_BOUNDARY_REQUIRED')
73
76
  if(config.funding==='api'&&(!Number.isSafeInteger(config.maxOutputTokens)||config.maxOutputTokens<1))fail('MODEL_ESTIMATE_REQUIRED')
74
- // A declared request allowance is not a tokenizer. The trusted Host must
75
- // supply an upper bound, normally the pinned model's whole context window.
76
- // This also covers images and adapter framing instead of guessing from URL bytes.
77
+ // A declared request allowance is a price, not a capacity: the trusted
78
+ // input upper bound is the pinned model's own context window, resolved
79
+ // per admission because an adapter registration can be replaced under a
80
+ // live Host. That whole window already covers images, tool declarations
81
+ // and adapter framing, so no tokenizer and no source-length estimate
82
+ // stands in for it. A route whose adapter declares no window has no bound
83
+ // to admit, which is a required-bound failure rather than a smaller number
84
+ // to pretend with.
85
+ // A prepared call (the independent path) already bound one adapter generation and
86
+ // recorded its exact capacities in a Host-owned frame keyed by this request's signal.
87
+ // When that frame is present and names this exact route, admission uses it instead of
88
+ // reading the current registration: the window and default it prices are the ones the
89
+ // prepared dispatch will actually stream, so no metadata read can combine one
90
+ // generation's window with another's endpoint. A frame about another route is ignored.
91
+ const frame=preparedFrameFor?await preparedFrameFor(options):null
92
+ if (!managed && (config.fundingRevision || frame?.fundingRevision)) {
93
+ if (!frame || !Object.hasOwn(frame, 'fundingRevision')) fail('MODEL_FUNDING_FRAME_MISSING')
94
+ if (frame.fundingRevision !== (config.fundingRevision ?? null)) fail('MODEL_FUNDING_CHANGED')
95
+ }
96
+ const assertFunding=()=>{
97
+ if (config.fundingRevision && fundingRevisionFor?.(fundingQuery) !== config.fundingRevision) fail('MODEL_FUNDING_CHANGED')
98
+ }
99
+ assertFunding()
100
+ if(closed)fail('MODEL_ACCOUNTING_CLOSED')
101
+ if(active.size>=maxActive||active.has(options))fail('MODEL_CONCURRENCY_LIMIT')
102
+ options.signal?.throwIfAborted()
103
+ const frameBounds=frame&&frame.bounds&&frame.provider===options.provider&&frame.model===options.model?frame.bounds:null
104
+ let outputDefault=null
77
105
  if(config.funding==='api'){
78
- const inputBound=await inputBoundFor?.({options,identity,config})
106
+ let inputBound
107
+ if(frameBounds){
108
+ inputBound=frameBounds.contextWindow
109
+ outputDefault=frameBounds.defaultMaxTokens??null
110
+ }else{
111
+ const bounds=modelBoundsFor?await modelBoundsFor({options,identity,config,signal:options.signal??null}):null
112
+ inputBound=bounds?.inputBound??(inputBoundFor?await inputBoundFor({options,identity,config}):null)
113
+ outputDefault=bounds?.outputDefault??null
114
+ }
79
115
  if(!Number.isSafeInteger(inputBound)||inputBound<1||!Number.isSafeInteger(config.maxInputTokens)||inputBound>config.maxInputTokens)fail('MODEL_INPUT_BOUNDARY_REQUIRED')
80
116
  }
117
+ // The output bound the provider will actually see. An omitted bound is not
118
+ // an unbounded one: the pinned adapter materializes its own per-request
119
+ // cap into the request before dispatch, so that declared cap — and nothing
120
+ // else — is what arrives. A request the Host cannot bring under the
121
+ // allowance, whether frozen above it or declaring no bound at all, is
122
+ // refused here: before a receipt, a reservation, a lease, an active slot or
123
+ // any provider call exists.
81
124
  const outputBound=config.maxOutputTokens
82
- const effectiveOutput=Number.isSafeInteger(outputBound)&&outputBound>0?Math.min(options.maxTokens??outputBound,outputBound):options.maxTokens
83
- // The SDK AgentLoop hands the adapter a frozen request that already carries the model's own
84
- // output bound, so the Host cannot lower it. Such a request is admitted with the configured
85
- // allowance as its estimate (decision: usability first); the daily/monthly budget stays the
86
- // hard limit, and the per-call output bound is advisory where the request cannot be changed.
87
- const descriptor=Object.getOwnPropertyDescriptor(options,'signal'),tokenDescriptor=Object.getOwnPropertyDescriptor(options,'maxTokens')
88
- const carrier=descriptor?descriptor.writable:Object.isExtensible(options)
89
- const clampable=carrier&&!(tokenDescriptor&&!tokenDescriptor.writable)&&Boolean(tokenDescriptor||Object.isExtensible(options))
125
+ let plan
126
+ if(frameBounds){
127
+ // The prepared config already carries the dispatched bound (clamped before it
128
+ // froze). Admit it as-is and only validate it against the current allowance; a
129
+ // second clamp here would change the dispatched config and fail the prepared call
130
+ // with INVALID_PREPARED_CALL, so a lowered allowance is a refusal, never a re-clamp.
131
+ const dispatched=options.maxTokens??null
132
+ if(config.funding==='api'){
133
+ if(!Number.isSafeInteger(dispatched)||dispatched<1)fail('MODEL_ESTIMATE_REQUIRED')
134
+ if(dispatched>outputBound)fail('MODEL_OUTPUT_BOUND_EXCEEDED')
135
+ }
136
+ plan={ok:true,maxTokens:options.maxTokens??undefined,source:'prepared'}
137
+ }else{
138
+ plan=Number.isSafeInteger(outputBound)&&outputBound>0
139
+ ?planOutputBound({requested:options.maxTokens??null,allowance:outputBound,adapterDefault:outputDefault})
140
+ :{ok:true,maxTokens:options.maxTokens??undefined,source:'none'}
141
+ }
142
+ if(!plan.ok)fail(plan.code)
143
+ // Mutability is per field and independent of every other field's: a request
144
+ // whose signal is read-only may still carry a writable maxTokens.
145
+ const tokenAccess=requestFieldAccess(options,'maxTokens'),signalAccess=requestFieldAccess(options,'signal')
146
+ if(plan.source==='clamp'&&!tokenAccess.canWrite)fail(plan.refusal)
147
+ const effectiveOutput=plan.maxTokens
148
+ // The bounds lookup is asynchronous, so the gates it ran past are re-run
149
+ // here: an accounting that closed while the adapter was being asked, or an
150
+ // active set another admission filled, must not gain a slot because a
151
+ // metadata read was slow.
152
+ if(closed)fail('MODEL_ACCOUNTING_CLOSED')
153
+ if(active.size>=maxActive||active.has(options))fail('MODEL_CONCURRENCY_LIMIT')
90
154
  const record=receiptBase(identity,{...options,...(effectiveOutput!==undefined?{maxTokens:effectiveOutput}:{})},config,managed,task)
91
- let manager,accepted,binding,lease,guard,inner,estimate,previousSignal=options.signal,changedOutput=false,leaseSignal=null
155
+ let manager,accepted,binding,lease,guard,inner,estimate,previousSignal=options.signal,changedOutput=false,changedSignal=false,leaseSignal=null
92
156
  let started=false,exhausted=false,terminal=false,finishKind=null,observed={usage:null,complete:false},primaryError,cleanupError,propagated=false,aborted=false,consumerClosed=false
93
157
  const controller=new AbortController();controllerSet.add(controller);active.add(options)
94
158
  const build=()=>{
@@ -116,37 +180,35 @@ export function createModelAccounting({store,authority,pricingFor,inputBoundFor,
116
180
  estimate=record.funding==='subscription'?0:(input*config.pricing.input+output*config.pricing.output)/1e6
117
181
  if(!Number.isFinite(estimate))fail('MODEL_ESTIMATE_REQUIRED')
118
182
  const pending={id:record.runId,createdAt:record.createdAt,status:'running',finishedAt:null,packages:[],reviews:[],modelCall:{schemaVersion:1,id:record.id,botId:record.botId,source:record.source,...(record.fundingScopeId?{fundingScopeId:record.fundingScopeId}:{}),...(record.fundingProjectId?{fundingProjectId:record.fundingProjectId}:{})}}
119
- manager=createBudgetReservations({state:callStore(record,config,estimate)})
183
+ manager=createBudgetReservations({state:callStore(record,config,estimate,assertFunding)})
120
184
  accepted=await manager.beginExecution({run:pending,packageEstimates:[{id:'inference',billing:record.funding,estimatedCostUsd:estimate}],...limits})
121
185
  if(!accepted.accepted)fail(accepted.reason==='subscription-call-limit'?'MODEL_SUBSCRIPTION_LIMIT':'MODEL_WAITING_BUDGET')
122
186
  }
123
187
  binding={taskId:record.taskId??record.runId,attemptId:record.attemptId,machineId,workspaceId,location:'local'}
124
188
  lease=authority.issue(identity.subject,{binding,tools:['model/infer'],ttlMs})
125
189
  // The caller request may be frozen (the SDK AgentLoop passes one). A
126
- // mutable request carries the combined lease signal so the provider
127
- // aborts directly; a frozen request cannot, so the lease gates the
190
+ // request whose signal field is writable carries the combined lease
191
+ // signal so the provider aborts directly; otherwise the lease gates the
128
192
  // iteration instead and cleanup still returns the provider iterator.
129
- // Clamping happens when the request allows it; when it does not, the
130
- // reservation above already covers the bound the provider will see.
131
- const clampNeeded=effectiveOutput!==undefined&&options.maxTokens!==undefined&&options.maxTokens!==effectiveOutput
132
- if(clampNeeded&&clampable){
133
- options.maxTokens=effectiveOutput;changedOutput=true
134
- }else if(carrier&&effectiveOutput!==undefined&&options.maxTokens===undefined&&(!tokenDescriptor||tokenDescriptor.writable)&&Object.isExtensible(options)){
135
- // Adoption is best effort: the estimate above already uses the full allowance, so a
136
- // sealed request without an output bound is admitted as it is instead of failing.
137
- options.maxTokens=effectiveOutput;changedOutput=true
193
+ if(plan.source==='clamp'){
194
+ // Write the bound the provider will see, then read it back: a field
195
+ // that reports writable but does not take the value is not one the
196
+ // Host may claim it clamped.
197
+ if(!Reflect.set(options,'maxTokens',effectiveOutput)||options.maxTokens!==effectiveOutput)fail(plan.refusal)
198
+ changedOutput=true
138
199
  }
139
200
  guard=authority.run(lease.token,binding,'model/infer',async({signal})=>{
140
- if(carrier){options.signal=AbortSignal.any([signal,...(previousSignal?[previousSignal]:[])]);options.signal.throwIfAborted()}
201
+ if(signalAccess.canWrite){options.signal=AbortSignal.any([signal,...(previousSignal?[previousSignal]:[])]);changedSignal=true;options.signal.throwIfAborted()}
141
202
  else{leaseSignal=signal;signal.throwIfAborted()}
142
203
  record.state='running'
143
204
  if(managed)await store.recordModelReceipt(record)
144
205
  else{
145
- const value=await createBudgetReservations({state:callStore(record,config,estimate)}).checkpointExecution({reservationId:accepted.reservationId,ownerToken:accepted.ownerToken,run:{id:record.runId,createdAt:record.createdAt,status:'running',packages:[],reviews:[]},startedIds:['inference'],...limits,checkBudget:true})
206
+ const value=await createBudgetReservations({state:callStore(record,config,estimate,assertFunding)}).checkpointExecution({reservationId:accepted.reservationId,ownerToken:accepted.ownerToken,run:{id:record.runId,createdAt:record.createdAt,status:'running',packages:[],reviews:[]},startedIds:['inference'],...limits,checkBudget:true})
146
207
  if(value.accepted===false)fail('MODEL_WAITING_BUDGET')
147
208
  }
148
209
  authority.assertModel(identity.subject,options.provider,options.model)
149
- if(carrier)options.signal.throwIfAborted();else signal.throwIfAborted()
210
+ assertFunding()
211
+ if(signalAccess.canWrite)options.signal.throwIfAborted();else signal.throwIfAborted()
150
212
  inner=next()[Symbol.asyncIterator]();started=true;ready.resolve(true)
151
213
  await done.promise
152
214
  },{signal:controller.signal})
@@ -176,15 +238,17 @@ export function createModelAccounting({store,authority,pricingFor,inputBoundFor,
176
238
  try{await inner?.return?.()}catch(e){cleanupError=e}
177
239
  done.resolve();try{await guard}catch(e){primaryError??=e}
178
240
  aborted=Boolean(options.signal?.aborted||controller.signal.aborted||previousSignal?.aborted)
179
- if(descriptor)Object.defineProperty(options,'signal',descriptor);else delete options.signal
180
- if(changedOutput){if(tokenDescriptor)Object.defineProperty(options,'maxTokens',tokenDescriptor);else delete options.maxTokens}
241
+ // Restore exactly what this call wrote, and nothing it did not: a request
242
+ // the Host never touched keeps its own shape, sealed or frozen alike.
243
+ if(changedSignal){if(signalAccess.descriptor)Object.defineProperty(options,'signal',signalAccess.descriptor);else delete options.signal}
244
+ if(changedOutput){if(tokenAccess.descriptor)Object.defineProperty(options,'maxTokens',tokenAccess.descriptor);else delete options.maxTokens}
181
245
  record.finishedAt=now();record.usage=observed.usage;record.usageComplete=terminal&&observed.complete;record.finishKind=finishKind
182
246
  record.state=started&&(!terminal||cleanupError)?'unknown':'settled'
183
247
  record.outcome=!started?'cancelled':!terminal||cleanupError?'unknown':aborted||finishKind==='aborted'?'cancelled':finishKind==='error'?'failed':'succeeded'
184
248
  try{
185
249
  if(managed){if(lease)await store.recordModelReceipt(record)}
186
250
  else if(accepted?.accepted){
187
- const run=build();await createBudgetReservations({state:callStore(record,config,estimate)}).finishExecution({reservationId:accepted.reservationId,ownerToken:accepted.ownerToken,run})
251
+ const run=build();await createBudgetReservations({state:callStore(record,config,estimate,assertFunding)}).finishExecution({reservationId:accepted.reservationId,ownerToken:accepted.ownerToken,run})
188
252
  }
189
253
  }finally{
190
254
  if(lease)authority.release(lease.token);active.delete(options);controllerSet.delete(controller)
@@ -0,0 +1,187 @@
1
+ // The same-generation seam for the Bot's auxiliary model calls.
2
+ //
3
+ // DS Bot makes three model requests that are not a conversation step: the handoff note
4
+ // (`writeNote`), the in-place checkpoint (`writeCheckpoint`) and the context steward's
5
+ // decision (`askSteward`). All three are bare `ctx.llm.stream({purpose:'compaction'})`
6
+ // calls, so the SDK resolves the route's current registration at dispatch — the same
7
+ // mispricing window `model-prepared-generation.mjs` closes for the independent path and
8
+ // `model-loop-frame.mjs` reads around for the integrated conversation. An auxiliary call
9
+ // cannot borrow either: the session log's `request/header` fold is the conversation's own
10
+ // metadata (experiment E3 in
11
+ // tmp/integration/integrated-generation-study-1/experiment.mjs: an aux call read
12
+ // generation A and dispatched generation B), and an aux request carries no loop marker.
13
+ //
14
+ // This module puts the auxiliary calls on the one public seam that binds metadata to its
15
+ // own dispatch: `LlmRuntime.prepareCall(config, signal, configure)` (dsh-llm
16
+ // lib/index.js:2220-2261). It composes the product's own prepared dispatcher, so the
17
+ // auxiliary request is prepared once, its exact capacities are recorded in the Host-owned
18
+ // frame store the accounting already reads, and its one-shot stream dispatches that same
19
+ // capture through the ordinary `llm/stream` waterfall exactly once — no listener
20
+ // re-prepares, no second waterfall, no second receipt.
21
+ //
22
+ // Three rules hold the trust boundary:
23
+ //
24
+ // 1. Identity is Host state, resolved before anything is prepared. `identityFor` is the
25
+ // parent's own `policy.modelIdentity`, which reads the principal directory for the
26
+ // request's Session. A request that names a Bot, a subject, a grant or a funding
27
+ // allowance of its own is refused: those are never caller inputs.
28
+ // 2. The Host's own redactor runs before preparation. The frozen ds-bot secret listener
29
+ // (vendor src/host/secrets.js:505-514) rebuilds a request and re-enters
30
+ // `ctx.llm.stream` when it still finds a value; a prepared generation cannot survive
31
+ // that rebuild, and the product listener refuses it
32
+ // (`MODEL_PREPARED_REQUEST_REWRITTEN`) rather than price one generation and stream
33
+ // another. Redacting first is what makes the rewrite unnecessary, exactly as the
34
+ // independent path does.
35
+ // 3. Nothing here holds money. The allowance read through `fundingFor` is only a capacity
36
+ // to clamp the dispatched output bound against, before the SDK freezes the config; the
37
+ // one accounting remains the sole authority over reservations, receipts and cost, and
38
+ // a missing or throwing read is treated as unpriced so it refuses in its own coded way.
39
+ //
40
+ // The steward's `reasoningEffort:'off'` is decided from the generation being prepared. The
41
+ // prepared handle deliberately does not expose the model's reasoning metadata (dsh-llm
42
+ // lib/index.js:2241-2248), and `resolveModelInfo` reads the current registration — another
43
+ // generation — so the only public place that knows is the `configure` callback the SDK
44
+ // itself calls with the captured `modelInfo`. `createPreparedModelDispatch` owns that
45
+ // callback and does not expose it, so this module supplies the runtime it prepares through:
46
+ // a Host-owned service whose `prepareCall` wraps the real one and adds the caller's control
47
+ // selection to the SDK's own callback. No SDK file, vendor file or global is touched, the
48
+ // wrapper is built per call (no shared state, no concurrency hazard), and an effort the
49
+ // prepared generation does not support is never sent.
50
+
51
+ import { createPreparedModelDispatch, redactPreparedModelRequest } from './model-prepared-generation.mjs'
52
+
53
+ const fail = (code, detail) => { throw Object.assign(new Error(code), { code, ...detail }) }
54
+
55
+ /**
56
+ * Request keys an auxiliary caller may never supply.
57
+ *
58
+ * Identity, authority and money are committed Host state. A request that carries any of
59
+ * them is refused rather than silently ignored: a caller that believes it named the Bot
60
+ * must not go on believing the call ran for that Bot.
61
+ */
62
+ const UNTRUSTED_KEYS = Object.freeze([
63
+ 'botId', 'subject', 'principal', 'grant', 'authority', 'identity', 'token', 'lease', 'binding',
64
+ 'funding', 'allowance', 'maxOutputTokens', 'maxInputTokens', 'pricing', 'scopeId', 'projectId',
65
+ ])
66
+
67
+ /**
68
+ * The steward's control selection: `reasoningEffort:'off'` when — and only when — the
69
+ * generation being prepared declares that effort.
70
+ *
71
+ * @param info - the prepared generation's normalized model info, exactly as the SDK passes
72
+ * it to `configure` (dsh-llm lib/index.js:2228).
73
+ * @returns `{reasoningEffort:'off'}`, or `undefined` when the generation supports no `off`
74
+ * effort or declares no reasoning at all, in which case the adapter's own default stands
75
+ * — the behaviour the bare stream had.
76
+ */
77
+ export function auxiliaryReasoningOff(info) {
78
+ const efforts = info?.reasoning?.efforts
79
+ if (!Array.isArray(efforts)) return undefined
80
+ return efforts.some(effort => effort && effort.id === 'off') ? { reasoningEffort: 'off' } : undefined
81
+ }
82
+
83
+ /**
84
+ * Wrap one runtime so its `prepareCall` adds the caller's control selection to the SDK's
85
+ * own `configure` callback.
86
+ *
87
+ * The wrapper is a Host-owned object whose prototype is the live service, so every other
88
+ * member of `LlmRuntime` is the real one and the real service stays `this` inside
89
+ * `prepareCall`. The decorated object is built per call and never escapes this module.
90
+ */
91
+ function withConfiguredControls(runtime, controlsFor) {
92
+ if (typeof controlsFor !== 'function') return runtime
93
+ const decorated = Object.create(runtime)
94
+ Object.defineProperty(decorated, 'prepareCall', {
95
+ value: (config, signal, configure) => runtime.prepareCall(config, signal, (controls, info) => {
96
+ let selected
97
+ try { selected = controlsFor(info) } catch (error) {
98
+ fail('MODEL_AUXILIARY_CONTROLS_REJECTED', { reason: String(error?.code ?? error?.message ?? error) })
99
+ }
100
+ // Only the call-config fields matter to the SDK; anything else a control selection
101
+ // returns is dropped rather than forwarded into the dispatched request.
102
+ let next = controls
103
+ if (selected && typeof selected === 'object') {
104
+ next = { ...controls }
105
+ for (const [key, value] of Object.entries(selected)) if (value !== undefined) next[key] = value
106
+ }
107
+ // The dispatcher's own configure still decides the output bound and records the
108
+ // capacities the accounting admits on; this wrapper only adds the caller's selection.
109
+ return configure(next, info)
110
+ }),
111
+ writable: true, configurable: true, enumerable: false,
112
+ })
113
+ return decorated
114
+ }
115
+
116
+ /**
117
+ * The prepared auxiliary stream for the Bot's note, checkpoint and steward calls.
118
+ *
119
+ * @param llm - the live `llm` service, or a function returning it. A Host whose runtime
120
+ * exposes no `prepareCall` is refused per call (`MODEL_PREPARED_UNAVAILABLE`), never
121
+ * downgraded to the unprepared stream that re-resolves the model at dispatch.
122
+ * @param frames - the shared {@link createModelFrameStore} the one accounting reads. The
123
+ * same store the independent path and the composition use, so an auxiliary frame is
124
+ * admitted by the same authority and for exactly as long as its one dispatch.
125
+ * @param fundingFor - `({botId, provider, model}) => funding | null`, the Host's committed
126
+ * funding read. Only its priced output allowance is used, and only as a capacity to clamp
127
+ * against; a throw or a null is unpriced, and the accounting still refuses in its own way.
128
+ * @param identityFor - `(request) => {botId, subject, ...} | null`, the parent's trusted
129
+ * Host identity read (`policy.modelIdentity`). It is the only source of the Bot and the
130
+ * subject; a request that names its own is refused.
131
+ * @param redact - the Host's public string redactor, applied to `messages` and `system`
132
+ * before preparation so the frozen secret listener has nothing to rebuild.
133
+ * @param carryUsageTag - optional `(from, to) => void`, the runtime's own usage-tag carry,
134
+ * so a redaction rebuild keeps the DS Bot usage provenance the tag records.
135
+ * @returns a callable `stream(request, {controls})` — also reachable as `.stream` — that is
136
+ * an async iterable of chunks, plus `reasoningOff` for the steward's decision.
137
+ */
138
+ export function createAuxiliaryModelStream({
139
+ llm = null, frames = null, fundingFor = null, identityFor = null, redact = null, carryUsageTag = null,
140
+ } = {}) {
141
+ if (!frames || typeof frames.set !== 'function' || typeof frames.read !== 'function') fail('MODEL_FRAME_STORE_REQUIRED')
142
+ if (typeof identityFor !== 'function') fail('MODEL_AUXILIARY_IDENTITY_REQUIRED')
143
+ if (typeof redact !== 'function') fail('MODEL_AUXILIARY_REDACTION_REQUIRED')
144
+ const service = () => (typeof llm === 'function' ? llm() : llm)
145
+
146
+ async function* stream(request, context = {}) {
147
+ if (!request || typeof request !== 'object' || Array.isArray(request)) fail('MODEL_AUXILIARY_REQUEST_REQUIRED')
148
+ for (const key of UNTRUSTED_KEYS) if (request[key] !== undefined) {
149
+ fail('MODEL_AUXILIARY_IDENTITY_UNTRUSTED', { reason: key })
150
+ }
151
+ if (typeof request.provider !== 'string' || request.provider === '' || typeof request.model !== 'string' || request.model === '') {
152
+ fail('MODEL_AUXILIARY_ROUTE_REQUIRED')
153
+ }
154
+ if (!Array.isArray(request.messages)) fail('MODEL_AUXILIARY_MESSAGES_REQUIRED')
155
+
156
+ // 1. Trusted identity, before anything is prepared: whose call this is, decided by the
157
+ // Host's own directory for this request's Session — never by the request.
158
+ let identity = null
159
+ try { identity = await identityFor(request) } catch (error) {
160
+ fail('MODEL_AUXILIARY_IDENTITY_REQUIRED', { reason: String(error?.code ?? error?.message ?? error) })
161
+ }
162
+ const botId = identity?.botId
163
+ if (typeof botId !== 'string' || botId === '') fail('MODEL_AUXILIARY_IDENTITY_REQUIRED')
164
+
165
+ // 2. Host redaction, before the generation is bound. The redactor is the Host's own; a
166
+ // value it finds is replaced here, so the frozen secret listener passes this request
167
+ // straight through instead of rebuilding it and losing the prepared generation.
168
+ const redacted = redactPreparedModelRequest(request, redact)
169
+ if (typeof carryUsageTag === 'function') carryUsageTag(request.messages, redacted.messages)
170
+
171
+ // 3. One preparation, one dispatch, one frame. The dispatcher is built per call so the
172
+ // control selection above belongs to this request alone, and the shared frame store
173
+ // is what the accounting reads.
174
+ const runtime = service()
175
+ if (!runtime || typeof runtime.prepareCall !== 'function') fail('MODEL_PREPARED_UNAVAILABLE')
176
+ const dispatch = createPreparedModelDispatch({
177
+ llm: () => withConfiguredControls(runtime, typeof context.controls === 'function' ? context.controls : null),
178
+ frames,
179
+ ...(typeof fundingFor === 'function' ? { fundingFor } : {}),
180
+ })
181
+ yield* dispatch.dispatch(redacted, { botId })
182
+ }
183
+
184
+ stream.stream = stream
185
+ stream.reasoningOff = auxiliaryReasoningOff
186
+ return Object.freeze(stream)
187
+ }
@@ -0,0 +1,179 @@
1
+ // The same-generation frame for the integrated AgentLoop path, read from the session log.
2
+ //
3
+ // The installed SDK's `LlmRuntime.prepareCall` is the only public seam that binds one
4
+ // adapter registration's metadata to its own dispatch (dsh-llm lib/index.js 2220-2261:
5
+ // capture the registration at 2224, one-shot dispatch at 2249-2258). The integrated
6
+ // AgentLoop already uses it (dsh-agent-loop lib/index.js:1201) and, BEFORE dispatch,
7
+ // synchronously logs that exact generation into its Session: `request/header` with the
8
+ // resolved config plus the `adapterDefaults` flags (lib/index.js:1226-1240) and
9
+ // `request/context` with the context window (lib/index.js:1262-1271). The fold of those
10
+ // events (`Session.requestHeader()` / `Session.requestContext()`, dsh-session
11
+ // lib/index.js:1612/1627) therefore carries the generation the loop's frozen marked
12
+ // request will dispatch, even when the current registration was replaced after the
13
+ // loop's prepareCall — proven causally by experiment E1 in
14
+ // tmp/integration/integrated-generation-study-1/experiment.mjs (dispatch A, current
15
+ // registration read B, session-log fold A).
16
+ //
17
+ // The fold is the CONVERSATION's latest state, not a per-call receipt: it is not, by
18
+ // itself, proof that it describes this one call. This module therefore derives a frame
19
+ // only for a request that carries every public anchor the AgentLoop leaves on it, and
20
+ // answers with a verdict the caller must act on rather than a bare frame-or-null:
21
+ //
22
+ // frame the fold describes this exact request; price and admit on it.
23
+ // aux not a conversation step (no loop marker, or an aux purpose). The
24
+ // caller keeps its existing path — an ordinary request, or a
25
+ // caller-owned prepared call that carries its own frame.
26
+ // unsupportedHost the Host could not answer at all (no session lookup wired, a
27
+ // sessions service that is not a SessionStore, a throwing read).
28
+ // The product refuses API funding; subscription callers may retain
29
+ // their unpriced path. This verdict supplies no captured frame.
30
+ // missing a loop step whose record is absent or unusable (no fold, no live
31
+ // session, no materialized output bound). The caller refuses the
32
+ // funded call rather than price another generation.
33
+ // invalid a loop step whose record contradicts the request (route drift,
34
+ // control drift, session identity mismatch). The caller refuses.
35
+ //
36
+ // SDK-free by construction: the loop-marker predicate and the session lookup are
37
+ // injected, so orchestration keeps its no-direct-SDK-import shape and a request cannot
38
+ // reach a conversation frame it did not earn from the SDK's own marker.
39
+
40
+ const positive = value => Number.isSafeInteger(value) && value > 0 ? value : null
41
+ const isId = value => typeof value === 'string' && value.length > 0 && value.length <= 512
42
+
43
+ /** What one resolution found. See the module comment for what the caller owes each. */
44
+ export const LOOP_FRAME_VERDICT = Object.freeze({
45
+ FRAME: 'frame', AUX: 'aux', UNSUPPORTED_HOST: 'unsupportedHost', MISSING: 'missing', INVALID: 'invalid',
46
+ })
47
+
48
+ /** The reason attached to a non-frame verdict; stable, loggable, never a message. */
49
+ export const LOOP_FRAME_REASON = Object.freeze({
50
+ HOST_LOOKUP_UNWIRED: 'host-lookup-unwired',
51
+ HOST_LOOKUP_FAILED: 'host-lookup-failed',
52
+ HOST_SESSION_SERVICE_UNUSABLE: 'host-session-service-unusable',
53
+ HOST_SESSION_NOT_LIVE: 'host-session-not-live',
54
+ HOST_SESSION_IDENTITY_MISMATCH: 'host-session-identity-mismatch',
55
+ HOST_FOLD_UNREADABLE: 'host-fold-unreadable',
56
+ LOOP_SESSION_ID_MISSING: 'loop-session-id-missing',
57
+ LOOP_REQUEST_NOT_CANONICAL: 'loop-request-not-canonical',
58
+ LOOP_HEADER_FOLD_ABSENT: 'loop-header-fold-absent',
59
+ LOOP_CONTEXT_FOLD_ABSENT: 'loop-context-fold-absent',
60
+ LOOP_ROUTE_DRIFT: 'loop-route-drift',
61
+ LOOP_CONTROL_DRIFT: 'loop-control-drift',
62
+ LOOP_CONTEXT_WINDOW_UNBOUND: 'loop-context-window-unbound',
63
+ LOOP_OUTPUT_BOUND_UNMATERIALIZED: 'loop-output-bound-unmaterialized',
64
+ })
65
+
66
+ const verdict = (kind, reason) => Object.freeze(reason === undefined ? { kind } : { kind, reason })
67
+
68
+ // The call-config fields the SDK compares for one call (`callConfigEquals`, dsh-llm
69
+ // lib/index.js:367-371). The loop request is spread from the logged header's config, so
70
+ // any disagreement means the fold in hand belongs to a different step of the
71
+ // conversation — a stale fold, not this dispatch.
72
+ const CONTROL_FIELDS = Object.freeze(['provider', 'model', 'reasoningEffort', 'temperature', 'maxTokens'])
73
+ const controlDrift = (config, request) => {
74
+ for (const field of CONTROL_FIELDS) if (config[field] !== request[field]) return LOOP_FRAME_REASON.LOOP_CONTROL_DRIFT
75
+ if (config.stop === undefined || request.stop === undefined) {
76
+ if (config.stop !== request.stop) return LOOP_FRAME_REASON.LOOP_CONTROL_DRIFT
77
+ } else if (config.stop.length !== request.stop.length || config.stop.some((item, index) => item !== request.stop[index])) {
78
+ return LOOP_FRAME_REASON.LOOP_CONTROL_DRIFT
79
+ }
80
+ return null
81
+ }
82
+
83
+ /**
84
+ * Derive a Host-owned same-generation frame for one integrated-loop request.
85
+ *
86
+ * @param sessionFor - `(sessionId) => session | null | undefined`; in the composed
87
+ * product this is `id => ctx.get('sessions')?.get?.(id)` — the Host SessionStore the
88
+ * AgentLoop itself appends through (dsh-session lib/index.js:1980). Absent, throwing
89
+ * or not a SessionStore is `unsupportedHost`, never an error and never a frame.
90
+ * @param isLoopRequest - the SDK's `isAgentLoopRequest` (re-exported through
91
+ * modules/bot-runtime), so a hand-built request carrying a conversation's `sessionId`
92
+ * cannot reach that conversation's frame: the marker is a process-private WeakSet the
93
+ * SDK alone can add to (dsh-llm lib/index.js:377-388), and it does not survive a copy.
94
+ * @returns `(request) => verdict`, where a `frame` verdict carries the frozen
95
+ * `{provider, model, bounds: {contextWindow, defaultMaxTokens}, request}` shape
96
+ * model-accounting.mjs:89-96 consumes.
97
+ */
98
+ export function createSessionLoopFrameResolver({ sessionFor = null, isLoopRequest = null, allowUnbounded = null } = {}) {
99
+ const lookup = typeof sessionFor === 'function' ? sessionFor : null
100
+ const marked = typeof isLoopRequest === 'function' ? isLoopRequest : null
101
+ const aux = () => verdict(LOOP_FRAME_VERDICT.AUX)
102
+ const unsupported = reason => verdict(LOOP_FRAME_VERDICT.UNSUPPORTED_HOST, reason)
103
+
104
+ // Without the SDK's marker predicate nothing can be told apart, so the resolver is
105
+ // inert. A request shaped like a loop envelope is reported as unsupported — the gap
106
+ // is open — while everything else keeps the path it already had. The shape is a
107
+ // heuristic for reporting only; it grants nothing.
108
+ const loopShaped = request => request && typeof request === 'object' && isId(request.sessionId)
109
+ && request.purpose === undefined && Object.isFrozen(request)
110
+ && Array.isArray(request.messages) && Object.isFrozen(request.messages)
111
+ if (!marked) return request => (loopShaped(request) ? unsupported(LOOP_FRAME_REASON.HOST_LOOKUP_UNWIRED) : aux())
112
+ // The Host did not wire the session seam at all. A loop step is reported as
113
+ // unsupported — the gap is open — while an ordinary or aux request keeps its path.
114
+ if (!lookup) return request => (request && typeof request === 'object' && marked(request) && request.purpose === undefined
115
+ ? unsupported(LOOP_FRAME_REASON.HOST_LOOKUP_UNWIRED)
116
+ : aux())
117
+
118
+ return request => {
119
+ if (!request || typeof request !== 'object' || !marked(request)) return aux()
120
+ // A marked request that names an aux purpose is not a conversation step: the
121
+ // conversation's frame must never price a compaction, a title or a note
122
+ // (experiment E3), and a caller-owned prepared aux binds its own dispatch anyway.
123
+ if (request.purpose !== undefined) return aux()
124
+ if (!isId(request.sessionId)) return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.LOOP_SESSION_ID_MISSING)
125
+ // The loop freezes its request envelope and the messages it derived
126
+ // (dsh-agent-loop lib/index.js:1274-1288). A request that is not that envelope is
127
+ // not the one the fold was logged for, whatever it claims.
128
+ if (!Object.isFrozen(request) || !Array.isArray(request.messages) || !Object.isFrozen(request.messages)) {
129
+ return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.LOOP_REQUEST_NOT_CANONICAL)
130
+ }
131
+ let session = null
132
+ try { session = lookup(request.sessionId) } catch { return unsupported(LOOP_FRAME_REASON.HOST_LOOKUP_FAILED) }
133
+ if (session === null || session === undefined) return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.HOST_SESSION_NOT_LIVE)
134
+ if (typeof session.requestContext !== 'function' || typeof session.requestHeader !== 'function') {
135
+ return unsupported(LOOP_FRAME_REASON.HOST_SESSION_SERVICE_UNUSABLE)
136
+ }
137
+ // The lookup must answer about the session the request names, not some other one.
138
+ if (session.id !== request.sessionId) return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.HOST_SESSION_IDENTITY_MISMATCH)
139
+ let context = null, header = null
140
+ try { context = session.requestContext(); header = session.requestHeader() } catch { return unsupported(LOOP_FRAME_REASON.HOST_FOLD_UNREADABLE) }
141
+ if (!context || typeof context !== 'object') return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.LOOP_CONTEXT_FOLD_ABSENT)
142
+ if (!header || typeof header !== 'object' || !header.config || typeof header.config !== 'object') {
143
+ return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.LOOP_HEADER_FOLD_ABSENT)
144
+ }
145
+ // The fold must describe this exact route on both surfaces. A conversation that
146
+ // moved to another route between the loop's log and this read has no frame for it,
147
+ // and pricing the old route's window against the new endpoint is the very mixing
148
+ // this module exists to prevent.
149
+ if (context.provider !== request.provider || context.model !== request.model
150
+ || header.config.provider !== request.provider || header.config.model !== request.model) {
151
+ return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.LOOP_ROUTE_DRIFT)
152
+ }
153
+ const contextWindow = positive(context.contextWindow)
154
+ if (controlDrift(header.config, request) !== null) return verdict(LOOP_FRAME_VERDICT.INVALID, LOOP_FRAME_REASON.LOOP_CONTROL_DRIFT)
155
+ // Only an explicitly Host-classified subscription route may preserve unknown
156
+ // capacities. It still carries the same canonical prepared route and controls;
157
+ // accounting never receives fabricated capacity or an API estimate from it.
158
+ const unbounded = typeof allowUnbounded === 'function' && allowUnbounded(request) === true
159
+ if (contextWindow === null && !unbounded) return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.LOOP_CONTEXT_WINDOW_UNBOUND)
160
+ // The dispatched output bound is already materialized into the frozen request by
161
+ // the SDK's own call-config resolution (dsh-llm lib/index.js:2183-2186), so the
162
+ // generation that streams is the one admitted. A loop step that carries no
163
+ // materialized bound (the NO_ADAPTER fallback at dsh-agent-loop lib/index.js:1084
164
+ // dispatches a bare `llm.stream`) has no same-generation frame: the caller refuses
165
+ // it instead of falling back to a read that can name the other generation.
166
+ const dispatched = positive(request.maxTokens)
167
+ if (dispatched === null && !unbounded) return verdict(LOOP_FRAME_VERDICT.MISSING, LOOP_FRAME_REASON.LOOP_OUTPUT_BOUND_UNMATERIALIZED)
168
+ // `adapterDefaults.maxTokens === true` marks that the SDK materialized the adapter's
169
+ // default into the logged config (dsh-llm lib/index.js:2236-2239); that value is the
170
+ // output default this generation dispatches. A bound the caller declared needs no
171
+ // default: the request already carries it.
172
+ const defaultMaxTokens = header.adapterDefaults?.maxTokens === true ? dispatched : null
173
+ const frame = Object.freeze({
174
+ provider: request.provider, model: request.model,
175
+ bounds: Object.freeze({ contextWindow, defaultMaxTokens }), request,
176
+ })
177
+ return Object.freeze({ kind: LOOP_FRAME_VERDICT.FRAME, frame })
178
+ }
179
+ }