@opengeni/sdk 3.3.2 → 3.4.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 (299) hide show
  1. package/README.md +1007 -72
  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 +42 -0
  15. package/dist/app-links.js +66 -0
  16. package/dist/app-links.js.map +1 -0
  17. package/dist/artifact-catalog.d.ts +40 -0
  18. package/dist/artifact-client.d.ts +20 -8
  19. package/dist/artifacts.d.ts +1 -1
  20. package/dist/artifacts.js +8 -6
  21. package/dist/automations.js +8 -2
  22. package/dist/automations.js.map +1 -1
  23. package/dist/azure-live-transport.d.ts +32 -0
  24. package/dist/browser.d.ts +7 -2
  25. package/dist/browser.js +27 -5
  26. package/dist/chat/fold.d.ts +75 -0
  27. package/dist/chat/handler.d.ts +41 -0
  28. package/dist/chat/http.d.ts +67 -0
  29. package/dist/chat/ids.d.ts +34 -0
  30. package/dist/chat/index.d.ts +9 -0
  31. package/dist/chat/index.js +1369 -0
  32. package/dist/chat/index.js.map +1 -0
  33. package/dist/chat/openai.d.ts +30 -0
  34. package/dist/chat/opengeni.d.ts +129 -0
  35. package/dist/chat/types.d.ts +207 -0
  36. package/dist/chat/vercel.d.ts +49 -0
  37. package/dist/chats.d.ts +8 -0
  38. package/dist/chunk-4AHR4KGA.js +301 -0
  39. package/dist/chunk-4AHR4KGA.js.map +1 -0
  40. package/dist/chunk-6GYGMLX4.js +332 -0
  41. package/dist/chunk-6GYGMLX4.js.map +1 -0
  42. package/dist/chunk-7EWT3PV6.js +368 -0
  43. package/dist/chunk-7EWT3PV6.js.map +1 -0
  44. package/dist/chunk-FTWVVPR6.js +1665 -0
  45. package/dist/chunk-FTWVVPR6.js.map +1 -0
  46. package/dist/{chunk-VYCTL62C.js → chunk-GZQSWAKD.js} +1 -1
  47. package/dist/chunk-GZQSWAKD.js.map +1 -0
  48. package/dist/chunk-KGEABAID.js +71 -0
  49. package/dist/chunk-KGEABAID.js.map +1 -0
  50. package/dist/{chunk-YKZME56C.js → chunk-KWADQ227.js} +3 -3
  51. package/dist/{chunk-YKZME56C.js.map → chunk-KWADQ227.js.map} +1 -1
  52. package/dist/{chunk-JUS2UYRP.js → chunk-NCU5OAN7.js} +27 -2
  53. package/dist/chunk-NCU5OAN7.js.map +1 -0
  54. package/dist/{chunk-YXQX7RQT.js → chunk-OI5YLASH.js} +73 -2
  55. package/dist/chunk-OI5YLASH.js.map +1 -0
  56. package/dist/chunk-OV3POE62.js +359 -0
  57. package/dist/chunk-OV3POE62.js.map +1 -0
  58. package/dist/chunk-P7UWKORG.js +561 -0
  59. package/dist/chunk-P7UWKORG.js.map +1 -0
  60. package/dist/chunk-QGEIL4TR.js +195 -0
  61. package/dist/chunk-QGEIL4TR.js.map +1 -0
  62. package/dist/{chunk-TRNI7KEO.js → chunk-S2NLS3ES.js} +3669 -1281
  63. package/dist/chunk-S2NLS3ES.js.map +1 -0
  64. package/dist/chunk-UM4ZXXJR.js +9 -0
  65. package/dist/chunk-UM4ZXXJR.js.map +1 -0
  66. package/dist/{chunk-QT3KFVDP.js → chunk-UME3X7CP.js} +28 -32
  67. package/dist/chunk-UME3X7CP.js.map +1 -0
  68. package/dist/chunk-VT4YM4XQ.js +210 -0
  69. package/dist/chunk-VT4YM4XQ.js.map +1 -0
  70. package/dist/chunk-WSYYXONG.js +1602 -0
  71. package/dist/chunk-WSYYXONG.js.map +1 -0
  72. package/dist/{chunk-C2H7HBNP.js → chunk-WVCJRSGJ.js} +119 -14
  73. package/dist/chunk-WVCJRSGJ.js.map +1 -0
  74. package/dist/chunk-X5ITQVXE.js +15 -0
  75. package/dist/chunk-X5ITQVXE.js.map +1 -0
  76. package/dist/chunk-XA7HVIHI.js +126 -0
  77. package/dist/chunk-XA7HVIHI.js.map +1 -0
  78. package/dist/{chunk-KKRO2VCP.js → chunk-YJCHB7R3.js} +60 -12
  79. package/dist/chunk-YJCHB7R3.js.map +1 -0
  80. package/dist/chunk-YKIHNLWD.js +153 -0
  81. package/dist/chunk-YKIHNLWD.js.map +1 -0
  82. package/dist/chunk-ZDUBPV33.js +277 -0
  83. package/dist/chunk-ZDUBPV33.js.map +1 -0
  84. package/dist/client.d.ts +1068 -118
  85. package/dist/codex-realtime-controller.d.ts +13 -0
  86. package/dist/codex-realtime-controller.js +4 -2
  87. package/dist/codex-realtime-v3-wire.d.ts +3 -2
  88. package/dist/codex-realtime.d.ts +5 -1
  89. package/dist/company-brain.d.ts +1 -11
  90. package/dist/core.d.ts +7 -2
  91. package/dist/core.js +25 -6
  92. package/dist/deprecation.d.ts +47 -0
  93. package/dist/document-authority-client.d.ts +24 -2
  94. package/dist/document-authority.js +7 -5
  95. package/dist/editable-artifacts/index.d.ts +1 -1
  96. package/dist/editable-artifacts/types.d.ts +12 -2
  97. package/dist/editable-artifacts.js +19 -3
  98. package/dist/editable-artifacts.js.map +1 -1
  99. package/dist/embedding-client.d.ts +129 -0
  100. package/dist/errors.d.ts +76 -1
  101. package/dist/external-vault-receiver.d.ts +203 -0
  102. package/dist/external-vault-receiver.js +564 -0
  103. package/dist/external-vault-receiver.js.map +1 -0
  104. package/dist/feedback.d.ts +23 -0
  105. package/dist/gateway-realtime-transport.js +1 -1
  106. package/dist/github-repositories.d.ts +10 -0
  107. package/dist/github-repositories.js +36 -0
  108. package/dist/github-repositories.js.map +1 -0
  109. package/dist/index.d.ts +62 -11
  110. package/dist/index.js +557 -338
  111. package/dist/index.js.map +1 -1
  112. package/dist/insights-usage.d.ts +33 -0
  113. package/dist/insights-usage.js +7 -0
  114. package/dist/insights-usage.js.map +1 -0
  115. package/dist/interaction.d.ts +165 -71
  116. package/dist/interaction.js +2 -2
  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/known-tools.d.ts +4 -0
  121. package/dist/message-links.d.ts +68 -0
  122. package/dist/model-context.d.ts +68 -0
  123. package/dist/model-display.d.ts +2 -0
  124. package/dist/model-display.js +17 -0
  125. package/dist/model-display.js.map +1 -0
  126. package/dist/model-picker-order.d.ts +1 -0
  127. package/dist/model-picker-order.js +10 -0
  128. package/dist/model-picker-order.js.map +1 -0
  129. package/dist/organization-integration-policy.d.ts +26 -0
  130. package/dist/organization-integration-policy.js +26 -0
  131. package/dist/organization-integration-policy.js.map +1 -0
  132. package/dist/pr-review-client.d.ts +1 -1
  133. package/dist/pr-review.d.ts +1 -1
  134. package/dist/pr-review.js.map +1 -1
  135. package/dist/preference-registry.d.ts +1 -1
  136. package/dist/prepared-mcp-setup.d.ts +5 -0
  137. package/dist/proxy.d.ts +13 -11
  138. package/dist/realtime.d.ts +1 -1
  139. package/dist/realtime.js +253 -7
  140. package/dist/realtime.js.map +1 -1
  141. package/dist/session-history-import.d.ts +22 -0
  142. package/dist/session-history-import.js +36 -0
  143. package/dist/session-history-import.js.map +1 -0
  144. package/dist/session-list-entries.d.ts +1 -0
  145. package/dist/session-list-entries.js +7 -0
  146. package/dist/session-list-entries.js.map +1 -0
  147. package/dist/session-message-search.d.ts +53 -0
  148. package/dist/session-proxy-client.d.ts +28 -0
  149. package/dist/session-proxy.d.ts +334 -0
  150. package/dist/session-proxy.js +24 -0
  151. package/dist/session-proxy.js.map +1 -0
  152. package/dist/session-titles.d.ts +2 -28
  153. package/dist/session-usage-limits.d.ts +10 -0
  154. package/dist/session-usage-limits.js +43 -0
  155. package/dist/session-usage-limits.js.map +1 -0
  156. package/dist/shared-catalog-loader.d.ts +34 -0
  157. package/dist/site-ai.d.ts +35 -0
  158. package/dist/site-bridge-host.d.ts +112 -0
  159. package/dist/site-bridge-host.js +286 -0
  160. package/dist/site-bridge-host.js.map +1 -0
  161. package/dist/site-browser-entry.d.ts +1 -0
  162. package/dist/site-browser-runtime.gen.d.ts +1 -0
  163. package/dist/site-data.d.ts +178 -0
  164. package/dist/site-document.d.ts +5 -0
  165. package/dist/site-document.js +20 -0
  166. package/dist/site-document.js.map +1 -0
  167. package/dist/site-document.test.d.ts +1 -0
  168. package/dist/site-http.d.ts +17 -0
  169. package/dist/site-sql.d.ts +31 -0
  170. package/dist/site-tool-bridge.d.ts +57 -0
  171. package/dist/site.d.ts +186 -0
  172. package/dist/site.js +63 -0
  173. package/dist/site.js.map +1 -0
  174. package/dist/skills.d.ts +75 -0
  175. package/dist/sse.d.ts +1 -1
  176. package/dist/stream.d.ts +2 -0
  177. package/dist/tenant-workspaces.d.ts +47 -0
  178. package/dist/tenant-workspaces.js +8 -0
  179. package/dist/tenant-workspaces.js.map +1 -0
  180. package/dist/tool-auth.d.ts +87 -0
  181. package/dist/tool-auth.js +23 -0
  182. package/dist/tool-auth.js.map +1 -0
  183. package/dist/tool-display-metadata.d.ts +3 -0
  184. package/dist/tools-37DCTCNZ.js +12 -0
  185. package/dist/tools-37DCTCNZ.js.map +1 -0
  186. package/dist/tools.d.ts +81 -0
  187. package/dist/transcription.d.ts +1 -1
  188. package/dist/types.d.ts +2807 -583
  189. package/dist/usage-allowances.d.ts +36 -0
  190. package/dist/usage-allowances.js +80 -0
  191. package/dist/usage-allowances.js.map +1 -0
  192. package/dist/vault-types.d.ts +324 -0
  193. package/dist/vaults-api.d.ts +65 -0
  194. package/dist/workspace-artifacts.d.ts +47 -5
  195. package/dist/workspace-instruction-policies.d.ts +2 -2
  196. package/dist/workspace-integrations.d.ts +74 -0
  197. package/dist/workspace-integrations.js +67 -0
  198. package/dist/workspace-integrations.js.map +1 -0
  199. package/dist/workspace-state.d.ts +8 -71
  200. package/package.json +103 -5
  201. package/src/adapters/express.ts +136 -0
  202. package/src/adapters/hono.ts +18 -0
  203. package/src/adapters/next.ts +42 -0
  204. package/src/allowance-refusal.ts +5 -0
  205. package/src/app-links.ts +117 -0
  206. package/src/artifact-catalog.ts +51 -0
  207. package/src/artifact-client.ts +40 -49
  208. package/src/artifacts.ts +1 -0
  209. package/src/automations-client.ts +8 -2
  210. package/src/azure-live-transport.ts +286 -0
  211. package/src/browser.ts +37 -0
  212. package/src/chat/fold.ts +431 -0
  213. package/src/chat/handler.ts +240 -0
  214. package/src/chat/http.ts +257 -0
  215. package/src/chat/ids.ts +85 -0
  216. package/src/chat/index.ts +88 -0
  217. package/src/chat/openai.ts +439 -0
  218. package/src/chat/opengeni.ts +737 -0
  219. package/src/chat/types.ts +219 -0
  220. package/src/chat/vercel.ts +204 -0
  221. package/src/chats.ts +16 -0
  222. package/src/client.ts +5697 -1833
  223. package/src/codex-realtime-controller.ts +163 -8
  224. package/src/codex-realtime-v3-wire.ts +4 -3
  225. package/src/codex-realtime-v3.ts +12 -3
  226. package/src/codex-realtime.ts +11 -3
  227. package/src/company-brain.ts +0 -40
  228. package/src/core.ts +20 -0
  229. package/src/deprecation.ts +149 -0
  230. package/src/document-authority-client.ts +104 -1
  231. package/src/editable-artifacts/controller.ts +36 -2
  232. package/src/editable-artifacts/index.ts +1 -0
  233. package/src/editable-artifacts/types.ts +29 -16
  234. package/src/embedding-client.ts +519 -0
  235. package/src/errors.ts +303 -4
  236. package/src/external-vault-receiver.ts +914 -0
  237. package/src/feedback.ts +20 -0
  238. package/src/gateway-realtime-transport.ts +3 -3
  239. package/src/github-repositories.ts +60 -0
  240. package/src/index.ts +603 -65
  241. package/src/insights-usage.ts +64 -0
  242. package/src/interaction.ts +315 -93
  243. package/src/knowledge.ts +72 -0
  244. package/src/known-tools.ts +279 -0
  245. package/src/message-links.ts +178 -0
  246. package/src/model-context.ts +113 -0
  247. package/src/model-display.ts +11 -0
  248. package/src/model-picker-order.ts +6 -0
  249. package/src/organization-integration-policy.ts +54 -0
  250. package/src/pr-review-client.ts +1 -1
  251. package/src/pr-review.ts +1 -1
  252. package/src/preference-registry.ts +2 -0
  253. package/src/prepared-mcp-setup.ts +123 -0
  254. package/src/proxy.ts +23 -12
  255. package/src/realtime.ts +15 -6
  256. package/src/session-history-import.ts +84 -0
  257. package/src/session-list-entries.ts +1 -0
  258. package/src/session-message-search.ts +47 -0
  259. package/src/session-proxy-client.ts +68 -0
  260. package/src/session-proxy.ts +2098 -0
  261. package/src/session-titles.ts +8 -82
  262. package/src/session-usage-limits.ts +71 -0
  263. package/src/shared-catalog-loader.ts +169 -0
  264. package/src/site-ai.ts +284 -0
  265. package/src/site-bridge-host.ts +422 -0
  266. package/src/site-browser-entry.ts +6 -0
  267. package/src/site-browser-runtime.gen.ts +2 -0
  268. package/src/site-data.ts +372 -0
  269. package/src/site-document.test.ts +64 -0
  270. package/src/site-document.ts +18 -0
  271. package/src/site-http.ts +171 -0
  272. package/src/site-sql.ts +153 -0
  273. package/src/site-tool-bridge.ts +401 -0
  274. package/src/site.ts +878 -0
  275. package/src/skills.ts +69 -0
  276. package/src/sse.ts +9 -3
  277. package/src/stream.ts +33 -10
  278. package/src/tenant-workspaces.ts +148 -0
  279. package/src/tool-auth.ts +301 -0
  280. package/src/tool-display-metadata.ts +27 -0
  281. package/src/tools.ts +452 -0
  282. package/src/transcription.ts +3 -0
  283. package/src/types.ts +3170 -582
  284. package/src/usage-allowances.ts +189 -0
  285. package/src/vault-types.ts +379 -0
  286. package/src/vaults-api.ts +339 -0
  287. package/src/workspace-artifacts.ts +55 -6
  288. package/src/workspace-instruction-policies.ts +2 -1
  289. package/src/workspace-integrations.ts +402 -0
  290. package/src/workspace-state.ts +8 -82
  291. package/dist/chunk-C2H7HBNP.js.map +0 -1
  292. package/dist/chunk-C4DJPZRU.js +0 -119
  293. package/dist/chunk-C4DJPZRU.js.map +0 -1
  294. package/dist/chunk-JUS2UYRP.js.map +0 -1
  295. package/dist/chunk-KKRO2VCP.js.map +0 -1
  296. package/dist/chunk-QT3KFVDP.js.map +0 -1
  297. package/dist/chunk-TRNI7KEO.js.map +0 -1
  298. package/dist/chunk-VYCTL62C.js.map +0 -1
  299. package/dist/chunk-YXQX7RQT.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @opengeni/sdk
2
2
 
3
- 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,
4
4
  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.
@@ -10,21 +10,342 @@ workspaces, Personal-workspace exclusion, tenant mapping, and external Skill
10
10
  ownership—start with the canonical
11
11
  [product integration guide](../../docs/product-integration.md).
12
12
 
13
- Zero runtime dependencies. Needs only WHATWG `fetch` and streams, so it runs in
14
- 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.
15
22
 
16
23
  Browser clients may call the public API from any origin with an explicitly safe
17
24
  bearer design, but an organization API key belongs on the product server.
18
25
  Browser cookies are accepted cross-origin only from operator-configured trusted
19
26
  origins; arbitrary embedding origins never receive credentialed CORS responses.
20
27
 
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
+
75
+ ```ts
76
+ // lib/opengeni.ts (server only: the organization API key never reaches the browser)
77
+ import { Opengeni } from "@opengeni/sdk/chat";
78
+
79
+ export const og = new Opengeni({ apiKey: process.env.OPENGENI_API_KEY! });
80
+ ```
81
+
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
+ },
94
+ createSession: (input) => input, // let the browser start chats
95
+ });
96
+ ```
97
+
98
+ ```tsx
99
+ // app/assistant/assistant.tsx (browser): no provider, client, or workspace id.
100
+ "use client";
101
+ import { OpenGeniChat } from "@opengeni/react";
102
+ import "@opengeni/react/compiled.css";
103
+
104
+ export function Assistant() {
105
+ return <OpenGeniChat baseUrl="/api/opengeni" />; // chat list + conversation
106
+ }
107
+ ```
108
+
109
+ `baseUrl` makes the component create the browser client for your mount and use
110
+ the workspace the proxy resolved for the signed-in user (reported in the
111
+ proxy's client config). One conversation alone is
112
+ `<SessionConversation baseUrl="/api/opengeni" sessionId={id} />`. The
113
+ `<OpenGeniProvider client={client} workspaceId={workspaceId}>` form keeps
114
+ working, for example for several components or the headless hooks.
115
+
116
+ What `resolve` returns decides the workspace:
117
+
118
+ - `{ user, tenant }`: one workspace per tenant; chats are private per user by
119
+ default.
120
+ - `{ user }`: one workspace per user. It stays single-user: the SDK adds its
121
+ owner once, and Opengeni never admits anyone else to it.
122
+ - `{ user, workspaceId }`: bring your own workspace id.
123
+
124
+ Workspaces are created on first use and cached per `Opengeni` instance.
125
+ `og.workspaceId({ tenant })` and `og.workspaceId({ user })` return the same ids
126
+ the proxy uses. There is no onboarding step: the first request a user makes
127
+ adds them to a tenant (or your own) workspace with conversation permissions
128
+ (workspace read, session create/read/control, file upload/read, and attaching
129
+ your per-session MCP servers; no admin). That needs an organization API key
130
+ with `members:manage` (full access includes it) plus those permissions.
131
+ Opengeni never changes an existing membership, so narrowing or widening one
132
+ with `updateExternalWorkspaceMember` sticks. A user you remove from a tenant
133
+ workspace is added again on their next request, because your product decides
134
+ who its users are; stop resolving a user you no longer admit. A removal from a
135
+ per-user workspace sticks. `organizationId` is optional:
136
+ the facade reads it once from the key. Keep `user` and `tenant`
137
+ host-authenticated, never from the request body or path.
138
+
139
+ The proxy and facade accept `chats`. `"private"` (the default with a user)
140
+ uses private visibility, session-only agent reach and personal Knowledge;
141
+ `"shared"` uses workspace visibility, agent reach and Knowledge, so a tenant's
142
+ users see each other's chats. Without a user and with `chats` omitted, the
143
+ facade keeps workspace visibility, session-only agent reach and Knowledge
144
+ authoring off. The API wire values are `visibility: "private" | "workspace"`;
145
+ stored visibility is `user_private | workspace_shared`. Explicit hook/create
146
+ fields override defaults. Keep MCP URLs and credentials in the server's
147
+ `createSession` hook; the browser cannot choose them through the proxy.
148
+
149
+ You can also pass a plain `OpenGeniClient` (organization key) instead of the
150
+ facade; then `resolve` must return `{ user, workspaceId }`. The explicit
151
+ membership APIs (`ensureWorkspace`, `addExternalWorkspaceMember`,
152
+ `updateExternalWorkspaceMember`, `cancelExternalWorkspaceMemberGrant`) keep
153
+ working for products that manage access themselves. Legacy
154
+ `chats: "isolated"` with `{ tenant, user }` still provisions a separate
155
+ workspace per user and tenant. Per-user workspaces add their owner with a
156
+ stable onboarding key, so a retry never restores a revoked grant;
157
+ `memberPermissions` replaces that owner's permissions on first onboarding
158
+ only. The standalone resolver is on the server-only
159
+ `@opengeni/sdk/tenant-workspaces` subpath.
160
+
161
+ The facade also accepts `agent` (identity, capabilities, instructions, renderer)
162
+ and defaults its renderer to `"markdown"`. Current servers always admit agent
163
+ configuration. For older servers that still gate it, only an implicit renderer
164
+ retries once without `agent` on `422 agent_config_not_enabled`, and that refusal
165
+ is cached per facade instance. Explicit agent settings are never stripped: the
166
+ 422 names `OPENGENI_AGENT_CONFIG_ADMISSION_ENABLED` for that older server's
167
+ operator. The private default with a user now turns personal Knowledge
168
+ on; pass `memory: false` to keep authoring off.
169
+ Missing organization private-session enablement raises `OpenGeniSetupError`,
170
+ with owner/admin API, SDK and web-app instructions; the proxy preserves that
171
+ actionable error for browser clients.
172
+
173
+ Framework adapters are thin wrappers over the same web-standard handler (they
174
+ also accept `createChatHandler` or any `(Request) => Promise<Response>`):
175
+
176
+ ```ts
177
+ // Next.js App Router: app/api/opengeni/[...path]/route.ts
178
+ import { createSessionProxyRoute } from "@opengeni/sdk/next";
179
+ export const dynamic = "force-dynamic";
180
+ export const { GET, POST, PUT, PATCH, DELETE } = createSessionProxyRoute(og, { resolve });
181
+
182
+ // Express / Connect / node:http (mount before body parsers, or they are re-serialized)
183
+ import { toNodeMiddleware } from "@opengeni/sdk/express";
184
+ app.use("/api/opengeni", toNodeMiddleware(handler));
185
+
186
+ // Hono
187
+ import { toHonoHandler } from "@opengeni/sdk/hono";
188
+ app.all("/api/opengeni/*", toHonoHandler(handler));
189
+ ```
190
+
191
+ The Next adapter passes the full URL, including any `basePath`; the Node
192
+ middleware rebuilds it from `originalUrl`, streams bodies and SSE, and aborts
193
+ the request when the client disconnects.
194
+
195
+ The chat list (`SessionList` / `OpenGeniChat`) uses `listSessionPage`. By
196
+ default (`sessionList: "mine"`) the proxy adds a creator filter for the
197
+ resolved user server-side, so each user sees only the chats they started;
198
+ `sessionList: "visible"` lists every chat Opengeni lets the user read in the
199
+ workspace, and `false` disables listing. Archive and restore go through
200
+ `updateSessionArchive` unless `archive: false`. That personal archive only hides
201
+ a chat from one member's list. The deployment's idle-session archive is separate:
202
+ it makes long-idle sessions read-only and is reported on `session.retention`.
203
+ Pass `keepLive: true` to `createSession`, or call
204
+ `updateSessionRetention(workspaceId, sessionId, { keepLive: true })`, to keep a
205
+ persistent agent's session out of it. A "New chat" in `OpenGeniChat`
206
+ sends `{ initialMessage, idempotencyKey }` plus any attached file `resources`
207
+ and, unless `modelSelection: false`, the user's explicit model choices, so it
208
+ needs the `createSession` hook. The hook sees them as input; the proxy adds the
209
+ files and applies those choices to the request the hook returns. Voice input
210
+ (`POST .../transcriptions`) is forwarded as the resolved user unless
211
+ `voiceInput: false`. The client config the proxy serves reports what it can do:
212
+ `sessionCreation` (a `createSession` hook exists), `archive`, and `artifacts`
213
+ (`false` unless `artifacts: true`). The stock UI hides "New chat" and
214
+ "Archive" when they are off, shows Site previews as unavailable without a
215
+ request, and shows the composer microphone only when `voiceInput.available`.
216
+
217
+ Composer uploads are refused for anonymous visitors: return `visitor: true`
218
+ from `resolve` for a user your product has not signed in (for example a
219
+ website visitor keyed by a cookie), and the proxy reports file uploads off and
220
+ refuses the upload routes unless `visitorUploads: true`. Agent-produced media
221
+ still loads under `files`.
222
+
223
+ Live voice (speech-to-speech) is forwarded the same way unless
224
+ `realtimeVoice: false`. The stock conversation shows its voice button only when
225
+ you opt in, with `realtimeVoice: true` here (reported in the client config) or
226
+ the component's `realtimeVoice` prop. The proxy forwards the workspace's voice model catalog and the call routes
227
+ under `.../sessions/{id}/realtime` (start, provider connect, heartbeat,
228
+ activate, transcript sync, end). Opengeni still checks the user's
229
+ `sessions:control` permission, binds the call to that user and browser, runs
230
+ spoken requests as steers of the same chat, and meters deployment-funded voice
231
+ against your credits. `authorizeSession` runs on every call route, and
232
+ `beforeForwardMessage` sees `delivery: "realtime"` when a call starts (refuse
233
+ it, or return `mcpCredentialUpdates`; the proxy also refreshes the `toolServer`
234
+ token) and before transcripts and spoken requests are saved (refuse them, or add
235
+ `modelContext`, which must be the same when a request is retried).
236
+
237
+ `listSessionPage` includes personal pinned details by default. A caller with a
238
+ separate `pinsOnly: true` read can pass `includePinned: false` on ordinary pages
239
+ to avoid repeatedly loading those details. Ordinary pages still exclude pinned
240
+ rows; `pinsOnly: true` and `includePinned: false` cannot be combined. Older API
241
+ versions may ignore `includePinned` and return the default pin projection.
242
+
243
+ For navigation lists, `listSessionSummaryPage` uses the same filters, ordering,
244
+ pin rules and cursors, returning `SessionListEntry` records. Entries include
245
+ display/rename titles, creator, lifecycle, personal state, hierarchy counts and
246
+ the model and reasoning effort shown with a row; prompts and the rest of the
247
+ execution configuration remain on `getSession`. Older APIs return
248
+ full pages that the SDK projects locally. Unknown filters/cursors still fail
249
+ explicitly rather than returning an unfiltered result.
250
+
251
+ Every request calls `resolve`, then runs through `asUser(user, { source })`
252
+ (there is no service-authority fallback) against exactly the resolved
253
+ workspace. Only the native routes `OpenGeniProvider` and the conversation use
254
+ are served: client config; workspace read, model catalog, live control stream,
255
+ and workspace Resume; session read/rename, events (list and SSE with
256
+ `Last-Event-ID` resume), send/steer/approval/human-input, queue, composer
257
+ draft, pause/resume, and the session goal (read; pause/resume, forwarding only
258
+ `{ status: "paused" | "active" }`; and clear, the proxy's only `DELETE`); and, unless `files: false`, attachment upload and download
259
+ URLs plus the media the session produced (generated images and video,
260
+ published files, and browser or computer screenshots). Workspace-level
261
+ artifact reads (`artifacts/:id/content` with `Range`, `artifacts/:id/playback-source`)
262
+ must name their session in `x-opengeni-session-id`; the proxy runs
263
+ `authorizeSession` and forwards only an artifact Opengeni proves that session
264
+ produced. Every other route or method is a 404 (cancel and workspace Pause are
265
+ refused); unknown query parameters on served reads pass through. Browser session creation is disabled
266
+ unless you supply `createSession`; the browser may then send only
267
+ `initialMessage` and `idempotencyKey`, and your hook returns the full request
268
+ (tools, MCP servers, Skills, instructions, model policy). Message bodies are
269
+ capped by `maxBodyBytes` (1 MiB), cannot rotate MCP credentials or attach
270
+ anything but files, and `modelSelection: false` strips per-message model
271
+ choices. `authorizeSession(sessionId, context)` adds a product-level session
272
+ check on top of Opengeni's own membership and visibility checks. Without
273
+ `authorizeMutation`, only cross-site (`Sec-Fetch-Site`) mutations are refused,
274
+ so cookie-authenticated hosts should pass their CSRF check.
275
+
276
+ ### Your own tools, as the signed-in user
277
+
278
+ `toolServer` attaches your product's MCP endpoint to every session the
279
+ `createSession` hook creates, with a short-lived token for the user `resolve`
280
+ authenticated. The proxy refreshes it on every send, steer, submit, approval,
281
+ and human-input answer. Your endpoint, built with any MCP library, verifies it:
282
+
283
+ ```ts
284
+ createSessionProxyHandler(og, {
285
+ resolve,
286
+ createSession,
287
+ toolServer: {
288
+ url: "https://app.example.com/api/mcp", // public HTTPS (tunnel locally)
289
+ approvals: { ask: ["rename_post"] }, // the user approves each call first
290
+ },
291
+ });
292
+
293
+ // In the MCP route (Web Request, or an Express/Node request):
294
+ import { ToolRequestError, verifyToolRequest } from "@opengeni/sdk/tool-auth";
295
+ const { user, tenant, workspaceId } = await verifyToolRequest(request); // or ToolRequestError (401)
296
+ ```
297
+
298
+ Scope every tool to the verified `user` and `tenant`; never trust ids the model
299
+ sends; list write tools in `approvals.ask`. Tools act as the chat's creator, also
300
+ in shared chats. Members need `mcp_servers:attach`. Tokens are HS256 JWTs signed
301
+ with a key derived from `OPENGENI_API_KEY` (or the same `secret` on both sides),
302
+ bound to the full tool URL by `aud` (set `OPENGENI_TOOL_SERVER_URL` to configure
303
+ both sides at once), and valid for `ttlSeconds` (default 24 hours). Non-Node
304
+ verifiers use the hex key from `deriveToolTokenKey()`, never the organization key;
305
+ the format is documented in `src/tool-auth.ts`. See
306
+ `examples/tool-server` for a runnable Express version.
307
+
308
+ `beforeForwardMessage(message, context)` runs before every forwarded user
309
+ message (send, steer, composer submit, and a browser-started create) and may
310
+ return server-owned additions, or a `Response` to refuse the message:
311
+
312
+ ```ts
313
+ createSessionProxyHandler(og, {
314
+ resolve,
315
+ beforeForwardMessage: async ({ sessionId, delivery }, { user }) => ({
316
+ // Model-visible, placed before any context the browser sent.
317
+ modelContext: `Page ${currentPage(user)} · ${timeZone(user)} · ${today()}`,
318
+ // Header-only rotation of MCP servers already attached to the session,
319
+ // applied atomically as the message is accepted (ignored on create).
320
+ mcpCredentialUpdates: [
321
+ { id: "acme", headers: { Authorization: `Bearer ${await mintUserToken(user)}` } },
322
+ ],
323
+ }),
324
+ });
325
+ ```
326
+
327
+ This is the per-user tool token pattern: create the session with
328
+ `mcpServers: [{ id: "acme", url, headers }]` (and `tools: [{ kind: "mcp", id:
329
+ "acme" }]`, which the acting user needs `mcp_servers:attach` to attach), then
330
+ hand the MCP server a fresh short-lived bearer on every message. The browser
331
+ still cannot send `mcpCredentialUpdates` itself. With `modelSelection: false`
332
+ the proxy reports `modelSelection: false` in the client config, and
333
+ `SessionConversation` hides its model picker.
334
+
335
+ Opengeni adds each user to the workspace on their first request (see
336
+ [External users](#external-users-asuser)), with `workspace:read`,
337
+ `sessions:create`, `sessions:read`, `sessions:control`, `files:upload`,
338
+ `files:read`, and `mcp_servers:attach`. Pass the facade from `@opengeni/sdk/chat`
339
+ instead of a client to resolve `{ tenant, user }` or `{ user }` to a workspace
340
+ created on first use.
341
+
21
342
  ## Quick start
22
343
 
23
344
  ```ts
24
345
  import { OpenGeniClient } from "@opengeni/sdk";
25
346
 
26
347
  const client = new OpenGeniClient({
27
- baseUrl: "https://api.example.com",
348
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
28
349
  apiKey: process.env.OPENGENI_API_KEY!,
29
350
  });
30
351
 
@@ -45,14 +366,14 @@ for await (const event of client.streamEvents(workspaceId, session.id)) {
45
366
  ## Organization-backed product setup
46
367
 
47
368
  A multi-tenant product should hold one organization API key on its backend and
48
- idempotently map each product tenant to an OpenGeni organization workspace. The
369
+ idempotently map each product tenant to an Opengeni organization workspace. The
49
370
  wire kind for an organization workspace is `"shared"`; Personal workspaces are
50
371
  excluded.
51
372
 
52
373
  ```ts
53
374
  const client = new OpenGeniClient({
54
375
  baseUrl: process.env.OPENGENI_API_BASE_URL!,
55
- apiKey: process.env.OPENGENI_ORGANIZATION_API_KEY!,
376
+ apiKey: process.env.OPENGENI_API_KEY!,
56
377
  });
57
378
 
58
379
  const organizationId = process.env.OPENGENI_ORGANIZATION_ID!;
@@ -71,12 +392,42 @@ if (workspace.kind !== "shared") {
71
392
  The response is `{ workspace, created }`. Replays return the original nested
72
393
  workspace with `created: false`. For an organization key, `getAccessContext()`
73
394
  does not enumerate every workspace grant; call `listWorkspaces()` for the
74
- complete organization-workspace inventory.
395
+ organization-workspace inventory allowed by the key's live scope.
75
396
 
76
397
  Organization key administration uses `listOrganizationApiKeys`,
77
- `createOrganizationApiKey`, and `deleteOrganizationApiKey`. The key token from a
398
+ `getOrganizationApiKey`, `createOrganizationApiKey`, `updateOrganizationApiKey`,
399
+ and `deleteOrganizationApiKey`. GET detail and PATCH return the raw `ApiKey`;
400
+ creation returns `{ apiKey, token }`. The key token from a
78
401
  create response is shown once and must be stored in the backend's secret
79
- manager.
402
+ manager. `createOrganizationApiKey(organizationId, { name, access: "read" })`
403
+ mints a read-only master key: it inventories shared workspaces and reads their
404
+ sessions, events, and files, but cannot create sessions, send messages, or mint
405
+ keys. This legacy `access: "read"` tier is distinct from the broader explicit
406
+ `read_only` preset, which includes every read/list/view/search permission
407
+ except the secret-value read `secrets:read`.
408
+
409
+ Create or edit with `policy: { preset, permissions, workspaceScope }` for exact
410
+ grants. `OrganizationAccessPreset` is `"read_only" | "full" | "custom"`;
411
+ the label never adds permissions and the server recomputes it from the selected
412
+ set. Explicit `full` selects every canonical non-deprecated permission; `custom`
413
+ has no implicit grants or scope. Even `workspace:admin` grants no wildcard under
414
+ an explicit policy. Scope is `{ kind: "all" }` (current and future shared
415
+ workspaces) or `{ kind: "selected", workspaceIds }` (up to 500 unique same-org
416
+ shared-workspace IDs; empty reaches none). Personal workspaces are always excluded.
417
+
418
+ `updateOrganizationApiKey(organizationId, apiKeyId, { name?, description?, policy? })`
419
+ requires at least one change; `description: null` clears it. Metadata-only edits
420
+ preserve legacy stored permissions and wildcard semantics. A policy edit replaces
421
+ the policy, transitions legacy keys to explicit semantics, and applies narrowing
422
+ on the next request without rotating the token. Omitting policy at creation
423
+ retains legacy `access`/`preset` inputs and the full legacy default.
424
+ See [the canonical policy guide](../../docs/product-integration.md#explicit-organization-key-policies).
425
+
426
+ Keys with session-read authority can call
427
+ `listOrganizationSessions(organizationId, { limit, cursor, scopeSubjectId, status })`
428
+ for one page of sessions across authorized shared workspaces (each row carries its
429
+ `workspaceId`; private sessions and Personal workspaces never appear), or
430
+ `iterateOrganizationSessions` to follow `nextCursor` to the end.
80
431
 
81
432
  The external backend also owns its Skill catalog. Load the selected definitions
82
433
  and pass them inline in `CreateSessionRequest.skills` for each product-created
@@ -84,10 +435,22 @@ session. There is no organization-wide Skill registry or Skill inheritance in
84
435
  this integration contract. See the canonical guide for the complete route map,
85
436
  security boundary, and delivery checklist.
86
437
 
438
+ Installed Skills can be workspace-managed or marked `session_selected`.
439
+ Session-selected guidance is exposed to no agent
440
+ until `createSession` names the reviewed immutable component in
441
+ `installedSkillIds`. Opengeni copies that exact artifact into the new session,
442
+ so omitting the field from customer-facing creates is a real contamination
443
+ boundary rather than a prompt convention.
444
+ The ordered selection is part of keyed-create identity: reusing an
445
+ `idempotencyKey` with a different selection returns a conflict instead of the
446
+ session configured by the earlier request.
447
+
87
448
  For session lists, `deriveSessionDisplayTitle(session)` returns the durable
88
449
  agent/user title when available. While the automatic title is still pending, it
89
- shows a short, sensitive-safe preview of the opening prompt and automatically
90
- yields to the later `session.title_set` value.
450
+ shows the first short, sensitive-safe line of the opening prompt, skipping an
451
+ unsafe pasted URL or identifier when later safe text exists. If no safe line is
452
+ available, a short session reference keeps rows distinguishable. The display
453
+ automatically yields to the later `session.title_set` value.
91
454
 
92
455
  Browser consoles that do not need operator-only surfaces can import
93
456
  `OpenGeniBrowserClient` from `@opengeni/sdk/browser`. Document authority migration
@@ -99,11 +462,200 @@ root and `@opengeni/sdk/core` clients, or through the focused
99
462
  import { OpenGeniDocumentAuthorityClient } from "@opengeni/sdk/document-authority";
100
463
 
101
464
  const operatorClient = new OpenGeniDocumentAuthorityClient({
102
- baseUrl: "https://api.example.com",
465
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
103
466
  apiKey: process.env.OPENGENI_API_KEY!,
104
467
  });
105
468
  ```
106
469
 
470
+ ## External users (`asUser`)
471
+
472
+ On a product backend, `client.asUser(externalId, { source? })` returns a separate
473
+ client that sends the external actor assertion with the organization key. IDs
474
+ are opaque and case-sensitive; the default source is `default`. Keep the key and
475
+ this client server-side. Each request requires the intersection of the key's
476
+ permissions and the user's workspace membership. API-key rotation does not
477
+ change the external identity.
478
+
479
+ Membership is automatic: when an `asUser` request reaches a shared workspace of
480
+ the key's own organization and the user has no membership there, Opengeni adds
481
+ one with conversation permissions (workspace read, session create/read/control,
482
+ file upload/read, `mcp_servers:attach`) and continues the request. This needs
483
+ the same authority as explicit onboarding: the key holds `members:manage` (or a
484
+ legacy `workspace:admin`) and every one of those permissions, with the
485
+ workspace in its scope; otherwise the request is a `403` as before. It never
486
+ applies to linked native identities, Personal workspaces, SDK per-user
487
+ workspaces (`opengeni-sdk:user-isolation:*` external source), other
488
+ organizations, requests that need a permission outside those defaults, or any
489
+ non-key caller, and it never changes an existing membership. The identity is
490
+ re-checked as active under the same lock, and the grant writes the same
491
+ receipt and lifecycle event as explicit onboarding, attributed to the key. A
492
+ user removed from a shared workspace is added again on their next request; a
493
+ suspended or offboarded identity stays refused.
494
+
495
+ Use the service client—not an `asUser` client—to call
496
+ `addExternalWorkspaceMember(workspaceId, { identity: { externalId, source? }, permissions })`
497
+ to choose permissions up front. It requires `members:manage`, cannot grant more
498
+ than the key permits, excludes Personal workspaces, and refuses to overwrite an
499
+ existing membership with different permissions (`409`, also when automatic
500
+ membership already created it); change an existing member's permissions with the keyed
501
+ `updateExternalWorkspaceMember(organizationId, workspaceId, organizationMembershipId,
502
+ { operationId, permissions })`, which never tears down work. `asUser` calls never
503
+ reactivate disabled identities.
504
+
505
+ Service `removeWorkspaceMember` supports external members through the existing
506
+ fenced workspace teardown. Account-wide `updateExternalIdentityMembership(
507
+ organizationId, organizationMembershipId, { kind, expectedAuthorizationRevision,
508
+ operationId, reason? })` requires explicit `account:admin` and supports suspend,
509
+ reactivate, and offboard. The membership ID is returned by onboarding; its initial
510
+ authorization revision is 1. Retain the returned revision for subsequent changes.
511
+ Reactivation restores admission, not revoked memberships or work. Offboarding
512
+ follows existing retention policy and cannot be reactivated through this API.
513
+ Native-user targets and `asUser` calls are rejected by this service endpoint.
514
+
515
+ An `asUser` client can discover/access its own provisioned Personal workspace
516
+ under the key's permission ceiling. Core private session operations retain
517
+ organization readiness/settings and explicit sharing acknowledgments; service
518
+ clients gain no Personal-workspace fallback.
519
+
520
+ Optional native-account delegation uses `beginIdentityLink`, authenticated native
521
+ `previewIdentityLink`/`confirmIdentityLink`, and explicit server-side
522
+ `asLinkedUser(externalId, { source?, linkId, expectedLinkRevision })`. Confirmation
523
+ requires the actual native login plus the one-time host challenge; an organization
524
+ key cannot confirm for the native user. `listIdentityLinks(workspaceId, cursor?)`
525
+ returns only the effective participant's links in that organization (50 per page).
526
+ `revokeIdentityLink` requires the observed revision. Linking never changes ordinary
527
+ `asUser`, merges histories or transfers credentials. Link-dependent accepted work
528
+ retains revocation checks across schedules and children. Short-lived inline MCP
529
+ credentials remain supported, and durable renewal stays opt-in.
530
+
531
+ Connect accepts an optional `installationTarget: { instanceKey, displayName,
532
+ expectedInstanceVersion? }`. Keep the exact observed version for an existing named
533
+ account. Setup freezes the target through callback and operation review; omitting
534
+ it creates an independent named account instead of overwriting a default instance.
535
+
536
+ ## Chat facade fallback (`@opengeni/sdk/chat`)
537
+
538
+ Use this only when your product already has its own chat UI speaking Vercel
539
+ `useChat` or an OpenAI-shaped protocol and you want a compatible drop-in
540
+ backend, or for server-side bots (`og.chat(...).send()`). It is a text-only
541
+ projection: tool outputs are dropped (the Vercel adapter emits no tool parts
542
+ by default, and only `output: { status }` with `toolParts: true`), there are no
543
+ files, attachments, artifacts, or images,
544
+ no goals/queue/steer UI, and reopening restores only a text snapshot. For
545
+ anything else, use [the default conversation embed](#embed-the-conversation-default).
546
+
547
+ One option object per conversation, one server handler for your endpoint.
548
+ Tenants map to organization workspaces, conversations map to deterministic
549
+ sessions, and the organization API key never leaves your server.
550
+
551
+ ```ts
552
+ import { Opengeni, createChatHandler } from "@opengeni/sdk/chat";
553
+
554
+ const og = new Opengeni({
555
+ baseUrl: process.env.OPENGENI_API_BASE_URL!, // omitted = production https://app.opengeni.ai
556
+ apiKey: process.env.OPENGENI_API_KEY!, // the organization id is derived from the key
557
+ source: "acme-app", // labels your product's tenants and users (default "app")
558
+ });
559
+
560
+ const chat = await og.chat({
561
+ tenant: "acme", // one workspace per customer, created on first use; omit for one per user
562
+ user: "u_42", // authenticated product user; added to the workspace on first use
563
+ conversation: "c_9", // stable conversation id, independent of the acting user
564
+ agentAccess: "session", // "session" (default) | "user" | "workspace"
565
+ memory: "user", // "user" | "workspace" | false; session-only agent reach defaults to false
566
+ create: { sandboxBackend: "none" }, // raw create-request passthrough for a pure chat
567
+ });
568
+
569
+ const reply = await chat.send("hello"); // creates the session on the first send
570
+ console.log(reply.text); // or String(reply); the answer, without progress commentary
571
+
572
+ // Per message: model policy and model-visible page context (not shown in the transcript).
573
+ await chat.send("Why did revenue dip?", {
574
+ model: "gpt-5.5",
575
+ reasoningEffort: "low",
576
+ modelContext: "Viewing dashboard 42, range 2026-09-01..2026-09-28",
577
+ });
578
+
579
+ for await (const chunk of chat.stream("and then?")) {
580
+ if (chunk.type === "text") process.stdout.write(chunk.text);
581
+ if (chunk.type === "pending") await chat.respond({ requestId: chunk.pending.requestId, decision: "approve" });
582
+ }
583
+
584
+ // Your endpoint. `resolve` is your auth hook: identity comes from the request
585
+ // you authenticated, never from the body. The handler reads the client's
586
+ // conversation id itself (x-opengeni-conversation header, or the wire format's
587
+ // own field) and authorizes as `user`; return `conversation` from resolve only
588
+ // when the host names it, which is required when there is no `user`.
589
+ export const handler = createChatHandler(og, {
590
+ resolve: async (request) => {
591
+ const session = await getSessionFromCookie(request);
592
+ if (!session) return new Response("Unauthorized", { status: 401 });
593
+ return { tenant: session.accountId, user: session.userId };
594
+ },
595
+ // format: "vercel" | "openai-chat" | "openai-responses" (default "native");
596
+ // a request may override it with the x-opengeni-chat-format header.
597
+ });
598
+ export const GET = handler; // conversation history, for restoring the chat on reload
599
+ export const POST = handler; // send a message, or answer a pending request at .../respond
600
+ ```
601
+
602
+ Conversation IDs do not change with the acting user. Authorized collaborators
603
+ can use the same session; private visibility prevents access by other users.
604
+ Use `chatBySessionId` for existing and legacy user-namespaced conversations.
605
+ A user is added to the workspace on their first request (see
606
+ [External users](#external-users-asuser)). Without a `user`, the host must name the conversation
607
+ from `resolve`. The Vercel and OpenAI adapters send only the latest user
608
+ message; the earlier messages in that request are imported once, as context on
609
+ the first message of a conversation, after which Opengeni owns the history.
610
+
611
+ Pick the isolation per session with `agentAccess` (which other sessions the
612
+ agent may reach) and `memory` (what it remembers), all inside one workspace that
613
+ shares the customer's documents, instructions, and integrations:
614
+
615
+ | Scenario | `agentAccess` | `memory` |
616
+ | ------------------------------------------------- | ------------- | ------------- |
617
+ | Agent confined to its chat tree (support desk) | `"session"` | `false` |
618
+ | One user's chats see each other, not other users' | `"user"` | `"user"` |
619
+ | Everything in the tenant shared | `"workspace"` | `"workspace"` |
620
+ | Shared agent access, no memory | any | `false` |
621
+
622
+ To keep an existing `useChat` route with its own request body, stream the chat
623
+ into your AI SDK response yourself. The Vercel format speaks the v1 UI message
624
+ stream used by AI SDK 5, 6, and 7 (tool approval requests need 6 or later):
625
+
626
+ ```ts
627
+ import { createUIMessageStream, createUIMessageStreamResponse } from "ai";
628
+ import { uiMessageStreamParts } from "@opengeni/sdk/chat";
629
+
630
+ export async function POST(request: Request) {
631
+ const { messages, dashboardId } = await request.json(); // your own body
632
+ const me = await authenticate(request);
633
+ const chat = await og.chat({ tenant: me.accountId, user: me.userId, conversation: me.chatId });
634
+ const text = lastUserText(messages);
635
+ const stream = createUIMessageStream({
636
+ execute: async ({ writer }) => {
637
+ const chunks = chat.stream(text, { modelContext: `Dashboard ${dashboardId}` });
638
+ for await (const part of uiMessageStreamParts(chunks, { framing: false })) {
639
+ writer.write(part as never);
640
+ }
641
+ },
642
+ });
643
+ return createUIMessageStreamResponse({ stream });
644
+ }
645
+ ```
646
+
647
+ Opengeni's own tool activity is omitted by default because those tools are not
648
+ in your typed tool set; pass `toolParts: true` (or `createChatHandler(og, {
649
+ toolParts: true })`) to emit them as `dynamic`, provider-executed tool parts.
650
+ Pending approvals are always emitted as dynamic tool parts with a
651
+ `tool-approval-request`.
652
+
653
+ The chat handler is backend-only: connect a custom or compatible frontend to
654
+ its protocol. For the full React agent experience, use `SessionConversation`
655
+ with `OpenGeniClient` and authenticated session routes, not this simplified
656
+ chat protocol. The normal SDK preserves files, tools, approvals with policies,
657
+ forks, and realtime voice.
658
+
107
659
  ## Related-work discovery
108
660
 
109
661
  `listAgentTopology` returns a bounded hierarchy page plus provider-neutral
@@ -143,6 +695,35 @@ the advisory UI without changing stored evidence or API authority. See
143
695
  [`docs/work-discovery.md`](../../docs/work-discovery.md) for ranking, bounds,
144
696
  claim lifecycle, and rollout semantics.
145
697
 
698
+ ## Full-history message search
699
+
700
+ `searchSessionMessages(workspaceId, { query, sessionId?, groupBy?, archiveStatus?, limit?,
701
+ cursor? }, { signal? })` searches durable user and **completed assistant** text,
702
+ including unloaded history. It returns every non-overlapping literal occurrence,
703
+ not tools, reasoning, model context, or delta-only assistant output.
704
+
705
+ Each match has a durable `eventId`/`sequence`, a UTF-16 `messageMatchOffset`, and
706
+ `snippet: { text, matchStart, matchEnd }` with original-text UTF-16 offsets.
707
+ Results include `nextCursor`, `hasMore`, cumulative `matchedMessageCount` and
708
+ `matchedOccurrenceCount`, `scannedMessages`, and `countIsExact`. Keep following
709
+ cursors even on empty pages while `hasMore` is true; counts are provisional until
710
+ exhaustion. Cancel superseded searches with the third argument's AbortSignal.
711
+ This is a live, authorized traversal rather than a frozen snapshot; restart to
712
+ refresh after concurrent history or visibility changes.
713
+
714
+ For workspace search, `groupBy: "session"` returns the first hit per matching
715
+ session and skips that session's remaining history, keeping prolific messages
716
+ from filling multiple picker pages. It cannot be combined with `sessionId`.
717
+ In grouped mode both matched counters count session representatives, not full
718
+ per-session message/occurrence totals; `scannedMessages` counts only visited
719
+ messages. Cursors bind the grouping mode. Omit it for every-occurrence Find.
720
+
721
+ The method and types are exported from the ordinary SDK and browser entry.
722
+ Use `listEventPage` around the hit's sequence for bounded context, and use the
723
+ returned search snippet when a large source message's ordinary projection does
724
+ not include the hit. Never download complete history to perform Find locally.
725
+ See [the API contract and bounds](../../docs/session-message-search.md).
726
+
146
727
  ## Session visibility and forks
147
728
 
148
729
  For organizations with session-tenancy activation, a canonical managed-cookie
@@ -178,49 +759,143 @@ user decision. A quiescence conflict identifies the stable blocker after live
178
759
  turns, goals, realtime, schedules, workspace writers, retained processes, and
179
760
  sandbox access have been settled. Forks copy exact same-workspace durable
180
761
  conversation content but no live turn, goal, credential, Connection/delegation,
181
- personal grant, Variable Set, Rig, MCP server configuration, process, sandbox
762
+ personal grant, vault policy, task vault, Sandbox Environment, MCP server configuration, process, sandbox
182
763
  identity, pin, or workflow. Destination visibility and acknowledgement are
183
764
  idempotency-bound.
184
765
 
185
- ## Personal-resource grants
766
+ ## Archived session history import (server only)
767
+
768
+ Use `@opengeni/sdk/session-history-import` to migrate historical conversations
769
+ into read-only archives. These functions take `client` first, use its ordinary
770
+ `requestJson` transport, and are not eager client methods or root helper exports:
186
771
 
187
- The same canonical managed-cookie owner can manage explicit personal-resource
188
- delegations after organization activation. Pages are bounded to one exact kind;
189
- the server derives the only valid action and returns the full credential-free
190
- delegation to attach through the resource's ordinary session API.
772
+ Create requires `sessions:create`; append requires `sessions:control` and the same
773
+ authenticated importer. With `asUser`, the user's permissions apply rather than
774
+ the organization key's permissions.
191
775
 
192
776
  ```ts
193
- const page = await browserClient.listUserResourceAuthorities(workspaceId, {
194
- resourceKind: "connection",
195
- limit: 50,
777
+ import { OpenGeniClient } from "@opengeni/sdk";
778
+ import {
779
+ importArchivedSession,
780
+ appendArchivedSessionEvents,
781
+ } from "@opengeni/sdk/session-history-import";
782
+
783
+ const og = new OpenGeniClient({
784
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
785
+ apiKey: process.env.OPENGENI_API_KEY!,
786
+ });
787
+ // Existing tenant mapping; the original creator is admitted on first use.
788
+ const actor = og.asUser(originalExternalUserId, { source });
789
+ const imported = await importArchivedSession(actor, workspaceId, {
790
+ importId: "embedded:thread-42", // Stable; persist the exact request before sending.
791
+ title: "Original conversation",
792
+ createdAt: "2025-04-01T09:30:00Z",
793
+ visibility: "user_private",
196
794
  });
197
- const authority = page.authorities[0];
198
- if (!authority) throw new Error("No personal Connection is available");
795
+ const appended = await appendArchivedSessionEvents(actor, workspaceId, imported.importId, {
796
+ batchId: "thread-42:batch-0001", // Stable; also persist this exact body first.
797
+ offset: imported.nextOffset,
798
+ events: [{
799
+ type: "agent.message.completed",
800
+ createdAt: "2025-04-01T09:31:00Z",
801
+ payload: { text: "Historical answer", channel: "final" },
802
+ }],
803
+ });
804
+ // Persist imported.session.id and appended.nextOffset in the migration ledger.
805
+ ```
199
806
 
200
- const issued = await browserClient.issueUserResourceGrant(workspaceId, authority.authorityId, {
201
- scope: "user",
202
- resourceKind: "connection",
203
- mode: "session",
204
- context: "workspace_shared",
205
- sessionId,
206
- expectedAuthorityEpoch: current.tenancy.authorityEpoch,
207
- workspaceSharedAcknowledged: true,
807
+ External mapping equivalents take `(client, source, externalId, ...)`:
808
+ `importExternalWorkspaceArchivedSession` and
809
+ `appendExternalWorkspaceArchivedSessionEvents`. These resolve an existing
810
+ mapping; neither import form creates a workspace or grants membership. A bare
811
+ organization key creates a shared, ownerless archive. Use `asUser` for the
812
+ verified creator/owner; private imports require existing private-session
813
+ enablement. Preserve source visibility rather than widening it on failure.
814
+
815
+ The subpath exports `ImportArchivedSessionRequest/Response`,
816
+ `AppendArchivedSessionEventsRequest/Response`, `ArchivedSessionImportEvent` and
817
+ `SessionImportedArchive` types. Requests accept at most 100 events / 1 MiB
818
+ serialized UTF-8 JSON and 256 KiB per event. IDs and titles are bounded to 200
819
+ characters. Import events default to `[]`; append batches must be non-empty.
820
+ Events carry `{ type, createdAt, turnId?, payload }`: a finite supported historical
821
+ type, ISO timestamp, optional UUID/null correlation and JSON-object payload.
822
+ Source timestamps support at most millisecond precision; normalize finer dates
823
+ explicitly and retain originals in the ledger. Negative-zero JSON is rejected.
824
+
825
+ Exact create replay returns `created: false`; exact append replay returns
826
+ `replayed: true`. `offset` is a zero-based imported-event count, not a timeline
827
+ sequence. Retain the exact actor, mapping, IDs, offset and bodies for uncertain
828
+ retries; new batches use the acknowledged `nextOffset`. Changed key reuse or
829
+ an out-of-order new offset returns 409. Helpers propagate `OpenGeniApiError`,
830
+ including `outcomeUnknown`, and do not automatically retry mutations.
831
+
832
+ Import is timeline-only: completed messages, calls/results and goals may be
833
+ historical facts, but no model-facing history, live goal, pending decision or
834
+ execution is restored. `session.importedArchive` has
835
+ `{ importId, importedAt, readOnly: true }`, independent of personal archive state.
836
+ Render through the unchanged `SessionConversation` and session proxy; never
837
+ offer Send or Steer. Continuing an imported archive is unsupported in v1.
838
+ The proxy does not expose import routes. Re-upload files through existing APIs
839
+ and replace references before import; preserve additional source metadata and
840
+ source/destination IDs in the host ledger. See
841
+ [Migrating from embedded Opengeni](../../docs/product-integration.md#migrating-from-embedded-opengeni).
842
+
843
+ ## Connected accounts
844
+
845
+ Authenticated messages use the initiating user's eligible connected accounts.
846
+ Conversation visibility does not share account access. Queued work, retries and
847
+ child work retain the initiating user. No per-conversation connection grant is
848
+ required. Multiple eligible accounts require an explicit account choice:
849
+
850
+ ```ts
851
+ const accounts = await browserClient.listOwnConnectionAccounts(workspaceId);
852
+ // When choosing among multiple accounts, use the exact eligible server/account pair.
853
+ const selection = { serverId: "mail", connectionId: accounts[0]!.id };
854
+ ```
855
+
856
+ General personal-resource grants for documents and other resource
857
+ kinds remain available through the root/core SDK. They do not authorize native
858
+ connected accounts.
859
+
860
+ ## Workspace credentials, webhooks, and sandbox image
861
+
862
+ Configure these with an organization key or workspace admin session. Secrets are
863
+ returned once; store them when you create the resource. Protocol and payloads:
864
+ [`docs/workspace-integrations.md`](../../docs/workspace-integrations.md).
865
+
866
+ ```ts
867
+ const { secret: providerSecret } = await client.putWorkspaceCredentialProvider(workspaceId, {
868
+ url: "https://product.example/opengeni/credentials",
869
+ });
870
+ const { secret: webhookSecret } = await client.createWorkspaceWebhook(workspaceId, {
871
+ url: "https://product.example/opengeni/events",
872
+ eventTypes: ["turn.completed", "turn.failed"],
208
873
  });
209
874
 
210
- await browserClient.revokeUserResourceGrant(workspaceId, issued.grant.grantId);
875
+ // In your HTTP handlers, verify the raw body before parsing it:
876
+ const { event } = await verifyWebhookEvent({ body: rawBody, headers, secret: webhookSecret });
877
+ const request = await verifyCredentialProviderRequest({
878
+ body: rawBody,
879
+ headers,
880
+ secret: providerSecret,
881
+ });
211
882
  ```
212
883
 
213
- The SDK deliberately exposes only exact-session and standing (`always`) grant
214
- management. It does not expose standalone `once`, custom expiry, scheduled or
215
- cross-workspace authority, or an atomic create-session-and-attach workflow.
216
- Revocation prevents future reads but cannot retract output already shared.
884
+ To check an endpoint now, `testWorkspaceWebhook(client, workspaceId, webhookId)` sends a
885
+ signed `webhook.test` event (acknowledge it with any 2xx) and
886
+ `testWorkspaceCredentialProvider(client, workspaceId)` sends a request with
887
+ `purpose: "test"`; both come from `@opengeni/sdk/workspace-integrations` and return
888
+ what the endpoint answered, naming returned credentials but never their values.
889
+
890
+ `listWorkspaceSandboxImages` returns the deployment's allowlisted images; set one
891
+ with `updateWorkspaceSettings(workspaceId, { defaultSandboxImage })`.
217
892
 
218
- ## Scheduled connection authority
893
+ ## Personal schedules
219
894
 
220
- Agent schedules may carry explicit personal Connection authority. The public
221
- request contains only the credential-free server, Connection, and common-user
222
- grant tuple returned by the authority-selection API; the SDK never receives or
223
- stores provider credentials:
895
+ A schedule created by an authenticated human or their active agent records that
896
+ human as its immutable owner. Only that owner or their verified agent may edit,
897
+ run, pause or delete it. Each occurrence resolves the owner's current accounts;
898
+ accepted retries retain their original identity and account selection.
224
899
 
225
900
  ```ts
226
901
  const task = await client.createScheduledTask(workspaceId, {
@@ -228,18 +903,27 @@ const task = await client.createScheduledTask(workspaceId, {
228
903
  schedule: { type: "calendar", hour: 8, minute: 0, timeZone: "Europe/Oslo" },
229
904
  runMode: "reusable_session",
230
905
  agentConfig: { prompt: "Triage the new support issues" },
231
- connectionAuthorities: [selection],
906
+ connectionAccounts: [selection],
232
907
  });
233
908
  ```
234
909
 
235
- PATCH semantics are deliberate: omit `connectionAuthorities` to preserve the
236
- exact prior immutable selection, pass `[]` to clear it, or pass a non-empty
237
- array to replace and revalidate it. Execution-changing edits that preserve
238
- personal authority must be made by the same causal human; use an explicit fresh
239
- selection when authority should move. `once` grants are consumed by the durable
240
- scheduled occurrence, not by an activity attempt, so retries reuse the same run
241
- receipt. `listScheduledTaskRuns` exposes terminal occurrence state while private
242
- Connection ids, subjects, and authority snapshots stay server-side.
910
+ Account selections narrow eligible accounts; they never transfer ownership.
911
+ Omitting `connectionAccounts` on update preserves the selection. Passing an
912
+ empty array clears explicit account choices without changing the schedule owner.
913
+ Service-owned schedules retain service execution and do not acquire a human's
914
+ personal accounts. Run history remains credential-free.
915
+
916
+ A schedule freezes its connectors, accounts and (when an agent created it)
917
+ Opengeni tools. For the owner, `listScheduledTasks` and `getScheduledTask`
918
+ include a read-only `policyDrift` naming what is out of date, and
919
+ `listScheduledTaskRuns` includes `accessFailures` for runs that could not use a
920
+ connector. `listScheduledTaskAccessAttention` lists schedules whose latest run
921
+ failed that way, and schedules that cannot start because a chosen account can no
922
+ longer be used (`unavailableAccounts`, with a null `runId`). A signed-in owner
923
+ re-freezes with their current access through
924
+ `refreshScheduledTaskAccess(workspaceId, taskId, { executionDigest, leaveOut })`,
925
+ where the optional `leaveOut` keeps named default connectors or Opengeni tools
926
+ off; API keys and agents cannot. See [`docs/scheduled-task-access.md`](../../docs/scheduled-task-access.md).
243
927
 
244
928
  Deleting a task is externally idempotent and immediately removes it from live
245
929
  lists and quota, but the server retains a tombstone plus run/session/turn audit
@@ -311,7 +995,7 @@ import {
311
995
  createBrowserEditableArtifactSession,
312
996
  } from "@opengeni/sdk/editable-artifacts";
313
997
 
314
- const client = new OpenGeniClient({ baseUrl: "https://api.example.com" });
998
+ const client = new OpenGeniClient({ baseUrl: process.env.OPENGENI_API_BASE_URL! });
315
999
  const artifact = await client.getEditableArtifact(workspaceId, artifactId, {
316
1000
  replicaId,
317
1001
  });
@@ -322,7 +1006,7 @@ const kernels = {
322
1006
  } as const;
323
1007
  const { editableArtifactKernelRuntime } = await kernels[artifact.modality]();
324
1008
  const session = createBrowserEditableArtifactSession({
325
- baseUrl: "https://api.example.com",
1009
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
326
1010
  workspaceId,
327
1011
  artifact,
328
1012
  storageAuthority,
@@ -360,7 +1044,7 @@ shown below; it owns no state or authority of its own.
360
1044
 
361
1045
  `OpenGeniClient.interaction` is the framework-free resource facade over the same
362
1046
  public BrowserSession, ComputerSession, identity, auth, network-route, and human
363
- intervention APIs used by OpenGeni itself. It adds no second state or execution
1047
+ intervention APIs used by Opengeni itself. It adds no second state or execution
364
1048
  path: facade methods call the ordinary typed transport, and every mutation still
365
1049
  uses the canonical generation fences, operation receipt, permission check, and
366
1050
  placement controller.
@@ -375,6 +1059,8 @@ const browser = await client.interaction.browsers.currentOrOpen({
375
1059
  const { targets } = await browser.tabs.list();
376
1060
  const target = targets.find((candidate) => candidate.selected) ?? targets[0]!;
377
1061
  const page = await browser.observe(target.id);
1062
+ const screenshot = await browser.screenshot(target.id, { fullPage: true });
1063
+ // screenshot.data is bounded image bytes; mediaType and dimensions travel with it.
378
1064
 
379
1065
  const receipt = await browser.act({
380
1066
  operationId: crypto.randomUUID(),
@@ -391,6 +1077,17 @@ if (receipt.state === "outcome_unknown") {
391
1077
  }
392
1078
  ```
393
1079
 
1080
+ `browser.targetState(targetId)` reads generation fences without an accessibility
1081
+ tree. `browser.readDom(targetId, request)` returns a bounded element or count
1082
+ query using those fences; sensitive values are redacted.
1083
+
1084
+ The flat `client.openBrowserTargetWithInventory(workspaceId, browserSessionId,
1085
+ { url })` explicitly opens a tab and returns the owned tab inventory and its
1086
+ generation fences without a page observation. The default
1087
+ `client.openBrowserTarget(...)` and `browser.tabs.open(...)` still return page
1088
+ observations. Older controllers can refuse the inventory method; do not retry an
1089
+ uncertain creation through another tab-open method.
1090
+
394
1091
  Browser identities are immutable version graphs: live browser state stays
395
1092
  private until `browser.publishRevision(...)` explicitly creates a new revision.
396
1093
  Protected auth fills resolve connection secrets only inside the broker/controller
@@ -410,10 +1107,34 @@ assign product types such as app, page, dashboard, or gallery. List pages are
410
1107
  bounded and expose both `truncated` and an opaque `nextCursor` so callers never
411
1108
  mistake a partial page for the complete workspace catalog.
412
1109
 
413
- The initial web renderer supports semantic HTML, inline CSS, CSS-only
414
- interactions, and inline SVG. It removes JavaScript, event handlers, forms,
415
- embeds, external URLs, and other active or navigation-capable markup before
416
- rendering. Executable artifacts require a later, stronger isolation boundary.
1110
+ Each Site version retains one self-contained executable HTML runtime, its
1111
+ bounded editable source bundle, and the exact canonical tool identities it
1112
+ requested. The web host renders that runtime in an opaque-origin sandboxed
1113
+ iframe. Credentials, cookies, API URLs, workspace ids, and parent DOM authority
1114
+ never enter the iframe; `@opengeni/sdk/site` uses a single parent-owned
1115
+ `MessagePort` that exposes only the retained tool allowlist intersected with the
1116
+ current viewer's live workspace catalog.
1117
+
1118
+ Site AI can return directly into an app without any conversation UI:
1119
+
1120
+ ```ts
1121
+ import { createOpenGeniSiteClient } from "@opengeni/sdk/site";
1122
+ const site = createOpenGeniSiteClient();
1123
+ const result = await site.ai.run({ initialMessage: appPrompt, idempotencyKey: actionId });
1124
+ if (result.status === "completed") renderResult(result.text);
1125
+ ```
1126
+
1127
+ The app defines the prompt, context and output presentation. This helper uses
1128
+ native sessions and events with the same permissions, approvals, model access
1129
+ and billing; it is not a second executor. New Site work is private per viewer
1130
+ and excluded from ordinary workspace discovery. `pending` and `cancelled` are
1131
+ not successful results; model failure rejects. Retain the `onSession` handle
1132
+ to observe again using `site.ai.result(id, { turnId })`, without creating work.
1133
+ An `after` cursor returns only subsequent output; omit it for full replay.
1134
+ Aborting observation does not cancel native execution. See
1135
+ [`site-conversations.md`](../../docs/site-conversations.md) and
1136
+ [`site-session-embed`](../../examples/site-session-embed) for optional chat and
1137
+ headless inline examples. Do not store private handles in shared Site Data.
417
1138
 
418
1139
  ```ts
419
1140
  let cursor: string | undefined;
@@ -433,7 +1154,131 @@ return the exact source session, turn, attempt, and execution generation that
433
1154
  published them. Version and event history are bounded; inspect
434
1155
  `versionsTruncated` and `eventsTruncated` on the detail response.
435
1156
 
436
- Omit `firstPartyMcpTools` for the complete OpenGeni tool catalog. An explicit
1157
+ ## Workspace tools
1158
+
1159
+ The browser-safe SDK exposes the same gateway catalog and executor used by
1160
+ model MCP calls and Codemode. Generated declarations augment the lazy namespace
1161
+ for exact installed-tool types; canonical `{ serverId, toolName }` identity and
1162
+ the server-side catalog remain authoritative.
1163
+
1164
+ Known calls use `POST tools/invoke` directly, with no preceding workspace
1165
+ catalog request. The API selects one exact authorized account/connector; a
1166
+ selected MCP provider may still require its own `tools/list`. Symbolic paths
1167
+ use the canonical registry projection and reject ambiguity before connecting.
1168
+ Cold calls use the current definition. `$resolve({ identity })` returns one
1169
+ entry and a `definitionDigest`; pass it as `expectedDefinitionDigest` to require
1170
+ that complete public definition before execution. Digests never grant access.
1171
+ Returned executed metadata preserves structured-output unwrapping without a
1172
+ catalog load.
1173
+
1174
+ `$catalog()` and `$declarations()` remain explicit full discovery. For an older
1175
+ API or Site host, set `toolGatewayMode: "catalog"` explicitly on the client;
1176
+ there is no automatic downgrade or retry after an unknown invocation outcome.
1177
+ The targeted SDK caches bounded per-target metadata, shares only resolution
1178
+ loads, and retries at most once after a typed pre-execution
1179
+ `tool_definition_stale`; explicit nonretryable or outcome-unknown flags veto
1180
+ replay even when a transport labels an error stale. Explicit pins are not
1181
+ silently dropped when malformed: only `undefined` means no explicit pin.
1182
+ The same retry veto applies to retained v1/catalog mode and Site host adapters.
1183
+ For saved Site bundles with code-only retry classifiers, the host's legacy call
1184
+ projection wraps executed stale-named provider errors as
1185
+ `site_tool_execution_failed`, retaining the complete original diagnostic at
1186
+ `result.structuredContent.error.providerError`. Success outputs and ordinary
1187
+ provider errors are unchanged; genuine known preexecution stale refresh remains
1188
+ available. Stale-named transport errors without that proof use
1189
+ `site_tool_call_failed` and retain the original code in their message.
1190
+ Pins are not silently advanced. An approved call uses the server's executable-effect and
1191
+ private-authority binding, not an implicit full-public-entry pin; supply
1192
+ `expectedDefinitionDigest` when that additional precondition is wanted.
1193
+ Changed executable effect or private authority requires reapproval. Operation IDs are correlation
1194
+ keys, not general idempotency for unapproved calls.
1195
+
1196
+ ```ts
1197
+ const tools = client.tools.forWorkspace(workspaceId);
1198
+ const documents = await tools.docs.search({ query: "launch plan" });
1199
+ ```
1200
+
1201
+ An approval-required call needs a server-issued capability. A trusted host
1202
+ shows its approval UI first, then asks for one token bound to the current human,
1203
+ workspace, operation id, exact tool identity, executable effect, Site context,
1204
+ private authority and arguments. Targeted approvals use a distinct version-2 binding, never a v1
1205
+ catalog digest.
1206
+ An Ask-to-Allow policy transition can settle matching pending provenance with
1207
+ or without the token, but never ignores a supplied invalid token or a consumed
1208
+ operation. Public definition pins remain optional and may reject policy/prose
1209
+ drift without consuming the pending approval.
1210
+ The token expires after five minutes, is stored only as a hash, and is consumed
1211
+ once by the matching call.
1212
+
1213
+ Connection-backed tools configured for both provider policy and one-shot human
1214
+ approval are omitted from this HTTP catalog until their provider adapter can
1215
+ preflight credential and resource authorization before consuming the token.
1216
+
1217
+ ```ts
1218
+ const operationId = crypto.randomUUID();
1219
+ const identity = { serverId: "linear", toolName: "issues_update" };
1220
+ const input = { issueId, state: "Done" };
1221
+ const approval = await tools.$approveTarget(identity, input, { operationId });
1222
+
1223
+ await tools.$call(identity, input, {
1224
+ operationId,
1225
+ approvalToken: approval.approvalToken,
1226
+ });
1227
+ ```
1228
+
1229
+ The retained `$approve()` method and explicit `toolGatewayMode: "catalog"` use
1230
+ the legacy protocol described below. Tokens returned by `$approve()` on the
1231
+ same facade retain that routing; externally retained legacy tokens must pass
1232
+ `approvalBinding: "catalog"`. Never reinterpret a legacy token as a targeted
1233
+ approval.
1234
+
1235
+ When a legacy call or approval request receives the typed
1236
+ `409 details.code = "catalog_stale"` response, the SDK refreshes the catalog,
1237
+ re-resolves the exact identity or generated namespace path, preserves the
1238
+ operation id (generating it before the first attempt when omitted), and retries
1239
+ once. A call that already carries an approval token cannot reuse that
1240
+ single-use capability after the catalog digest changes; instead it throws
1241
+ `OpenGeniToolReapprovalRequiredError` with the refreshed identity and digest so
1242
+ the trusted host can show approval again for the same operation id.
1243
+
1244
+ Concurrent callers share one in-flight catalog request (`$catalog` and every
1245
+ call path). Each caller's `signal` cancels only its own wait; the shared request
1246
+ is cancelled only when every waiter has aborted, and a failed load is not
1247
+ cached. Calls rejected as stale on the same digest share one refresh started
1248
+ after the first such rejection while it remains the current catalog, and an
1249
+ explicit `$catalog({ refresh: true })` always starts a new request.
1250
+
1251
+ A call's `catalogDigest` is checked against the exact catalog entry it invokes:
1252
+ the API may accept an unchanged entry from a catalog digest whose unrelated
1253
+ entries have since changed, and then echoes that digest in the response. A
1254
+ changed, removed, or no-longer-authorized target entry still returns
1255
+ `catalog_stale` before execution.
1256
+
1257
+ Reapproval is valid only before the original capability is consumed. Once a
1258
+ call crosses that boundary, the server retains a hash-only tombstone for the
1259
+ operation id and rejects another approval with
1260
+ `409 details.code = "tool_gateway_operation_already_started"` and
1261
+ `outcomeUnknown: true`. The host must reconcile the provider outcome; it must
1262
+ not turn an ambiguous call into a second approved execution by minting a fresh
1263
+ operation id automatically.
1264
+
1265
+ Do not request approval capabilities speculatively or expose them to Site iframe
1266
+ code. The host projects only the active immutable version's requested identities,
1267
+ supplies the Site context on direct calls, and the server revalidates the active
1268
+ version and viewer's live authority. The requested set is a maximum allowlist,
1269
+ not an authority grant; ordinary gateway approval still applies. Archived Sites
1270
+ receive no tool bridge.
1271
+
1272
+ An upgraded host advertises targeted calls to modern Site clients. Existing
1273
+ bundled Site clients retain their catalog/call protocol: the host obtains a
1274
+ bounded manifest for only that saved version's requested identities and
1275
+ translates calls using retained per-entry pins. They still make their initial
1276
+ catalog request; republishing with the modern client removes it. Unknown or
1277
+ evicted manifest digests fail before execution. Host-owned Site/version context
1278
+ and approval tokens are stripped from iframe requests. Local previews instead
1279
+ use their frozen attempt catalog and existing Codemode operation journal.
1280
+
1281
+ Omit `firstPartyMcpTools` for the complete Opengeni tool catalog. An explicit
437
1282
  `[]` exposes no broad first-party tools; attached resources and separately
438
1283
  selected `files`/`docs` MCP servers are unaffected.
439
1284
 
@@ -610,15 +1455,78 @@ await client.cancelSession(workspaceId, sessionId, {
610
1455
  reason: "host record deleted",
611
1456
  clientEventId: crypto.randomUUID(),
612
1457
  });
1458
+ // approvalId is `approvals[].id` from the latest `session.requiresAction`
1459
+ // event (type SessionApprovalRequest: { id, name, arguments }); it is the
1460
+ // pending tool call id, not the event id.
613
1461
  await client.sendApprovalDecision(workspaceId, sessionId, { approvalId, decision: "approve" });
614
1462
  ```
615
1463
 
1464
+ ## Retry a failed session without changing its intent
1465
+
1466
+ Try again is separate from Pause/Resume and `sendMessage`. Retain the request
1467
+ across an ambiguous transport response; never synthesize a continuation prompt:
1468
+
1469
+ ```ts
1470
+ const retryRequest = {
1471
+ clientEventId: crypto.randomUUID(),
1472
+ failureEventId, // the current durable turn.failed event id
1473
+ model: selectedModel,
1474
+ reasoningEffort: "high" as const,
1475
+ latencyMode: "standard" as const,
1476
+ };
1477
+ const retry = await client.retrySession(workspaceId, sessionId, retryRequest);
1478
+ // { outcome: "accepted" | "replayed", turnId, failureEventId }
1479
+ ```
1480
+
1481
+ Requires `sessions:control` and session access. Recovery keeps the logical turn,
1482
+ original question, frozen authority, and completed history/tool results; it
1483
+ does not create a user message. Omitted policy fields retain the failed turn's
1484
+ selection. The selected policy is revalidated for new admission. A duplicate
1485
+ operation replays its receipt even after execution advances.
1486
+
1487
+ HTTP 409 codes distinguish `RETRY_STALE_FAILURE`, `RETRY_EXECUTION_UNRESOLVED`,
1488
+ `RETRY_TOOL_OUTCOME_UNKNOWN`, `RETRY_PAUSED`, `RETRY_UNSUPPORTED_FAILURE`, and
1489
+ `IDEMPOTENCY_KEY_REUSED`. `RETRY_EXECUTION_UNRESOLVED` is transient: earlier
1490
+ execution is still settling. `RETRY_TOOL_OUTCOME_UNKNOWN` is permanent: a tool
1491
+ call in the turn has an unknown outcome and cannot be blindly replayed, so
1492
+ continue with a new message instead. A call proven never started does not block
1493
+ Retry. A deliberate Pause must
1494
+ be resumed separately. Failure without a retained logical turn, a settled
1495
+ scheduled occurrence, and idle credit exhaustion are not supported retry
1496
+ boundaries. See [run lifecycle](../../docs/run-lifecycle.md).
1497
+
616
1498
  ## Session tool policy and native web search
617
1499
 
1500
+ For standalone credential maintenance, use the dedicated operation rather than
1501
+ sending a message:
1502
+
1503
+ ```ts
1504
+ const request = {
1505
+ operationKey: crypto.randomUUID(),
1506
+ updates: [{
1507
+ id: "crm",
1508
+ expectedCredentialVersion: 1,
1509
+ expectedServerUrl: "https://tools.example.test/mcp",
1510
+ headers: { Authorization: `Bearer ${replacementToken}` },
1511
+ }],
1512
+ };
1513
+ const receipt = await client.rotateSessionMcpCredentials(workspaceId, sessionId, request);
1514
+ ```
1515
+
1516
+ Only exact existing inline servers without `connectionRef` are eligible. Both
1517
+ session control and MCP attach permissions, live session authorization, expected
1518
+ versions, and a quiescent credential-consumer boundary are required. This never
1519
+ sends a message, starts work, retries a failed turn, or refreshes an active
1520
+ client. Reconcile an ambiguous response with the same request/key; the SDK does
1521
+ not retry mutations automatically. Exact authorized replay returns the original
1522
+ receipt without a second write. Old receipt verification fails explicitly with
1523
+ 503 if the deployment encryption key has been replaced. See the full
1524
+ [rotation contract](../../docs/session-mcp-servers.md#standalone-inline-credential-rotation).
1525
+
618
1526
  Omitting `tools` when creating a top-level session selects the current
619
1527
  workspace-default capability policy, including the built-in `files` server.
620
1528
  Passing `tools`, including `[]`, is an intentional fixed narrowing and can
621
- therefore disable file-download access for that session. OpenGeni's own web UI
1529
+ therefore disable file-download access for that session. Opengeni's own web UI
622
1530
  keeps `files` enabled as a hidden default, while API and embedded clients retain
623
1531
  exact control over the explicit list. Supported Responses providers attach
624
1532
  their native bounded web-search tool independently of this MCP policy.
@@ -636,7 +1544,7 @@ const updated = await client.updateSessionToolPolicy(workspaceId, sessionId, {
636
1544
  ```
637
1545
 
638
1546
  To keep a fixed allow-list, replace both connected MCP servers and individual
639
- OpenGeni tools atomically:
1547
+ Opengeni tools atomically:
640
1548
 
641
1549
  ```ts
642
1550
  await client.updateSessionToolPolicy(workspaceId, sessionId, {
@@ -655,6 +1563,7 @@ it is not public web search.
655
1563
 
656
1564
  ```ts
657
1565
  const goal = await client.getGoal(workspaceId, sessionId); // counters: autoContinuations, noProgressStreak
1566
+ const maybeGoal = await client.findGoal(workspaceId, sessionId); // null for a goal-less session (no 404)
658
1567
  await client.pauseGoal(workspaceId, sessionId, { rationale: "manual review" });
659
1568
  await client.resumeGoal(workspaceId, sessionId); // resets counters, re-arms continuations
660
1569
 
@@ -676,7 +1585,7 @@ await client.rollbackGoalRevision(workspaceId, sessionId, appliedRevisionId, {
676
1585
  call; the lower-level steps are exported for resumable/custom flows.
677
1586
 
678
1587
  Browser hosts need no storage credentials or per-application registration.
679
- OpenGeni authorizes the workspace request and returns a short-lived,
1588
+ Opengeni authorizes the workspace request and returns a short-lived,
680
1589
  object-scoped signed URL; operators must configure the private object store to
681
1590
  allow CORS from `*` so any product embedding the SDK can use that URL.
682
1591
 
@@ -748,15 +1657,15 @@ Every public endpoint group has typed methods:
748
1657
  | Group | Methods |
749
1658
  | --- | --- |
750
1659
  | Access + workspaces | `getAccessContext`, `listWorkspaces`, `createWorkspace`, `ensureWorkspace`, `getWorkspace`, `updateWorkspace` |
1660
+ | Managed organizations | `createOrganization` for first-time compatibility; `createAdditionalOrganization` for an already-onboarded human; `listOrganizationMemberships`, `listOrganizationInvitations`, `acceptOrganizationInvitation` |
751
1661
  | Sessions + events | `createSession`, `listSessions`, `listSessionPage`, `listAgentTopology`, `getSession`, `getSessionLineage`, `updateSession`, `listEvents`, `sendEvent`, `sendMessage`, `steerMessage`, `pauseSession`, `resumeSession`, `cancelSession`, `sendApprovalDecision`, `streamEvents`, `openEventStream` |
752
1662
  | Machines (bring-your-own-compute) | `listMachines`, `machineMetricsSeries`, `swapActiveSandbox`, `mintEnrollToken`, `lookupDeviceEnrollment`, `approveDeviceEnrollment`, `denyDeviceEnrollment` |
753
1663
  | Turn queue | `getQueue`, `moveQueueItem`, `editQueueItem`, `steerQueueItem`, `deleteQueueItem` |
754
- | Goal | `getGoal`, `updateGoal`, `pauseGoal`, `resumeGoal`, `listGoalRevisions`, `listGoalRevisionPage`, `applyGoalRevision`, `rejectGoalRevision`, `rollbackGoalRevision` |
755
- | Scheduled tasks | `createScheduledTask`, `listScheduledTasks`, `getScheduledTask`, `updateScheduledTask`, `pauseScheduledTask`, `resumeScheduledTask`, `triggerScheduledTask`, `deleteScheduledTask`, `listScheduledTaskRuns` |
756
- | 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 |
1664
+ | Goal | `getGoal`, `findGoal`, `updateGoal`, `pauseGoal`, `resumeGoal`, `listGoalRevisions`, `listGoalRevisionPage`, `applyGoalRevision`, `rejectGoalRevision`, `rollbackGoalRevision` |
1665
+ | Scheduled tasks | `createScheduledTask`, `listScheduledTasks`, `getScheduledTask`, `updateScheduledTask`, `pauseScheduledTask`, `resumeScheduledTask`, `triggerScheduledTask`, `deleteScheduledTask`, `listScheduledTaskRuns`, `refreshScheduledTaskAccess`, `listScheduledTaskAccessAttention` |
1666
+ | 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 |
757
1667
  | Files | `uploadFile`, `beginFileUpload`, `completeFileUpload`, `getFile`, `createFileDownloadUrl` |
758
1668
  | Documents | `createDocumentBase`, `listDocumentBases`, `getDocumentBase`, `addDocument`, `listDocuments`, `reindexDocument`, `searchDocuments`, `searchKnowledge` (effective organization + workspace + immutable initiating-user personal scope) |
759
- | Packs | `listPacks`, `registerPack`, `getPack`, `enablePack`, `deletePack`, `listPackInstallations` |
760
1669
  | Capabilities | `listCapabilities`, `createCapability`, `enableCapability`, `disableCapability`, `discoverMcpCapabilities` |
761
1670
  | Plugin packages | `previewPlugin`, `installPlugin`, `previewPluginUninstall`, `uninstallPlugin` |
762
1671
  | API Integrations | `listIntegrationDefinitions`, `listApiIntegrations`, `previewApiIntegration`, `startApiIntegrationOAuth`, `installApiIntegration`, `previewApiIntegrationUninstall`, `uninstallApiIntegration`, `listIntegrationFacets`, `configureIntegrationFacet`, `pauseIntegrationFacet`, `resumeIntegrationFacet`, `removeIntegrationFacet`, `browseGoogleDriveFacetSource`, `saveGoogleDriveFacetSource` |
@@ -796,15 +1705,21 @@ reconsider — they can change with their counterpart, not with the SDK.
796
1705
 
797
1706
  ## Compatibility
798
1707
 
799
- Clients and servers are compatible within the same **major** release-train
800
- version; evolution is additive within a major and both sides are tolerant
801
- readers. Official server builds expose `serverVersion` on `/healthz` and
802
- `/v1/config/client`. Full policy: `docs/architecture.md` §3.10.
1708
+ Clients and servers are compatible within the same **major** version of this
1709
+ SDK; evolution is additive within a major and both sides are tolerant readers.
1710
+ Official server builds expose `serverVersion` on `/healthz` and
1711
+ `/v1/config/client`. A route scheduled for removal answers with `Deprecation`
1712
+ and `Sunset` headers at least 90 days (and a major) ahead; the client reports
1713
+ each deprecated route once through the `onDeprecation` option (default: one
1714
+ `console.warn` per route; `false` silences it). Full policy:
1715
+ `docs/design/api-compatibility-policy.md`.
803
1716
 
804
1717
  ## Proxy through your own API
805
1718
 
806
- Keep your organization API key on your server and re-emit the stream to your
807
- own browser clients. The re-emitted wire format is identical to OpenGeni's SSE
1719
+ For the React conversation, use the packaged
1720
+ [`createSessionProxyHandler`](#embed-the-conversation-default). For a custom
1721
+ backend route, keep your organization API key on your server and re-emit the
1722
+ stream to your own browser clients. The re-emitted wire format is identical to Opengeni's SSE
808
1723
  stream, so the browser side can consume it with this same SDK (or a plain
809
1724
  `EventSource`), including resume via `?after=` / `Last-Event-ID`:
810
1725
 
@@ -812,7 +1727,10 @@ stream, so the browser side can consume it with this same SDK (or a plain
812
1727
  // Your server (Hono, Next.js route handler, Bun.serve, workers, ...):
813
1728
  import { OpenGeniClient, proxySessionEventStream } from "@opengeni/sdk";
814
1729
 
815
- const client = new OpenGeniClient({ baseUrl, apiKey });
1730
+ const client = new OpenGeniClient({
1731
+ baseUrl: process.env.OPENGENI_API_BASE_URL!,
1732
+ apiKey: process.env.OPENGENI_API_KEY!,
1733
+ });
816
1734
 
817
1735
  export function GET(request: Request): Response {
818
1736
  // authenticate *your* user, resolve their session id, then:
@@ -841,3 +1759,20 @@ Hosts, test doubles, and same-origin proxies must import
841
1759
  configuration or contract header. Do not copy its string value: the revision is
842
1760
  an executable compatibility boundary, and `getClientConfig()` intentionally
843
1761
  fails closed when server and SDK revisions differ.
1762
+
1763
+ ### Action review facts
1764
+
1765
+ `getToolActionReview(workspaceId, sessionId, approvalId)` returns version 1 facts
1766
+ for an exact prepared connector action: title, selected account/count, material
1767
+ effects, optional provider-sourced examples, bounded fields, reason, status and
1768
+ available actions. Use `sendApprovalDecision` for the existing authenticated
1769
+ human decision. Facts never grant execution authority.
1770
+
1771
+ `getToolReviewDetails(workspaceId, sessionId, approvalId, { actionDigest, path,
1772
+ offset })` pages through the saved selection/values (25 rows per page). The
1773
+ reference is tied to the original action digest and requires the same session
1774
+ visibility permission; it never re-runs a search. Secret/write-only fields stay
1775
+ hidden. A missing legacy snapshot may return 404; other authorization or transport
1776
+ failures must not be interpreted as permission to execute. Responses are private
1777
+ and non-cacheable. Review states distinguish waiting, execution, cancellation,
1778
+ failure and uncertain outcome. Exact stored arguments are never client-editable.