@oberik/sdk 0.2.0 → 0.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.
package/dist/cjs/index.js CHANGED
@@ -67,14 +67,66 @@ exports.AgentCancelledError = AgentCancelledError;
67
67
  // ============================================================================
68
68
  // Errors
69
69
  // ============================================================================
70
+ /** Does this look like a page rather than an answer? */
71
+ function looksLikeHtml(text) {
72
+ return /^\s*(<!doctype html|<html|<head|<body)/i.test(text) || /<\/html>\s*$/i.test(text);
73
+ }
74
+ /**
75
+ * One readable sentence out of whatever an error carried.
76
+ *
77
+ * `limits-and-errors.md` promises `{"detail": "…"}` and an `AgentApiError` with
78
+ * `.status` and `.detail`, and two shapes broke that badly enough to cost an afternoon:
79
+ *
80
+ * * **A 502/524 from the edge is an HTML page.** The whole ~4 KB of it went into
81
+ * `message` and `detail`, so logging one error printed ninety lines of
82
+ * `<div class="cf-...">`. There is nothing programmatic in that markup — the useful
83
+ * part is the title and the status — so it is summarised rather than preserved.
84
+ * * **A 422's detail is an array of validation objects**, so the documented
85
+ * `` `${e.detail}` `` prints `[object Object]`. The content was there and the
86
+ * documented shape gave you no reason to go looking for it.
87
+ */
88
+ function describeDetail(status, detail) {
89
+ if (typeof detail === "string") {
90
+ if (looksLikeHtml(detail)) {
91
+ const title = /<title[^>]*>([^<]{1,160})<\/title>/i.exec(detail)?.[1]?.trim();
92
+ const where = status >= 502 ? " — this came from a proxy in front of the API, not from the API" : "";
93
+ return `HTTP ${status}${title ? `: ${title}` : ""}${where}`;
94
+ }
95
+ return detail.slice(0, 2000) || `HTTP ${status}`;
96
+ }
97
+ if (Array.isArray(detail)) {
98
+ // FastAPI validation: [{loc: ["body","a","b"], msg, type}]
99
+ const lines = detail.map((d) => {
100
+ const item = d;
101
+ const loc = Array.isArray(item?.loc) ? item.loc.join(".") : undefined;
102
+ const msg = item?.msg ?? JSON.stringify(d);
103
+ return loc ? `${loc}: ${msg}` : String(msg);
104
+ });
105
+ return lines.join("; ").slice(0, 2000) || `HTTP ${status}`;
106
+ }
107
+ if (detail && typeof detail === "object") {
108
+ try {
109
+ return JSON.stringify(detail).slice(0, 2000);
110
+ }
111
+ catch {
112
+ return `HTTP ${status}`;
113
+ }
114
+ }
115
+ return `HTTP ${status}`;
116
+ }
70
117
  class AgentApiError extends Error {
71
118
  status;
119
+ /** The server's `detail`, in whatever shape it sent — except an HTML error page from
120
+ * an intermediary, which is replaced by the same summary as `message`. Keeping four
121
+ * kilobytes of someone else's markup here helped nobody and buried the status. */
72
122
  detail;
73
123
  constructor(status, detail) {
74
- super(typeof detail === "string" ? detail : `HTTP ${status}`);
124
+ const described = describeDetail(status, detail);
125
+ super(described);
75
126
  this.name = "AgentApiError";
76
127
  this.status = status;
77
- this.detail = detail;
128
+ this.detail =
129
+ typeof detail === "string" && looksLikeHtml(detail) ? described : detail;
78
130
  }
79
131
  }
80
132
  exports.AgentApiError = AgentApiError;
@@ -110,6 +162,75 @@ function uiToolSchemas(ui) {
110
162
  },
111
163
  }));
112
164
  }
165
+ /**
166
+ * Fold a later turn's payload into the running one.
167
+ *
168
+ * A pause is a new server turn. When the agent stops for a client tool, a question or an
169
+ * approval, resuming starts a *fresh* request, and the server builds its `done` from that
170
+ * turn alone — so the citations from the retrieval it did before the pause simply are not
171
+ * in the payload the loop finally returns.
172
+ *
173
+ * Which is what happened: an app that combined retrieval with its own tools — the app the
174
+ * docs tell you to build — got `citations: 0` and `sources: 0` on every turn where a tool
175
+ * ran, and an empty `ui` for anything drawn before the pause. Meanwhile both were streamed
176
+ * live and both rendered, so it worked on screen and vanished from the object. The docs
177
+ * promise the opposite in as many words: "the `done` payload carries the final state of
178
+ * everything above, so a client that ignored the incremental frames still ends up with the
179
+ * whole turn."
180
+ *
181
+ * Three kinds of field, and getting the kind wrong is its own bug:
182
+ * - accumulated: prose and lists the turn produced across all its segments
183
+ * - deduped: retrieval, which is very likely to repeat across segments
184
+ * - last-wins: what the *server* recomputes each time and is authoritative about
185
+ */
186
+ function mergeDone(prev, next) {
187
+ if (!prev)
188
+ return next;
189
+ const byKey = (items, key) => {
190
+ const seen = new Set();
191
+ const out = [];
192
+ for (const it of items) {
193
+ const k = key(it);
194
+ if (seen.has(k))
195
+ continue;
196
+ seen.add(k);
197
+ out.push(it);
198
+ }
199
+ return out;
200
+ };
201
+ return {
202
+ ...next,
203
+ // Accumulated: everything the agent said and drew, in order.
204
+ content: [prev.content, next.content].filter(Boolean).join(""),
205
+ // Claims accumulate, and the later segment's offsets shift by however much prose came
206
+ // before it. Getting this wrong points a footnote at the wrong sentence, which is worse
207
+ // than having no footnote.
208
+ claims: [
209
+ ...(prev.claims ?? []),
210
+ ...(next.claims ?? []).map((claim) => ({
211
+ ...claim,
212
+ start: claim.start == null ? null : claim.start + prev.content.length,
213
+ end: claim.end == null ? null : claim.end + prev.content.length,
214
+ })),
215
+ ],
216
+ attribution: prev.attribution === "per-claim" || next.attribution === "per-claim"
217
+ ? "per-claim"
218
+ : prev.attribution === "retrieval-only" || next.attribution === "retrieval-only"
219
+ ? "retrieval-only"
220
+ : "none",
221
+ reasoning: [prev.reasoning ?? "", next.reasoning ?? ""].filter(Boolean).join("") || undefined,
222
+ ui: [...(prev.ui ?? []), ...(next.ui ?? [])],
223
+ guard_flags: [...new Set([...(prev.guard_flags ?? []), ...(next.guard_flags ?? [])])],
224
+ // Deduped: the same chunk retrieved twice is one citation, and an attachment carried
225
+ // forward across a pause is one file.
226
+ citations: byKey([...(prev.citations ?? []), ...(next.citations ?? [])], (c) => `${c.document_id}:${c.chunk_index}`),
227
+ sources: byKey([...(prev.sources ?? []), ...(next.sources ?? [])], (s) => String(s.url ?? `${s.document_id}:${s.chunk_index ?? ""}`)),
228
+ attachments: byKey([...(prev.attachments ?? []), ...(next.attachments ?? [])], (a) => String(a.id ?? a.s3_key ?? a.filename)),
229
+ // Last-wins: the server recomputes these per turn and is right about them. `todos` is
230
+ // the whole current plan, not a delta; `subagents` is every subagent of the
231
+ // conversation; `context` describes the window as it is now.
232
+ };
233
+ }
113
234
  /** Run each pending client tool via its handler; failures become error results
114
235
  * (not thrown) so one bad tool doesn't abort the whole turn. */
115
236
  async function executeToolCalls(calls, tools) {
@@ -878,13 +999,34 @@ class AgentFramework {
878
999
  };
879
1000
  // -- tools ---------------------------------------------------------------
880
1001
  tools = {
1002
+ /** Every tool this token would actually be handed on its next request.
1003
+ *
1004
+ * `mcp_servers` is present when the project has any, and answers the question the
1005
+ * listing alone cannot: "still connecting", "handshake failed" and "this server has
1006
+ * no tools" all look identical as an absence of rows. Each entry says whether the
1007
+ * server answered, how many tools it contributed, and why not. */
881
1008
  list: () => this.request("GET", "/tools"),
882
1009
  };
883
1010
  // -- chat ----------------------------------------------------------------
884
1011
  chat = {
885
1012
  /** One round-trip. Returns `requires_action` + `tool_calls` for you to handle
886
- * manually — use `chat.run` to auto-dispatch client tools instead. */
887
- send: (body) => this.request("POST", "/chat", { body }),
1013
+ * manually — use `chat.run` to auto-dispatch client tools instead.
1014
+ *
1015
+ * Client *tools* stay manual here; that is the whole difference from `run`. UI
1016
+ * components do not, because there is no manual handling of one: the registry
1017
+ * passed to `createClient({ ui: [...] })` IS the handling, and `chat.stream`
1018
+ * already both declares it and draws from it. A blocking client that registered a
1019
+ * chart renderer got an empty `ui` and no error — the declarations were never sent,
1020
+ * so the agent was not offered the component at all; and when they were repeated by
1021
+ * hand as `ui_tools`, the renderer still never ran. The one wiring `tools.md`
1022
+ * documents produced nothing on the one call it documents it with. */
1023
+ send: async (body) => {
1024
+ const ui = this.resolveUi();
1025
+ const withUi = ui.size && body.ui_tools == null ? { ...body, ui_tools: uiToolSchemas(ui) } : body;
1026
+ const resp = await this.request("POST", "/chat", { body: withUi });
1027
+ this.renderUi(resp.ui, ui);
1028
+ return resp;
1029
+ },
888
1030
  /** Blocking chat that AUTO-EXECUTES registered client tools: it sends the
889
1031
  * message, and whenever the agent asks for client tools it runs their
890
1032
  * handlers, submits the results, and repeats until the agent is done. */
@@ -939,7 +1081,15 @@ class AgentFramework {
939
1081
  body: { session_id: sessionId, question_answers: answers },
940
1082
  }),
941
1083
  sessions: {
942
- list: (userRef) => this.request("GET", "/chat/sessions", { query: { user_ref: userRef } }),
1084
+ /** Conversations this token may see, newest first.
1085
+ *
1086
+ * Paged: `limit` defaults to 100 and is capped at 500. This used to return every
1087
+ * session in one unbounded response — fine for one end-user, and an admin token
1088
+ * on a busy project gets the whole tenant's history to render a sidebar showing
1089
+ * twenty. Page with `offset`. */
1090
+ list: (userRef, opts = {}) => this.request("GET", "/chat/sessions", {
1091
+ query: { user_ref: userRef, limit: opts.limit, offset: opts.offset },
1092
+ }),
943
1093
  /** The agent's plan for this conversation — what a UI renders on a page load,
944
1094
  * or between turns. A turn that touched the list also returns it directly. */
945
1095
  todos: (sessionId) => this.request("GET", `/chat/sessions/${sessionId}/todos`),
@@ -1213,6 +1363,35 @@ class AgentFramework {
1213
1363
  get: (id) => this.request("GET", `/tasks/${id}`),
1214
1364
  cancel: (id) => this.request("POST", `/tasks/${id}/cancel`),
1215
1365
  };
1366
+ /**
1367
+ * Turns that start because something happened somewhere else.
1368
+ *
1369
+ * A trigger is a URL you give another system — a ticket tracker, a CI job, a Zapier step
1370
+ * — and the path is the credential, so it runs as a fixed subject chosen at creation.
1371
+ * These were the only routes in the API reference with no SDK method: every integration
1372
+ * hand-wrote `fetch` for them.
1373
+ *
1374
+ * The URL comes back absolute and is not a secret we can show once: the whole point is
1375
+ * that someone else's configuration holds it. `secret` IS shown once — with it, the
1376
+ * sender signs the body and the URL stops being a bearer token.
1377
+ */
1378
+ triggers = {
1379
+ list: () => this.request("GET", "/triggers"),
1380
+ /** `prompt` may interpolate the event: `"A ticket arrived: {{ body.title }}"`. */
1381
+ create: (opts) => this.request("POST", "/triggers", {
1382
+ body: {
1383
+ prompt: opts.prompt,
1384
+ name: opts.name,
1385
+ system_prompt: opts.systemPrompt,
1386
+ session_id: opts.sessionId,
1387
+ signed: opts.signed ?? false,
1388
+ },
1389
+ }),
1390
+ delete: (triggerId) => this.request("DELETE", `/triggers/${triggerId}`),
1391
+ /** A new URL, with the old one alive for 24 hours — so telling the other system its new
1392
+ * address is not an outage. */
1393
+ rotate: (triggerId) => this.request("POST", `/triggers/${triggerId}/rotate`),
1394
+ };
1216
1395
  /**
1217
1396
  * What the agent wrote down — remembered facts and wiki pages.
1218
1397
  *
@@ -1306,6 +1485,10 @@ class AgentFramework {
1306
1485
  signal: opts.signal,
1307
1486
  });
1308
1487
  this.renderUi(resp.ui, ui);
1488
+ // What the caller gets back: every segment folded together. Each pause starts a new
1489
+ // server turn, so the last segment's payload knows nothing about the retrieval that
1490
+ // happened before it.
1491
+ let merged = resp;
1309
1492
  let rounds = 0;
1310
1493
  // Three ways a turn pauses: work for the client to run, a question for the user to
1311
1494
  // answer, or permission to ask for. All resume the same way, and the round budget
@@ -1315,7 +1498,7 @@ class AgentFramework {
1315
1498
  const hasQuestions = onQuestion != null && (resp.questions?.length ?? 0) > 0;
1316
1499
  const hasApprovals = onApproval != null && (resp.approvals?.length ?? 0) > 0;
1317
1500
  if (!hasTools && !hasQuestions && !hasApprovals)
1318
- return resp;
1501
+ return merged;
1319
1502
  if (rounds++ >= maxRounds)
1320
1503
  throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
1321
1504
  const next = {
@@ -1331,6 +1514,7 @@ class AgentFramework {
1331
1514
  next.approval_decisions = await collectDecisions(resp.approvals, onApproval);
1332
1515
  resp = await this.request("POST", "/chat", { body: next, signal: opts.signal });
1333
1516
  this.renderUi(resp.ui, ui);
1517
+ merged = mergeDone(merged, resp);
1334
1518
  }
1335
1519
  }
1336
1520
  /** Wrap startStream so a `done` carrying `requires_action` auto-executes the
@@ -1349,17 +1533,19 @@ class AgentFramework {
1349
1533
  const done = (async () => {
1350
1534
  const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
1351
1535
  let reqBody = { ...body, client_tools: schemas, ui_tools: uiSchemas };
1536
+ let merged;
1352
1537
  let rounds = 0;
1353
1538
  for (;;) {
1354
1539
  current = this.startStream(reqBody, handlers);
1355
1540
  const d = await current.done;
1541
+ merged = mergeDone(merged, d);
1356
1542
  const hasTools = d.requires_action && d.tool_calls.length > 0;
1357
1543
  const hasQuestions = onQuestion != null && (d.questions?.length ?? 0) > 0;
1358
1544
  const hasApprovals = onApproval != null && (d.approvals?.length ?? 0) > 0;
1359
1545
  if (!hasTools && !hasQuestions && !hasApprovals)
1360
- return d;
1546
+ return merged;
1361
1547
  if (stopped)
1362
- return d;
1548
+ return merged;
1363
1549
  if (rounds++ >= maxRounds)
1364
1550
  throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
1365
1551
  reqBody = { session_id: d.session_id, client_tools: schemas, ui_tools: uiSchemas };
@@ -1507,6 +1693,19 @@ class AgentFramework {
1507
1693
  const onOuterAbort = () => ac.abort();
1508
1694
  handlers.signal?.addEventListener("abort", onOuterAbort);
1509
1695
  const maxRetries = handlers.maxRetries ?? 10;
1696
+ // Did WE stop this, or did the connection?
1697
+ //
1698
+ // `cancel()` posts the cancel and then aborts the reader, and the abort always won
1699
+ // the race against the server's `cancelled` frame — so the documented way to tell
1700
+ // "the user pressed stop" from "the network died" never fired, and every cancel
1701
+ // surfaced as the transport's raw `AbortError`, which reads as a crash. The server
1702
+ // event still throws the same error for the case where cancellation arrives from
1703
+ // somewhere else (another tab, the dashboard).
1704
+ //
1705
+ // `disconnect()` and an aborted `handlers.signal` deliberately do NOT set this: the
1706
+ // run carries on server-side, so calling it cancelled would be a lie.
1707
+ let cancelled = false;
1708
+ const stopReason = () => cancelled ? new AgentCancelledError() : new DOMException("aborted", "AbortError");
1510
1709
  // The components this stream may draw. Resolved once: the registry can be added to
1511
1710
  // between turns, and a stream should draw with what it was started with.
1512
1711
  const uiRegistry = handlers.ui || this.uiRegistry.size ? this.resolveUi(handlers.ui) : undefined;
@@ -1539,7 +1738,7 @@ class AgentFramework {
1539
1738
  let refreshed = false; // one token refresh per connection, reset on progress
1540
1739
  for (;;) {
1541
1740
  if (ac.signal.aborted)
1542
- throw new DOMException("aborted", "AbortError");
1741
+ throw stopReason();
1543
1742
  let shouldRetry = false;
1544
1743
  try {
1545
1744
  let res = await openConnection();
@@ -1645,7 +1844,7 @@ class AgentFramework {
1645
1844
  }
1646
1845
  catch (err) {
1647
1846
  if (ac.signal.aborted)
1648
- throw new DOMException("aborted", "AbortError");
1847
+ throw stopReason();
1649
1848
  // Terminal errors: server 'error'/'cancelled' event, or an HTTP failure.
1650
1849
  if (err instanceof AgentApiError ||
1651
1850
  err instanceof AgentCancelledError ||
@@ -1665,6 +1864,10 @@ class AgentFramework {
1665
1864
  }
1666
1865
  })().finally(() => handlers.signal?.removeEventListener("abort", onOuterAbort));
1667
1866
  const cancel = async () => {
1867
+ // Set BEFORE the abort, and before the network call: the caller may already be
1868
+ // awaiting `done`, and whichever of the two finishes first must report a
1869
+ // cancellation rather than a transport error.
1870
+ cancelled = true;
1668
1871
  const id = runId;
1669
1872
  if (id) {
1670
1873
  try {
@@ -1979,6 +2182,18 @@ class OberikProject {
1979
2182
  };
1980
2183
  /** Documents owned by the project rather than by any one end-user: the corpus you
1981
2184
  * curate and your users only read. */
2185
+ /**
2186
+ * The corpus you curate.
2187
+ *
2188
+ * Uploads here are stored `tenant`-visible — readable by every end-user of the project —
2189
+ * which is what "a corpus you curate, that users only read" means. That is the default
2190
+ * and the only option, deliberately: a project key is not a person, so there is no
2191
+ * per-user subtree for it to write into. Per-user documents go through the data-plane
2192
+ * client with an end-user token, where the subject IS the owner.
2193
+ *
2194
+ * The docs used to show `visibility: "tenant"` being passed here, which was neither
2195
+ * accepted nor needed — a recipe that worked by luck rather than by expression.
2196
+ */
1982
2197
  documents = {
1983
2198
  list: () => this.request("GET", "/documents"),
1984
2199
  upload: (file, opts = {}) => {
@@ -1989,6 +2204,95 @@ class OberikProject {
1989
2204
  },
1990
2205
  delete: (documentId) => this.request("DELETE", `/documents/${documentId}`),
1991
2206
  };
2207
+ /**
2208
+ * The models this project runs on, and the retrieval it uses.
2209
+ *
2210
+ * These had no methods at all: `project-api.md` documents them in a table and every
2211
+ * integration hand-rolled `fetch` for half its setup. `providers.add` is the one that
2212
+ * matters most — it is the step a new project cannot answer a question without, and it
2213
+ * finishes the rest of the setup itself (see `derived` in the response).
2214
+ *
2215
+ * `providers.catalog` is deliberately absent: it is not project-scoped, so it does not
2216
+ * belong on a client whose every path hangs off one project.
2217
+ */
2218
+ providers = {
2219
+ list: () => this.request("GET", "/providers"),
2220
+ /** Name a chat model AND an embedding model: the first lets the agent answer, the
2221
+ * second lets it index. The response's `derived` says what was set for you. */
2222
+ add: (opts) => this.request("POST", "/providers", opts),
2223
+ edit: (credId, opts) => this.request("PATCH", `/providers/${credId}`, opts),
2224
+ /** Re-read the provider's catalog: a model registered before its price was published
2225
+ * bills nothing, so the usage cap never trips. */
2226
+ refresh: (credId) => this.request("POST", `/providers/${credId}/refresh`, {}),
2227
+ remove: (credId) => this.request("DELETE", `/providers/${credId}`),
2228
+ };
2229
+ /** The model used when a request does not name one. */
2230
+ defaultModel = {
2231
+ set: (model) => this.request("POST", "/default-model", { model }),
2232
+ };
2233
+ /** Embedding and rerank overrides. Set for you when you register an embedding model, so
2234
+ * this is for changing it rather than for getting started. */
2235
+ retrieval = {
2236
+ set: (opts) => this.request("PUT", "/retrieval", opts),
2237
+ /** How many floats a model returns, measured by embedding one word. No provider
2238
+ * publishes it, and a wrong one fails at the first ingest rather than here. */
2239
+ probe: (model) => this.request("POST", "/retrieval/probe", { model }),
2240
+ };
2241
+ /** How documents are read: the built-in parser, or a vision model you choose. */
2242
+ documentProcessor = {
2243
+ set: (opts) => this.request("PUT", "/document-processor", opts),
2244
+ };
2245
+ /** What happens when a conversation outgrows the model's window. */
2246
+ context = {
2247
+ get: () => this.request("GET", "/context"),
2248
+ set: (opts) => this.request("PUT", "/context", opts),
2249
+ };
2250
+ /** Spend and rate caps on this project's LLM key. Enforced by the biller, so they hold
2251
+ * even when the usage views cannot be read. */
2252
+ limits = {
2253
+ get: () => this.request("GET", "/limits"),
2254
+ set: (opts) => this.request("PUT", "/limits", opts),
2255
+ };
2256
+ /** Which models a delegate may run on, and how many may run at once. Without this the
2257
+ * subagents capability stays unavailable however it is granted. */
2258
+ subagents = {
2259
+ set: (opts) => this.request("PUT", "/subagents", opts),
2260
+ };
2261
+ /** Procedures you publish as Agent Plugins, and what your end-users have added. */
2262
+ skills = {
2263
+ list: () => this.request("GET", "/skills"),
2264
+ upload: (zip, filename) => {
2265
+ const form = new FormData();
2266
+ form.append("file", zip, filename ?? zip.name ?? "skill.zip");
2267
+ return this.request("POST", "/skills", undefined, form);
2268
+ },
2269
+ delete: (pluginId) => this.request("DELETE", `/skills/${pluginId}`),
2270
+ };
2271
+ /** MCP servers whose tools join this project's catalog. */
2272
+ mcp = {
2273
+ list: () => this.request("GET", "/mcp"),
2274
+ add: (opts) => this.request("POST", "/mcp", opts),
2275
+ remove: (mcpId) => this.request("DELETE", `/mcp/${mcpId}`),
2276
+ };
2277
+ /** Conversations, and what was said in them. */
2278
+ sessions = {
2279
+ list: () => this.request("GET", "/sessions"),
2280
+ messages: (sessionId) => this.request("GET", `/sessions/${sessionId}/messages`),
2281
+ };
2282
+ /** Scheduled work this project's end-users have created. */
2283
+ tasks = {
2284
+ list: () => this.request("GET", "/tasks"),
2285
+ };
2286
+ /** What the agent has written down: remembered facts and wiki pages. */
2287
+ wiki = {
2288
+ list: () => this.request("GET", "/wiki"),
2289
+ delete: (itemId) => this.request("DELETE", `/wiki/${itemId}`),
2290
+ };
2291
+ /** Live sandboxes, and what to do about one. */
2292
+ sandboxes = {
2293
+ list: () => this.request("GET", "/sandboxes"),
2294
+ action: (sessionId, action) => this.request("POST", `/sandboxes/${sessionId}/${action}`, {}),
2295
+ };
1992
2296
  /** Prepended to every request for this project, above anything a caller sends. */
1993
2297
  systemPrompt = {
1994
2298
  set: (systemPrompt) => this.request("PUT", "/system-prompt", { systemPrompt }),
@@ -2004,6 +2308,39 @@ class OberikProject {
2004
2308
  delete: (toolId) => this.request("DELETE", `/webhook-tools/${toolId}`),
2005
2309
  };
2006
2310
  /** Server-side keys. A created one is returned once and never again. */
2311
+ /**
2312
+ * Checks on what goes into the model and what comes back.
2313
+ *
2314
+ * The enforcement has existed for a long time and there was no way to configure it — no
2315
+ * route, no dashboard section, no column — so a documentation page described switches
2316
+ * that could not be reached. `set` takes a partial: what you do not mention is left as it
2317
+ * is.
2318
+ */
2319
+ guardrails = {
2320
+ get: () => this.request("GET", "/guardrails"),
2321
+ // `request(method, path, body, form)` — the payload is the THIRD positional
2322
+ // argument. Wrapping it as `{ body: policy }` sent `{"body":{…}}`, every field of
2323
+ // which is optional, so the route validated it, applied nothing, and answered with
2324
+ // the unchanged policy — which reads exactly like success. Worse, `onViolation`
2325
+ // carries a non-null default, so it WAS written: a project running `flag` that
2326
+ // called `set` to turn groundedness on started refusing answers instead, silently.
2327
+ set: (policy) => this.request("PUT", "/guardrails", policy),
2328
+ };
2329
+ /**
2330
+ * Whether this project can actually answer a question yet.
2331
+ *
2332
+ * A new project has no models, so it can neither answer nor index anything — and the
2333
+ * flag that used to be the closest thing to this (`hasLlm`) meant "a LiteLLM key was
2334
+ * provisioned", which is true from the moment a project exists. Every unfinished step
2335
+ * names what it blocks and the one call that fixes it.
2336
+ *
2337
+ * Worth calling in a deploy check: a project that is not ready fails every request with
2338
+ * the provider's own error, which reads as your bug rather than as missing setup.
2339
+ */
2340
+ readiness = () => this.request("GET", "/readiness");
2341
+ /** The starting snippet and this project's endpoints — the same one the dashboard and the
2342
+ * SSH gateway show, so there is one of it rather than three. */
2343
+ connect = () => this.request("GET", "/connect");
2007
2344
  /**
2008
2345
  * Further project keys.
2009
2346
  *