@opengeni/sdk 6.1.0 → 6.2.2-canary.41203942106001

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 (362) hide show
  1. package/README.md +974 -185
  2. package/dist/adapters/express.d.ts +31 -0
  3. package/dist/adapters/express.js +87 -0
  4. package/dist/adapters/express.js.map +1 -0
  5. package/dist/adapters/hono.d.ts +16 -0
  6. package/dist/adapters/hono.js +8 -0
  7. package/dist/adapters/hono.js.map +1 -0
  8. package/dist/adapters/next.d.ts +29 -0
  9. package/dist/adapters/next.js +22 -0
  10. package/dist/adapters/next.js.map +1 -0
  11. package/dist/allowance-refusal.d.ts +2 -0
  12. package/dist/allowance-refusal.js +50 -0
  13. package/dist/allowance-refusal.js.map +1 -0
  14. package/dist/app-links.d.ts +99 -0
  15. package/dist/app-links.js +125 -0
  16. package/dist/app-links.js.map +1 -0
  17. package/dist/artifact-catalog.d.ts +13 -0
  18. package/dist/artifacts.js +9 -6
  19. package/dist/azure-live-transport.d.ts +32 -0
  20. package/dist/browser.d.ts +6 -2
  21. package/dist/browser.js +31 -10
  22. package/dist/chat/fold.d.ts +30 -1
  23. package/dist/chat/handler.d.ts +7 -5
  24. package/dist/chat/http.d.ts +4 -5
  25. package/dist/chat/index.d.ts +4 -2
  26. package/dist/chat/index.js +288 -351
  27. package/dist/chat/index.js.map +1 -1
  28. package/dist/chat/openai.d.ts +3 -3
  29. package/dist/chat/opengeni.d.ts +47 -11
  30. package/dist/chat/types.d.ts +83 -8
  31. package/dist/chat/vercel.d.ts +22 -4
  32. package/dist/chats.d.ts +8 -0
  33. package/dist/chunk-4B6PFXUO.js +47 -0
  34. package/dist/chunk-4B6PFXUO.js.map +1 -0
  35. package/dist/{chunk-4ZQQQBOH.js → chunk-7BUHPUFW.js} +2 -2
  36. package/dist/chunk-7L7D2AQH.js +17 -0
  37. package/dist/chunk-7L7D2AQH.js.map +1 -0
  38. package/dist/chunk-7RROJRPC.js +347 -0
  39. package/dist/chunk-7RROJRPC.js.map +1 -0
  40. package/dist/chunk-7Y47O2IY.js +210 -0
  41. package/dist/chunk-7Y47O2IY.js.map +1 -0
  42. package/dist/chunk-CB6D3GDF.js +366 -0
  43. package/dist/chunk-CB6D3GDF.js.map +1 -0
  44. package/dist/{chunk-7S2MZ65L.js → chunk-CWNNZPKL.js} +285 -41
  45. package/dist/chunk-CWNNZPKL.js.map +1 -0
  46. package/dist/chunk-ED3E7UIK.js +20 -0
  47. package/dist/chunk-ED3E7UIK.js.map +1 -0
  48. package/dist/chunk-EJDJORK2.js +561 -0
  49. package/dist/chunk-EJDJORK2.js.map +1 -0
  50. package/dist/chunk-HW3HBKAA.js +410 -0
  51. package/dist/chunk-HW3HBKAA.js.map +1 -0
  52. package/dist/chunk-KFTMQIVN.js +15 -0
  53. package/dist/chunk-KFTMQIVN.js.map +1 -0
  54. package/dist/chunk-KPNODZ56.js +1483 -0
  55. package/dist/chunk-KPNODZ56.js.map +1 -0
  56. package/dist/{chunk-QTBAMHEF.js → chunk-LEZERTRP.js} +93 -18
  57. package/dist/chunk-LEZERTRP.js.map +1 -0
  58. package/dist/chunk-QGEIL4TR.js +195 -0
  59. package/dist/chunk-QGEIL4TR.js.map +1 -0
  60. package/dist/chunk-R3X7MEQH.js +697 -0
  61. package/dist/chunk-R3X7MEQH.js.map +1 -0
  62. package/dist/chunk-S5CGKCMV.js +153 -0
  63. package/dist/chunk-S5CGKCMV.js.map +1 -0
  64. package/dist/{chunk-IZH5F7QC.js → chunk-SWROK57B.js} +25 -2
  65. package/dist/chunk-SWROK57B.js.map +1 -0
  66. package/dist/chunk-UM4ZXXJR.js +9 -0
  67. package/dist/chunk-UM4ZXXJR.js.map +1 -0
  68. package/dist/chunk-XCOAVV67.js +120 -0
  69. package/dist/chunk-XCOAVV67.js.map +1 -0
  70. package/dist/chunk-XZ4W25HT.js +156 -0
  71. package/dist/chunk-XZ4W25HT.js.map +1 -0
  72. package/dist/chunk-Y7DCU6MW.js +1990 -0
  73. package/dist/chunk-Y7DCU6MW.js.map +1 -0
  74. package/dist/chunk-YSQU5CHX.js +81 -0
  75. package/dist/chunk-YSQU5CHX.js.map +1 -0
  76. package/dist/chunk-YTHNS27C.js +283 -0
  77. package/dist/chunk-YTHNS27C.js.map +1 -0
  78. package/dist/{chunk-64S3VKJU.js → chunk-YYSOUVE4.js} +3665 -908
  79. package/dist/chunk-YYSOUVE4.js.map +1 -0
  80. package/dist/{chunk-CMEU67C4.js → chunk-ZGO4RCSW.js} +26 -3
  81. package/dist/chunk-ZGO4RCSW.js.map +1 -0
  82. package/dist/{chunk-VYCTL62C.js → chunk-ZHX765BU.js} +1 -1
  83. package/dist/chunk-ZHX765BU.js.map +1 -0
  84. package/dist/client.d.ts +1084 -114
  85. package/dist/codex-realtime-controller.d.ts +38 -1
  86. package/dist/codex-realtime-controller.js +4 -2
  87. package/dist/codex-realtime-lifecycle.d.ts +3 -0
  88. package/dist/codex-realtime-v3-wire.d.ts +3 -2
  89. package/dist/codex-realtime-v3.d.ts +14 -0
  90. package/dist/codex-realtime.d.ts +5 -1
  91. package/dist/connected-machine-output.d.ts +6 -0
  92. package/dist/core.d.ts +3 -1
  93. package/dist/core.js +25 -11
  94. package/dist/desktop-bridge.d.ts +811 -0
  95. package/dist/desktop-bridge.js +33 -0
  96. package/dist/document-authority-client.d.ts +8 -2
  97. package/dist/document-authority.js +8 -5
  98. package/dist/editable-artifacts/index.d.ts +1 -1
  99. package/dist/editable-artifacts/types.d.ts +12 -2
  100. package/dist/editable-artifacts.js +22 -6
  101. package/dist/editable-artifacts.js.map +1 -1
  102. package/dist/embedding-client.d.ts +82 -25
  103. package/dist/errors.d.ts +91 -1
  104. package/dist/external-vault-receiver.d.ts +203 -0
  105. package/dist/external-vault-receiver.js +564 -0
  106. package/dist/external-vault-receiver.js.map +1 -0
  107. package/dist/feedback.d.ts +68 -0
  108. package/dist/gateway-realtime-transport.d.ts +4 -0
  109. package/dist/gateway-realtime-transport.js +6 -1
  110. package/dist/index.d.ts +58 -19
  111. package/dist/index.js +1535 -258
  112. package/dist/index.js.map +1 -1
  113. package/dist/insights-usage.d.ts +37 -0
  114. package/dist/insights-usage.js +7 -0
  115. package/dist/insights-usage.js.map +1 -0
  116. package/dist/interaction.d.ts +311 -74
  117. package/dist/interaction.js +3 -2
  118. package/dist/knowledge.d.ts +5 -3
  119. package/dist/knowledge.js +12 -3
  120. package/dist/knowledge.js.map +1 -1
  121. package/dist/known-tools.d.ts +4 -0
  122. package/dist/learning-refusal.d.ts +6 -0
  123. package/dist/learning-refusal.js +39 -0
  124. package/dist/learning-refusal.js.map +1 -0
  125. package/dist/message-links.d.ts +76 -0
  126. package/dist/model-context.d.ts +9 -1
  127. package/dist/model-display.d.ts +2 -0
  128. package/dist/model-display.js +17 -0
  129. package/dist/model-display.js.map +1 -0
  130. package/dist/model-picker-order.d.ts +1 -1
  131. package/dist/model-picker-order.js +2 -0
  132. package/dist/model-picker-order.js.map +1 -1
  133. package/dist/office/dialog.d.ts +13 -0
  134. package/dist/office/executor.d.ts +24 -0
  135. package/dist/office/host.d.ts +67 -0
  136. package/dist/office/index.d.ts +8 -0
  137. package/dist/office/index.js +601 -0
  138. package/dist/office/index.js.map +1 -0
  139. package/dist/office/manifest.d.ts +31 -0
  140. package/dist/office/selection.d.ts +58 -0
  141. package/dist/office/watch.d.ts +10 -0
  142. package/dist/office/workbook-link.d.ts +59 -0
  143. package/dist/office/workbook.d.ts +27 -0
  144. package/dist/organization-integration-policy.d.ts +35 -0
  145. package/dist/organization-integration-policy.js +26 -0
  146. package/dist/organization-integration-policy.js.map +1 -0
  147. package/dist/prepared-mcp-setup.d.ts +5 -0
  148. package/dist/proxy.d.ts +7 -7
  149. package/dist/read-retry.d.ts +61 -0
  150. package/dist/realtime-transcript.d.ts +5 -0
  151. package/dist/realtime-websocket-transport.d.ts +3 -0
  152. package/dist/realtime-websocket-transport.js +8 -0
  153. package/dist/realtime-websocket-transport.js.map +1 -0
  154. package/dist/realtime.d.ts +12 -6
  155. package/dist/realtime.js +260 -6
  156. package/dist/realtime.js.map +1 -1
  157. package/dist/recording-live.d.ts +146 -0
  158. package/dist/recording-owner.d.ts +11 -0
  159. package/dist/recordings.d.ts +643 -0
  160. package/dist/recordings.js +2239 -0
  161. package/dist/recordings.js.map +1 -0
  162. package/dist/secret-use-approval.d.ts +1 -0
  163. package/dist/secret-use-approval.js +17 -0
  164. package/dist/secret-use-approval.js.map +1 -0
  165. package/dist/session-history-import.d.ts +22 -0
  166. package/dist/session-history-import.js +36 -0
  167. package/dist/session-history-import.js.map +1 -0
  168. package/dist/session-list-entries.d.ts +1 -0
  169. package/dist/session-list-entries.js +7 -0
  170. package/dist/session-list-entries.js.map +1 -0
  171. package/dist/session-message-search.d.ts +56 -0
  172. package/dist/session-proxy-client.d.ts +17 -0
  173. package/dist/session-proxy.d.ts +373 -0
  174. package/dist/session-proxy.js +18 -0
  175. package/dist/session-proxy.js.map +1 -0
  176. package/dist/session-sequence.d.ts +20 -0
  177. package/dist/session-usage-limits.d.ts +10 -0
  178. package/dist/session-usage-limits.js +44 -0
  179. package/dist/session-usage-limits.js.map +1 -0
  180. package/dist/shared-catalog-loader.d.ts +34 -0
  181. package/dist/site-ai.d.ts +35 -0
  182. package/dist/site-bridge-host.d.ts +141 -0
  183. package/dist/site-bridge-host.js +348 -0
  184. package/dist/site-bridge-host.js.map +1 -0
  185. package/dist/site-browser-runtime.gen.d.ts +1 -1
  186. package/dist/site-data.d.ts +182 -0
  187. package/dist/site-document.js +10098 -1
  188. package/dist/site-document.js.map +1 -1
  189. package/dist/site-http.d.ts +2 -1
  190. package/dist/site-sql.d.ts +31 -0
  191. package/dist/site-tool-bridge.d.ts +70 -3
  192. package/dist/site.d.ts +198 -4
  193. package/dist/site.js +48 -6
  194. package/dist/skills.d.ts +9 -0
  195. package/dist/sse.d.ts +1 -1
  196. package/dist/stream.d.ts +37 -2
  197. package/dist/teams.d.ts +31 -0
  198. package/dist/teams.js +21 -0
  199. package/dist/teams.js.map +1 -0
  200. package/dist/tenant-workspaces.d.ts +81 -0
  201. package/dist/tenant-workspaces.js +8 -0
  202. package/dist/tenant-workspaces.js.map +1 -0
  203. package/dist/this-computer.d.ts +245 -0
  204. package/dist/this-computer.js +103 -0
  205. package/dist/this-computer.js.map +1 -0
  206. package/dist/timeline-order.d.ts +2 -0
  207. package/dist/timeline-order.js +170 -0
  208. package/dist/timeline-order.js.map +1 -0
  209. package/dist/tool-auth.d.ts +87 -0
  210. package/dist/tool-auth.js +23 -0
  211. package/dist/tool-auth.js.map +1 -0
  212. package/dist/tool-display-metadata.d.ts +3 -0
  213. package/dist/{tools-FSTY4FAY.js → tools-DUAU2VFG.js} +3 -3
  214. package/dist/tools-DUAU2VFG.js.map +1 -0
  215. package/dist/tools.d.ts +21 -1
  216. package/dist/transcription.d.ts +1 -1
  217. package/dist/types.d.ts +3381 -798
  218. package/dist/usage-allowances.d.ts +36 -0
  219. package/dist/usage-allowances.js +80 -0
  220. package/dist/usage-allowances.js.map +1 -0
  221. package/dist/vault-types.d.ts +378 -0
  222. package/dist/vaults-api.d.ts +71 -0
  223. package/dist/workspace-artifacts.d.ts +60 -0
  224. package/dist/workspace-control-stream.d.ts +5 -0
  225. package/dist/workspace-integrations.d.ts +74 -0
  226. package/dist/workspace-integrations.js +67 -0
  227. package/dist/workspace-integrations.js.map +1 -0
  228. package/dist/workspace-live-stream.d.ts +15 -1
  229. package/package.json +115 -13
  230. package/src/adapters/express.ts +136 -0
  231. package/src/adapters/hono.ts +18 -0
  232. package/src/adapters/next.ts +42 -0
  233. package/src/allowance-refusal.ts +5 -0
  234. package/src/app-links.ts +246 -0
  235. package/src/artifact-catalog.ts +14 -0
  236. package/src/azure-live-transport.ts +286 -0
  237. package/src/browser.ts +30 -1
  238. package/src/chat/fold.ts +199 -34
  239. package/src/chat/handler.ts +10 -6
  240. package/src/chat/http.ts +4 -8
  241. package/src/chat/index.ts +12 -1
  242. package/src/chat/openai.ts +3 -3
  243. package/src/chat/opengeni.ts +315 -75
  244. package/src/chat/types.ts +84 -8
  245. package/src/chat/vercel.ts +42 -11
  246. package/src/chats.ts +16 -0
  247. package/src/client.ts +6107 -2383
  248. package/src/codex-realtime-controller.ts +324 -29
  249. package/src/codex-realtime-lifecycle.ts +22 -10
  250. package/src/codex-realtime-v3-wire.ts +4 -3
  251. package/src/codex-realtime-v3.ts +113 -22
  252. package/src/codex-realtime.ts +11 -3
  253. package/src/connected-machine-output.ts +17 -0
  254. package/src/core.ts +6 -0
  255. package/src/desktop-bridge.ts +890 -0
  256. package/src/document-authority-client.ts +39 -1
  257. package/src/editable-artifacts/controller.ts +36 -2
  258. package/src/editable-artifacts/index.ts +1 -0
  259. package/src/editable-artifacts/types.ts +29 -16
  260. package/src/embedding-client.ts +281 -122
  261. package/src/errors.ts +381 -7
  262. package/src/external-vault-receiver.ts +914 -0
  263. package/src/feedback.ts +68 -0
  264. package/src/gateway-realtime-transport.ts +7 -786
  265. package/src/index.ts +606 -63
  266. package/src/insights-usage.ts +68 -0
  267. package/src/interaction-revision-stream.ts +12 -76
  268. package/src/interaction.ts +500 -103
  269. package/src/knowledge.ts +23 -1
  270. package/src/known-tools.ts +279 -0
  271. package/src/learning-refusal.ts +9 -0
  272. package/src/message-links.ts +195 -0
  273. package/src/model-context.ts +31 -1
  274. package/src/model-display.ts +11 -0
  275. package/src/model-picker-order.ts +4 -0
  276. package/src/office/dialog.ts +50 -0
  277. package/src/office/executor.ts +115 -0
  278. package/src/office/host.ts +69 -0
  279. package/src/office/index.ts +41 -0
  280. package/src/office/manifest.ts +149 -0
  281. package/src/office/selection.ts +197 -0
  282. package/src/office/watch.ts +89 -0
  283. package/src/office/workbook-link.ts +177 -0
  284. package/src/office/workbook.ts +72 -0
  285. package/src/organization-integration-policy.ts +63 -0
  286. package/src/prepared-mcp-setup.ts +123 -0
  287. package/src/proxy.ts +7 -7
  288. package/src/read-retry.ts +208 -0
  289. package/src/realtime-transcript.ts +10 -0
  290. package/src/realtime-websocket-transport.ts +849 -0
  291. package/src/realtime.ts +35 -11
  292. package/src/recording-live.ts +916 -0
  293. package/src/recording-owner.ts +29 -0
  294. package/src/recordings.ts +2396 -0
  295. package/src/retained-artifacts.ts +20 -2
  296. package/src/secret-use-approval.ts +7 -0
  297. package/src/session-history-import.ts +84 -0
  298. package/src/session-list-entries.ts +1 -0
  299. package/src/session-message-search.ts +50 -0
  300. package/src/session-proxy-client.ts +42 -0
  301. package/src/session-proxy.ts +2187 -0
  302. package/src/session-sequence.ts +35 -0
  303. package/src/session-usage-limits.ts +73 -0
  304. package/src/shared-catalog-loader.ts +169 -0
  305. package/src/site-ai.ts +284 -0
  306. package/src/site-bridge-host.ts +518 -0
  307. package/src/site-browser-runtime.gen.ts +10098 -1
  308. package/src/site-data.ts +376 -0
  309. package/src/site-http.ts +12 -3
  310. package/src/site-sql.ts +153 -0
  311. package/src/site-tool-bridge.ts +510 -33
  312. package/src/site.ts +749 -15
  313. package/src/skills.ts +10 -0
  314. package/src/sse.ts +9 -3
  315. package/src/stream.ts +128 -6
  316. package/src/teams.ts +31 -0
  317. package/src/tenant-workspaces.ts +243 -0
  318. package/src/this-computer.ts +418 -0
  319. package/src/timeline-order.ts +9 -0
  320. package/src/tool-auth.ts +301 -0
  321. package/src/tool-display-metadata.ts +27 -0
  322. package/src/tools.ts +81 -25
  323. package/src/transcription.ts +5 -1
  324. package/src/types.ts +3933 -962
  325. package/src/usage-allowances.ts +189 -0
  326. package/src/vault-types.ts +448 -0
  327. package/src/vaults-api.ts +355 -0
  328. package/src/workspace-artifacts.ts +66 -0
  329. package/src/workspace-control-stream.ts +35 -70
  330. package/src/workspace-integrations.ts +403 -0
  331. package/src/workspace-live-stream.ts +58 -84
  332. package/dist/automations-client.d.ts +0 -17
  333. package/dist/automations.d.ts +0 -3
  334. package/dist/automations.js +0 -78
  335. package/dist/automations.js.map +0 -1
  336. package/dist/chunk-64S3VKJU.js.map +0 -1
  337. package/dist/chunk-7S2MZ65L.js.map +0 -1
  338. package/dist/chunk-CFSYTYMA.js +0 -639
  339. package/dist/chunk-CFSYTYMA.js.map +0 -1
  340. package/dist/chunk-CMEU67C4.js.map +0 -1
  341. package/dist/chunk-FMLDFJ2Q.js +0 -246
  342. package/dist/chunk-FMLDFJ2Q.js.map +0 -1
  343. package/dist/chunk-IZH5F7QC.js.map +0 -1
  344. package/dist/chunk-OO5LPTO7.js +0 -132
  345. package/dist/chunk-OO5LPTO7.js.map +0 -1
  346. package/dist/chunk-QTBAMHEF.js.map +0 -1
  347. package/dist/chunk-VDACQXJZ.js +0 -253
  348. package/dist/chunk-VDACQXJZ.js.map +0 -1
  349. package/dist/chunk-VYCTL62C.js.map +0 -1
  350. package/dist/chunk-YKZME56C.js +0 -657
  351. package/dist/chunk-YKZME56C.js.map +0 -1
  352. package/dist/pr-review-client.d.ts +0 -16
  353. package/dist/pr-review.d.ts +0 -4
  354. package/dist/pr-review.js +0 -62
  355. package/dist/pr-review.js.map +0 -1
  356. package/dist/site-document.test.d.ts +0 -1
  357. package/src/automations-client.ts +0 -118
  358. package/src/automations.ts +0 -15
  359. package/src/pr-review-client.ts +0 -92
  360. package/src/pr-review.ts +0 -17
  361. /package/dist/{chunk-4ZQQQBOH.js.map → chunk-7BUHPUFW.js.map} +0 -0
  362. /package/dist/{tools-FSTY4FAY.js.map → desktop-bridge.js.map} +0 -0
package/README.md CHANGED
@@ -1,53 +1,6 @@
1
1
  # @opengeni/sdk
2
2
 
3
- ## External-user admission (implementation branch)
4
-
5
- On a product backend, `client.asUser(externalId, { source? })` returns a separate
6
- client that sends the external actor assertion with the organization key. IDs
7
- are opaque and case-sensitive; the default source is `default`. Keep the key and
8
- this client server-side. Each request requires the intersection of the key's
9
- permissions and the user's explicit workspace membership. API-key rotation does
10
- not change the external identity.
11
-
12
- Use the service client—not an `asUser` client—to call
13
- `addExternalWorkspaceMember(workspaceId, { identity: { externalId, source? }, permissions })`
14
- for explicit onboarding. It requires `members:manage`, cannot grant more than
15
- the key permits, excludes Personal workspaces, and refuses to overwrite an
16
- existing membership with different permissions. Ordinary `asUser` calls never
17
- restore removed workspace membership or reactivate disabled identities.
18
-
19
- Service `removeWorkspaceMember` supports external members through the existing
20
- fenced workspace teardown. Account-wide `updateExternalIdentityMembership(
21
- organizationId, organizationMembershipId, { kind, expectedAuthorizationRevision,
22
- operationId, reason? })` requires explicit `account:admin` and supports suspend,
23
- reactivate, and offboard. The membership ID is returned by onboarding; its initial
24
- authorization revision is 1. Retain the returned revision for subsequent changes.
25
- Reactivation restores admission, not revoked memberships or work. Offboarding
26
- follows existing retention policy and cannot be reactivated through this API.
27
- Native-user targets and `asUser` calls are rejected by this service endpoint.
28
-
29
- An `asUser` client can discover/access its own provisioned Personal workspace
30
- under the key's permission ceiling. Core private session operations retain
31
- organization readiness/settings and explicit sharing acknowledgments; service
32
- clients gain no Personal-workspace fallback.
33
-
34
- Optional native-account delegation uses `beginIdentityLink`, authenticated native
35
- `previewIdentityLink`/`confirmIdentityLink`, and explicit server-side
36
- `asLinkedUser(externalId, { source?, linkId, expectedLinkRevision })`. Confirmation
37
- requires the actual native login plus the one-time host challenge; an organization
38
- key cannot confirm for the native user. `listIdentityLinks(workspaceId, cursor?)`
39
- returns only the effective participant's links in that organization (50 per page).
40
- `revokeIdentityLink` requires the observed revision. Linking never changes ordinary
41
- `asUser`, merges histories or transfers credentials. Link-dependent accepted work
42
- retains revocation checks across schedules and children. Short-lived inline MCP
43
- credentials remain supported, and durable renewal stays opt-in.
44
-
45
- Connect accepts an optional `installationTarget: { instanceKey, displayName,
46
- expectedInstanceVersion? }`. Keep the exact observed version for an existing named
47
- account. Setup freezes the target through callback and operation review; omitting
48
- it creates an independent named account instead of overwriting a default instance.
49
-
50
- Framework-agnostic TypeScript SDK for the OpenGeni public API: a typed client,
3
+ Framework-agnostic TypeScript SDK for the Opengeni public API: a typed client,
51
4
  session lifecycle, and the streaming core — SSE event streaming with automatic
52
5
  reconnect, resume-by-sequence, gap backfill, and duplicate suppression — plus
53
6
  helpers for proxying the stream through your own API.
@@ -57,90 +10,358 @@ workspaces, Personal-workspace exclusion, tenant mapping, and external Skill
57
10
  ownership—start with the canonical
58
11
  [product integration guide](../../docs/product-integration.md).
59
12
 
60
- Zero runtime dependencies. Needs only WHATWG `fetch` and streams, so it runs in
61
- Node 18+, Bun, Deno, browsers, and edge runtimes.
13
+ Runtime dependencies are the published `@opengeni/contracts` wire schemas (which
14
+ bring Zod) and `@opengeni/connect`; nothing else. It needs only WHATWG `fetch`
15
+ and streams, so it runs in Node 18+, Bun, Deno, browsers, and edge runtimes.
16
+ The package is ESM-only (no `require` entry): use `import`, or `await import()`
17
+ from CommonJS.
18
+
19
+ Always pass `baseUrl` (for example `process.env.OPENGENI_API_BASE_URL`): the
20
+ chat facade defaults to production `https://app.opengeni.ai`, so omitting it
21
+ against a staging or self-hosted deployment silently talks to production.
62
22
 
63
23
  Browser clients may call the public API from any origin with an explicitly safe
64
24
  bearer design, but an organization API key belongs on the product server.
65
25
  Browser cookies are accepted cross-origin only from operator-configured trusted
66
26
  origins; arbitrary embedding origins never receive credentialed CORS responses.
67
27
 
68
- ## Chat quick start (`@opengeni/sdk/chat`)
69
-
70
- The fastest way to put OpenGeni behind an existing chat: one option object per
71
- conversation, one server handler for your endpoint. Tenants map to organization
72
- workspaces, conversations map to deterministic sessions, and the organization
73
- API key never leaves your server.
28
+ ## Task usage limits
29
+
30
+ `createSession(workspaceId, { initialMessage, usageLimitMicros })` sets an
31
+ optional API-equivalent model-usage limit on a new root task;
32
+ `getSessionUsageLimit` and `setSessionUsageLimit` (compare-and-set on
33
+ `expectedVersion`, idempotent on `operationId`, `limitMicros: null` clears)
34
+ read and change it. Both writes verify the server's echo and throw when a
35
+ server that predates usage limits would otherwise run the task unlimited. A
36
+ refused message raises `OpenGeniSessionUsageLimitReachedError` (HTTP 402
37
+ `session_usage_limit_reached`), never the allowance error. The browser-safe
38
+ `@opengeni/sdk/session-usage-limits` subpath exports the free functions, the
39
+ refusal parser and neutral wording. The session proxy never forwards limits
40
+ from a browser; set them from your server. See
41
+ [task usage limits](../../docs/session-usage-limits.md).
42
+
43
+ ## Allowance lifecycle recovery
44
+
45
+ `getWorkspaceAllowance(workspaceId)` still returns nullable configuration.
46
+ Use `getWorkspaceAllowanceState(workspaceId)` when you also need the lifecycle
47
+ version: `{ version: 0, config: null }` means never configured, while a positive
48
+ version with null configuration means cleared. Recreation uses that exact
49
+ version as `expectedVersion`.
50
+
51
+ `clearWorkspaceAllowance(workspaceId, { expectedVersion, operationId })` returns
52
+ `{ version }`. The optional operation ID makes a lost response recoverable:
53
+ reuse the same actor, operation ID and exact request. Replay rechecks current
54
+ authority and returns 409 after any later allowance lifecycle change. On 409,
55
+ read the current state and decide whether a new operation is intended; never
56
+ guess a CAS version. The SDK does not automatically retry mutations.
57
+
58
+ The same reads and administration are free functions on the
59
+ `@opengeni/sdk/usage-allowances` subpath for browser code that only has the
60
+ narrow `OpenGeniBrowserClient` (`getMyUsage(client, workspaceId)`,
61
+ `getAllUsage`, `setMemberAllowance`, ...); they call the same routes as the root methods.
62
+
63
+ State reads require the same budget-read authority as configuration reads.
64
+ Full usage additionally requires target workspace access; organization budget
65
+ authority alone does not expose the member roster. Agents cannot read or
66
+ change allowances.
67
+
68
+ ## Embed the conversation (default)
69
+
70
+ Embedding Opengeni is one backend route plus one component, and you only speak
71
+ in your own ids. The browser renders `@opengeni/react`'s `OpenGeniChat` (or
72
+ `SessionConversation`); your server mounts `createSessionProxyHandler`, a
73
+ same-origin proxy that acts as your authenticated user:
74
74
 
75
75
  ```ts
76
- import { OpenGeni, createChatHandler } from "@opengeni/sdk/chat";
76
+ // lib/opengeni.ts (server only: the organization API key never reaches the browser)
77
+ import { Opengeni } from "@opengeni/sdk/chat";
77
78
 
78
- const og = new OpenGeni({
79
- apiKey: process.env.OPENGENI_API_KEY!,
80
- organizationId: process.env.OPENGENI_ORGANIZATION_ID!,
81
- // baseUrl defaults to https://app.opengeni.ai; source (default "app") labels your product.
82
- });
79
+ export const og = new Opengeni({ apiKey: process.env.OPENGENI_API_KEY! });
80
+ ```
83
81
 
84
- const chat = await og.chat({
85
- tenant: "acme", // one workspace per customer, created on first use
86
- user: "u_42", // authenticated product user; onboard workspace membership first
87
- conversation: "c_9", // stable conversation id, independent of the acting user
88
- agentAccess: "session", // "session" (default) | "user" | "workspace"
89
- memory: "user", // "user" | "workspace" | false; session-only agent reach defaults to false
90
- create: { sandboxBackend: "none" }, // raw create-request passthrough for a pure chat
82
+ ```ts
83
+ // app/api/opengeni/[...path]/route.ts (any web-standard host works; see the adapters below)
84
+ import { createSessionProxyRoute } from "@opengeni/sdk/next";
85
+ import { og } from "@/lib/opengeni";
86
+
87
+ export const dynamic = "force-dynamic";
88
+ export const { GET, POST, PUT, PATCH, DELETE } = createSessionProxyRoute(og, {
89
+ resolve: async (request) => {
90
+ const me = await authenticate(request); // your existing session check
91
+ if (!me) return new Response("Unauthorized", { status: 401 });
92
+ return { user: me.id, tenant: me.orgId }; // or { user: me.id }
93
+ },
91
94
  });
95
+ ```
92
96
 
93
- const reply = await chat.send("hello"); // creates the session on the first send
94
- console.log(reply.text); // or String(reply)
97
+ ```tsx
98
+ // app/assistant/assistant.tsx (browser): no provider, client, or workspace id.
99
+ "use client";
100
+ import { OpenGeniChat } from "@opengeni/react";
101
+ import "@opengeni/react/compiled.css";
95
102
 
96
- for await (const chunk of chat.stream("and then?")) {
97
- if (chunk.type === "text") process.stdout.write(chunk.text);
98
- if (chunk.type === "pending") await chat.respond({ requestId: chunk.pending.requestId, decision: "approve" });
103
+ export function Assistant() {
104
+ return <OpenGeniChat baseUrl="/api/opengeni" />; // chat list + conversation
99
105
  }
106
+ ```
100
107
 
101
- // Your endpoint. `resolve` is your auth hook: identity comes from the request
102
- // you authenticated, never from the body. The handler reads the client's
103
- // conversation id itself (x-opengeni-conversation header, or the wire format's
104
- // own field) and authorizes as `user`; return `conversation` from resolve only
105
- // when the host names it, which is required when there is no `user`.
106
- export const handler = createChatHandler(og, {
107
- resolve: async (request) => {
108
- const session = await getSessionFromCookie(request);
109
- if (!session) return new Response("Unauthorized", { status: 401 });
110
- return { tenant: session.accountId, user: session.userId };
108
+ `baseUrl` makes the component create the browser client for your mount and use
109
+ the workspace the proxy resolved for the signed-in user (reported in the
110
+ proxy's client config). One conversation alone is
111
+ `<SessionConversation baseUrl="/api/opengeni" sessionId={id} />`. The
112
+ `<OpenGeniProvider client={client} workspaceId={workspaceId}>` form keeps
113
+ working, for example for several components or the headless hooks.
114
+
115
+ What `resolve` returns decides the workspace:
116
+
117
+ - `{ user, tenant }`: one workspace per tenant; chats are private per user by
118
+ default.
119
+ - `{ user }`: one workspace per user. It stays single-user: the SDK adds its
120
+ owner once, and Opengeni never admits anyone else to it.
121
+ - `{ user, workspaceId }`: bring your own workspace id.
122
+
123
+ Workspaces are created on first use and cached per `Opengeni` instance.
124
+ `og.workspaceId({ tenant })` and `og.workspaceId({ user })` return the same ids
125
+ the proxy uses. There is no onboarding step: the first request a user makes
126
+ adds them to a tenant (or your own) workspace with conversation permissions
127
+ (workspace read, session create/read/control, file upload/read, and attaching
128
+ your per-session MCP servers; no admin). That needs an organization API key
129
+ with `members:manage` (full access includes it) plus those permissions.
130
+ Opengeni never changes an existing membership, so narrowing or widening one
131
+ with `updateExternalWorkspaceMember` sticks. A user you remove from a tenant
132
+ workspace is added again on their next request, because your product decides
133
+ who its users are; stop resolving a user you no longer admit. A removal from a
134
+ per-user workspace sticks. `organizationId` is optional:
135
+ the facade reads it once from the key. Keep `user` and `tenant`
136
+ host-authenticated, never from the request body or path.
137
+
138
+ The proxy and facade accept `chats`. `"private"` (the default with a user)
139
+ uses private visibility, session-only agent reach and personal Knowledge;
140
+ `"shared"` uses workspace visibility, agent reach and Knowledge, so a tenant's
141
+ users see each other's chats. Without a user and with `chats` omitted, the
142
+ facade keeps workspace visibility, session-only agent reach and Knowledge
143
+ authoring off. The API wire values are `visibility: "private" | "workspace"`;
144
+ stored visibility is `user_private | workspace_shared`. Explicit hook/create
145
+ fields override defaults. Keep MCP URLs and credentials in the server's
146
+ `createSession` hook; the browser cannot choose them through the proxy.
147
+
148
+ You can also pass a plain `OpenGeniClient` (organization key) instead of the
149
+ facade; then `resolve` must return `{ user, workspaceId }`. The explicit
150
+ membership APIs (`ensureWorkspace`, `addExternalWorkspaceMember`,
151
+ `updateExternalWorkspaceMember`, `cancelExternalWorkspaceMemberGrant`) keep
152
+ working for products that manage access themselves. Legacy
153
+ `chats: "isolated"` with `{ tenant, user }` still provisions a separate
154
+ workspace per user and tenant. Per-user workspaces add their owner with a
155
+ stable onboarding key, so a retry never restores a revoked grant;
156
+ `memberPermissions` replaces that owner's permissions on first onboarding
157
+ only. Set `memberPermissions` for tenant workspaces too when your users need
158
+ another role than Opengeni's first-use defaults: the facade and the proxy then
159
+ add each signed-in user with exactly those permissions before their first
160
+ request, with the same stable onboarding key, and never change an existing
161
+ grant. The standalone resolver is on the server-only
162
+ `@opengeni/sdk/tenant-workspaces` subpath.
163
+
164
+ The facade also accepts `agent` (identity, capabilities, instructions, renderer)
165
+ and defaults its renderer to `"markdown"`. Current servers always admit agent
166
+ configuration. For older servers that still gate it, only an implicit renderer
167
+ retries once without `agent` on `422 agent_config_not_enabled`, and that refusal
168
+ is cached per facade instance. Explicit agent settings are never stripped: the
169
+ 422 names `OPENGENI_AGENT_CONFIG_ADMISSION_ENABLED` for that older server's
170
+ operator. The private default with a user now turns personal Knowledge
171
+ on; pass `memory: false` to keep authoring off.
172
+ Missing organization private-session enablement raises `OpenGeniSetupError`,
173
+ with owner/admin API, SDK and web-app instructions; the proxy preserves that
174
+ actionable error for browser clients.
175
+
176
+ Framework adapters are thin wrappers over the same web-standard handler (they
177
+ also accept `createChatHandler` or any `(Request) => Promise<Response>`):
178
+
179
+ ```ts
180
+ // Next.js App Router: app/api/opengeni/[...path]/route.ts
181
+ import { createSessionProxyRoute } from "@opengeni/sdk/next";
182
+ export const dynamic = "force-dynamic";
183
+ export const { GET, POST, PUT, PATCH, DELETE } = createSessionProxyRoute(og, { resolve });
184
+
185
+ // Express / Connect / node:http (mount before body parsers, or they are re-serialized)
186
+ import { toNodeMiddleware } from "@opengeni/sdk/express";
187
+ app.use("/api/opengeni", toNodeMiddleware(handler));
188
+
189
+ // Hono
190
+ import { toHonoHandler } from "@opengeni/sdk/hono";
191
+ app.all("/api/opengeni/*", toHonoHandler(handler));
192
+ ```
193
+
194
+ The Next adapter passes the full URL, including any `basePath`; the Node
195
+ middleware rebuilds it from `originalUrl`, streams bodies and SSE, and aborts
196
+ the request when the client disconnects.
197
+
198
+ The proxy is a relay: it passes Opengeni's own routes through as the resolved
199
+ user, so the browser can use everything Opengeni lets that user do in that
200
+ workspace (chats and their history, queue, goals, approvals and human input,
201
+ files, artifacts and Sites, voice, schedules, Knowledge) with the same
202
+ requests the Opengeni web app makes. What the user can do is decided by
203
+ Opengeni's permissions, not by a list in the proxy; what the user sees is
204
+ decided by your UI (component props and presets). The chat list
205
+ (`SessionList` / `OpenGeniChat`) shows every root chat Opengeni lets the user
206
+ read; `listProps={{ mineOnly: true }}` narrows it to the user's own chats.
207
+ `listProps={{ archive: false }}` and `newChat={false}` hide "Hide for me" and
208
+ "New chat". That personal archive only hides a chat from one member's list.
209
+ The deployment's idle-session archive is separate: it makes long-idle sessions
210
+ read-only and is reported on `session.retention`. Pass `keepLive: true` to
211
+ `createSession`, or call
212
+ `updateSessionRetention(workspaceId, sessionId, { keepLive: true })`, to keep a
213
+ persistent agent's session out of it.
214
+
215
+ Composer uploads are refused for anonymous visitors: return `visitor: true`
216
+ from `resolve` for a user your product has not signed in (for example a
217
+ website visitor keyed by a cookie), and the proxy reports file uploads off and
218
+ refuses the upload routes unless `visitorUploads: true`. Agent-produced media
219
+ still loads.
220
+
221
+ Live voice (speech-to-speech) and voice input are Opengeni routes like any
222
+ other. The stock conversation shows its voice button only when you opt in with
223
+ the component's `realtimeVoice` prop, and its microphone unless `voiceInput` is
224
+ `false`. Opengeni still checks the user's `sessions:control` permission, binds
225
+ a call to that user and browser, runs spoken requests as steers of the same
226
+ chat, and meters deployment-funded voice against your credits.
227
+ `authorizeSession` runs on every call route, and `beforeForwardMessage` sees
228
+ `delivery: "realtime"` when a call starts (refuse it, or return
229
+ `mcpCredentialUpdates`; the proxy also refreshes the `toolServer` token) and
230
+ before transcripts and spoken requests are saved (refuse them, or add
231
+ `modelContext`, which must be the same when a request is retried).
232
+
233
+ `listSessionPage` includes personal pinned details by default. A caller with a
234
+ separate `pinsOnly: true` read can pass `includePinned: false` on ordinary pages
235
+ to avoid repeatedly loading those details. Ordinary pages still exclude pinned
236
+ rows; `pinsOnly: true` and `includePinned: false` cannot be combined. Older API
237
+ versions may ignore `includePinned` and return the default pin projection.
238
+
239
+ For navigation lists, `listSessionSummaryPage` uses the same filters, ordering,
240
+ pin rules and cursors, returning `SessionListEntry` records. Entries include
241
+ display/rename titles, creator, lifecycle, personal state, hierarchy counts and
242
+ the model and reasoning effort shown with a row; prompts and the rest of the
243
+ execution configuration remain on `getSession`. Older APIs return
244
+ full pages that the SDK projects locally. Unknown filters/cursors still fail
245
+ explicitly rather than returning an unfiltered result.
246
+
247
+ Every request calls `resolve`, then runs through `asUser(user, { source })`
248
+ (there is no service-authority fallback) against exactly the resolved
249
+ workspace:
250
+
251
+ - **Inside the workspace** (`/v1/workspaces/{workspaceId}/...`), every route
252
+ and method is forwarded as the user, with its query string, streaming body,
253
+ and the request headers Opengeni reads (`Accept`, `Content-Type`, `Range`,
254
+ `Last-Event-ID`, conditional headers, `x-opengeni-session-id`, correlation
255
+ and chunked-upload headers). Responses keep Opengeni's status, error
256
+ envelope, and content headers; SSE streams get heartbeats. A path naming
257
+ another workspace is a 403.
258
+ - **Outside it**, only the bootstrap reads answer, for this workspace alone:
259
+ `GET /v1/workspaces` (just the resolved workspace), `GET /v1/access/me` (the
260
+ user's effective grant here, never their other workspaces or the key's
261
+ authority), and `GET /v1/config/client`, plus the user's own inbox:
262
+ `GET /v1/inbox` and `PATCH /v1/inbox/items/{id}` limited to this workspace's
263
+ items, and `GET`/`PUT /v1/inbox/settings`. Everything else is a 404. Your
264
+ users' questions, approvals, paused goals and agent notifications reach that
265
+ inbox like an Opengeni member's (the stock app shows it), so you do not need
266
+ to build notifications from session events.
267
+ - **What narrows it:** an `asUser` request holds only the permissions both the
268
+ organization key and the user's membership hold, so a key or member role
269
+ without `files:write`, `schedules:*` or `sessions:control` cannot upload,
270
+ schedule or pause. `refuse(route, context)` keeps routes closed that
271
+ Opengeni would allow (return `true` for a 404 or your own `Response`;
272
+ `route` is `{ method, segments, sessionId }` with the segments after
273
+ `/v1/workspaces/{workspaceId}`). `authorizeSession(sessionId, context)` adds
274
+ a product-level check for every `sessions/{id}/...` route and every request
275
+ naming a chat in `x-opengeni-session-id`.
276
+ - **Server-owned chat setup:** with `createSession`, the browser may send only
277
+ `initialMessage`, `idempotencyKey`, attached files, explicit model choices,
278
+ and a few UI fields; your hook returns the full request (tools, MCP servers,
279
+ Skills, instructions, model policy), and the proxy refuses the routes that
280
+ would change that setup afterwards (a chat's agent, tool policy, Skills,
281
+ variable sets, usage limit, visibility, active sandbox and Codex account,
282
+ forks, and `chat/completions`). Without the hook, the browser creates chats
283
+ with Opengeni's own request, limited by the user's permissions, with `chats`
284
+ defaults filling what it leaves out. MCP credential rotation and archived
285
+ history import (`session-imports`) from the browser are always refused.
286
+ - **Messages** (send, steer, draft submit, realtime) pass through
287
+ `beforeForwardMessage`; `modelSelection: false` strips per-message model
288
+ choices, and bodies are capped by `maxBodyBytes` (1 MiB).
289
+ - **Mutations:** without `authorizeMutation`, cross-site (`Sec-Fetch-Site`)
290
+ requests and the content types a cross-site form can send (`text/plain`,
291
+ `application/x-www-form-urlencoded`) are refused, so cookie-authenticated
292
+ hosts should pass their CSRF check.
293
+
294
+ The client config the proxy serves is Opengeni's for this workspace and user,
295
+ plus the resolved `workspaceId`, the SDK's own `apiContractRevision`, the
296
+ artifact viewer's `artifacts` capability (the editor's live socket and this
297
+ user's cache partition), file uploads off for visitors, and `modelSelection`
298
+ when you set it. Task usage limits stay server-side.
299
+
300
+ ### Your own tools, as the signed-in user
301
+
302
+ `toolServer` attaches your product's MCP endpoint to every session created
303
+ through the proxy, with a short-lived token for the user `resolve`
304
+ authenticated. The proxy refreshes it on every send, steer, submit, approval,
305
+ and human-input answer. Your endpoint, built with any MCP library, verifies it:
306
+
307
+ ```ts
308
+ createSessionProxyHandler(og, {
309
+ resolve,
310
+ createSession,
311
+ toolServer: {
312
+ url: "https://app.example.com/api/mcp", // public HTTPS (tunnel locally)
313
+ approvals: { ask: ["rename_post"] }, // the user approves each call first
111
314
  },
112
- // format: "vercel" | "openai-chat" | "openai-responses" (default "native");
113
- // a request may override it with the x-opengeni-chat-format header.
114
315
  });
115
- export const GET = handler; // conversation history, for restoring the chat on reload
116
- export const POST = handler; // send a message, or answer a pending request at .../respond
316
+
317
+ // In the MCP route (Web Request, or an Express/Node request):
318
+ import { ToolRequestError, verifyToolRequest } from "@opengeni/sdk/tool-auth";
319
+ const { user, tenant, workspaceId } = await verifyToolRequest(request); // or ToolRequestError (401)
117
320
  ```
118
321
 
119
- Conversation IDs do not change with the acting user. Authorized collaborators
120
- can use the same session; private visibility prevents access by other users.
121
- Use `chatBySessionId` for existing and legacy user-namespaced conversations.
122
- User mode requires explicit workspace membership and never restores removed
123
- membership automatically. Without a `user`, the host must name the conversation
124
- from `resolve`. The Vercel and OpenAI adapters send only the latest user
125
- message; the earlier messages in that request are imported once, as context on
126
- the first message of a conversation, after which OpenGeni owns the history.
322
+ Scope every tool to the verified `user` and `tenant`; never trust ids the model
323
+ sends; list write tools in `approvals.ask`. Tools act as the chat's creator, also
324
+ in shared chats. Members need `mcp_servers:attach`. Tokens are HS256 JWTs signed
325
+ with a key derived from `OPENGENI_API_KEY` (or the same `secret` on both sides),
326
+ bound to the full tool URL by `aud` (set `OPENGENI_TOOL_SERVER_URL` to configure
327
+ both sides at once), and valid for `ttlSeconds` (default 24 hours). Non-Node
328
+ verifiers use the hex key from `deriveToolTokenKey()`, never the organization key;
329
+ the format is documented in `src/tool-auth.ts`. See
330
+ `examples/tool-server` for a runnable Express version.
127
331
 
128
- Pick the isolation per session with `agentAccess` (which other sessions the
129
- agent may reach) and `memory` (what it remembers), all inside one workspace that
130
- shares the customer's documents, instructions, and integrations:
332
+ `beforeForwardMessage(message, context)` runs before every forwarded user
333
+ message (send, steer, composer submit, and a browser-started create) and may
334
+ return server-owned additions, or a `Response` to refuse the message:
131
335
 
132
- | Scenario | `agentAccess` | `memory` |
133
- | ------------------------------------------------- | ------------- | ------------- |
134
- | Agent confined to its chat tree (support desk) | `"session"` | `false` |
135
- | One user's chats see each other, not other users' | `"user"` | `"user"` |
136
- | Everything in the tenant shared | `"workspace"` | `"workspace"` |
137
- | Shared agent access, no memory | any | `false` |
336
+ ```ts
337
+ createSessionProxyHandler(og, {
338
+ resolve,
339
+ beforeForwardMessage: async ({ sessionId, delivery }, { user }) => ({
340
+ // Model-visible, placed before any context the browser sent.
341
+ modelContext: `Page ${currentPage(user)} · ${timeZone(user)} · ${today()}`,
342
+ // Header-only rotation of MCP servers already attached to the session,
343
+ // applied atomically as the message is accepted (ignored on create).
344
+ mcpCredentialUpdates: [
345
+ { id: "acme", headers: { Authorization: `Bearer ${await mintUserToken(user)}` } },
346
+ ],
347
+ }),
348
+ });
349
+ ```
138
350
 
139
- The chat handler is backend-only: connect a custom or compatible frontend to
140
- its protocol. For the full React agent experience, use `SessionConversation`
141
- with `OpenGeniClient` and authenticated session routes, not this simplified
142
- chat protocol. The normal SDK preserves files, tools, approvals with policies,
143
- forks, and realtime voice.
351
+ This is the per-user tool token pattern: create the session with
352
+ `mcpServers: [{ id: "acme", url, headers }]` (and `tools: [{ kind: "mcp", id:
353
+ "acme" }]`, which the acting user needs `mcp_servers:attach` to attach), then
354
+ hand the MCP server a fresh short-lived bearer on every message. The browser
355
+ still cannot send `mcpCredentialUpdates` itself. With `modelSelection: false`
356
+ the proxy reports `modelSelection: false` in the client config, and
357
+ `SessionConversation` hides its model picker.
358
+
359
+ Opengeni adds each user to the workspace on their first request (see
360
+ [External users](#external-users-asuser)), with `workspace:read`,
361
+ `sessions:create`, `sessions:read`, `sessions:control`, `files:upload`,
362
+ `files:read`, and `mcp_servers:attach`. Pass the facade from `@opengeni/sdk/chat`
363
+ instead of a client to resolve `{ tenant, user }` or `{ user }` to a workspace
364
+ created on first use.
144
365
 
145
366
  ## Quick start
146
367
 
@@ -148,7 +369,7 @@ forks, and realtime voice.
148
369
  import { OpenGeniClient } from "@opengeni/sdk";
149
370
 
150
371
  const client = new OpenGeniClient({
151
- baseUrl: "https://api.example.com",
372
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
152
373
  apiKey: process.env.OPENGENI_API_KEY!,
153
374
  });
154
375
 
@@ -169,14 +390,14 @@ for await (const event of client.streamEvents(workspaceId, session.id)) {
169
390
  ## Organization-backed product setup
170
391
 
171
392
  A multi-tenant product should hold one organization API key on its backend and
172
- idempotently map each product tenant to an OpenGeni organization workspace. The
393
+ idempotently map each product tenant to an Opengeni organization workspace. The
173
394
  wire kind for an organization workspace is `"shared"`; Personal workspaces are
174
395
  excluded.
175
396
 
176
397
  ```ts
177
398
  const client = new OpenGeniClient({
178
399
  baseUrl: process.env.OPENGENI_API_BASE_URL!,
179
- apiKey: process.env.OPENGENI_ORGANIZATION_API_KEY!,
400
+ apiKey: process.env.OPENGENI_API_KEY!,
180
401
  });
181
402
 
182
403
  const organizationId = process.env.OPENGENI_ORGANIZATION_ID!;
@@ -195,17 +416,40 @@ if (workspace.kind !== "shared") {
195
416
  The response is `{ workspace, created }`. Replays return the original nested
196
417
  workspace with `created: false`. For an organization key, `getAccessContext()`
197
418
  does not enumerate every workspace grant; call `listWorkspaces()` for the
198
- complete organization-workspace inventory.
419
+ organization-workspace inventory allowed by the key's live scope.
199
420
 
200
421
  Organization key administration uses `listOrganizationApiKeys`,
201
- `createOrganizationApiKey`, and `deleteOrganizationApiKey`. The key token from a
422
+ `getOrganizationApiKey`, `createOrganizationApiKey`, `updateOrganizationApiKey`,
423
+ and `deleteOrganizationApiKey`. GET detail and PATCH return the raw `ApiKey`;
424
+ creation returns `{ apiKey, token }`. The key token from a
202
425
  create response is shown once and must be stored in the backend's secret
203
426
  manager. `createOrganizationApiKey(organizationId, { name, access: "read" })`
204
427
  mints a read-only master key: it inventories shared workspaces and reads their
205
428
  sessions, events, and files, but cannot create sessions, send messages, or mint
206
- keys, and every key reports its tier as `apiKey.access`. Either tier can call
429
+ keys. This legacy `access: "read"` tier is distinct from the broader explicit
430
+ `read_only` preset, which includes every read/list/view/search permission
431
+ except the secret-value read `secrets:read`.
432
+
433
+ Create or edit with `policy: { preset, permissions, workspaceScope }` for exact
434
+ grants. `OrganizationAccessPreset` is `"read_only" | "full" | "custom"`;
435
+ the label never adds permissions and the server recomputes it from the selected
436
+ set. Explicit `full` selects every canonical non-deprecated permission; `custom`
437
+ has no implicit grants or scope. Even `workspace:admin` grants no wildcard under
438
+ an explicit policy. Scope is `{ kind: "all" }` (current and future shared
439
+ workspaces) or `{ kind: "selected", workspaceIds }` (up to 500 unique same-org
440
+ shared-workspace IDs; empty reaches none). Personal workspaces are always excluded.
441
+
442
+ `updateOrganizationApiKey(organizationId, apiKeyId, { name?, description?, policy? })`
443
+ requires at least one change; `description: null` clears it. Metadata-only edits
444
+ preserve legacy stored permissions and wildcard semantics. A policy edit replaces
445
+ the policy, transitions legacy keys to explicit semantics, and applies narrowing
446
+ on the next request without rotating the token. Omitting policy at creation
447
+ retains legacy `access`/`preset` inputs and the full legacy default.
448
+ See [the canonical policy guide](../../docs/product-integration.md#explicit-organization-key-policies).
449
+
450
+ Keys with session-read authority can call
207
451
  `listOrganizationSessions(organizationId, { limit, cursor, scopeSubjectId, status })`
208
- for one page of sessions across every shared workspace (each row carries its
452
+ for one page of sessions across authorized shared workspaces (each row carries its
209
453
  `workspaceId`; private sessions and Personal workspaces never appear), or
210
454
  `iterateOrganizationSessions` to follow `nextCursor` to the end.
211
455
 
@@ -215,10 +459,10 @@ session. There is no organization-wide Skill registry or Skill inheritance in
215
459
  this integration contract. See the canonical guide for the complete route map,
216
460
  security boundary, and delivery checklist.
217
461
 
218
- Installed Pack Skills normally remain workspace-managed. A Pack may instead
219
- mark guidance as `session_selected`; installation then exposes it to no agent
462
+ Installed Skills can be workspace-managed or marked `session_selected`.
463
+ Session-selected guidance is exposed to no agent
220
464
  until `createSession` names the reviewed immutable component in
221
- `installedSkillIds`. OpenGeni copies that exact artifact into the new session,
465
+ `installedSkillIds`. Opengeni copies that exact artifact into the new session,
222
466
  so omitting the field from customer-facing creates is a real contamination
223
467
  boundary rather than a prompt convention.
224
468
  The ordered selection is part of keyed-create identity: reusing an
@@ -242,11 +486,200 @@ root and `@opengeni/sdk/core` clients, or through the focused
242
486
  import { OpenGeniDocumentAuthorityClient } from "@opengeni/sdk/document-authority";
243
487
 
244
488
  const operatorClient = new OpenGeniDocumentAuthorityClient({
245
- baseUrl: "https://api.example.com",
489
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
246
490
  apiKey: process.env.OPENGENI_API_KEY!,
247
491
  });
248
492
  ```
249
493
 
494
+ ## External users (`asUser`)
495
+
496
+ On a product backend, `client.asUser(externalId, { source? })` returns a separate
497
+ client that sends the external actor assertion with the organization key. IDs
498
+ are opaque and case-sensitive; the default source is `default`. Keep the key and
499
+ this client server-side. Each request requires the intersection of the key's
500
+ permissions and the user's workspace membership. API-key rotation does not
501
+ change the external identity.
502
+
503
+ Membership is automatic: when an `asUser` request reaches a shared workspace of
504
+ the key's own organization and the user has no membership there, Opengeni adds
505
+ one with conversation permissions (workspace read, session create/read/control,
506
+ file upload/read, `mcp_servers:attach`) and continues the request. This needs
507
+ the same authority as explicit onboarding: the key holds `members:manage` (or a
508
+ legacy `workspace:admin`) and every one of those permissions, with the
509
+ workspace in its scope; otherwise the request is a `403` as before. It never
510
+ applies to linked native identities, Personal workspaces, SDK per-user
511
+ workspaces (`opengeni-sdk:user-isolation:*` external source), other
512
+ organizations, requests that need a permission outside those defaults, or any
513
+ non-key caller, and it never changes an existing membership. The identity is
514
+ re-checked as active under the same lock, and the grant writes the same
515
+ receipt and lifecycle event as explicit onboarding, attributed to the key. A
516
+ user removed from a shared workspace is added again on their next request; a
517
+ suspended or offboarded identity stays refused.
518
+
519
+ Use the service client—not an `asUser` client—to call
520
+ `addExternalWorkspaceMember(workspaceId, { identity: { externalId, source? }, permissions })`
521
+ to choose permissions up front. It requires `members:manage`, cannot grant more
522
+ than the key permits, excludes Personal workspaces, and refuses to overwrite an
523
+ existing membership with different permissions (`409`, also when automatic
524
+ membership already created it); change an existing member's permissions with the keyed
525
+ `updateExternalWorkspaceMember(organizationId, workspaceId, organizationMembershipId,
526
+ { operationId, permissions })`, which never tears down work. `asUser` calls never
527
+ reactivate disabled identities.
528
+
529
+ Service `removeWorkspaceMember` supports external members through the existing
530
+ fenced workspace teardown. Account-wide `updateExternalIdentityMembership(
531
+ organizationId, organizationMembershipId, { kind, expectedAuthorizationRevision,
532
+ operationId, reason? })` requires explicit `account:admin` and supports suspend,
533
+ reactivate, and offboard. The membership ID is returned by onboarding; its initial
534
+ authorization revision is 1. Retain the returned revision for subsequent changes.
535
+ Reactivation restores admission, not revoked memberships or work. Offboarding
536
+ follows existing retention policy and cannot be reactivated through this API.
537
+ Native-user targets and `asUser` calls are rejected by this service endpoint.
538
+
539
+ An `asUser` client can discover/access its own provisioned Personal workspace
540
+ under the key's permission ceiling. Core private session operations retain
541
+ organization readiness/settings and explicit sharing acknowledgments; service
542
+ clients gain no Personal-workspace fallback.
543
+
544
+ Optional native-account delegation uses `beginIdentityLink`, authenticated native
545
+ `previewIdentityLink`/`confirmIdentityLink`, and explicit server-side
546
+ `asLinkedUser(externalId, { source?, linkId, expectedLinkRevision })`. Confirmation
547
+ requires the actual native login plus the one-time host challenge; an organization
548
+ key cannot confirm for the native user. `listIdentityLinks(workspaceId, cursor?)`
549
+ returns only the effective participant's links in that organization (50 per page).
550
+ `revokeIdentityLink` requires the observed revision. Linking never changes ordinary
551
+ `asUser`, merges histories or transfers credentials. Link-dependent accepted work
552
+ retains revocation checks across schedules and children. Short-lived inline MCP
553
+ credentials remain supported, and durable renewal stays opt-in.
554
+
555
+ Connect accepts an optional `installationTarget: { instanceKey, displayName,
556
+ expectedInstanceVersion? }`. Keep the exact observed version for an existing named
557
+ account. Setup freezes the target through callback and operation review; omitting
558
+ it creates an independent named account instead of overwriting a default instance.
559
+
560
+ ## Chat facade fallback (`@opengeni/sdk/chat`)
561
+
562
+ Use this only when your product already has its own chat UI speaking Vercel
563
+ `useChat` or an OpenAI-shaped protocol and you want a compatible drop-in
564
+ backend, or for server-side bots (`og.chat(...).send()`). It is a text-only
565
+ projection: tool outputs are dropped (the Vercel adapter emits no tool parts
566
+ by default, and only `output: { status }` with `toolParts: true`), there are no
567
+ files, attachments, artifacts, or images,
568
+ no goals/queue/steer UI, and reopening restores only a text snapshot. For
569
+ anything else, use [the default conversation embed](#embed-the-conversation-default).
570
+
571
+ One option object per conversation, one server handler for your endpoint.
572
+ Tenants map to organization workspaces, conversations map to deterministic
573
+ sessions, and the organization API key never leaves your server.
574
+
575
+ ```ts
576
+ import { Opengeni, createChatHandler } from "@opengeni/sdk/chat";
577
+
578
+ const og = new Opengeni({
579
+ baseUrl: process.env.OPENGENI_API_BASE_URL!, // omitted = production https://app.opengeni.ai
580
+ apiKey: process.env.OPENGENI_API_KEY!, // the organization id is derived from the key
581
+ source: "acme-app", // labels your product's tenants and users (default "app")
582
+ });
583
+
584
+ const chat = await og.chat({
585
+ tenant: "acme", // one workspace per customer, created on first use; omit for one per user
586
+ user: "u_42", // authenticated product user; added to the workspace on first use
587
+ conversation: "c_9", // stable conversation id, independent of the acting user
588
+ agentAccess: "session", // "session" (default) | "user" | "workspace"
589
+ memory: "user", // "user" | "workspace" | false; session-only agent reach defaults to false
590
+ create: { sandboxBackend: "none" }, // raw create-request passthrough for a pure chat
591
+ });
592
+
593
+ const reply = await chat.send("hello"); // creates the session on the first send
594
+ console.log(reply.text); // or String(reply); the answer, without progress commentary
595
+
596
+ // Per message: model policy and model-visible page context (not shown in the transcript).
597
+ await chat.send("Why did revenue dip?", {
598
+ model: "gpt-5.5",
599
+ reasoningEffort: "low",
600
+ modelContext: "Viewing dashboard 42, range 2026-09-01..2026-09-28",
601
+ });
602
+
603
+ for await (const chunk of chat.stream("and then?")) {
604
+ if (chunk.type === "text") process.stdout.write(chunk.text);
605
+ if (chunk.type === "pending") await chat.respond({ requestId: chunk.pending.requestId, decision: "approve" });
606
+ }
607
+
608
+ // Your endpoint. `resolve` is your auth hook: identity comes from the request
609
+ // you authenticated, never from the body. The handler reads the client's
610
+ // conversation id itself (x-opengeni-conversation header, or the wire format's
611
+ // own field) and authorizes as `user`; return `conversation` from resolve only
612
+ // when the host names it, which is required when there is no `user`.
613
+ export const handler = createChatHandler(og, {
614
+ resolve: async (request) => {
615
+ const session = await getSessionFromCookie(request);
616
+ if (!session) return new Response("Unauthorized", { status: 401 });
617
+ return { tenant: session.accountId, user: session.userId };
618
+ },
619
+ // format: "vercel" | "openai-chat" | "openai-responses" (default "native");
620
+ // a request may override it with the x-opengeni-chat-format header.
621
+ });
622
+ export const GET = handler; // conversation history, for restoring the chat on reload
623
+ export const POST = handler; // send a message, or answer a pending request at .../respond
624
+ ```
625
+
626
+ Conversation IDs do not change with the acting user. Authorized collaborators
627
+ can use the same session; private visibility prevents access by other users.
628
+ Use `chatBySessionId` for existing and legacy user-namespaced conversations.
629
+ A user is added to the workspace on their first request (see
630
+ [External users](#external-users-asuser)). Without a `user`, the host must name the conversation
631
+ from `resolve`. The Vercel and OpenAI adapters send only the latest user
632
+ message; the earlier messages in that request are imported once, as context on
633
+ the first message of a conversation, after which Opengeni owns the history.
634
+
635
+ Pick the isolation per session with `agentAccess` (which other sessions the
636
+ agent may reach) and `memory` (what it remembers), all inside one workspace that
637
+ shares the customer's documents, instructions, and integrations:
638
+
639
+ | Scenario | `agentAccess` | `memory` |
640
+ | ------------------------------------------------- | ------------- | ------------- |
641
+ | Agent confined to its chat tree (support desk) | `"session"` | `false` |
642
+ | One user's chats see each other, not other users' | `"user"` | `"user"` |
643
+ | Everything in the tenant shared | `"workspace"` | `"workspace"` |
644
+ | Shared agent access, no memory | any | `false` |
645
+
646
+ To keep an existing `useChat` route with its own request body, stream the chat
647
+ into your AI SDK response yourself. The Vercel format speaks the v1 UI message
648
+ stream used by AI SDK 5, 6, and 7 (tool approval requests need 6 or later):
649
+
650
+ ```ts
651
+ import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
652
+ import { uiMessageStreamParts } from "@opengeni/sdk/chat";
653
+
654
+ export async function POST(request: Request) {
655
+ const { messages, dashboardId } = await request.json(); // your own body
656
+ const me = await authenticate(request);
657
+ const chat = await og.chat({ tenant: me.accountId, user: me.userId, conversation: me.chatId });
658
+ const text = lastUserText(messages);
659
+ const stream = createUIMessageStream({
660
+ execute: async ({ writer }) => {
661
+ const chunks = chat.stream(text, { modelContext: `Dashboard ${dashboardId}` });
662
+ for await (const part of uiMessageStreamParts(chunks, { framing: false })) {
663
+ writer.write(part as never);
664
+ }
665
+ },
666
+ });
667
+ return createUIMessageStreamResponse({ stream });
668
+ }
669
+ ```
670
+
671
+ Opengeni's own tool activity is omitted by default because those tools are not
672
+ in your typed tool set; pass `toolParts: true` (or `createChatHandler(og, {
673
+ toolParts: true })`) to emit them as `dynamic`, provider-executed tool parts.
674
+ Pending approvals are always emitted as dynamic tool parts with a
675
+ `tool-approval-request`.
676
+
677
+ The chat handler is backend-only: connect a custom or compatible frontend to
678
+ its protocol. For the full React agent experience, use `SessionConversation`
679
+ with `OpenGeniClient` and authenticated session routes, not this simplified
680
+ chat protocol. The normal SDK preserves files, tools, approvals with policies,
681
+ forks, and realtime voice.
682
+
250
683
  ## Related-work discovery
251
684
 
252
685
  `listAgentTopology` returns a bounded hierarchy page plus provider-neutral
@@ -286,6 +719,35 @@ the advisory UI without changing stored evidence or API authority. See
286
719
  [`docs/work-discovery.md`](../../docs/work-discovery.md) for ranking, bounds,
287
720
  claim lifecycle, and rollout semantics.
288
721
 
722
+ ## Full-history message search
723
+
724
+ `searchSessionMessages(workspaceId, { query, sessionId?, groupBy?, archiveStatus?, limit?,
725
+ cursor? }, { signal? })` searches durable user and **completed assistant** text,
726
+ including unloaded history. It returns every non-overlapping literal occurrence,
727
+ not tools, reasoning, model context, or delta-only assistant output.
728
+
729
+ Each match has a durable `eventId`/`sequence`, a UTF-16 `messageMatchOffset`, and
730
+ `snippet: { text, matchStart, matchEnd }` with original-text UTF-16 offsets.
731
+ Results include `nextCursor`, `hasMore`, cumulative `matchedMessageCount` and
732
+ `matchedOccurrenceCount`, `scannedMessages`, and `countIsExact`. Keep following
733
+ cursors even on empty pages while `hasMore` is true; counts are provisional until
734
+ exhaustion. Cancel superseded searches with the third argument's AbortSignal.
735
+ This is a live, authorized traversal rather than a frozen snapshot; restart to
736
+ refresh after concurrent history or visibility changes.
737
+
738
+ For workspace search, `groupBy: "session"` returns the first hit per matching
739
+ session and skips that session's remaining history, keeping prolific messages
740
+ from filling multiple picker pages. It cannot be combined with `sessionId`.
741
+ In grouped mode both matched counters count session representatives, not full
742
+ per-session message/occurrence totals; `scannedMessages` counts only visited
743
+ messages. Cursors bind the grouping mode. Omit it for every-occurrence Find.
744
+
745
+ The method and types are exported from the ordinary SDK and browser entry.
746
+ Use `listEventPage` around the hit's sequence for bounded context, and use the
747
+ returned search snippet when a large source message's ordinary projection does
748
+ not include the hit. Never download complete history to perform Find locally.
749
+ See [the API contract and bounds](../../docs/session-message-search.md).
750
+
289
751
  ## Session visibility and forks
290
752
 
291
753
  For organizations with session-tenancy activation, a canonical managed-cookie
@@ -321,49 +783,143 @@ user decision. A quiescence conflict identifies the stable blocker after live
321
783
  turns, goals, realtime, schedules, workspace writers, retained processes, and
322
784
  sandbox access have been settled. Forks copy exact same-workspace durable
323
785
  conversation content but no live turn, goal, credential, Connection/delegation,
324
- personal grant, Variable Set, Rig, MCP server configuration, process, sandbox
786
+ personal grant, vault policy, task vault, Sandbox Environment, MCP server configuration, process, sandbox
325
787
  identity, pin, or workflow. Destination visibility and acknowledgement are
326
788
  idempotency-bound.
327
789
 
328
- ## Personal-resource grants
790
+ ## Archived session history import (server only)
329
791
 
330
- The same canonical managed-cookie owner can manage explicit personal-resource
331
- delegations after organization activation. Pages are bounded to one exact kind;
332
- the server derives the only valid action and returns the full credential-free
333
- delegation to attach through the resource's ordinary session API.
792
+ Use `@opengeni/sdk/session-history-import` to migrate historical conversations
793
+ into read-only archives. These functions take `client` first, use its ordinary
794
+ `requestJson` transport, and are not eager client methods or root helper exports:
795
+
796
+ Create requires `sessions:create`; append requires `sessions:control` and the same
797
+ authenticated importer. With `asUser`, the user's permissions apply rather than
798
+ the organization key's permissions.
334
799
 
335
800
  ```ts
336
- const page = await browserClient.listUserResourceAuthorities(workspaceId, {
337
- resourceKind: "connection",
338
- limit: 50,
801
+ import { OpenGeniClient } from "@opengeni/sdk";
802
+ import {
803
+ importArchivedSession,
804
+ appendArchivedSessionEvents,
805
+ } from "@opengeni/sdk/session-history-import";
806
+
807
+ const og = new OpenGeniClient({
808
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
809
+ apiKey: process.env.OPENGENI_API_KEY!,
339
810
  });
340
- const authority = page.authorities[0];
341
- if (!authority) throw new Error("No personal Connection is available");
342
-
343
- const issued = await browserClient.issueUserResourceGrant(workspaceId, authority.authorityId, {
344
- scope: "user",
345
- resourceKind: "connection",
346
- mode: "session",
347
- context: "workspace_shared",
348
- sessionId,
349
- expectedAuthorityEpoch: current.tenancy.authorityEpoch,
350
- workspaceSharedAcknowledged: true,
811
+ // Existing tenant mapping; the original creator is admitted on first use.
812
+ const actor = og.asUser(originalExternalUserId, { source });
813
+ const imported = await importArchivedSession(actor, workspaceId, {
814
+ importId: "embedded:thread-42", // Stable; persist the exact request before sending.
815
+ title: "Original conversation",
816
+ createdAt: "2025-04-01T09:30:00Z",
817
+ visibility: "user_private",
818
+ });
819
+ const appended = await appendArchivedSessionEvents(actor, workspaceId, imported.importId, {
820
+ batchId: "thread-42:batch-0001", // Stable; also persist this exact body first.
821
+ offset: imported.nextOffset,
822
+ events: [{
823
+ type: "agent.message.completed",
824
+ createdAt: "2025-04-01T09:31:00Z",
825
+ payload: { text: "Historical answer", channel: "final" },
826
+ }],
827
+ });
828
+ // Persist imported.session.id and appended.nextOffset in the migration ledger.
829
+ ```
830
+
831
+ External mapping equivalents take `(client, source, externalId, ...)`:
832
+ `importExternalWorkspaceArchivedSession` and
833
+ `appendExternalWorkspaceArchivedSessionEvents`. These resolve an existing
834
+ mapping; neither import form creates a workspace or grants membership. A bare
835
+ organization key creates a shared, ownerless archive. Use `asUser` for the
836
+ verified creator/owner; private imports require existing private-session
837
+ enablement. Preserve source visibility rather than widening it on failure.
838
+
839
+ The subpath exports `ImportArchivedSessionRequest/Response`,
840
+ `AppendArchivedSessionEventsRequest/Response`, `ArchivedSessionImportEvent` and
841
+ `SessionImportedArchive` types. Requests accept at most 100 events / 1 MiB
842
+ serialized UTF-8 JSON and 256 KiB per event. IDs and titles are bounded to 200
843
+ characters. Import events default to `[]`; append batches must be non-empty.
844
+ Events carry `{ type, createdAt, turnId?, payload }`: a finite supported historical
845
+ type, ISO timestamp, optional UUID/null correlation and JSON-object payload.
846
+ Source timestamps support at most millisecond precision; normalize finer dates
847
+ explicitly and retain originals in the ledger. Negative-zero JSON is rejected.
848
+
849
+ Exact create replay returns `created: false`; exact append replay returns
850
+ `replayed: true`. `offset` is a zero-based imported-event count, not a timeline
851
+ sequence. Retain the exact actor, mapping, IDs, offset and bodies for uncertain
852
+ retries; new batches use the acknowledged `nextOffset`. Changed key reuse or
853
+ an out-of-order new offset returns 409. Helpers propagate `OpenGeniApiError`,
854
+ including `outcomeUnknown`, and do not automatically retry mutations.
855
+
856
+ Import is timeline-only: completed messages, calls/results and goals may be
857
+ historical facts, but no model-facing history, live goal, pending decision or
858
+ execution is restored. `session.importedArchive` has
859
+ `{ importId, importedAt, readOnly: true }`, independent of personal archive state.
860
+ Render through the unchanged `SessionConversation` and session proxy; never
861
+ offer Send or Steer. Continuing an imported archive is unsupported in v1.
862
+ The proxy does not expose import routes. Re-upload files through existing APIs
863
+ and replace references before import; preserve additional source metadata and
864
+ source/destination IDs in the host ledger. See
865
+ [Migrating from embedded Opengeni](../../docs/product-integration.md#migrating-from-embedded-opengeni).
866
+
867
+ ## Connected accounts
868
+
869
+ Authenticated messages use the initiating user's eligible connected accounts.
870
+ Conversation visibility does not share account access. Queued work, retries and
871
+ child work retain the initiating user. No per-conversation connection grant is
872
+ required. Multiple eligible accounts require an explicit account choice:
873
+
874
+ ```ts
875
+ const accounts = await browserClient.listOwnConnectionAccounts(workspaceId);
876
+ // When choosing among multiple accounts, use the exact eligible server/account pair.
877
+ const selection = { serverId: "mail", connectionId: accounts[0]!.id };
878
+ ```
879
+
880
+ General personal-resource grants for documents and other resource
881
+ kinds remain available through the root/core SDK. They do not authorize native
882
+ connected accounts.
883
+
884
+ ## Workspace credentials, webhooks, and sandbox image
885
+
886
+ Configure these with an organization key or workspace admin session. Secrets are
887
+ returned once; store them when you create the resource. Protocol and payloads:
888
+ [`docs/workspace-integrations.md`](../../docs/workspace-integrations.md).
889
+
890
+ ```ts
891
+ const { secret: providerSecret } = await client.putWorkspaceCredentialProvider(workspaceId, {
892
+ url: "https://product.example/opengeni/credentials",
893
+ });
894
+ const { secret: webhookSecret } = await client.createWorkspaceWebhook(workspaceId, {
895
+ url: "https://product.example/opengeni/events",
896
+ eventTypes: ["turn.completed", "turn.failed"],
351
897
  });
352
898
 
353
- await browserClient.revokeUserResourceGrant(workspaceId, issued.grant.grantId);
899
+ // In your HTTP handlers, verify the raw body before parsing it:
900
+ const { event } = await verifyWebhookEvent({ body: rawBody, headers, secret: webhookSecret });
901
+ const request = await verifyCredentialProviderRequest({
902
+ body: rawBody,
903
+ headers,
904
+ secret: providerSecret,
905
+ });
354
906
  ```
355
907
 
356
- The SDK deliberately exposes only exact-session and standing (`always`) grant
357
- management. It does not expose standalone `once`, custom expiry, scheduled or
358
- cross-workspace authority, or an atomic create-session-and-attach workflow.
359
- Revocation prevents future reads but cannot retract output already shared.
908
+ To check an endpoint now, `testWorkspaceWebhook(client, workspaceId, webhookId)` sends a
909
+ signed `webhook.test` event (acknowledge it with any 2xx) and
910
+ `testWorkspaceCredentialProvider(client, workspaceId)` sends a request with
911
+ `purpose: "test"`; both come from `@opengeni/sdk/workspace-integrations` and return
912
+ what the endpoint answered, naming returned credentials but never their values.
913
+
914
+ `listWorkspaceSandboxImages` returns the deployment's allowlisted images; set one
915
+ with `updateWorkspaceSettings(workspaceId, { defaultSandboxImage })`.
360
916
 
361
- ## Scheduled connection authority
917
+ ## Personal schedules
362
918
 
363
- Agent schedules may carry explicit personal Connection authority. The public
364
- request contains only the credential-free server, Connection, and common-user
365
- grant tuple returned by the authority-selection API; the SDK never receives or
366
- stores provider credentials:
919
+ A schedule created by an authenticated human or their active agent records that
920
+ human as its immutable owner. Only that owner or their verified agent may edit,
921
+ run, pause or delete it. Each occurrence resolves the owner's current accounts;
922
+ accepted retries retain their original identity and account selection.
367
923
 
368
924
  ```ts
369
925
  const task = await client.createScheduledTask(workspaceId, {
@@ -371,18 +927,27 @@ const task = await client.createScheduledTask(workspaceId, {
371
927
  schedule: { type: "calendar", hour: 8, minute: 0, timeZone: "Europe/Oslo" },
372
928
  runMode: "reusable_session",
373
929
  agentConfig: { prompt: "Triage the new support issues" },
374
- connectionAuthorities: [selection],
930
+ connectionAccounts: [selection],
375
931
  });
376
932
  ```
377
933
 
378
- PATCH semantics are deliberate: omit `connectionAuthorities` to preserve the
379
- exact prior immutable selection, pass `[]` to clear it, or pass a non-empty
380
- array to replace and revalidate it. Execution-changing edits that preserve
381
- personal authority must be made by the same causal human; use an explicit fresh
382
- selection when authority should move. `once` grants are consumed by the durable
383
- scheduled occurrence, not by an activity attempt, so retries reuse the same run
384
- receipt. `listScheduledTaskRuns` exposes terminal occurrence state while private
385
- Connection ids, subjects, and authority snapshots stay server-side.
934
+ Account selections narrow eligible accounts; they never transfer ownership.
935
+ Omitting `connectionAccounts` on update preserves the selection. Passing an
936
+ empty array clears explicit account choices without changing the schedule owner.
937
+ Service-owned schedules retain service execution and do not acquire a human's
938
+ personal accounts. Run history remains credential-free.
939
+
940
+ A schedule freezes its connectors, accounts and (when an agent created it)
941
+ Opengeni tools. For the owner, `listScheduledTasks` and `getScheduledTask`
942
+ include a read-only `policyDrift` naming what is out of date, and
943
+ `listScheduledTaskRuns` includes `accessFailures` for runs that could not use a
944
+ connector. `listScheduledTaskAccessAttention` lists schedules whose latest run
945
+ failed that way, and schedules that cannot start because a chosen account can no
946
+ longer be used (`unavailableAccounts`, with a null `runId`). A signed-in owner
947
+ re-freezes with their current access through
948
+ `refreshScheduledTaskAccess(workspaceId, taskId, { executionDigest, leaveOut })`,
949
+ where the optional `leaveOut` keeps named default connectors or Opengeni tools
950
+ off; API keys and agents cannot. See [`docs/scheduled-task-access.md`](../../docs/scheduled-task-access.md).
386
951
 
387
952
  Deleting a task is externally idempotent and immediately removes it from live
388
953
  lists and quota, but the server retains a tombstone plus run/session/turn audit
@@ -454,7 +1019,7 @@ import {
454
1019
  createBrowserEditableArtifactSession,
455
1020
  } from "@opengeni/sdk/editable-artifacts";
456
1021
 
457
- const client = new OpenGeniClient({ baseUrl: "https://api.example.com" });
1022
+ const client = new OpenGeniClient({ baseUrl: process.env.OPENGENI_API_BASE_URL! });
458
1023
  const artifact = await client.getEditableArtifact(workspaceId, artifactId, {
459
1024
  replicaId,
460
1025
  });
@@ -465,7 +1030,7 @@ const kernels = {
465
1030
  } as const;
466
1031
  const { editableArtifactKernelRuntime } = await kernels[artifact.modality]();
467
1032
  const session = createBrowserEditableArtifactSession({
468
- baseUrl: "https://api.example.com",
1033
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
469
1034
  workspaceId,
470
1035
  artifact,
471
1036
  storageAuthority,
@@ -503,7 +1068,7 @@ shown below; it owns no state or authority of its own.
503
1068
 
504
1069
  `OpenGeniClient.interaction` is the framework-free resource facade over the same
505
1070
  public BrowserSession, ComputerSession, identity, auth, network-route, and human
506
- intervention APIs used by OpenGeni itself. It adds no second state or execution
1071
+ intervention APIs used by Opengeni itself. It adds no second state or execution
507
1072
  path: facade methods call the ordinary typed transport, and every mutation still
508
1073
  uses the canonical generation fences, operation receipt, permission check, and
509
1074
  placement controller.
@@ -518,6 +1083,8 @@ const browser = await client.interaction.browsers.currentOrOpen({
518
1083
  const { targets } = await browser.tabs.list();
519
1084
  const target = targets.find((candidate) => candidate.selected) ?? targets[0]!;
520
1085
  const page = await browser.observe(target.id);
1086
+ const screenshot = await browser.screenshot(target.id, { fullPage: true });
1087
+ // screenshot.data is bounded image bytes; mediaType and dimensions travel with it.
521
1088
 
522
1089
  const receipt = await browser.act({
523
1090
  operationId: crypto.randomUUID(),
@@ -534,6 +1101,17 @@ if (receipt.state === "outcome_unknown") {
534
1101
  }
535
1102
  ```
536
1103
 
1104
+ `browser.targetState(targetId)` reads generation fences without an accessibility
1105
+ tree. `browser.readDom(targetId, request)` returns a bounded element or count
1106
+ query using those fences; sensitive values are redacted.
1107
+
1108
+ The flat `client.openBrowserTargetWithInventory(workspaceId, browserSessionId,
1109
+ { url })` explicitly opens a tab and returns the owned tab inventory and its
1110
+ generation fences without a page observation. The default
1111
+ `client.openBrowserTarget(...)` and `browser.tabs.open(...)` still return page
1112
+ observations. Older controllers can refuse the inventory method; do not retry an
1113
+ uncertain creation through another tab-open method.
1114
+
537
1115
  Browser identities are immutable version graphs: live browser state stays
538
1116
  private until `browser.publishRevision(...)` explicitly creates a new revision.
539
1117
  Protected auth fills resolve connection secrets only inside the broker/controller
@@ -561,6 +1139,27 @@ never enter the iframe; `@opengeni/sdk/site` uses a single parent-owned
561
1139
  `MessagePort` that exposes only the retained tool allowlist intersected with the
562
1140
  current viewer's live workspace catalog.
563
1141
 
1142
+ Site AI can return directly into an app without any conversation UI:
1143
+
1144
+ ```ts
1145
+ import { createOpenGeniSiteClient } from "@opengeni/sdk/site";
1146
+ const site = createOpenGeniSiteClient();
1147
+ const result = await site.ai.run({ initialMessage: appPrompt, idempotencyKey: actionId });
1148
+ if (result.status === "completed") renderResult(result.text);
1149
+ ```
1150
+
1151
+ The app defines the prompt, context and output presentation. This helper uses
1152
+ native sessions and events with the same permissions, approvals, model access
1153
+ and billing; it is not a second executor. New Site work is private per viewer
1154
+ and excluded from ordinary workspace discovery. `pending` and `cancelled` are
1155
+ not successful results; model failure rejects. Retain the `onSession` handle
1156
+ to observe again using `site.ai.result(id, { turnId })`, without creating work.
1157
+ An `after` cursor returns only subsequent output; omit it for full replay.
1158
+ Aborting observation does not cancel native execution. See
1159
+ [`site-conversations.md`](../../docs/site-conversations.md) and
1160
+ [`site-session-embed`](../../examples/site-session-embed) for optional chat and
1161
+ headless inline examples. Do not store private handles in shared Site Data.
1162
+
564
1163
  ```ts
565
1164
  let cursor: string | undefined;
566
1165
  do {
@@ -586,6 +1185,38 @@ model MCP calls and Codemode. Generated declarations augment the lazy namespace
586
1185
  for exact installed-tool types; canonical `{ serverId, toolName }` identity and
587
1186
  the server-side catalog remain authoritative.
588
1187
 
1188
+ Known calls use `POST tools/invoke` directly, with no preceding workspace
1189
+ catalog request. The API selects one exact authorized account/connector; a
1190
+ selected MCP provider may still require its own `tools/list`. Symbolic paths
1191
+ use the canonical registry projection and reject ambiguity before connecting.
1192
+ Cold calls use the current definition. `$resolve({ identity })` returns one
1193
+ entry and a `definitionDigest`; pass it as `expectedDefinitionDigest` to require
1194
+ that complete public definition before execution. Digests never grant access.
1195
+ Returned executed metadata preserves structured-output unwrapping without a
1196
+ catalog load.
1197
+
1198
+ `$catalog()` and `$declarations()` remain explicit full discovery. For an older
1199
+ API or Site host, set `toolGatewayMode: "catalog"` explicitly on the client;
1200
+ there is no automatic downgrade or retry after an unknown invocation outcome.
1201
+ The targeted SDK caches bounded per-target metadata, shares only resolution
1202
+ loads, and retries at most once after a typed pre-execution
1203
+ `tool_definition_stale`; explicit nonretryable or outcome-unknown flags veto
1204
+ replay even when a transport labels an error stale. Explicit pins are not
1205
+ silently dropped when malformed: only `undefined` means no explicit pin.
1206
+ The same retry veto applies to retained v1/catalog mode and Site host adapters.
1207
+ For saved Site bundles with code-only retry classifiers, the host's legacy call
1208
+ projection wraps executed stale-named provider errors as
1209
+ `site_tool_execution_failed`, retaining the complete original diagnostic at
1210
+ `result.structuredContent.error.providerError`. Success outputs and ordinary
1211
+ provider errors are unchanged; genuine known preexecution stale refresh remains
1212
+ available. Stale-named transport errors without that proof use
1213
+ `site_tool_call_failed` and retain the original code in their message.
1214
+ Pins are not silently advanced. An approved call uses the server's executable-effect and
1215
+ private-authority binding, not an implicit full-public-entry pin; supply
1216
+ `expectedDefinitionDigest` when that additional precondition is wanted.
1217
+ Changed executable effect or private authority requires reapproval. Operation IDs are correlation
1218
+ keys, not general idempotency for unapproved calls.
1219
+
589
1220
  ```ts
590
1221
  const tools = client.tools.forWorkspace(workspaceId);
591
1222
  const documents = await tools.docs.search({ query: "launch plan" });
@@ -593,7 +1224,13 @@ const documents = await tools.docs.search({ query: "launch plan" });
593
1224
 
594
1225
  An approval-required call needs a server-issued capability. A trusted host
595
1226
  shows its approval UI first, then asks for one token bound to the current human,
596
- workspace, operation id, catalog digest, exact tool identity, and arguments.
1227
+ workspace, operation id, exact tool identity, executable effect, Site context,
1228
+ private authority and arguments. Targeted approvals use a distinct version-2 binding, never a v1
1229
+ catalog digest.
1230
+ An Ask-to-Allow policy transition can settle matching pending provenance with
1231
+ or without the token, but never ignores a supplied invalid token or a consumed
1232
+ operation. Public definition pins remain optional and may reject policy/prose
1233
+ drift without consuming the pending approval.
597
1234
  The token expires after five minutes, is stored only as a hash, and is consumed
598
1235
  once by the matching call.
599
1236
 
@@ -605,7 +1242,7 @@ preflight credential and resource authorization before consuming the token.
605
1242
  const operationId = crypto.randomUUID();
606
1243
  const identity = { serverId: "linear", toolName: "issues_update" };
607
1244
  const input = { issueId, state: "Done" };
608
- const approval = await tools.$approve(identity, input, { operationId });
1245
+ const approval = await tools.$approveTarget(identity, input, { operationId });
609
1246
 
610
1247
  await tools.$call(identity, input, {
611
1248
  operationId,
@@ -613,7 +1250,13 @@ await tools.$call(identity, input, {
613
1250
  });
614
1251
  ```
615
1252
 
616
- When a call or approval request receives the typed
1253
+ The retained `$approve()` method and explicit `toolGatewayMode: "catalog"` use
1254
+ the legacy protocol described below. Tokens returned by `$approve()` on the
1255
+ same facade retain that routing; externally retained legacy tokens must pass
1256
+ `approvalBinding: "catalog"`. Never reinterpret a legacy token as a targeted
1257
+ approval.
1258
+
1259
+ When a legacy call or approval request receives the typed
617
1260
  `409 details.code = "catalog_stale"` response, the SDK refreshes the catalog,
618
1261
  re-resolves the exact identity or generated namespace path, preserves the
619
1262
  operation id (generating it before the first attempt when omitted), and retries
@@ -622,6 +1265,19 @@ single-use capability after the catalog digest changes; instead it throws
622
1265
  `OpenGeniToolReapprovalRequiredError` with the refreshed identity and digest so
623
1266
  the trusted host can show approval again for the same operation id.
624
1267
 
1268
+ Concurrent callers share one in-flight catalog request (`$catalog` and every
1269
+ call path). Each caller's `signal` cancels only its own wait; the shared request
1270
+ is cancelled only when every waiter has aborted, and a failed load is not
1271
+ cached. Calls rejected as stale on the same digest share one refresh started
1272
+ after the first such rejection while it remains the current catalog, and an
1273
+ explicit `$catalog({ refresh: true })` always starts a new request.
1274
+
1275
+ A call's `catalogDigest` is checked against the exact catalog entry it invokes:
1276
+ the API may accept an unchanged entry from a catalog digest whose unrelated
1277
+ entries have since changed, and then echoes that digest in the response. A
1278
+ changed, removed, or no-longer-authorized target entry still returns
1279
+ `catalog_stale` before execution.
1280
+
625
1281
  Reapproval is valid only before the original capability is consumed. Once a
626
1282
  call crosses that boundary, the server retains a hash-only tombstone for the
627
1283
  operation id and rejects another approval with
@@ -637,7 +1293,16 @@ version and viewer's live authority. The requested set is a maximum allowlist,
637
1293
  not an authority grant; ordinary gateway approval still applies. Archived Sites
638
1294
  receive no tool bridge.
639
1295
 
640
- Omit `firstPartyMcpTools` for the complete OpenGeni tool catalog. An explicit
1296
+ An upgraded host advertises targeted calls to modern Site clients. Existing
1297
+ bundled Site clients retain their catalog/call protocol: the host obtains a
1298
+ bounded manifest for only that saved version's requested identities and
1299
+ translates calls using retained per-entry pins. They still make their initial
1300
+ catalog request; republishing with the modern client removes it. Unknown or
1301
+ evicted manifest digests fail before execution. Host-owned Site/version context
1302
+ and approval tokens are stripped from iframe requests. Local previews instead
1303
+ use their frozen attempt catalog and existing Codemode operation journal.
1304
+
1305
+ Omit `firstPartyMcpTools` for the complete Opengeni tool catalog. An explicit
641
1306
  `[]` exposes no broad first-party tools; attached resources and separately
642
1307
  selected `files`/`docs` MCP servers are unaffected.
643
1308
 
@@ -691,6 +1356,53 @@ unexpected successful non-JSON response to a mutation has `outcomeUnknown:
691
1356
  true` because the mutation might already have been accepted. Never turn that
692
1357
  condition into a new operation by changing its idempotency key.
693
1358
 
1359
+ ### Transient read retry
1360
+
1361
+ Safe JSON reads (`GET` and `HEAD`) that fail transiently are retried quietly
1362
+ inside a small budget, so an API rolling deploy or a short network blip does
1363
+ not surface as an error. A failure is transient when the request never got a
1364
+ response (`fetch` rejected with a `TypeError`, such as a reset connection) or a
1365
+ gateway answered `502`, `503` or `504`. Retries use jittered exponential
1366
+ backoff, or the server's `Retry-After` when it fits the budget, and stop as
1367
+ soon as the caller's signal aborts. Every attempt of one call carries the same
1368
+ correlation ID. Mutations are never retried; they keep the `outcomeUnknown`
1369
+ semantics above. Auth, permission, not-found, validation, rate-limit and other
1370
+ server errors, and a success response the client cannot accept, surface
1371
+ immediately. A `Retry-After` gets up to 20% extra spread so clients told the
1372
+ same instant do not return together.
1373
+
1374
+ ```ts
1375
+ const client = new OpenGeniClient({
1376
+ baseUrl,
1377
+ // Defaults: 4 attempts, 250 ms first backoff, 2000 ms backoff cap, 5000 ms total.
1378
+ readRetry: { maxAttempts: 4, initialDelayMs: 250, maxDelayMs: 2_000, maxElapsedMs: 5_000 },
1379
+ // readRetry: false turns it off.
1380
+ });
1381
+ ```
1382
+
1383
+ `isTransientRequestError(error)` reports whether an error is this kind of
1384
+ transient failure, for callers that keep their own longer retry loop.
1385
+
1386
+ While a deployment is parked for an update, the API answers every request with
1387
+ `{"error":"maintenance"}` (normally HTTP 503). The client decodes it into an
1388
+ `OpenGeniApiError` with `code: "maintenance"` (`OPENGENI_MAINTENANCE_ERROR_CODE`),
1389
+ `retryable: true` and `outcomeUnknown: false`: nothing ran, so a mutation can be
1390
+ retried with the same idempotency key. Its message carries no support
1391
+ reference. `isMaintenanceError(error)` detects it, and `isTransientRequestError`
1392
+ treats it as transient. A park can last minutes, so keep retrying it at a calm
1393
+ pace rather than reporting a failure.
1394
+
1395
+ ```ts
1396
+ import { isMaintenanceError } from "@opengeni/sdk";
1397
+
1398
+ try {
1399
+ await client.sendMessage(workspaceId, sessionId, { text, clientEventId });
1400
+ } catch (error) {
1401
+ if (isMaintenanceError(error)) showUpdating(); // retry later with the same clientEventId
1402
+ else throw error;
1403
+ }
1404
+ ```
1405
+
694
1406
  ## Streaming guarantees
695
1407
 
696
1408
  `client.streamEvents(...)` (and the underlying `streamSessionEvents`) delivers
@@ -769,8 +1481,17 @@ for await (const event of client.streamEvents(workspaceId, sessionId, {
769
1481
  ## Messages, the turn queue, and steering
770
1482
 
771
1483
  Messages sent while a turn is running **queue by default** — visible,
772
- editable, reorderable, and deletable until the worker claims them. Steering is
773
- the explicit alternative: deliver now by interrupting the running turn.
1484
+ editable, reorderable, and deletable until the worker claims them, and they run
1485
+ after the whole running turn. Steering is the explicit alternative: the running
1486
+ turn reads the message within seconds and nothing in progress is killed. A
1487
+ model answer still being written is cut short (its completed text stays), short
1488
+ tool calls finish, and long command or session waits return early while the
1489
+ work keeps running. The turn then hands over (`turn.superseded` with reason
1490
+ `steer`, `atModelStep: true` and, when an answer was cut short, `cutOffText`)
1491
+ and the Steer runs next with the full history. The session screens show it
1492
+ only as the person's message, and the queue snapshot does not list it.
1493
+ `steerQueueItem` with `interrupt: true` on the turn that `steerMessage`
1494
+ returned stops the running attempt now instead, including tool calls in flight.
774
1495
 
775
1496
  ```ts
776
1497
  // Queue (default): stacks behind the running turn.
@@ -779,12 +1500,20 @@ await client.sendMessage(workspaceId, sessionId, {
779
1500
  clientEventId: crypto.randomUUID(),
780
1501
  });
781
1502
 
782
- // Steer: send + promote to the queue front + interrupt the running turn.
783
- await client.steerMessage(workspaceId, sessionId, {
1503
+ // Steer: send + promote to the queue front; read within seconds.
1504
+ const steered = await client.steerMessage(workspaceId, sessionId, {
784
1505
  text: "Stop — prod is paging, look at that first",
785
1506
  clientEventId: crypto.randomUUID(),
786
1507
  });
787
1508
 
1509
+ // Hard interrupt: stop the running attempt for that Steer now, including
1510
+ // tool calls in flight (rarely needed; the Steer is read within seconds).
1511
+ await client.steerQueueItem(workspaceId, sessionId, steered.turn.id, {
1512
+ expectedTurnVersion: steered.turn.version,
1513
+ interrupt: true,
1514
+ clientEventId: crypto.randomUUID(),
1515
+ });
1516
+
788
1517
  // Manage the server-authoritative queue while it waits.
789
1518
  const queue = await client.getQueue(workspaceId, sessionId);
790
1519
  const waiting = queue.items.at(-1)!;
@@ -814,9 +1543,46 @@ await client.cancelSession(workspaceId, sessionId, {
814
1543
  reason: "host record deleted",
815
1544
  clientEventId: crypto.randomUUID(),
816
1545
  });
1546
+ // approvalId is `approvals[].id` from the latest `session.requiresAction`
1547
+ // event (type SessionApprovalRequest: { id, name, arguments }); it is the
1548
+ // pending tool call id, not the event id.
817
1549
  await client.sendApprovalDecision(workspaceId, sessionId, { approvalId, decision: "approve" });
818
1550
  ```
819
1551
 
1552
+ ## Retry a failed session without changing its intent
1553
+
1554
+ Try again is separate from Pause/Resume and `sendMessage`. Retain the request
1555
+ across an ambiguous transport response; never synthesize a continuation prompt:
1556
+
1557
+ ```ts
1558
+ const retryRequest = {
1559
+ clientEventId: crypto.randomUUID(),
1560
+ failureEventId, // the current durable turn.failed event id
1561
+ model: selectedModel,
1562
+ reasoningEffort: "high" as const,
1563
+ latencyMode: "standard" as const,
1564
+ };
1565
+ const retry = await client.retrySession(workspaceId, sessionId, retryRequest);
1566
+ // { outcome: "accepted" | "replayed", turnId, failureEventId }
1567
+ ```
1568
+
1569
+ Requires `sessions:control` and session access. Recovery keeps the logical turn,
1570
+ original question, frozen authority, and completed history/tool results; it
1571
+ does not create a user message. Omitted policy fields retain the failed turn's
1572
+ selection. The selected policy is revalidated for new admission. A duplicate
1573
+ operation replays its receipt even after execution advances.
1574
+
1575
+ HTTP 409 codes distinguish `RETRY_STALE_FAILURE`, `RETRY_EXECUTION_UNRESOLVED`,
1576
+ `RETRY_TOOL_OUTCOME_UNKNOWN`, `RETRY_PAUSED`, `RETRY_UNSUPPORTED_FAILURE`, and
1577
+ `IDEMPOTENCY_KEY_REUSED`. `RETRY_EXECUTION_UNRESOLVED` is transient: earlier
1578
+ execution is still settling. `RETRY_TOOL_OUTCOME_UNKNOWN` is permanent: a tool
1579
+ call in the turn has an unknown outcome and cannot be blindly replayed, so
1580
+ continue with a new message instead. A call proven never started does not block
1581
+ Retry. A deliberate Pause must
1582
+ be resumed separately. Failure without a retained logical turn, a settled
1583
+ scheduled occurrence, and idle credit exhaustion are not supported retry
1584
+ boundaries. See [run lifecycle](../../docs/run-lifecycle.md).
1585
+
820
1586
  ## Session tool policy and native web search
821
1587
 
822
1588
  For standalone credential maintenance, use the dedicated operation rather than
@@ -848,7 +1614,7 @@ receipt without a second write. Old receipt verification fails explicitly with
848
1614
  Omitting `tools` when creating a top-level session selects the current
849
1615
  workspace-default capability policy, including the built-in `files` server.
850
1616
  Passing `tools`, including `[]`, is an intentional fixed narrowing and can
851
- therefore disable file-download access for that session. OpenGeni's own web UI
1617
+ therefore disable file-download access for that session. Opengeni's own web UI
852
1618
  keeps `files` enabled as a hidden default, while API and embedded clients retain
853
1619
  exact control over the explicit list. Supported Responses providers attach
854
1620
  their native bounded web-search tool independently of this MCP policy.
@@ -866,7 +1632,7 @@ const updated = await client.updateSessionToolPolicy(workspaceId, sessionId, {
866
1632
  ```
867
1633
 
868
1634
  To keep a fixed allow-list, replace both connected MCP servers and individual
869
- OpenGeni tools atomically:
1635
+ Opengeni tools atomically:
870
1636
 
871
1637
  ```ts
872
1638
  await client.updateSessionToolPolicy(workspaceId, sessionId, {
@@ -885,6 +1651,7 @@ it is not public web search.
885
1651
 
886
1652
  ```ts
887
1653
  const goal = await client.getGoal(workspaceId, sessionId); // counters: autoContinuations, noProgressStreak
1654
+ const maybeGoal = await client.findGoal(workspaceId, sessionId); // null for a goal-less session (no 404)
888
1655
  await client.pauseGoal(workspaceId, sessionId, { rationale: "manual review" });
889
1656
  await client.resumeGoal(workspaceId, sessionId); // resets counters, re-arms continuations
890
1657
 
@@ -906,7 +1673,7 @@ await client.rollbackGoalRevision(workspaceId, sessionId, appliedRevisionId, {
906
1673
  call; the lower-level steps are exported for resumable/custom flows.
907
1674
 
908
1675
  Browser hosts need no storage credentials or per-application registration.
909
- OpenGeni authorizes the workspace request and returns a short-lived,
1676
+ Opengeni authorizes the workspace request and returns a short-lived,
910
1677
  object-scoped signed URL; operators must configure the private object store to
911
1678
  allow CORS from `*` so any product embedding the SDK can use that URL.
912
1679
 
@@ -982,12 +1749,11 @@ Every public endpoint group has typed methods:
982
1749
  | Sessions + events | `createSession`, `listSessions`, `listSessionPage`, `listAgentTopology`, `getSession`, `getSessionLineage`, `updateSession`, `listEvents`, `sendEvent`, `sendMessage`, `steerMessage`, `pauseSession`, `resumeSession`, `cancelSession`, `sendApprovalDecision`, `streamEvents`, `openEventStream` |
983
1750
  | Machines (bring-your-own-compute) | `listMachines`, `machineMetricsSeries`, `swapActiveSandbox`, `mintEnrollToken`, `lookupDeviceEnrollment`, `approveDeviceEnrollment`, `denyDeviceEnrollment` |
984
1751
  | Turn queue | `getQueue`, `moveQueueItem`, `editQueueItem`, `steerQueueItem`, `deleteQueueItem` |
985
- | Goal | `getGoal`, `updateGoal`, `pauseGoal`, `resumeGoal`, `listGoalRevisions`, `listGoalRevisionPage`, `applyGoalRevision`, `rejectGoalRevision`, `rollbackGoalRevision` |
986
- | Scheduled tasks | `createScheduledTask`, `listScheduledTasks`, `getScheduledTask`, `updateScheduledTask`, `pauseScheduledTask`, `resumeScheduledTask`, `triggerScheduledTask`, `deleteScheduledTask`, `listScheduledTaskRuns` |
987
- | Variable sets | `listVariableSets`, `createVariableSet`, `getVariableSet`, `updateVariableSet`, `deleteVariableSet`, `setVariableSetVariable`, `deleteVariableSetVariable`; generic reads are metadata-only, while dedicated permissioned exact-value reads are part of the held client train |
1752
+ | Goal | `getGoal`, `findGoal`, `updateGoal`, `pauseGoal`, `resumeGoal`, `listGoalRevisions`, `listGoalRevisionPage`, `applyGoalRevision`, `rejectGoalRevision`, `rollbackGoalRevision` |
1753
+ | Scheduled tasks | `createScheduledTask`, `listScheduledTasks`, `getScheduledTask`, `updateScheduledTask`, `pauseScheduledTask`, `resumeScheduledTask`, `triggerScheduledTask`, `deleteScheduledTask`, `listScheduledTaskRuns`, `refreshScheduledTaskAccess`, `listScheduledTaskAccessAttention` |
1754
+ | Vaults | `client.vaults(workspaceId)` returns `listVaults`, `getVault`, `createVault`, `updateVault`, `deleteVault`, `syncVault`, `listItems`, `getItem`, `createItem`, `updateItem`, `deleteItem`, `restoreItem`, `moveItem`, `revealField`, `listFieldVersions`, `restoreFieldVersion`, `getSessionSecrets`, `setSessionVaultPolicy`, `installOnePassword`, `listOnePasswordConnections`, `replaceOnePasswordToken`, `disconnectOnePassword`, `listProviderVaults`, `linkProviderVault` and `createManagedProviderVault`. Reads are metadata-only; `revealField` is the one audited plaintext read. Agents use `{{secret:Vault/Item/field}}` references instead of values. Variable Sets were removed in 2.0.0 |
988
1755
  | Files | `uploadFile`, `beginFileUpload`, `completeFileUpload`, `getFile`, `createFileDownloadUrl` |
989
1756
  | Documents | `createDocumentBase`, `listDocumentBases`, `getDocumentBase`, `addDocument`, `listDocuments`, `reindexDocument`, `searchDocuments`, `searchKnowledge` (effective organization + workspace + immutable initiating-user personal scope) |
990
- | Packs | `listPacks`, `registerPack`, `getPack`, `enablePack`, `deletePack`, `listPackInstallations` |
991
1757
  | Capabilities | `listCapabilities`, `createCapability`, `enableCapability`, `disableCapability`, `discoverMcpCapabilities` |
992
1758
  | Plugin packages | `previewPlugin`, `installPlugin`, `previewPluginUninstall`, `uninstallPlugin` |
993
1759
  | API Integrations | `listIntegrationDefinitions`, `listApiIntegrations`, `previewApiIntegration`, `startApiIntegrationOAuth`, `installApiIntegration`, `previewApiIntegrationUninstall`, `uninstallApiIntegration`, `listIntegrationFacets`, `configureIntegrationFacet`, `pauseIntegrationFacet`, `resumeIntegrationFacet`, `removeIntegrationFacet`, `browseGoogleDriveFacetSource`, `saveGoogleDriveFacetSource` |
@@ -1027,15 +1793,18 @@ reconsider — they can change with their counterpart, not with the SDK.
1027
1793
 
1028
1794
  ## Compatibility
1029
1795
 
1030
- Clients and servers are compatible within the same **major** release-train
1031
- version; evolution is additive within a major and both sides are tolerant
1032
- readers. Official server builds expose `serverVersion` on `/healthz` and
1033
- `/v1/config/client`. Full policy: `docs/architecture.md` §3.10.
1796
+ Clients and servers are compatible within the same **major** version of this
1797
+ SDK; evolution is additive within a major and both sides are tolerant readers.
1798
+ Official server builds expose `serverVersion` on `/healthz` and
1799
+ `/v1/config/client`. A breaking change ships only in a new major, announced in
1800
+ the changelog. Full policy: `docs/design/api-compatibility-policy.md`.
1034
1801
 
1035
1802
  ## Proxy through your own API
1036
1803
 
1037
- Keep your organization API key on your server and re-emit the stream to your
1038
- own browser clients. The re-emitted wire format is identical to OpenGeni's SSE
1804
+ For the React conversation, use the packaged
1805
+ [`createSessionProxyHandler`](#embed-the-conversation-default). For a custom
1806
+ backend route, keep your organization API key on your server and re-emit the
1807
+ stream to your own browser clients. The re-emitted wire format is identical to Opengeni's SSE
1039
1808
  stream, so the browser side can consume it with this same SDK (or a plain
1040
1809
  `EventSource`), including resume via `?after=` / `Last-Event-ID`:
1041
1810
 
@@ -1043,7 +1812,10 @@ stream, so the browser side can consume it with this same SDK (or a plain
1043
1812
  // Your server (Hono, Next.js route handler, Bun.serve, workers, ...):
1044
1813
  import { OpenGeniClient, proxySessionEventStream } from "@opengeni/sdk";
1045
1814
 
1046
- const client = new OpenGeniClient({ baseUrl, apiKey });
1815
+ const client = new OpenGeniClient({
1816
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
1817
+ apiKey: process.env.OPENGENI_API_KEY!,
1818
+ });
1047
1819
 
1048
1820
  export function GET(request: Request): Response {
1049
1821
  // authenticate *your* user, resolve their session id, then:
@@ -1072,3 +1844,20 @@ Hosts, test doubles, and same-origin proxies must import
1072
1844
  configuration or contract header. Do not copy its string value: the revision is
1073
1845
  an executable compatibility boundary, and `getClientConfig()` intentionally
1074
1846
  fails closed when server and SDK revisions differ.
1847
+
1848
+ ### Action review facts
1849
+
1850
+ `getToolActionReview(workspaceId, sessionId, approvalId)` returns version 1 facts
1851
+ for an exact prepared connector action: title, selected account/count, material
1852
+ effects, optional provider-sourced examples, bounded fields, reason, status and
1853
+ available actions. Use `sendApprovalDecision` for the existing authenticated
1854
+ human decision. Facts never grant execution authority.
1855
+
1856
+ `getToolReviewDetails(workspaceId, sessionId, approvalId, { actionDigest, path,
1857
+ offset })` pages through the saved selection/values (25 rows per page). The
1858
+ reference is tied to the original action digest and requires the same session
1859
+ visibility permission; it never re-runs a search. Secret/write-only fields stay
1860
+ hidden. A missing legacy snapshot may return 404; other authorization or transport
1861
+ failures must not be interpreted as permission to execute. Responses are private
1862
+ and non-cacheable. Review states distinguish waiting, execution, cancellation,
1863
+ failure and uncertain outcome. Exact stored arguments are never client-editable.