@opengeni/sdk 0.57.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +1017 -26
  2. package/dist/accounts.d.ts +153 -0
  3. package/dist/accounts.js +307 -0
  4. package/dist/accounts.js.map +1 -0
  5. package/dist/adapters/express.d.ts +31 -0
  6. package/dist/adapters/express.js +87 -0
  7. package/dist/adapters/express.js.map +1 -0
  8. package/dist/adapters/hono.d.ts +16 -0
  9. package/dist/adapters/hono.js +8 -0
  10. package/dist/adapters/hono.js.map +1 -0
  11. package/dist/adapters/next.d.ts +29 -0
  12. package/dist/adapters/next.js +22 -0
  13. package/dist/adapters/next.js.map +1 -0
  14. package/dist/allowance-refusal.d.ts +2 -0
  15. package/dist/allowance-refusal.js +50 -0
  16. package/dist/allowance-refusal.js.map +1 -0
  17. package/dist/artifact-catalog.d.ts +31 -0
  18. package/dist/artifact-client.d.ts +23 -11
  19. package/dist/artifacts.d.ts +1 -1
  20. package/dist/artifacts.js +7 -5
  21. package/dist/automations-client.d.ts +17 -0
  22. package/dist/automations.d.ts +3 -0
  23. package/dist/automations.js +84 -0
  24. package/dist/automations.js.map +1 -0
  25. package/dist/browser.d.ts +11 -0
  26. package/dist/browser.js +47 -0
  27. package/dist/browser.js.map +1 -0
  28. package/dist/chat/fold.d.ts +75 -0
  29. package/dist/chat/handler.d.ts +41 -0
  30. package/dist/chat/http.d.ts +67 -0
  31. package/dist/chat/ids.d.ts +34 -0
  32. package/dist/chat/index.d.ts +9 -0
  33. package/dist/chat/index.js +1694 -0
  34. package/dist/chat/index.js.map +1 -0
  35. package/dist/chat/openai.d.ts +30 -0
  36. package/dist/chat/opengeni.d.ts +128 -0
  37. package/dist/chat/types.d.ts +207 -0
  38. package/dist/chat/vercel.d.ts +49 -0
  39. package/dist/chats.d.ts +8 -0
  40. package/dist/{chunk-JDLDDRNE.js → chunk-2UXZUCEZ.js} +291 -34
  41. package/dist/chunk-2UXZUCEZ.js.map +1 -0
  42. package/dist/chunk-4SMEOV3P.js +246 -0
  43. package/dist/chunk-4SMEOV3P.js.map +1 -0
  44. package/dist/{chunk-JRPFWI6T.js → chunk-AH5D5Q5V.js} +31 -35
  45. package/dist/chunk-AH5D5Q5V.js.map +1 -0
  46. package/dist/chunk-APXVSSSY.js +301 -0
  47. package/dist/chunk-APXVSSSY.js.map +1 -0
  48. package/dist/chunk-DUVIYER4.js +639 -0
  49. package/dist/chunk-DUVIYER4.js.map +1 -0
  50. package/dist/{chunk-PKQ377ED.js → chunk-FG3OO5LM.js} +2686 -611
  51. package/dist/chunk-FG3OO5LM.js.map +1 -0
  52. package/dist/chunk-GZYZT22T.js +260 -0
  53. package/dist/chunk-GZYZT22T.js.map +1 -0
  54. package/dist/chunk-HMGADJGZ.js +150 -0
  55. package/dist/chunk-HMGADJGZ.js.map +1 -0
  56. package/dist/{chunk-YKZME56C.js → chunk-KWADQ227.js} +3 -3
  57. package/dist/{chunk-YKZME56C.js.map → chunk-KWADQ227.js.map} +1 -1
  58. package/dist/chunk-LY2K6ELJ.js +126 -0
  59. package/dist/chunk-LY2K6ELJ.js.map +1 -0
  60. package/dist/chunk-MOTJYIYL.js +340 -0
  61. package/dist/chunk-MOTJYIYL.js.map +1 -0
  62. package/dist/{chunk-ST5DDKJP.js → chunk-NC57YMUO.js} +121 -3
  63. package/dist/chunk-NC57YMUO.js.map +1 -0
  64. package/dist/chunk-RWKB6YZG.js +1270 -0
  65. package/dist/chunk-RWKB6YZG.js.map +1 -0
  66. package/dist/chunk-U6CV2SXO.js +195 -0
  67. package/dist/chunk-U6CV2SXO.js.map +1 -0
  68. package/dist/chunk-UM4ZXXJR.js +9 -0
  69. package/dist/chunk-UM4ZXXJR.js.map +1 -0
  70. package/dist/chunk-V6GIY62B.js +229 -0
  71. package/dist/chunk-V6GIY62B.js.map +1 -0
  72. package/dist/chunk-WU6GQG25.js +268 -0
  73. package/dist/chunk-WU6GQG25.js.map +1 -0
  74. package/dist/{chunk-VWE2QIVO.js → chunk-XNG7YGR3.js} +23 -6
  75. package/dist/chunk-XNG7YGR3.js.map +1 -0
  76. package/dist/{chunk-HJ3HHUT5.js → chunk-YIKA2QT4.js} +33 -2
  77. package/dist/chunk-YIKA2QT4.js.map +1 -0
  78. package/dist/chunk-ZKPY6YAJ.js +135 -0
  79. package/dist/chunk-ZKPY6YAJ.js.map +1 -0
  80. package/dist/client.d.ts +740 -94
  81. package/dist/codex-realtime-controller.d.ts +3 -0
  82. package/dist/codex-realtime-controller.js +2 -2
  83. package/dist/codex-realtime-v3.d.ts +13 -2
  84. package/dist/company-brain.d.ts +22 -0
  85. package/dist/company-profile.d.ts +14 -1
  86. package/dist/core.d.ts +9 -3
  87. package/dist/core.js +25 -6
  88. package/dist/deprecation.d.ts +47 -0
  89. package/dist/document-authority-client.d.ts +51 -0
  90. package/dist/document-authority.d.ts +2 -0
  91. package/dist/document-authority.js +12 -0
  92. package/dist/document-authority.js.map +1 -0
  93. package/dist/editable-artifacts/browser-session.d.ts +7 -3
  94. package/dist/editable-artifacts/controller.d.ts +4 -1
  95. package/dist/editable-artifacts/errors.d.ts +1 -1
  96. package/dist/editable-artifacts/http-live-transport.d.ts +6 -2
  97. package/dist/editable-artifacts/index.d.ts +2 -0
  98. package/dist/editable-artifacts/open-failure.d.ts +9 -0
  99. package/dist/editable-artifacts/types.d.ts +6 -3
  100. package/dist/editable-artifacts/worker/kernel-adapter.d.ts +1 -1
  101. package/dist/editable-artifacts-worker.js +44 -17
  102. package/dist/editable-artifacts-worker.js.map +1 -1
  103. package/dist/editable-artifacts.js +240 -121
  104. package/dist/editable-artifacts.js.map +1 -1
  105. package/dist/embedding-client.d.ts +129 -0
  106. package/dist/errors.d.ts +36 -1
  107. package/dist/feedback.d.ts +23 -0
  108. package/dist/gateway-realtime-transport.js +1 -1
  109. package/dist/github-repositories.d.ts +10 -0
  110. package/dist/github-repositories.js +36 -0
  111. package/dist/github-repositories.js.map +1 -0
  112. package/dist/index.d.ts +56 -11
  113. package/dist/index.js +242 -125
  114. package/dist/index.js.map +1 -1
  115. package/dist/interaction.d.ts +84 -1
  116. package/dist/interaction.js +6 -1
  117. package/dist/knowledge.d.ts +7 -0
  118. package/dist/knowledge.js +20 -0
  119. package/dist/knowledge.js.map +1 -0
  120. package/dist/message-links.d.ts +68 -0
  121. package/dist/model-context.d.ts +68 -0
  122. package/dist/model-display.d.ts +2 -0
  123. package/dist/model-display.js +15 -0
  124. package/dist/model-display.js.map +1 -0
  125. package/dist/model-picker-order.d.ts +1 -0
  126. package/dist/model-picker-order.js +10 -0
  127. package/dist/model-picker-order.js.map +1 -0
  128. package/dist/organization-integration-policy.d.ts +26 -0
  129. package/dist/organization-integration-policy.js +26 -0
  130. package/dist/organization-integration-policy.js.map +1 -0
  131. package/dist/organization-private-session-settings.d.ts +6 -0
  132. package/dist/organization-private-session-settings.js +22 -0
  133. package/dist/organization-private-session-settings.js.map +1 -0
  134. package/dist/organization-user-setup.d.ts +8 -0
  135. package/dist/organization-user-setup.js +16 -0
  136. package/dist/organization-user-setup.js.map +1 -0
  137. package/dist/pr-review-client.d.ts +16 -0
  138. package/dist/pr-review.d.ts +4 -0
  139. package/dist/pr-review.js +62 -0
  140. package/dist/pr-review.js.map +1 -0
  141. package/dist/preference-registry.d.ts +9 -1
  142. package/dist/proxy.d.ts +7 -5
  143. package/dist/realtime.d.ts +3 -1
  144. package/dist/realtime.js +6 -6
  145. package/dist/realtime.js.map +1 -1
  146. package/dist/retained-artifacts.d.ts +5 -1
  147. package/dist/session-history-import.d.ts +22 -0
  148. package/dist/session-history-import.js +36 -0
  149. package/dist/session-history-import.js.map +1 -0
  150. package/dist/session-list-entries.d.ts +1 -0
  151. package/dist/session-list-entries.js +7 -0
  152. package/dist/session-list-entries.js.map +1 -0
  153. package/dist/session-message-search.d.ts +53 -0
  154. package/dist/session-proxy-client.d.ts +24 -0
  155. package/dist/session-proxy.d.ts +245 -0
  156. package/dist/session-proxy.js +24 -0
  157. package/dist/session-proxy.js.map +1 -0
  158. package/dist/session-titles.d.ts +2 -0
  159. package/dist/site-browser-entry.d.ts +1 -0
  160. package/dist/site-browser-runtime.gen.d.ts +1 -0
  161. package/dist/site-document.d.ts +5 -0
  162. package/dist/site-document.js +20 -0
  163. package/dist/site-document.js.map +1 -0
  164. package/dist/site-document.test.d.ts +1 -0
  165. package/dist/site-http.d.ts +16 -0
  166. package/dist/site-tool-bridge.d.ts +44 -0
  167. package/dist/site.d.ts +101 -0
  168. package/dist/site.js +49 -0
  169. package/dist/site.js.map +1 -0
  170. package/dist/skills.d.ts +75 -0
  171. package/dist/stream.d.ts +2 -0
  172. package/dist/tenant-workspaces.d.ts +47 -0
  173. package/dist/tenant-workspaces.js +8 -0
  174. package/dist/tenant-workspaces.js.map +1 -0
  175. package/dist/tool-auth.d.ts +87 -0
  176. package/dist/tool-auth.js +23 -0
  177. package/dist/tool-auth.js.map +1 -0
  178. package/dist/tool-display-metadata.d.ts +3 -0
  179. package/dist/tools-FXBLUWAE.js +12 -0
  180. package/dist/tools-FXBLUWAE.js.map +1 -0
  181. package/dist/tools.d.ts +61 -0
  182. package/dist/types.d.ts +2863 -424
  183. package/dist/usage-allowances.d.ts +36 -0
  184. package/dist/usage-allowances.js +80 -0
  185. package/dist/usage-allowances.js.map +1 -0
  186. package/dist/workspace-artifacts.d.ts +33 -5
  187. package/dist/workspace-instruction-policies.d.ts +24 -2
  188. package/dist/workspace-integrations.d.ts +74 -0
  189. package/dist/workspace-integrations.js +65 -0
  190. package/dist/workspace-integrations.js.map +1 -0
  191. package/dist/workspace-learning.d.ts +124 -0
  192. package/dist/workspace-state.d.ts +8 -71
  193. package/package.json +108 -3
  194. package/src/accounts.ts +443 -0
  195. package/src/adapters/express.ts +136 -0
  196. package/src/adapters/hono.ts +18 -0
  197. package/src/adapters/next.ts +42 -0
  198. package/src/allowance-refusal.ts +5 -0
  199. package/src/artifact-catalog.ts +41 -0
  200. package/src/artifact-client.ts +43 -52
  201. package/src/artifacts.ts +7 -1
  202. package/src/automations-client.ts +124 -0
  203. package/src/automations.ts +15 -0
  204. package/src/browser.ts +54 -0
  205. package/src/chat/fold.ts +431 -0
  206. package/src/chat/handler.ts +240 -0
  207. package/src/chat/http.ts +257 -0
  208. package/src/chat/ids.ts +85 -0
  209. package/src/chat/index.ts +87 -0
  210. package/src/chat/openai.ts +439 -0
  211. package/src/chat/opengeni.ts +722 -0
  212. package/src/chat/types.ts +219 -0
  213. package/src/chat/vercel.ts +204 -0
  214. package/src/chats.ts +16 -0
  215. package/src/client.ts +4084 -515
  216. package/src/codex-realtime-controller.ts +235 -5
  217. package/src/codex-realtime-v3.ts +162 -31
  218. package/src/codex-realtime.ts +1 -1
  219. package/src/company-brain.ts +59 -0
  220. package/src/company-profile.ts +17 -1
  221. package/src/core.ts +23 -1
  222. package/src/deprecation.ts +149 -0
  223. package/src/document-authority-client.ts +219 -0
  224. package/src/document-authority.ts +17 -0
  225. package/src/editable-artifacts/browser-session.ts +36 -22
  226. package/src/editable-artifacts/controller.ts +102 -47
  227. package/src/editable-artifacts/errors.ts +2 -0
  228. package/src/editable-artifacts/http-live-transport.ts +62 -37
  229. package/src/editable-artifacts/index.ts +6 -0
  230. package/src/editable-artifacts/open-failure.ts +79 -0
  231. package/src/editable-artifacts/storage.ts +31 -40
  232. package/src/editable-artifacts/types.ts +6 -3
  233. package/src/editable-artifacts/worker/kernel-adapter.ts +23 -7
  234. package/src/editable-artifacts/worker/runtime.ts +26 -13
  235. package/src/editable-artifacts/worker/wire-codec.ts +23 -5
  236. package/src/embedding-client.ts +519 -0
  237. package/src/errors.ts +169 -2
  238. package/src/feedback.ts +20 -0
  239. package/src/gateway-realtime-transport.ts +3 -3
  240. package/src/github-repositories.ts +60 -0
  241. package/src/index.ts +561 -41
  242. package/src/interaction.ts +276 -4
  243. package/src/knowledge.ts +72 -0
  244. package/src/message-links.ts +178 -0
  245. package/src/model-context.ts +112 -0
  246. package/src/model-display.ts +10 -0
  247. package/src/model-picker-order.ts +6 -0
  248. package/src/organization-integration-policy.ts +54 -0
  249. package/src/organization-private-session-settings.ts +33 -0
  250. package/src/organization-user-setup.ts +31 -0
  251. package/src/pr-review-client.ts +92 -0
  252. package/src/pr-review.ts +17 -0
  253. package/src/preference-registry.ts +11 -0
  254. package/src/proxy.ts +17 -6
  255. package/src/realtime.ts +1 -0
  256. package/src/retained-artifacts.ts +101 -0
  257. package/src/session-history-import.ts +84 -0
  258. package/src/session-list-entries.ts +1 -0
  259. package/src/session-message-search.ts +47 -0
  260. package/src/session-proxy-client.ts +64 -0
  261. package/src/session-proxy.ts +1473 -0
  262. package/src/session-titles.ts +10 -0
  263. package/src/site-browser-entry.ts +6 -0
  264. package/src/site-browser-runtime.gen.ts +2 -0
  265. package/src/site-document.test.ts +64 -0
  266. package/src/site-document.ts +18 -0
  267. package/src/site-http.ts +162 -0
  268. package/src/site-tool-bridge.ts +146 -0
  269. package/src/site.ts +585 -0
  270. package/src/skills.ts +69 -0
  271. package/src/sse.ts +8 -2
  272. package/src/stream.ts +38 -4
  273. package/src/tenant-workspaces.ts +148 -0
  274. package/src/tool-auth.ts +301 -0
  275. package/src/tool-display-metadata.ts +19 -0
  276. package/src/tools.ts +396 -0
  277. package/src/types.ts +3346 -501
  278. package/src/usage-allowances.ts +189 -0
  279. package/src/workspace-artifacts.ts +40 -6
  280. package/src/workspace-instruction-policies.ts +26 -1
  281. package/src/workspace-integrations.ts +402 -0
  282. package/src/workspace-learning.ts +138 -0
  283. package/src/workspace-state.ts +8 -82
  284. package/dist/chunk-GU6JF75T.js +0 -119
  285. package/dist/chunk-GU6JF75T.js.map +0 -1
  286. package/dist/chunk-HJ3HHUT5.js.map +0 -1
  287. package/dist/chunk-JDLDDRNE.js.map +0 -1
  288. package/dist/chunk-JRPFWI6T.js.map +0 -1
  289. package/dist/chunk-PKQ377ED.js.map +0 -1
  290. package/dist/chunk-ST5DDKJP.js.map +0 -1
  291. package/dist/chunk-VWE2QIVO.js.map +0 -1
package/README.md CHANGED
@@ -5,13 +5,284 @@ session lifecycle, and the streaming core — SSE event streaming with automatic
5
5
  reconnect, resume-by-sequence, gap backfill, and duplicate suppression — plus
6
6
  helpers for proxying the stream through your own API.
7
7
 
8
- Zero runtime dependencies. Needs only WHATWG `fetch` and streams, so it runs in
9
- Node 18+, Bun, Deno, browsers, and edge runtimes.
8
+ For the complete product boundary—organization API keys, organization
9
+ workspaces, Personal-workspace exclusion, tenant mapping, and external Skill
10
+ ownership—start with the canonical
11
+ [product integration guide](../../docs/product-integration.md).
12
+
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.
22
+
23
+ Browser clients may call the public API from any origin with an explicitly safe
24
+ bearer design, but an organization API key belongs on the product server.
25
+ Browser cookies are accepted cross-origin only from operator-configured trusted
26
+ origins; arbitrary embedding origins never receive credentialed CORS responses.
27
+
28
+ ## Allowance lifecycle recovery
29
+
30
+ `getWorkspaceAllowance(workspaceId)` still returns nullable configuration.
31
+ Use `getWorkspaceAllowanceState(workspaceId)` when you also need the lifecycle
32
+ version: `{ version: 0, config: null }` means never configured, while a positive
33
+ version with null configuration means cleared. Recreation uses that exact
34
+ version as `expectedVersion`.
35
+
36
+ `clearWorkspaceAllowance(workspaceId, { expectedVersion, operationId })` returns
37
+ `{ version }`. The optional operation ID makes a lost response recoverable:
38
+ reuse the same actor, operation ID and exact request. Replay rechecks current
39
+ authority and returns 409 after any later allowance lifecycle change. On 409,
40
+ read the current state and decide whether a new operation is intended; never
41
+ guess a CAS version. The SDK does not automatically retry mutations.
42
+
43
+ The same reads and administration are free functions on the
44
+ `@opengeni/sdk/usage-allowances` subpath for browser code that only has the
45
+ narrow `OpenGeniBrowserClient` (`getMyUsage(client, workspaceId)`,
46
+ `getAllUsage`, `setMemberAllowance`, ...); they call the same routes as the root methods.
47
+
48
+ State reads require the same budget-read authority as configuration reads.
49
+ Full usage additionally requires target workspace access; organization budget
50
+ authority alone does not expose the member roster. Agents cannot read or
51
+ change allowances.
52
+
53
+ ## Embed the conversation (default)
54
+
55
+ Embedding Opengeni is one backend route plus one component, and you only speak
56
+ in your own ids. The browser renders `@opengeni/react`'s `OpenGeniChat` (or
57
+ `SessionConversation`); your server mounts `createSessionProxyHandler`, a
58
+ same-origin proxy that acts as your authenticated user:
10
59
 
11
- Browser clients may call the public API from any origin with an API key or
12
- other bearer credential. Browser cookies are accepted cross-origin only from
13
- operator-configured trusted origins; arbitrary embedding origins never receive
14
- credentialed CORS responses.
60
+ ```ts
61
+ // lib/opengeni.ts (server only: the organization API key never reaches the browser)
62
+ import { OpenGeni } from "@opengeni/sdk/chat";
63
+
64
+ export const og = new OpenGeni({ apiKey: process.env.OPENGENI_API_KEY! });
65
+ ```
66
+
67
+ ```ts
68
+ // app/api/opengeni/[...path]/route.ts (any web-standard host works; see the adapters below)
69
+ import { createSessionProxyRoute } from "@opengeni/sdk/next";
70
+ import { og } from "@/lib/opengeni";
71
+
72
+ export const dynamic = "force-dynamic";
73
+ export const { GET, POST, PUT, PATCH, DELETE } = createSessionProxyRoute(og, {
74
+ resolve: async (request) => {
75
+ const me = await authenticate(request); // your existing session check
76
+ if (!me) return new Response("Unauthorized", { status: 401 });
77
+ return { user: me.id, tenant: me.orgId }; // or { user: me.id }
78
+ },
79
+ createSession: (input) => input, // let the browser start chats
80
+ });
81
+ ```
82
+
83
+ ```tsx
84
+ // app/assistant/assistant.tsx (browser): no provider, client, or workspace id.
85
+ "use client";
86
+ import { OpenGeniChat } from "@opengeni/react";
87
+ import "@opengeni/react/compiled.css";
88
+
89
+ export function Assistant() {
90
+ return <OpenGeniChat baseUrl="/api/opengeni" />; // chat list + conversation
91
+ }
92
+ ```
93
+
94
+ `baseUrl` makes the component create the browser client for your mount and use
95
+ the workspace the proxy resolved for the signed-in user (reported in the
96
+ proxy's client config). One conversation alone is
97
+ `<SessionConversation baseUrl="/api/opengeni" sessionId={id} />`. The
98
+ `<OpenGeniProvider client={client} workspaceId={workspaceId}>` form keeps
99
+ working, for example for several components or the headless hooks.
100
+
101
+ What `resolve` returns decides the workspace:
102
+
103
+ - `{ user, tenant }`: one workspace per tenant; chats are private per user by
104
+ default.
105
+ - `{ user }`: one workspace per user. It stays single-user: the SDK adds its
106
+ owner once, and Opengeni never admits anyone else to it.
107
+ - `{ user, workspaceId }`: bring your own workspace id.
108
+
109
+ Workspaces are created on first use and cached per `OpenGeni` instance.
110
+ `og.workspaceId({ tenant })` and `og.workspaceId({ user })` return the same ids
111
+ the proxy uses. There is no onboarding step: the first request a user makes
112
+ adds them to a tenant (or your own) workspace with conversation permissions
113
+ (workspace read, session create/read/control, file upload/read, and attaching
114
+ your per-session MCP servers; no admin). That needs an organization API key
115
+ with `members:manage` (full access includes it) plus those permissions.
116
+ Opengeni never changes an existing membership, so narrowing or widening one
117
+ with `updateExternalWorkspaceMember` sticks. A user you remove from a tenant
118
+ workspace is added again on their next request, because your product decides
119
+ who its users are; stop resolving a user you no longer admit. A removal from a
120
+ per-user workspace sticks. `organizationId` is optional:
121
+ the facade reads it once from the key. Keep `user` and `tenant`
122
+ host-authenticated, never from the request body or path.
123
+
124
+ The proxy and facade accept `chats`. `"private"` (the default with a user)
125
+ uses private visibility, session-only agent reach and personal Knowledge;
126
+ `"shared"` uses workspace visibility, agent reach and Knowledge, so a tenant's
127
+ users see each other's chats. Without a user and with `chats` omitted, the
128
+ facade keeps workspace visibility, session-only agent reach and Knowledge
129
+ authoring off. The API wire values are `visibility: "private" | "workspace"`;
130
+ stored visibility is `user_private | workspace_shared`. Explicit hook/create
131
+ fields override defaults. Keep MCP URLs and credentials in the server's
132
+ `createSession` hook; the browser cannot choose them through the proxy.
133
+
134
+ You can also pass a plain `OpenGeniClient` (organization key) instead of the
135
+ facade; then `resolve` must return `{ user, workspaceId }`. The explicit
136
+ membership APIs (`ensureWorkspace`, `addExternalWorkspaceMember`,
137
+ `updateExternalWorkspaceMember`, `cancelExternalWorkspaceMemberGrant`) keep
138
+ working for products that manage access themselves. Legacy
139
+ `chats: "isolated"` with `{ tenant, user }` still provisions a separate
140
+ workspace per user and tenant. Per-user workspaces add their owner with a
141
+ stable onboarding key, so a retry never restores a revoked grant;
142
+ `memberPermissions` replaces that owner's permissions on first onboarding
143
+ only. The standalone resolver is on the server-only
144
+ `@opengeni/sdk/tenant-workspaces` subpath.
145
+
146
+ The facade also accepts `agent` (identity, capabilities, instructions, renderer)
147
+ and defaults its renderer to `"markdown"`. Current servers always admit agent
148
+ configuration. For older servers that still gate it, only an implicit renderer
149
+ retries once without `agent` on `422 agent_config_not_enabled`, and that refusal
150
+ is cached per facade instance. Explicit agent settings are never stripped: the
151
+ 422 names `OPENGENI_AGENT_CONFIG_ADMISSION_ENABLED` for that older server's
152
+ operator. The private default with a user now turns personal Knowledge
153
+ on; pass `memory: false` to keep authoring off.
154
+ Missing organization private-session enablement raises `OpenGeniSetupError`,
155
+ with owner/admin API, SDK and web-app instructions; the proxy preserves that
156
+ actionable error for browser clients.
157
+
158
+ Framework adapters are thin wrappers over the same web-standard handler (they
159
+ also accept `createChatHandler` or any `(Request) => Promise<Response>`):
160
+
161
+ ```ts
162
+ // Next.js App Router: app/api/opengeni/[...path]/route.ts
163
+ import { createSessionProxyRoute } from "@opengeni/sdk/next";
164
+ export const dynamic = "force-dynamic";
165
+ export const { GET, POST, PUT, PATCH, DELETE } = createSessionProxyRoute(og, { resolve });
166
+
167
+ // Express / Connect / node:http (mount before body parsers, or they are re-serialized)
168
+ import { toNodeMiddleware } from "@opengeni/sdk/express";
169
+ app.use("/api/opengeni", toNodeMiddleware(handler));
170
+
171
+ // Hono
172
+ import { toHonoHandler } from "@opengeni/sdk/hono";
173
+ app.all("/api/opengeni/*", toHonoHandler(handler));
174
+ ```
175
+
176
+ The Next adapter passes the full URL, including any `basePath`; the Node
177
+ middleware rebuilds it from `originalUrl`, streams bodies and SSE, and aborts
178
+ the request when the client disconnects.
179
+
180
+ The chat list (`SessionList` / `OpenGeniChat`) uses `listSessionPage`. By
181
+ default (`sessionList: "mine"`) the proxy adds a creator filter for the
182
+ resolved user server-side, so each user sees only the chats they started;
183
+ `sessionList: "visible"` lists every chat OpenGeni lets the user read in the
184
+ workspace, and `false` disables listing. Archive and restore go through
185
+ `updateSessionArchive` unless `archive: false`. A "New chat" in `OpenGeniChat`
186
+ sends only `{ initialMessage, idempotencyKey }`, so it needs the
187
+ `createSession` hook.
188
+
189
+ `listSessionPage` includes personal pinned details by default. A caller with a
190
+ separate `pinsOnly: true` read can pass `includePinned: false` on ordinary pages
191
+ to avoid repeatedly loading those details. Ordinary pages still exclude pinned
192
+ rows; `pinsOnly: true` and `includePinned: false` cannot be combined. Older API
193
+ versions may ignore `includePinned` and return the default pin projection.
194
+
195
+ For navigation lists, `listSessionSummaryPage` uses the same filters, ordering,
196
+ pin rules and cursors, returning `SessionListEntry` records. Entries include
197
+ display/rename titles, creator, lifecycle, personal state and hierarchy counts;
198
+ prompts and execution configuration remain on `getSession`. Older APIs return
199
+ full pages that the SDK projects locally. Unknown filters/cursors still fail
200
+ explicitly rather than returning an unfiltered result.
201
+
202
+ Every request calls `resolve`, then runs through `asUser(user, { source })`
203
+ (there is no service-authority fallback) against exactly the resolved
204
+ workspace. Only the native routes `OpenGeniProvider` and the conversation use
205
+ are served: client config; workspace read, model catalog, live control stream,
206
+ and workspace Resume; session read/rename, events (list and SSE with
207
+ `Last-Event-ID` resume), send/steer/approval/human-input, queue, composer
208
+ draft, pause/resume; and, unless `files: false`, attachment upload and download
209
+ URLs. Every other route or method is a 404 (cancel and workspace Pause are
210
+ refused); unknown query parameters on served reads pass through. Browser session creation is disabled
211
+ unless you supply `createSession`; the browser may then send only
212
+ `initialMessage` and `idempotencyKey`, and your hook returns the full request
213
+ (tools, MCP servers, Skills, instructions, model policy). Message bodies are
214
+ capped by `maxBodyBytes` (1 MiB), cannot rotate MCP credentials or attach
215
+ anything but files, and `modelSelection: false` strips per-message model
216
+ choices. `authorizeSession(sessionId, context)` adds a product-level session
217
+ check on top of OpenGeni's own membership and visibility checks. Without
218
+ `authorizeMutation`, only cross-site (`Sec-Fetch-Site`) mutations are refused,
219
+ so cookie-authenticated hosts should pass their CSRF check.
220
+
221
+ ### Your own tools, as the signed-in user
222
+
223
+ `toolServer` attaches your product's MCP endpoint to every session the
224
+ `createSession` hook creates, with a short-lived token for the user `resolve`
225
+ authenticated. The proxy refreshes it on every send, steer, submit, approval,
226
+ and human-input answer. Your endpoint, built with any MCP library, verifies it:
227
+
228
+ ```ts
229
+ createSessionProxyHandler(og, {
230
+ resolve,
231
+ createSession,
232
+ toolServer: {
233
+ url: "https://app.example.com/api/mcp", // public HTTPS (tunnel locally)
234
+ approvals: { ask: ["rename_post"] }, // the user approves each call first
235
+ },
236
+ });
237
+
238
+ // In the MCP route (Web Request, or an Express/Node request):
239
+ import { ToolRequestError, verifyToolRequest } from "@opengeni/sdk/tool-auth";
240
+ const { user, tenant, workspaceId } = await verifyToolRequest(request); // or ToolRequestError (401)
241
+ ```
242
+
243
+ Scope every tool to the verified `user` and `tenant`; never trust ids the model
244
+ sends; list write tools in `approvals.ask`. Tools act as the chat's creator, also
245
+ in shared chats. Members need `mcp_servers:attach`. Tokens are HS256 JWTs signed
246
+ with a key derived from `OPENGENI_API_KEY` (or the same `secret` on both sides),
247
+ bound to the full tool URL by `aud` (set `OPENGENI_TOOL_SERVER_URL` to configure
248
+ both sides at once), and valid for `ttlSeconds` (default 24 hours). Non-Node
249
+ verifiers use the hex key from `deriveToolTokenKey()`, never the organization key;
250
+ the format is documented in `src/tool-auth.ts`. See
251
+ `examples/tool-server` for a runnable Express version.
252
+
253
+ `beforeForwardMessage(message, context)` runs before every forwarded user
254
+ message (send, steer, composer submit, and a browser-started create) and may
255
+ return server-owned additions, or a `Response` to refuse the message:
256
+
257
+ ```ts
258
+ createSessionProxyHandler(og, {
259
+ resolve,
260
+ beforeForwardMessage: async ({ sessionId, delivery }, { user }) => ({
261
+ // Model-visible, placed before any context the browser sent.
262
+ modelContext: `Page ${currentPage(user)} · ${timeZone(user)} · ${today()}`,
263
+ // Header-only rotation of MCP servers already attached to the session,
264
+ // applied atomically as the message is accepted (ignored on create).
265
+ mcpCredentialUpdates: [
266
+ { id: "acme", headers: { Authorization: `Bearer ${await mintUserToken(user)}` } },
267
+ ],
268
+ }),
269
+ });
270
+ ```
271
+
272
+ This is the per-user tool token pattern: create the session with
273
+ `mcpServers: [{ id: "acme", url, headers }]` (and `tools: [{ kind: "mcp", id:
274
+ "acme" }]`, which the acting user needs `mcp_servers:attach` to attach), then
275
+ hand the MCP server a fresh short-lived bearer on every message. The browser
276
+ still cannot send `mcpCredentialUpdates` itself. With `modelSelection: false`
277
+ the proxy reports `modelSelection: false` in the client config, and
278
+ `SessionConversation` hides its model picker.
279
+
280
+ Opengeni adds each user to the workspace on their first request (see
281
+ [External users](#external-users-asuser)), with `workspace:read`,
282
+ `sessions:create`, `sessions:read`, `sessions:control`, `files:upload`,
283
+ `files:read`, and `mcp_servers:attach`. Pass the facade from `@opengeni/sdk/chat`
284
+ instead of a client to resolve `{ tenant, user }` or `{ user }` to a workspace
285
+ created on first use.
15
286
 
16
287
  ## Quick start
17
288
 
@@ -19,7 +290,7 @@ credentialed CORS responses.
19
290
  import { OpenGeniClient } from "@opengeni/sdk";
20
291
 
21
292
  const client = new OpenGeniClient({
22
- baseUrl: "https://api.example.com",
293
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
23
294
  apiKey: process.env.OPENGENI_API_KEY!,
24
295
  });
25
296
 
@@ -37,6 +308,574 @@ for await (const event of client.streamEvents(workspaceId, session.id)) {
37
308
  }
38
309
  ```
39
310
 
311
+ ## Organization-backed product setup
312
+
313
+ A multi-tenant product should hold one organization API key on its backend and
314
+ idempotently map each product tenant to an OpenGeni organization workspace. The
315
+ wire kind for an organization workspace is `"shared"`; Personal workspaces are
316
+ excluded.
317
+
318
+ ```ts
319
+ const client = new OpenGeniClient({
320
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
321
+ apiKey: process.env.OPENGENI_API_KEY!,
322
+ });
323
+
324
+ const organizationId = process.env.OPENGENI_ORGANIZATION_ID!;
325
+ const { workspace, created } = await client.ensureWorkspace({
326
+ accountId: organizationId,
327
+ externalSource: "acme-product",
328
+ externalId: tenant.id,
329
+ name: tenant.displayName,
330
+ });
331
+
332
+ if (workspace.kind !== "shared") {
333
+ throw new Error("Product integrations require an organization workspace");
334
+ }
335
+ ```
336
+
337
+ The response is `{ workspace, created }`. Replays return the original nested
338
+ workspace with `created: false`. For an organization key, `getAccessContext()`
339
+ does not enumerate every workspace grant; call `listWorkspaces()` for the
340
+ organization-workspace inventory allowed by the key's live scope.
341
+
342
+ Organization key administration uses `listOrganizationApiKeys`,
343
+ `getOrganizationApiKey`, `createOrganizationApiKey`, `updateOrganizationApiKey`,
344
+ and `deleteOrganizationApiKey`. GET detail and PATCH return the raw `ApiKey`;
345
+ creation returns `{ apiKey, token }`. The key token from a
346
+ create response is shown once and must be stored in the backend's secret
347
+ manager. `createOrganizationApiKey(organizationId, { name, access: "read" })`
348
+ mints a read-only master key: it inventories shared workspaces and reads their
349
+ sessions, events, and files, but cannot create sessions, send messages, or mint
350
+ keys. This legacy `access: "read"` tier is distinct from the broader explicit
351
+ `read_only` preset, which includes every read/list/view/search permission
352
+ except the secret-value reads `secrets:read` and `variable-sets:read`.
353
+
354
+ Create or edit with `policy: { preset, permissions, workspaceScope }` for exact
355
+ grants. `OrganizationAccessPreset` is `"read_only" | "full" | "custom"`;
356
+ the label never adds permissions and the server recomputes it from the selected
357
+ set. Explicit `full` selects every canonical non-deprecated permission; `custom`
358
+ has no implicit grants or scope. Even `workspace:admin` grants no wildcard under
359
+ an explicit policy. Scope is `{ kind: "all" }` (current and future shared
360
+ workspaces) or `{ kind: "selected", workspaceIds }` (up to 500 unique same-org
361
+ shared-workspace IDs; empty reaches none). Personal workspaces are always excluded.
362
+
363
+ `updateOrganizationApiKey(organizationId, apiKeyId, { name?, description?, policy? })`
364
+ requires at least one change; `description: null` clears it. Metadata-only edits
365
+ preserve legacy stored permissions and wildcard semantics. A policy edit replaces
366
+ the policy, transitions legacy keys to explicit semantics, and applies narrowing
367
+ on the next request without rotating the token. Omitting policy at creation
368
+ retains legacy `access`/`preset` inputs and the full legacy default.
369
+ See [the canonical policy guide](../../docs/product-integration.md#explicit-organization-key-policies).
370
+
371
+ Keys with session-read authority can call
372
+ `listOrganizationSessions(organizationId, { limit, cursor, scopeSubjectId, status })`
373
+ for one page of sessions across authorized shared workspaces (each row carries its
374
+ `workspaceId`; private sessions and Personal workspaces never appear), or
375
+ `iterateOrganizationSessions` to follow `nextCursor` to the end.
376
+
377
+ The external backend also owns its Skill catalog. Load the selected definitions
378
+ and pass them inline in `CreateSessionRequest.skills` for each product-created
379
+ session. There is no organization-wide Skill registry or Skill inheritance in
380
+ this integration contract. See the canonical guide for the complete route map,
381
+ security boundary, and delivery checklist.
382
+
383
+ Installed Skills can be workspace-managed or marked `session_selected`.
384
+ Session-selected guidance is exposed to no agent
385
+ until `createSession` names the reviewed immutable component in
386
+ `installedSkillIds`. OpenGeni copies that exact artifact into the new session,
387
+ so omitting the field from customer-facing creates is a real contamination
388
+ boundary rather than a prompt convention.
389
+ The ordered selection is part of keyed-create identity: reusing an
390
+ `idempotencyKey` with a different selection returns a conflict instead of the
391
+ session configured by the earlier request.
392
+
393
+ For session lists, `deriveSessionDisplayTitle(session)` returns the durable
394
+ agent/user title when available. While the automatic title is still pending, it
395
+ shows the first short, sensitive-safe line of the opening prompt, skipping an
396
+ unsafe pasted URL or identifier when later safe text exists. If no safe line is
397
+ available, a short session reference keeps rows distinguishable. The display
398
+ automatically yields to the later `session.title_set` value.
399
+
400
+ Browser consoles that do not need operator-only surfaces can import
401
+ `OpenGeniBrowserClient` from `@opengeni/sdk/browser`. Document authority migration
402
+ and Default-collection backfill methods stay available on the backward-compatible
403
+ root and `@opengeni/sdk/core` clients, or through the focused
404
+ `@opengeni/sdk/document-authority` entry:
405
+
406
+ ```ts
407
+ import { OpenGeniDocumentAuthorityClient } from "@opengeni/sdk/document-authority";
408
+
409
+ const operatorClient = new OpenGeniDocumentAuthorityClient({
410
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
411
+ apiKey: process.env.OPENGENI_API_KEY!,
412
+ });
413
+ ```
414
+
415
+ ## External users (`asUser`)
416
+
417
+ On a product backend, `client.asUser(externalId, { source? })` returns a separate
418
+ client that sends the external actor assertion with the organization key. IDs
419
+ are opaque and case-sensitive; the default source is `default`. Keep the key and
420
+ this client server-side. Each request requires the intersection of the key's
421
+ permissions and the user's workspace membership. API-key rotation does not
422
+ change the external identity.
423
+
424
+ Membership is automatic: when an `asUser` request reaches a shared workspace of
425
+ the key's own organization and the user has no membership there, Opengeni adds
426
+ one with conversation permissions (workspace read, session create/read/control,
427
+ file upload/read, `mcp_servers:attach`) and continues the request. This needs
428
+ the same authority as explicit onboarding: the key holds `members:manage` (or a
429
+ legacy `workspace:admin`) and every one of those permissions, with the
430
+ workspace in its scope; otherwise the request is a `403` as before. It never
431
+ applies to linked native identities, Personal workspaces, SDK per-user
432
+ workspaces (`opengeni-sdk:user-isolation:*` external source), other
433
+ organizations, requests that need a permission outside those defaults, or any
434
+ non-key caller, and it never changes an existing membership. The identity is
435
+ re-checked as active under the same lock, and the grant writes the same
436
+ receipt and lifecycle event as explicit onboarding, attributed to the key. A
437
+ user removed from a shared workspace is added again on their next request; a
438
+ suspended or offboarded identity stays refused.
439
+
440
+ Use the service client—not an `asUser` client—to call
441
+ `addExternalWorkspaceMember(workspaceId, { identity: { externalId, source? }, permissions })`
442
+ to choose permissions up front. It requires `members:manage`, cannot grant more
443
+ than the key permits, excludes Personal workspaces, and refuses to overwrite an
444
+ existing membership with different permissions (`409`, also when automatic
445
+ membership already created it); change an existing member's permissions with the keyed
446
+ `updateExternalWorkspaceMember(organizationId, workspaceId, organizationMembershipId,
447
+ { operationId, permissions })`, which never tears down work. `asUser` calls never
448
+ reactivate disabled identities.
449
+
450
+ Service `removeWorkspaceMember` supports external members through the existing
451
+ fenced workspace teardown. Account-wide `updateExternalIdentityMembership(
452
+ organizationId, organizationMembershipId, { kind, expectedAuthorizationRevision,
453
+ operationId, reason? })` requires explicit `account:admin` and supports suspend,
454
+ reactivate, and offboard. The membership ID is returned by onboarding; its initial
455
+ authorization revision is 1. Retain the returned revision for subsequent changes.
456
+ Reactivation restores admission, not revoked memberships or work. Offboarding
457
+ follows existing retention policy and cannot be reactivated through this API.
458
+ Native-user targets and `asUser` calls are rejected by this service endpoint.
459
+
460
+ An `asUser` client can discover/access its own provisioned Personal workspace
461
+ under the key's permission ceiling. Core private session operations retain
462
+ organization readiness/settings and explicit sharing acknowledgments; service
463
+ clients gain no Personal-workspace fallback.
464
+
465
+ Optional native-account delegation uses `beginIdentityLink`, authenticated native
466
+ `previewIdentityLink`/`confirmIdentityLink`, and explicit server-side
467
+ `asLinkedUser(externalId, { source?, linkId, expectedLinkRevision })`. Confirmation
468
+ requires the actual native login plus the one-time host challenge; an organization
469
+ key cannot confirm for the native user. `listIdentityLinks(workspaceId, cursor?)`
470
+ returns only the effective participant's links in that organization (50 per page).
471
+ `revokeIdentityLink` requires the observed revision. Linking never changes ordinary
472
+ `asUser`, merges histories or transfers credentials. Link-dependent accepted work
473
+ retains revocation checks across schedules and children. Short-lived inline MCP
474
+ credentials remain supported, and durable renewal stays opt-in.
475
+
476
+ Connect accepts an optional `installationTarget: { instanceKey, displayName,
477
+ expectedInstanceVersion? }`. Keep the exact observed version for an existing named
478
+ account. Setup freezes the target through callback and operation review; omitting
479
+ it creates an independent named account instead of overwriting a default instance.
480
+
481
+ ## Chat facade fallback (`@opengeni/sdk/chat`)
482
+
483
+ Use this only when your product already has its own chat UI speaking Vercel
484
+ `useChat` or an OpenAI-shaped protocol and you want a compatible drop-in
485
+ backend, or for server-side bots (`og.chat(...).send()`). It is a text-only
486
+ projection: tool outputs are dropped (the Vercel adapter emits no tool parts
487
+ by default, and only `output: { status }` with `toolParts: true`), there are no
488
+ files, attachments, artifacts, or images,
489
+ no goals/queue/steer UI, and reopening restores only a text snapshot. For
490
+ anything else, use [the default conversation embed](#embed-the-conversation-default).
491
+
492
+ One option object per conversation, one server handler for your endpoint.
493
+ Tenants map to organization workspaces, conversations map to deterministic
494
+ sessions, and the organization API key never leaves your server.
495
+
496
+ ```ts
497
+ import { OpenGeni, createChatHandler } from "@opengeni/sdk/chat";
498
+
499
+ const og = new OpenGeni({
500
+ baseUrl: process.env.OPENGENI_API_BASE_URL!, // omitted = production https://app.opengeni.ai
501
+ apiKey: process.env.OPENGENI_API_KEY!, // the organization id is derived from the key
502
+ source: "acme-app", // labels your product's tenants and users (default "app")
503
+ });
504
+
505
+ const chat = await og.chat({
506
+ tenant: "acme", // one workspace per customer, created on first use; omit for one per user
507
+ user: "u_42", // authenticated product user; added to the workspace on first use
508
+ conversation: "c_9", // stable conversation id, independent of the acting user
509
+ agentAccess: "session", // "session" (default) | "user" | "workspace"
510
+ memory: "user", // "user" | "workspace" | false; session-only agent reach defaults to false
511
+ create: { sandboxBackend: "none" }, // raw create-request passthrough for a pure chat
512
+ });
513
+
514
+ const reply = await chat.send("hello"); // creates the session on the first send
515
+ console.log(reply.text); // or String(reply); the answer, without progress commentary
516
+
517
+ // Per message: model policy and model-visible page context (not shown in the transcript).
518
+ await chat.send("Why did revenue dip?", {
519
+ model: "gpt-5.5",
520
+ reasoningEffort: "low",
521
+ modelContext: "Viewing dashboard 42, range 2026-09-01..2026-09-28",
522
+ });
523
+
524
+ for await (const chunk of chat.stream("and then?")) {
525
+ if (chunk.type === "text") process.stdout.write(chunk.text);
526
+ if (chunk.type === "pending") await chat.respond({ requestId: chunk.pending.requestId, decision: "approve" });
527
+ }
528
+
529
+ // Your endpoint. `resolve` is your auth hook: identity comes from the request
530
+ // you authenticated, never from the body. The handler reads the client's
531
+ // conversation id itself (x-opengeni-conversation header, or the wire format's
532
+ // own field) and authorizes as `user`; return `conversation` from resolve only
533
+ // when the host names it, which is required when there is no `user`.
534
+ export const handler = createChatHandler(og, {
535
+ resolve: async (request) => {
536
+ const session = await getSessionFromCookie(request);
537
+ if (!session) return new Response("Unauthorized", { status: 401 });
538
+ return { tenant: session.accountId, user: session.userId };
539
+ },
540
+ // format: "vercel" | "openai-chat" | "openai-responses" (default "native");
541
+ // a request may override it with the x-opengeni-chat-format header.
542
+ });
543
+ export const GET = handler; // conversation history, for restoring the chat on reload
544
+ export const POST = handler; // send a message, or answer a pending request at .../respond
545
+ ```
546
+
547
+ Conversation IDs do not change with the acting user. Authorized collaborators
548
+ can use the same session; private visibility prevents access by other users.
549
+ Use `chatBySessionId` for existing and legacy user-namespaced conversations.
550
+ A user is added to the workspace on their first request (see
551
+ [External users](#external-users-asuser)). Without a `user`, the host must name the conversation
552
+ from `resolve`. The Vercel and OpenAI adapters send only the latest user
553
+ message; the earlier messages in that request are imported once, as context on
554
+ the first message of a conversation, after which OpenGeni owns the history.
555
+
556
+ Pick the isolation per session with `agentAccess` (which other sessions the
557
+ agent may reach) and `memory` (what it remembers), all inside one workspace that
558
+ shares the customer's documents, instructions, and integrations:
559
+
560
+ | Scenario | `agentAccess` | `memory` |
561
+ | ------------------------------------------------- | ------------- | ------------- |
562
+ | Agent confined to its chat tree (support desk) | `"session"` | `false` |
563
+ | One user's chats see each other, not other users' | `"user"` | `"user"` |
564
+ | Everything in the tenant shared | `"workspace"` | `"workspace"` |
565
+ | Shared agent access, no memory | any | `false` |
566
+
567
+ To keep an existing `useChat` route with its own request body, stream the chat
568
+ into your AI SDK response yourself. The Vercel format speaks the v1 UI message
569
+ stream used by AI SDK 5, 6, and 7 (tool approval requests need 6 or later):
570
+
571
+ ```ts
572
+ import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
573
+ import { uiMessageStreamParts } from "@opengeni/sdk/chat";
574
+
575
+ export async function POST(request: Request) {
576
+ const { messages, dashboardId } = await request.json(); // your own body
577
+ const me = await authenticate(request);
578
+ const chat = await og.chat({ tenant: me.accountId, user: me.userId, conversation: me.chatId });
579
+ const text = lastUserText(messages);
580
+ const stream = createUIMessageStream({
581
+ execute: async ({ writer }) => {
582
+ const chunks = chat.stream(text, { modelContext: `Dashboard ${dashboardId}` });
583
+ for await (const part of uiMessageStreamParts(chunks, { framing: false })) {
584
+ writer.write(part as never);
585
+ }
586
+ },
587
+ });
588
+ return createUIMessageStreamResponse({ stream });
589
+ }
590
+ ```
591
+
592
+ OpenGeni's own tool activity is omitted by default because those tools are not
593
+ in your typed tool set; pass `toolParts: true` (or `createChatHandler(og, {
594
+ toolParts: true })`) to emit them as `dynamic`, provider-executed tool parts.
595
+ Pending approvals are always emitted as dynamic tool parts with a
596
+ `tool-approval-request`.
597
+
598
+ The chat handler is backend-only: connect a custom or compatible frontend to
599
+ its protocol. For the full React agent experience, use `SessionConversation`
600
+ with `OpenGeniClient` and authenticated session routes, not this simplified
601
+ chat protocol. The normal SDK preserves files, tools, approvals with policies,
602
+ forks, and realtime voice.
603
+
604
+ ## Related-work discovery
605
+
606
+ `listAgentTopology` returns a bounded hierarchy page plus provider-neutral
607
+ `relatedWork` evidence. A text query searches durable semantic titles, active
608
+ goals, and typed work claims; it never searches the opening prompt. An exact
609
+ subject filter looks up one canonical typed identity:
610
+
611
+ ```ts
612
+ const page = await client.listAgentTopology(workspaceId, {
613
+ query: "permission scoped discovery",
614
+ statuses: ["running", "requires_action"],
615
+ activeOnly: true,
616
+ recentHours: 72,
617
+ claimLimit: 4,
618
+ });
619
+
620
+ for (const session of page.sessions) {
621
+ if (!session.relatedWork.possibleOverlap) continue;
622
+ console.log(session.title, session.relatedWork.match, session.relatedWork.claims);
623
+ }
624
+
625
+ const exact = await client.listAgentTopology(workspaceId, {
626
+ subject: {
627
+ namespace: "github",
628
+ type: "pull_request",
629
+ canonicalKey: "acme/app#42",
630
+ },
631
+ });
632
+ ```
633
+
634
+ The evidence is always advisory: `advisoryOnly` and `noAdditionalAccess` are
635
+ literal `true` fields, and receiving a row does not authorize detail, message,
636
+ or control operations. Preserve `nextCursor` with the exact same filters;
637
+ relevance cursors are filter- and activity-snapshot-bound. A topology response
638
+ may carry `humanAdvisoriesEnabled: false`, which lets a human-facing client hide
639
+ the advisory UI without changing stored evidence or API authority. See
640
+ [`docs/work-discovery.md`](../../docs/work-discovery.md) for ranking, bounds,
641
+ claim lifecycle, and rollout semantics.
642
+
643
+ ## Full-history message search
644
+
645
+ `searchSessionMessages(workspaceId, { query, sessionId?, groupBy?, archiveStatus?, limit?,
646
+ cursor? }, { signal? })` searches durable user and **completed assistant** text,
647
+ including unloaded history. It returns every non-overlapping literal occurrence,
648
+ not tools, reasoning, model context, or delta-only assistant output.
649
+
650
+ Each match has a durable `eventId`/`sequence`, a UTF-16 `messageMatchOffset`, and
651
+ `snippet: { text, matchStart, matchEnd }` with original-text UTF-16 offsets.
652
+ Results include `nextCursor`, `hasMore`, cumulative `matchedMessageCount` and
653
+ `matchedOccurrenceCount`, `scannedMessages`, and `countIsExact`. Keep following
654
+ cursors even on empty pages while `hasMore` is true; counts are provisional until
655
+ exhaustion. Cancel superseded searches with the third argument's AbortSignal.
656
+ This is a live, authorized traversal rather than a frozen snapshot; restart to
657
+ refresh after concurrent history or visibility changes.
658
+
659
+ For workspace search, `groupBy: "session"` returns the first hit per matching
660
+ session and skips that session's remaining history, keeping prolific messages
661
+ from filling multiple picker pages. It cannot be combined with `sessionId`.
662
+ In grouped mode both matched counters count session representatives, not full
663
+ per-session message/occurrence totals; `scannedMessages` counts only visited
664
+ messages. Cursors bind the grouping mode. Omit it for every-occurrence Find.
665
+
666
+ The method and types are exported from the ordinary SDK and browser entry.
667
+ Use `listEventPage` around the hit's sequence for bounded context, and use the
668
+ returned search snippet when a large source message's ordinary projection does
669
+ not include the hit. Never download complete history to perform Find locally.
670
+ See [the API contract and bounds](../../docs/session-message-search.md).
671
+
672
+ ## Session visibility and forks
673
+
674
+ For organizations with session-tenancy activation, a canonical managed-cookie
675
+ owner can change a fully quiescent session between private and workspace
676
+ visibility or create an independent same-workspace private or workspace-visible fork. API keys and
677
+ delegated/service bearers are intentionally not authority for these methods.
678
+ The browser client below intentionally omits `apiKey`; its same-origin request
679
+ carries the authenticated managed-session cookie.
680
+
681
+ ```ts
682
+ const browserClient = new OpenGeniClient({ baseUrl: window.location.origin });
683
+ const current = await browserClient.getSession(workspaceId, sessionId);
684
+ if (!current.tenancy) throw new Error("Session tenancy is not activated");
685
+
686
+ await browserClient.updateSessionVisibility(workspaceId, sessionId, {
687
+ visibility: "private",
688
+ expectedAuthorityEpoch: current.tenancy.authorityEpoch,
689
+ idempotencyKey: crypto.randomUUID(),
690
+ });
691
+
692
+ const fork = await browserClient.forkSession(workspaceId, sessionId, {
693
+ idempotencyKey: crypto.randomUUID(),
694
+ visibility: "workspace",
695
+ // Required when private conversation content is copied into workspace scope.
696
+ workspaceSharedAcknowledged: current.tenancy.visibility === "private",
697
+ });
698
+ ```
699
+
700
+ Retain each idempotency key until the request has a known outcome. If a
701
+ transport failure reports `outcomeUnknown`, retry the same operation with the
702
+ same key. An authority-epoch conflict requires a fresh `getSession` and a new
703
+ user decision. A quiescence conflict identifies the stable blocker after live
704
+ turns, goals, realtime, schedules, workspace writers, retained processes, and
705
+ sandbox access have been settled. Forks copy exact same-workspace durable
706
+ conversation content but no live turn, goal, credential, Connection/delegation,
707
+ personal grant, Variable Set, Sandbox Environment, MCP server configuration, process, sandbox
708
+ identity, pin, or workflow. Destination visibility and acknowledgement are
709
+ idempotency-bound.
710
+
711
+ ## Archived session history import (server only)
712
+
713
+ Use `@opengeni/sdk/session-history-import` to migrate historical conversations
714
+ into read-only archives. These functions take `client` first, use its ordinary
715
+ `requestJson` transport, and are not eager client methods or root helper exports:
716
+
717
+ Create requires `sessions:create`; append requires `sessions:control` and the same
718
+ authenticated importer. With `asUser`, the user's permissions apply rather than
719
+ the organization key's permissions.
720
+
721
+ ```ts
722
+ import { OpenGeniClient } from "@opengeni/sdk";
723
+ import {
724
+ importArchivedSession,
725
+ appendArchivedSessionEvents,
726
+ } from "@opengeni/sdk/session-history-import";
727
+
728
+ const og = new OpenGeniClient({
729
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
730
+ apiKey: process.env.OPENGENI_API_KEY!,
731
+ });
732
+ // Existing tenant mapping; the original creator is admitted on first use.
733
+ const actor = og.asUser(originalExternalUserId, { source });
734
+ const imported = await importArchivedSession(actor, workspaceId, {
735
+ importId: "embedded:thread-42", // Stable; persist the exact request before sending.
736
+ title: "Original conversation",
737
+ createdAt: "2025-04-01T09:30:00Z",
738
+ visibility: "user_private",
739
+ });
740
+ const appended = await appendArchivedSessionEvents(actor, workspaceId, imported.importId, {
741
+ batchId: "thread-42:batch-0001", // Stable; also persist this exact body first.
742
+ offset: imported.nextOffset,
743
+ events: [{
744
+ type: "agent.message.completed",
745
+ createdAt: "2025-04-01T09:31:00Z",
746
+ payload: { text: "Historical answer", channel: "final" },
747
+ }],
748
+ });
749
+ // Persist imported.session.id and appended.nextOffset in the migration ledger.
750
+ ```
751
+
752
+ External mapping equivalents take `(client, source, externalId, ...)`:
753
+ `importExternalWorkspaceArchivedSession` and
754
+ `appendExternalWorkspaceArchivedSessionEvents`. These resolve an existing
755
+ mapping; neither import form creates a workspace or grants membership. A bare
756
+ organization key creates a shared, ownerless archive. Use `asUser` for the
757
+ verified creator/owner; private imports require existing private-session
758
+ enablement. Preserve source visibility rather than widening it on failure.
759
+
760
+ The subpath exports `ImportArchivedSessionRequest/Response`,
761
+ `AppendArchivedSessionEventsRequest/Response`, `ArchivedSessionImportEvent` and
762
+ `SessionImportedArchive` types. Requests accept at most 100 events / 1 MiB
763
+ serialized UTF-8 JSON and 256 KiB per event. IDs and titles are bounded to 200
764
+ characters. Import events default to `[]`; append batches must be non-empty.
765
+ Events carry `{ type, createdAt, turnId?, payload }`: a finite supported historical
766
+ type, ISO timestamp, optional UUID/null correlation and JSON-object payload.
767
+ Source timestamps support at most millisecond precision; normalize finer dates
768
+ explicitly and retain originals in the ledger. Negative-zero JSON is rejected.
769
+
770
+ Exact create replay returns `created: false`; exact append replay returns
771
+ `replayed: true`. `offset` is a zero-based imported-event count, not a timeline
772
+ sequence. Retain the exact actor, mapping, IDs, offset and bodies for uncertain
773
+ retries; new batches use the acknowledged `nextOffset`. Changed key reuse or
774
+ an out-of-order new offset returns 409. Helpers propagate `OpenGeniApiError`,
775
+ including `outcomeUnknown`, and do not automatically retry mutations.
776
+
777
+ Import is timeline-only: completed messages, calls/results and goals may be
778
+ historical facts, but no model-facing history, live goal, pending decision or
779
+ execution is restored. `session.importedArchive` has
780
+ `{ importId, importedAt, readOnly: true }`, independent of personal archive state.
781
+ Render through the unchanged `SessionConversation` and session proxy; never
782
+ offer Send or Steer. Continuing an imported archive is unsupported in v1.
783
+ The proxy does not expose import routes. Re-upload files through existing APIs
784
+ and replace references before import; preserve additional source metadata and
785
+ source/destination IDs in the host ledger. See
786
+ [Migrating from embedded OpenGeni](../../docs/product-integration.md#migrating-from-embedded-opengeni).
787
+
788
+ ## Connected accounts
789
+
790
+ Authenticated messages use the initiating user's eligible connected accounts.
791
+ Conversation visibility does not share account access. Queued work, retries and
792
+ child work retain the initiating user. No per-conversation connection grant is
793
+ required. Multiple eligible accounts require an explicit account choice:
794
+
795
+ ```ts
796
+ const accounts = await browserClient.listOwnConnectionAccounts(workspaceId);
797
+ // When choosing among multiple accounts, use the exact eligible server/account pair.
798
+ const selection = { serverId: "mail", connectionId: accounts[0]!.id };
799
+ ```
800
+
801
+ General personal-resource grants for documents, variable sets and other resource
802
+ kinds remain available through the root/core SDK. They do not authorize native
803
+ connected accounts.
804
+
805
+ ## Workspace credentials, webhooks, and sandbox image
806
+
807
+ Configure these with an organization key or workspace admin session. Secrets are
808
+ returned once; store them when you create the resource. Protocol and payloads:
809
+ [`docs/workspace-integrations.md`](../../docs/workspace-integrations.md).
810
+
811
+ ```ts
812
+ const { secret: providerSecret } = await client.putWorkspaceCredentialProvider(workspaceId, {
813
+ url: "https://product.example/opengeni/credentials",
814
+ });
815
+ const { secret: webhookSecret } = await client.createWorkspaceWebhook(workspaceId, {
816
+ url: "https://product.example/opengeni/events",
817
+ eventTypes: ["turn.completed", "turn.failed"],
818
+ });
819
+
820
+ // In your HTTP handlers, verify the raw body before parsing it:
821
+ const { event } = await verifyWebhookEvent({ body: rawBody, headers, secret: webhookSecret });
822
+ const request = await verifyCredentialProviderRequest({
823
+ body: rawBody,
824
+ headers,
825
+ secret: providerSecret,
826
+ });
827
+ ```
828
+
829
+ To check an endpoint now, `testWorkspaceWebhook(client, workspaceId, webhookId)` sends a
830
+ signed `webhook.test` event (acknowledge it with any 2xx) and
831
+ `testWorkspaceCredentialProvider(client, workspaceId)` sends a request with
832
+ `purpose: "test"`; both come from `@opengeni/sdk/workspace-integrations` and return
833
+ what the endpoint answered, naming returned credentials but never their values.
834
+
835
+ `listWorkspaceSandboxImages` returns the deployment's allowlisted images; set one
836
+ with `updateWorkspaceSettings(workspaceId, { defaultSandboxImage })`.
837
+
838
+ ## Personal schedules
839
+
840
+ A schedule created by an authenticated human or their active agent records that
841
+ human as its immutable owner. Only that owner or their verified agent may edit,
842
+ run, pause or delete it. Each occurrence resolves the owner's current accounts;
843
+ accepted retries retain their original identity and account selection.
844
+
845
+ ```ts
846
+ const task = await client.createScheduledTask(workspaceId, {
847
+ name: "Daily triage",
848
+ schedule: { type: "calendar", hour: 8, minute: 0, timeZone: "Europe/Oslo" },
849
+ runMode: "reusable_session",
850
+ agentConfig: { prompt: "Triage the new support issues" },
851
+ connectionAccounts: [selection],
852
+ });
853
+ ```
854
+
855
+ Account selections narrow eligible accounts; they never transfer ownership.
856
+ Omitting `connectionAccounts` on update preserves the selection. Passing an
857
+ empty array clears explicit account choices without changing the schedule owner.
858
+ Service-owned schedules retain service execution and do not acquire a human's
859
+ personal accounts. Run history remains credential-free.
860
+
861
+ A schedule freezes its connectors, accounts and (when an agent created it)
862
+ OpenGeni tools. For the owner, `listScheduledTasks` and `getScheduledTask`
863
+ include a read-only `policyDrift` naming what is out of date, and
864
+ `listScheduledTaskRuns` includes `accessFailures` for runs that could not use a
865
+ connector. `listScheduledTaskAccessAttention` lists schedules whose latest run
866
+ failed that way, and schedules that cannot start because a chosen account can no
867
+ longer be used (`unavailableAccounts`, with a null `runId`). A signed-in owner
868
+ re-freezes with their current access through
869
+ `refreshScheduledTaskAccess(workspaceId, taskId, { executionDigest, leaveOut })`,
870
+ where the optional `leaveOut` keeps named default connectors or OpenGeni tools
871
+ off; API keys and agents cannot. See [`docs/scheduled-task-access.md`](../../docs/scheduled-task-access.md).
872
+
873
+ Deleting a task is externally idempotent and immediately removes it from live
874
+ lists and quota, but the server retains a tombstone plus run/session/turn audit
875
+ evidence until workspace/account retention cleanup. A stable `triggerId` is
876
+ still required when a caller wants manual-trigger retries to coalesce; ordinary
877
+ scheduled fires derive their producer identity from the Temporal fire workflow.
878
+
40
879
  ## Realtime browser controller (`@opengeni/sdk/realtime`)
41
880
 
42
881
  The public realtime subpath owns the provider-neutral browser controller and
@@ -101,7 +940,7 @@ import {
101
940
  createBrowserEditableArtifactSession,
102
941
  } from "@opengeni/sdk/editable-artifacts";
103
942
 
104
- const client = new OpenGeniClient({ baseUrl: "https://api.example.com" });
943
+ const client = new OpenGeniClient({ baseUrl: process.env.OPENGENI_API_BASE_URL! });
105
944
  const artifact = await client.getEditableArtifact(workspaceId, artifactId, {
106
945
  replicaId,
107
946
  });
@@ -112,7 +951,7 @@ const kernels = {
112
951
  } as const;
113
952
  const { editableArtifactKernelRuntime } = await kernels[artifact.modality]();
114
953
  const session = createBrowserEditableArtifactSession({
115
- baseUrl: "https://api.example.com",
954
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
116
955
  workspaceId,
117
956
  artifact,
118
957
  storageAuthority,
@@ -165,6 +1004,8 @@ const browser = await client.interaction.browsers.currentOrOpen({
165
1004
  const { targets } = await browser.tabs.list();
166
1005
  const target = targets.find((candidate) => candidate.selected) ?? targets[0]!;
167
1006
  const page = await browser.observe(target.id);
1007
+ const screenshot = await browser.screenshot(target.id, { fullPage: true });
1008
+ // screenshot.data is bounded image bytes; mediaType and dimensions travel with it.
168
1009
 
169
1010
  const receipt = await browser.act({
170
1011
  operationId: crypto.randomUUID(),
@@ -181,6 +1022,10 @@ if (receipt.state === "outcome_unknown") {
181
1022
  }
182
1023
  ```
183
1024
 
1025
+ `browser.targetState(targetId)` reads generation fences without an accessibility
1026
+ tree. `browser.readDom(targetId, request)` returns a bounded element or count
1027
+ query using those fences; sensitive values are redacted.
1028
+
184
1029
  Browser identities are immutable version graphs: live browser state stays
185
1030
  private until `browser.publishRevision(...)` explicitly creates a new revision.
186
1031
  Protected auth fills resolve connection secrets only inside the broker/controller
@@ -200,10 +1045,13 @@ assign product types such as app, page, dashboard, or gallery. List pages are
200
1045
  bounded and expose both `truncated` and an opaque `nextCursor` so callers never
201
1046
  mistake a partial page for the complete workspace catalog.
202
1047
 
203
- The initial web renderer supports semantic HTML, inline CSS, CSS-only
204
- interactions, and inline SVG. It removes JavaScript, event handlers, forms,
205
- embeds, external URLs, and other active or navigation-capable markup before
206
- rendering. Executable artifacts require a later, stronger isolation boundary.
1048
+ Each Site version retains one self-contained executable HTML runtime, its
1049
+ bounded editable source bundle, and the exact canonical tool identities it
1050
+ requested. The web host renders that runtime in an opaque-origin sandboxed
1051
+ iframe. Credentials, cookies, API URLs, workspace ids, and parent DOM authority
1052
+ never enter the iframe; `@opengeni/sdk/site` uses a single parent-owned
1053
+ `MessagePort` that exposes only the retained tool allowlist intersected with the
1054
+ current viewer's live workspace catalog.
207
1055
 
208
1056
  ```ts
209
1057
  let cursor: string | undefined;
@@ -223,6 +1071,64 @@ return the exact source session, turn, attempt, and execution generation that
223
1071
  published them. Version and event history are bounded; inspect
224
1072
  `versionsTruncated` and `eventsTruncated` on the detail response.
225
1073
 
1074
+ ## Workspace tools
1075
+
1076
+ The browser-safe SDK exposes the same gateway catalog and executor used by
1077
+ model MCP calls and Codemode. Generated declarations augment the lazy namespace
1078
+ for exact installed-tool types; canonical `{ serverId, toolName }` identity and
1079
+ the server-side catalog remain authoritative.
1080
+
1081
+ ```ts
1082
+ const tools = client.tools.forWorkspace(workspaceId);
1083
+ const documents = await tools.docs.search({ query: "launch plan" });
1084
+ ```
1085
+
1086
+ An approval-required call needs a server-issued capability. A trusted host
1087
+ shows its approval UI first, then asks for one token bound to the current human,
1088
+ workspace, operation id, catalog digest, exact tool identity, and arguments.
1089
+ The token expires after five minutes, is stored only as a hash, and is consumed
1090
+ once by the matching call.
1091
+
1092
+ Connection-backed tools configured for both provider policy and one-shot human
1093
+ approval are omitted from this HTTP catalog until their provider adapter can
1094
+ preflight credential and resource authorization before consuming the token.
1095
+
1096
+ ```ts
1097
+ const operationId = crypto.randomUUID();
1098
+ const identity = { serverId: "linear", toolName: "issues_update" };
1099
+ const input = { issueId, state: "Done" };
1100
+ const approval = await tools.$approve(identity, input, { operationId });
1101
+
1102
+ await tools.$call(identity, input, {
1103
+ operationId,
1104
+ approvalToken: approval.approvalToken,
1105
+ });
1106
+ ```
1107
+
1108
+ When a call or approval request receives the typed
1109
+ `409 details.code = "catalog_stale"` response, the SDK refreshes the catalog,
1110
+ re-resolves the exact identity or generated namespace path, preserves the
1111
+ operation id (generating it before the first attempt when omitted), and retries
1112
+ once. A call that already carries an approval token cannot reuse that
1113
+ single-use capability after the catalog digest changes; instead it throws
1114
+ `OpenGeniToolReapprovalRequiredError` with the refreshed identity and digest so
1115
+ the trusted host can show approval again for the same operation id.
1116
+
1117
+ Reapproval is valid only before the original capability is consumed. Once a
1118
+ call crosses that boundary, the server retains a hash-only tombstone for the
1119
+ operation id and rejects another approval with
1120
+ `409 details.code = "tool_gateway_operation_already_started"` and
1121
+ `outcomeUnknown: true`. The host must reconcile the provider outcome; it must
1122
+ not turn an ambiguous call into a second approved execution by minting a fresh
1123
+ operation id automatically.
1124
+
1125
+ Do not request approval capabilities speculatively or expose them to Site iframe
1126
+ code. The host projects only the active immutable version's requested identities,
1127
+ supplies the Site context on direct calls, and the server revalidates the active
1128
+ version and viewer's live authority. The requested set is a maximum allowlist,
1129
+ not an authority grant; ordinary gateway approval still applies. Archived Sites
1130
+ receive no tool bridge.
1131
+
226
1132
  Omit `firstPartyMcpTools` for the complete OpenGeni tool catalog. An explicit
227
1133
  `[]` exposes no broad first-party tools; attached resources and separately
228
1134
  selected `files`/`docs` MCP servers are unaffected.
@@ -400,11 +1306,70 @@ await client.cancelSession(workspaceId, sessionId, {
400
1306
  reason: "host record deleted",
401
1307
  clientEventId: crypto.randomUUID(),
402
1308
  });
1309
+ // approvalId is `approvals[].id` from the latest `session.requiresAction`
1310
+ // event (type SessionApprovalRequest: { id, name, arguments }); it is the
1311
+ // pending tool call id, not the event id.
403
1312
  await client.sendApprovalDecision(workspaceId, sessionId, { approvalId, decision: "approve" });
404
1313
  ```
405
1314
 
1315
+ ## Retry a failed session without changing its intent
1316
+
1317
+ Try again is separate from Pause/Resume and `sendMessage`. Retain the request
1318
+ across an ambiguous transport response; never synthesize a continuation prompt:
1319
+
1320
+ ```ts
1321
+ const retryRequest = {
1322
+ clientEventId: crypto.randomUUID(),
1323
+ failureEventId, // the current durable turn.failed event id
1324
+ model: selectedModel,
1325
+ reasoningEffort: "high" as const,
1326
+ latencyMode: "standard" as const,
1327
+ };
1328
+ const retry = await client.retrySession(workspaceId, sessionId, retryRequest);
1329
+ // { outcome: "accepted" | "replayed", turnId, failureEventId }
1330
+ ```
1331
+
1332
+ Requires `sessions:control` and session access. Recovery keeps the logical turn,
1333
+ original question, frozen authority, and completed history/tool results; it
1334
+ does not create a user message. Omitted policy fields retain the failed turn's
1335
+ selection. The selected policy is revalidated for new admission. A duplicate
1336
+ operation replays its receipt even after execution advances.
1337
+
1338
+ HTTP 409 codes distinguish `RETRY_STALE_FAILURE`, `RETRY_EXECUTION_UNRESOLVED`,
1339
+ `RETRY_PAUSED`, `RETRY_UNSUPPORTED_FAILURE`, and `IDEMPOTENCY_KEY_REUSED`.
1340
+ Unresolved tool outcomes cannot be blindly replayed. A deliberate Pause must
1341
+ be resumed separately. Failure without a retained logical turn, a settled
1342
+ scheduled occurrence, and idle credit exhaustion are not supported retry
1343
+ boundaries. See [run lifecycle](../../docs/run-lifecycle.md).
1344
+
406
1345
  ## Session tool policy and native web search
407
1346
 
1347
+ For standalone credential maintenance, use the dedicated operation rather than
1348
+ sending a message:
1349
+
1350
+ ```ts
1351
+ const request = {
1352
+ operationKey: crypto.randomUUID(),
1353
+ updates: [{
1354
+ id: "crm",
1355
+ expectedCredentialVersion: 1,
1356
+ expectedServerUrl: "https://tools.example.test/mcp",
1357
+ headers: { Authorization: `Bearer ${replacementToken}` },
1358
+ }],
1359
+ };
1360
+ const receipt = await client.rotateSessionMcpCredentials(workspaceId, sessionId, request);
1361
+ ```
1362
+
1363
+ Only exact existing inline servers without `connectionRef` are eligible. Both
1364
+ session control and MCP attach permissions, live session authorization, expected
1365
+ versions, and a quiescent credential-consumer boundary are required. This never
1366
+ sends a message, starts work, retries a failed turn, or refreshes an active
1367
+ client. Reconcile an ambiguous response with the same request/key; the SDK does
1368
+ not retry mutations automatically. Exact authorized replay returns the original
1369
+ receipt without a second write. Old receipt verification fails explicitly with
1370
+ 503 if the deployment encryption key has been replaced. See the full
1371
+ [rotation contract](../../docs/session-mcp-servers.md#standalone-inline-credential-rotation).
1372
+
408
1373
  Omitting `tools` when creating a top-level session selects the current
409
1374
  workspace-default capability policy, including the built-in `files` server.
410
1375
  Passing `tools`, including `[]`, is an intentional fixed narrowing and can
@@ -447,6 +1412,17 @@ it is not public web search.
447
1412
  const goal = await client.getGoal(workspaceId, sessionId); // counters: autoContinuations, noProgressStreak
448
1413
  await client.pauseGoal(workspaceId, sessionId, { rationale: "manual review" });
449
1414
  await client.resumeGoal(workspaceId, sessionId); // resets counters, re-arms continuations
1415
+
1416
+ const revisions = await client.listGoalRevisions(workspaceId, sessionId); // legacy raw array
1417
+ const page = await client.listGoalRevisionPage(workspaceId, sessionId, { limit: 25 });
1418
+ await client.rejectGoalRevision(workspaceId, sessionId, proposalId, {
1419
+ expectedObjectiveRevision: goal.objectiveRevision,
1420
+ rationale: "Keep the existing objective",
1421
+ });
1422
+ await client.rollbackGoalRevision(workspaceId, sessionId, appliedRevisionId, {
1423
+ expectedObjectiveRevision: goal.objectiveRevision,
1424
+ rationale: "Restore the last known-good objective",
1425
+ });
450
1426
  ```
451
1427
 
452
1428
  ## Files
@@ -526,23 +1502,23 @@ Every public endpoint group has typed methods:
526
1502
 
527
1503
  | Group | Methods |
528
1504
  | --- | --- |
529
- | Access + workspaces | `getAccessContext`, `listWorkspaces`, `createWorkspace`, `getWorkspace`, `updateWorkspace` |
1505
+ | Access + workspaces | `getAccessContext`, `listWorkspaces`, `createWorkspace`, `ensureWorkspace`, `getWorkspace`, `updateWorkspace` |
1506
+ | Managed organizations | `createOrganization` for first-time compatibility; `createAdditionalOrganization` for an already-onboarded human; `listOrganizationMemberships`, `listOrganizationInvitations`, `acceptOrganizationInvitation` |
530
1507
  | Sessions + events | `createSession`, `listSessions`, `listSessionPage`, `listAgentTopology`, `getSession`, `getSessionLineage`, `updateSession`, `listEvents`, `sendEvent`, `sendMessage`, `steerMessage`, `pauseSession`, `resumeSession`, `cancelSession`, `sendApprovalDecision`, `streamEvents`, `openEventStream` |
531
1508
  | Machines (bring-your-own-compute) | `listMachines`, `machineMetricsSeries`, `swapActiveSandbox`, `mintEnrollToken`, `lookupDeviceEnrollment`, `approveDeviceEnrollment`, `denyDeviceEnrollment` |
532
1509
  | Turn queue | `getQueue`, `moveQueueItem`, `editQueueItem`, `steerQueueItem`, `deleteQueueItem` |
533
- | Goal | `getGoal`, `updateGoal`, `pauseGoal`, `resumeGoal` |
534
- | Scheduled tasks | `createScheduledTask`, `listScheduledTasks`, `getScheduledTask`, `updateScheduledTask`, `pauseScheduledTask`, `resumeScheduledTask`, `triggerScheduledTask`, `deleteScheduledTask`, `listScheduledTaskRuns` |
1510
+ | Goal | `getGoal`, `updateGoal`, `pauseGoal`, `resumeGoal`, `listGoalRevisions`, `listGoalRevisionPage`, `applyGoalRevision`, `rejectGoalRevision`, `rollbackGoalRevision` |
1511
+ | Scheduled tasks | `createScheduledTask`, `listScheduledTasks`, `getScheduledTask`, `updateScheduledTask`, `pauseScheduledTask`, `resumeScheduledTask`, `triggerScheduledTask`, `deleteScheduledTask`, `listScheduledTaskRuns`, `refreshScheduledTaskAccess`, `listScheduledTaskAccessAttention` |
535
1512
  | 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 |
536
1513
  | Files | `uploadFile`, `beginFileUpload`, `completeFileUpload`, `getFile`, `createFileDownloadUrl` |
537
1514
  | Documents | `createDocumentBase`, `listDocumentBases`, `getDocumentBase`, `addDocument`, `listDocuments`, `reindexDocument`, `searchDocuments`, `searchKnowledge` (effective organization + workspace + immutable initiating-user personal scope) |
538
- | Packs | `listPacks`, `registerPack`, `getPack`, `enablePack`, `deletePack`, `listPackInstallations` |
539
1515
  | Capabilities | `listCapabilities`, `createCapability`, `enableCapability`, `disableCapability`, `discoverMcpCapabilities` |
540
1516
  | Plugin packages | `previewPlugin`, `installPlugin`, `previewPluginUninstall`, `uninstallPlugin` |
541
1517
  | API Integrations | `listIntegrationDefinitions`, `listApiIntegrations`, `previewApiIntegration`, `startApiIntegrationOAuth`, `installApiIntegration`, `previewApiIntegrationUninstall`, `uninstallApiIntegration`, `listIntegrationFacets`, `configureIntegrationFacet`, `pauseIntegrationFacet`, `resumeIntegrationFacet`, `removeIntegrationFacet`, `browseGoogleDriveFacetSource`, `saveGoogleDriveFacetSource` |
542
1518
  | Remote Skills | `previewSkillImport`, `installSkill`, `previewSkillUninstall`, `uninstallSkill` |
543
1519
  | GitHub | `getGitHubApp`, `githubConnectUrl`, `listGitHubRepositories`, `syncGitHubRepositories`, `createGitHubAppManifest` |
544
- | API keys | `listApiKeys`, `createApiKey`, `deleteApiKey` |
545
- | Billing | `getBilling`, `getBillingUsage`, `getBillingEntitlements`, `createBillingCheckout` |
1520
+ | API keys | Workspace: `listApiKeys`, `createApiKey`, `deleteApiKey`; organization: `listOrganizationApiKeys`, `createOrganizationApiKey`, `deleteOrganizationApiKey` |
1521
+ | Billing | `getBilling`, `getBillingUsage`, `getBillingEntitlements`, `createBillingCheckout`, `createBillingPortalSession` |
546
1522
 
547
1523
  `listIntegrationDefinitions` returns safe Integration Definition metadata without any
548
1524
  deployment OAuth client credentials. API Integrations are multi-instance:
@@ -575,15 +1551,21 @@ reconsider — they can change with their counterpart, not with the SDK.
575
1551
 
576
1552
  ## Compatibility
577
1553
 
578
- Clients and servers are compatible within the same **major** release-train
579
- version; evolution is additive within a major and both sides are tolerant
580
- readers. Official server builds expose `serverVersion` on `/healthz` and
581
- `/v1/config/client`. Full policy: `docs/architecture.md` §3.10.
1554
+ Clients and servers are compatible within the same **major** version of this
1555
+ SDK; evolution is additive within a major and both sides are tolerant readers.
1556
+ Official server builds expose `serverVersion` on `/healthz` and
1557
+ `/v1/config/client`. A route scheduled for removal answers with `Deprecation`
1558
+ and `Sunset` headers at least 90 days (and a major) ahead; the client reports
1559
+ each deprecated route once through the `onDeprecation` option (default: one
1560
+ `console.warn` per route; `false` silences it). Full policy:
1561
+ `docs/design/api-compatibility-policy.md`.
582
1562
 
583
1563
  ## Proxy through your own API
584
1564
 
585
- Keep your OpenGeni API key on your server and re-emit the stream to your own
586
- browser clients. The re-emitted wire format is identical to OpenGeni's SSE
1565
+ For the React conversation, use the packaged
1566
+ [`createSessionProxyHandler`](#embed-the-conversation-default). For a custom
1567
+ backend route, keep your organization API key on your server and re-emit the
1568
+ stream to your own browser clients. The re-emitted wire format is identical to OpenGeni's SSE
587
1569
  stream, so the browser side can consume it with this same SDK (or a plain
588
1570
  `EventSource`), including resume via `?after=` / `Last-Event-ID`:
589
1571
 
@@ -591,7 +1573,10 @@ stream, so the browser side can consume it with this same SDK (or a plain
591
1573
  // Your server (Hono, Next.js route handler, Bun.serve, workers, ...):
592
1574
  import { OpenGeniClient, proxySessionEventStream } from "@opengeni/sdk";
593
1575
 
594
- const client = new OpenGeniClient({ baseUrl, apiKey });
1576
+ const client = new OpenGeniClient({
1577
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
1578
+ apiKey: process.env.OPENGENI_API_KEY!,
1579
+ });
595
1580
 
596
1581
  export function GET(request: Request): Response {
597
1582
  // authenticate *your* user, resolve their session id, then:
@@ -614,3 +1599,9 @@ packages. `test/contract-parity.test.ts` pins them to `@opengeni/contracts`,
614
1599
  so contract drift fails the repo gate instead of shipping. `SessionEvent.type`
615
1600
  is an open union: unknown event types from newer servers flow through instead
616
1601
  of breaking older SDK consumers.
1602
+
1603
+ Hosts, test doubles, and same-origin proxies must import
1604
+ `OPENGENI_API_CONTRACT_REVISION` from `@opengeni/sdk` when constructing a client
1605
+ configuration or contract header. Do not copy its string value: the revision is
1606
+ an executable compatibility boundary, and `getClientConfig()` intentionally
1607
+ fails closed when server and SDK revisions differ.