@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/esm/index.js CHANGED
@@ -61,14 +61,66 @@ export class AgentCancelledError extends Error {
61
61
  // ============================================================================
62
62
  // Errors
63
63
  // ============================================================================
64
+ /** Does this look like a page rather than an answer? */
65
+ function looksLikeHtml(text) {
66
+ return /^\s*(<!doctype html|<html|<head|<body)/i.test(text) || /<\/html>\s*$/i.test(text);
67
+ }
68
+ /**
69
+ * One readable sentence out of whatever an error carried.
70
+ *
71
+ * `limits-and-errors.md` promises `{"detail": "…"}` and an `AgentApiError` with
72
+ * `.status` and `.detail`, and two shapes broke that badly enough to cost an afternoon:
73
+ *
74
+ * * **A 502/524 from the edge is an HTML page.** The whole ~4 KB of it went into
75
+ * `message` and `detail`, so logging one error printed ninety lines of
76
+ * `<div class="cf-...">`. There is nothing programmatic in that markup — the useful
77
+ * part is the title and the status — so it is summarised rather than preserved.
78
+ * * **A 422's detail is an array of validation objects**, so the documented
79
+ * `` `${e.detail}` `` prints `[object Object]`. The content was there and the
80
+ * documented shape gave you no reason to go looking for it.
81
+ */
82
+ function describeDetail(status, detail) {
83
+ if (typeof detail === "string") {
84
+ if (looksLikeHtml(detail)) {
85
+ const title = /<title[^>]*>([^<]{1,160})<\/title>/i.exec(detail)?.[1]?.trim();
86
+ const where = status >= 502 ? " — this came from a proxy in front of the API, not from the API" : "";
87
+ return `HTTP ${status}${title ? `: ${title}` : ""}${where}`;
88
+ }
89
+ return detail.slice(0, 2000) || `HTTP ${status}`;
90
+ }
91
+ if (Array.isArray(detail)) {
92
+ // FastAPI validation: [{loc: ["body","a","b"], msg, type}]
93
+ const lines = detail.map((d) => {
94
+ const item = d;
95
+ const loc = Array.isArray(item?.loc) ? item.loc.join(".") : undefined;
96
+ const msg = item?.msg ?? JSON.stringify(d);
97
+ return loc ? `${loc}: ${msg}` : String(msg);
98
+ });
99
+ return lines.join("; ").slice(0, 2000) || `HTTP ${status}`;
100
+ }
101
+ if (detail && typeof detail === "object") {
102
+ try {
103
+ return JSON.stringify(detail).slice(0, 2000);
104
+ }
105
+ catch {
106
+ return `HTTP ${status}`;
107
+ }
108
+ }
109
+ return `HTTP ${status}`;
110
+ }
64
111
  export class AgentApiError extends Error {
65
112
  status;
113
+ /** The server's `detail`, in whatever shape it sent — except an HTML error page from
114
+ * an intermediary, which is replaced by the same summary as `message`. Keeping four
115
+ * kilobytes of someone else's markup here helped nobody and buried the status. */
66
116
  detail;
67
117
  constructor(status, detail) {
68
- super(typeof detail === "string" ? detail : `HTTP ${status}`);
118
+ const described = describeDetail(status, detail);
119
+ super(described);
69
120
  this.name = "AgentApiError";
70
121
  this.status = status;
71
- this.detail = detail;
122
+ this.detail =
123
+ typeof detail === "string" && looksLikeHtml(detail) ? described : detail;
72
124
  }
73
125
  }
74
126
  export class AgentStreamError extends Error {
@@ -102,6 +154,75 @@ function uiToolSchemas(ui) {
102
154
  },
103
155
  }));
104
156
  }
157
+ /**
158
+ * Fold a later turn's payload into the running one.
159
+ *
160
+ * A pause is a new server turn. When the agent stops for a client tool, a question or an
161
+ * approval, resuming starts a *fresh* request, and the server builds its `done` from that
162
+ * turn alone — so the citations from the retrieval it did before the pause simply are not
163
+ * in the payload the loop finally returns.
164
+ *
165
+ * Which is what happened: an app that combined retrieval with its own tools — the app the
166
+ * docs tell you to build — got `citations: 0` and `sources: 0` on every turn where a tool
167
+ * ran, and an empty `ui` for anything drawn before the pause. Meanwhile both were streamed
168
+ * live and both rendered, so it worked on screen and vanished from the object. The docs
169
+ * promise the opposite in as many words: "the `done` payload carries the final state of
170
+ * everything above, so a client that ignored the incremental frames still ends up with the
171
+ * whole turn."
172
+ *
173
+ * Three kinds of field, and getting the kind wrong is its own bug:
174
+ * - accumulated: prose and lists the turn produced across all its segments
175
+ * - deduped: retrieval, which is very likely to repeat across segments
176
+ * - last-wins: what the *server* recomputes each time and is authoritative about
177
+ */
178
+ function mergeDone(prev, next) {
179
+ if (!prev)
180
+ return next;
181
+ const byKey = (items, key) => {
182
+ const seen = new Set();
183
+ const out = [];
184
+ for (const it of items) {
185
+ const k = key(it);
186
+ if (seen.has(k))
187
+ continue;
188
+ seen.add(k);
189
+ out.push(it);
190
+ }
191
+ return out;
192
+ };
193
+ return {
194
+ ...next,
195
+ // Accumulated: everything the agent said and drew, in order.
196
+ content: [prev.content, next.content].filter(Boolean).join(""),
197
+ // Claims accumulate, and the later segment's offsets shift by however much prose came
198
+ // before it. Getting this wrong points a footnote at the wrong sentence, which is worse
199
+ // than having no footnote.
200
+ claims: [
201
+ ...(prev.claims ?? []),
202
+ ...(next.claims ?? []).map((claim) => ({
203
+ ...claim,
204
+ start: claim.start == null ? null : claim.start + prev.content.length,
205
+ end: claim.end == null ? null : claim.end + prev.content.length,
206
+ })),
207
+ ],
208
+ attribution: prev.attribution === "per-claim" || next.attribution === "per-claim"
209
+ ? "per-claim"
210
+ : prev.attribution === "retrieval-only" || next.attribution === "retrieval-only"
211
+ ? "retrieval-only"
212
+ : "none",
213
+ reasoning: [prev.reasoning ?? "", next.reasoning ?? ""].filter(Boolean).join("") || undefined,
214
+ ui: [...(prev.ui ?? []), ...(next.ui ?? [])],
215
+ guard_flags: [...new Set([...(prev.guard_flags ?? []), ...(next.guard_flags ?? [])])],
216
+ // Deduped: the same chunk retrieved twice is one citation, and an attachment carried
217
+ // forward across a pause is one file.
218
+ citations: byKey([...(prev.citations ?? []), ...(next.citations ?? [])], (c) => `${c.document_id}:${c.chunk_index}`),
219
+ sources: byKey([...(prev.sources ?? []), ...(next.sources ?? [])], (s) => String(s.url ?? `${s.document_id}:${s.chunk_index ?? ""}`)),
220
+ attachments: byKey([...(prev.attachments ?? []), ...(next.attachments ?? [])], (a) => String(a.id ?? a.s3_key ?? a.filename)),
221
+ // Last-wins: the server recomputes these per turn and is right about them. `todos` is
222
+ // the whole current plan, not a delta; `subagents` is every subagent of the
223
+ // conversation; `context` describes the window as it is now.
224
+ };
225
+ }
105
226
  /** Run each pending client tool via its handler; failures become error results
106
227
  * (not thrown) so one bad tool doesn't abort the whole turn. */
107
228
  async function executeToolCalls(calls, tools) {
@@ -870,13 +991,34 @@ export class AgentFramework {
870
991
  };
871
992
  // -- tools ---------------------------------------------------------------
872
993
  tools = {
994
+ /** Every tool this token would actually be handed on its next request.
995
+ *
996
+ * `mcp_servers` is present when the project has any, and answers the question the
997
+ * listing alone cannot: "still connecting", "handshake failed" and "this server has
998
+ * no tools" all look identical as an absence of rows. Each entry says whether the
999
+ * server answered, how many tools it contributed, and why not. */
873
1000
  list: () => this.request("GET", "/tools"),
874
1001
  };
875
1002
  // -- chat ----------------------------------------------------------------
876
1003
  chat = {
877
1004
  /** One round-trip. Returns `requires_action` + `tool_calls` for you to handle
878
- * manually — use `chat.run` to auto-dispatch client tools instead. */
879
- send: (body) => this.request("POST", "/chat", { body }),
1005
+ * manually — use `chat.run` to auto-dispatch client tools instead.
1006
+ *
1007
+ * Client *tools* stay manual here; that is the whole difference from `run`. UI
1008
+ * components do not, because there is no manual handling of one: the registry
1009
+ * passed to `createClient({ ui: [...] })` IS the handling, and `chat.stream`
1010
+ * already both declares it and draws from it. A blocking client that registered a
1011
+ * chart renderer got an empty `ui` and no error — the declarations were never sent,
1012
+ * so the agent was not offered the component at all; and when they were repeated by
1013
+ * hand as `ui_tools`, the renderer still never ran. The one wiring `tools.md`
1014
+ * documents produced nothing on the one call it documents it with. */
1015
+ send: async (body) => {
1016
+ const ui = this.resolveUi();
1017
+ const withUi = ui.size && body.ui_tools == null ? { ...body, ui_tools: uiToolSchemas(ui) } : body;
1018
+ const resp = await this.request("POST", "/chat", { body: withUi });
1019
+ this.renderUi(resp.ui, ui);
1020
+ return resp;
1021
+ },
880
1022
  /** Blocking chat that AUTO-EXECUTES registered client tools: it sends the
881
1023
  * message, and whenever the agent asks for client tools it runs their
882
1024
  * handlers, submits the results, and repeats until the agent is done. */
@@ -931,7 +1073,15 @@ export class AgentFramework {
931
1073
  body: { session_id: sessionId, question_answers: answers },
932
1074
  }),
933
1075
  sessions: {
934
- list: (userRef) => this.request("GET", "/chat/sessions", { query: { user_ref: userRef } }),
1076
+ /** Conversations this token may see, newest first.
1077
+ *
1078
+ * Paged: `limit` defaults to 100 and is capped at 500. This used to return every
1079
+ * session in one unbounded response — fine for one end-user, and an admin token
1080
+ * on a busy project gets the whole tenant's history to render a sidebar showing
1081
+ * twenty. Page with `offset`. */
1082
+ list: (userRef, opts = {}) => this.request("GET", "/chat/sessions", {
1083
+ query: { user_ref: userRef, limit: opts.limit, offset: opts.offset },
1084
+ }),
935
1085
  /** The agent's plan for this conversation — what a UI renders on a page load,
936
1086
  * or between turns. A turn that touched the list also returns it directly. */
937
1087
  todos: (sessionId) => this.request("GET", `/chat/sessions/${sessionId}/todos`),
@@ -1205,6 +1355,35 @@ export class AgentFramework {
1205
1355
  get: (id) => this.request("GET", `/tasks/${id}`),
1206
1356
  cancel: (id) => this.request("POST", `/tasks/${id}/cancel`),
1207
1357
  };
1358
+ /**
1359
+ * Turns that start because something happened somewhere else.
1360
+ *
1361
+ * A trigger is a URL you give another system — a ticket tracker, a CI job, a Zapier step
1362
+ * — and the path is the credential, so it runs as a fixed subject chosen at creation.
1363
+ * These were the only routes in the API reference with no SDK method: every integration
1364
+ * hand-wrote `fetch` for them.
1365
+ *
1366
+ * The URL comes back absolute and is not a secret we can show once: the whole point is
1367
+ * that someone else's configuration holds it. `secret` IS shown once — with it, the
1368
+ * sender signs the body and the URL stops being a bearer token.
1369
+ */
1370
+ triggers = {
1371
+ list: () => this.request("GET", "/triggers"),
1372
+ /** `prompt` may interpolate the event: `"A ticket arrived: {{ body.title }}"`. */
1373
+ create: (opts) => this.request("POST", "/triggers", {
1374
+ body: {
1375
+ prompt: opts.prompt,
1376
+ name: opts.name,
1377
+ system_prompt: opts.systemPrompt,
1378
+ session_id: opts.sessionId,
1379
+ signed: opts.signed ?? false,
1380
+ },
1381
+ }),
1382
+ delete: (triggerId) => this.request("DELETE", `/triggers/${triggerId}`),
1383
+ /** A new URL, with the old one alive for 24 hours — so telling the other system its new
1384
+ * address is not an outage. */
1385
+ rotate: (triggerId) => this.request("POST", `/triggers/${triggerId}/rotate`),
1386
+ };
1208
1387
  /**
1209
1388
  * What the agent wrote down — remembered facts and wiki pages.
1210
1389
  *
@@ -1298,6 +1477,10 @@ export class AgentFramework {
1298
1477
  signal: opts.signal,
1299
1478
  });
1300
1479
  this.renderUi(resp.ui, ui);
1480
+ // What the caller gets back: every segment folded together. Each pause starts a new
1481
+ // server turn, so the last segment's payload knows nothing about the retrieval that
1482
+ // happened before it.
1483
+ let merged = resp;
1301
1484
  let rounds = 0;
1302
1485
  // Three ways a turn pauses: work for the client to run, a question for the user to
1303
1486
  // answer, or permission to ask for. All resume the same way, and the round budget
@@ -1307,7 +1490,7 @@ export class AgentFramework {
1307
1490
  const hasQuestions = onQuestion != null && (resp.questions?.length ?? 0) > 0;
1308
1491
  const hasApprovals = onApproval != null && (resp.approvals?.length ?? 0) > 0;
1309
1492
  if (!hasTools && !hasQuestions && !hasApprovals)
1310
- return resp;
1493
+ return merged;
1311
1494
  if (rounds++ >= maxRounds)
1312
1495
  throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
1313
1496
  const next = {
@@ -1323,6 +1506,7 @@ export class AgentFramework {
1323
1506
  next.approval_decisions = await collectDecisions(resp.approvals, onApproval);
1324
1507
  resp = await this.request("POST", "/chat", { body: next, signal: opts.signal });
1325
1508
  this.renderUi(resp.ui, ui);
1509
+ merged = mergeDone(merged, resp);
1326
1510
  }
1327
1511
  }
1328
1512
  /** Wrap startStream so a `done` carrying `requires_action` auto-executes the
@@ -1341,17 +1525,19 @@ export class AgentFramework {
1341
1525
  const done = (async () => {
1342
1526
  const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
1343
1527
  let reqBody = { ...body, client_tools: schemas, ui_tools: uiSchemas };
1528
+ let merged;
1344
1529
  let rounds = 0;
1345
1530
  for (;;) {
1346
1531
  current = this.startStream(reqBody, handlers);
1347
1532
  const d = await current.done;
1533
+ merged = mergeDone(merged, d);
1348
1534
  const hasTools = d.requires_action && d.tool_calls.length > 0;
1349
1535
  const hasQuestions = onQuestion != null && (d.questions?.length ?? 0) > 0;
1350
1536
  const hasApprovals = onApproval != null && (d.approvals?.length ?? 0) > 0;
1351
1537
  if (!hasTools && !hasQuestions && !hasApprovals)
1352
- return d;
1538
+ return merged;
1353
1539
  if (stopped)
1354
- return d;
1540
+ return merged;
1355
1541
  if (rounds++ >= maxRounds)
1356
1542
  throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
1357
1543
  reqBody = { session_id: d.session_id, client_tools: schemas, ui_tools: uiSchemas };
@@ -1499,6 +1685,19 @@ export class AgentFramework {
1499
1685
  const onOuterAbort = () => ac.abort();
1500
1686
  handlers.signal?.addEventListener("abort", onOuterAbort);
1501
1687
  const maxRetries = handlers.maxRetries ?? 10;
1688
+ // Did WE stop this, or did the connection?
1689
+ //
1690
+ // `cancel()` posts the cancel and then aborts the reader, and the abort always won
1691
+ // the race against the server's `cancelled` frame — so the documented way to tell
1692
+ // "the user pressed stop" from "the network died" never fired, and every cancel
1693
+ // surfaced as the transport's raw `AbortError`, which reads as a crash. The server
1694
+ // event still throws the same error for the case where cancellation arrives from
1695
+ // somewhere else (another tab, the dashboard).
1696
+ //
1697
+ // `disconnect()` and an aborted `handlers.signal` deliberately do NOT set this: the
1698
+ // run carries on server-side, so calling it cancelled would be a lie.
1699
+ let cancelled = false;
1700
+ const stopReason = () => cancelled ? new AgentCancelledError() : new DOMException("aborted", "AbortError");
1502
1701
  // The components this stream may draw. Resolved once: the registry can be added to
1503
1702
  // between turns, and a stream should draw with what it was started with.
1504
1703
  const uiRegistry = handlers.ui || this.uiRegistry.size ? this.resolveUi(handlers.ui) : undefined;
@@ -1531,7 +1730,7 @@ export class AgentFramework {
1531
1730
  let refreshed = false; // one token refresh per connection, reset on progress
1532
1731
  for (;;) {
1533
1732
  if (ac.signal.aborted)
1534
- throw new DOMException("aborted", "AbortError");
1733
+ throw stopReason();
1535
1734
  let shouldRetry = false;
1536
1735
  try {
1537
1736
  let res = await openConnection();
@@ -1637,7 +1836,7 @@ export class AgentFramework {
1637
1836
  }
1638
1837
  catch (err) {
1639
1838
  if (ac.signal.aborted)
1640
- throw new DOMException("aborted", "AbortError");
1839
+ throw stopReason();
1641
1840
  // Terminal errors: server 'error'/'cancelled' event, or an HTTP failure.
1642
1841
  if (err instanceof AgentApiError ||
1643
1842
  err instanceof AgentCancelledError ||
@@ -1657,6 +1856,10 @@ export class AgentFramework {
1657
1856
  }
1658
1857
  })().finally(() => handlers.signal?.removeEventListener("abort", onOuterAbort));
1659
1858
  const cancel = async () => {
1859
+ // Set BEFORE the abort, and before the network call: the caller may already be
1860
+ // awaiting `done`, and whichever of the two finishes first must report a
1861
+ // cancellation rather than a transport error.
1862
+ cancelled = true;
1660
1863
  const id = runId;
1661
1864
  if (id) {
1662
1865
  try {
@@ -1970,6 +2173,18 @@ export class OberikProject {
1970
2173
  };
1971
2174
  /** Documents owned by the project rather than by any one end-user: the corpus you
1972
2175
  * curate and your users only read. */
2176
+ /**
2177
+ * The corpus you curate.
2178
+ *
2179
+ * Uploads here are stored `tenant`-visible — readable by every end-user of the project —
2180
+ * which is what "a corpus you curate, that users only read" means. That is the default
2181
+ * and the only option, deliberately: a project key is not a person, so there is no
2182
+ * per-user subtree for it to write into. Per-user documents go through the data-plane
2183
+ * client with an end-user token, where the subject IS the owner.
2184
+ *
2185
+ * The docs used to show `visibility: "tenant"` being passed here, which was neither
2186
+ * accepted nor needed — a recipe that worked by luck rather than by expression.
2187
+ */
1973
2188
  documents = {
1974
2189
  list: () => this.request("GET", "/documents"),
1975
2190
  upload: (file, opts = {}) => {
@@ -1980,6 +2195,95 @@ export class OberikProject {
1980
2195
  },
1981
2196
  delete: (documentId) => this.request("DELETE", `/documents/${documentId}`),
1982
2197
  };
2198
+ /**
2199
+ * The models this project runs on, and the retrieval it uses.
2200
+ *
2201
+ * These had no methods at all: `project-api.md` documents them in a table and every
2202
+ * integration hand-rolled `fetch` for half its setup. `providers.add` is the one that
2203
+ * matters most — it is the step a new project cannot answer a question without, and it
2204
+ * finishes the rest of the setup itself (see `derived` in the response).
2205
+ *
2206
+ * `providers.catalog` is deliberately absent: it is not project-scoped, so it does not
2207
+ * belong on a client whose every path hangs off one project.
2208
+ */
2209
+ providers = {
2210
+ list: () => this.request("GET", "/providers"),
2211
+ /** Name a chat model AND an embedding model: the first lets the agent answer, the
2212
+ * second lets it index. The response's `derived` says what was set for you. */
2213
+ add: (opts) => this.request("POST", "/providers", opts),
2214
+ edit: (credId, opts) => this.request("PATCH", `/providers/${credId}`, opts),
2215
+ /** Re-read the provider's catalog: a model registered before its price was published
2216
+ * bills nothing, so the usage cap never trips. */
2217
+ refresh: (credId) => this.request("POST", `/providers/${credId}/refresh`, {}),
2218
+ remove: (credId) => this.request("DELETE", `/providers/${credId}`),
2219
+ };
2220
+ /** The model used when a request does not name one. */
2221
+ defaultModel = {
2222
+ set: (model) => this.request("POST", "/default-model", { model }),
2223
+ };
2224
+ /** Embedding and rerank overrides. Set for you when you register an embedding model, so
2225
+ * this is for changing it rather than for getting started. */
2226
+ retrieval = {
2227
+ set: (opts) => this.request("PUT", "/retrieval", opts),
2228
+ /** How many floats a model returns, measured by embedding one word. No provider
2229
+ * publishes it, and a wrong one fails at the first ingest rather than here. */
2230
+ probe: (model) => this.request("POST", "/retrieval/probe", { model }),
2231
+ };
2232
+ /** How documents are read: the built-in parser, or a vision model you choose. */
2233
+ documentProcessor = {
2234
+ set: (opts) => this.request("PUT", "/document-processor", opts),
2235
+ };
2236
+ /** What happens when a conversation outgrows the model's window. */
2237
+ context = {
2238
+ get: () => this.request("GET", "/context"),
2239
+ set: (opts) => this.request("PUT", "/context", opts),
2240
+ };
2241
+ /** Spend and rate caps on this project's LLM key. Enforced by the biller, so they hold
2242
+ * even when the usage views cannot be read. */
2243
+ limits = {
2244
+ get: () => this.request("GET", "/limits"),
2245
+ set: (opts) => this.request("PUT", "/limits", opts),
2246
+ };
2247
+ /** Which models a delegate may run on, and how many may run at once. Without this the
2248
+ * subagents capability stays unavailable however it is granted. */
2249
+ subagents = {
2250
+ set: (opts) => this.request("PUT", "/subagents", opts),
2251
+ };
2252
+ /** Procedures you publish as Agent Plugins, and what your end-users have added. */
2253
+ skills = {
2254
+ list: () => this.request("GET", "/skills"),
2255
+ upload: (zip, filename) => {
2256
+ const form = new FormData();
2257
+ form.append("file", zip, filename ?? zip.name ?? "skill.zip");
2258
+ return this.request("POST", "/skills", undefined, form);
2259
+ },
2260
+ delete: (pluginId) => this.request("DELETE", `/skills/${pluginId}`),
2261
+ };
2262
+ /** MCP servers whose tools join this project's catalog. */
2263
+ mcp = {
2264
+ list: () => this.request("GET", "/mcp"),
2265
+ add: (opts) => this.request("POST", "/mcp", opts),
2266
+ remove: (mcpId) => this.request("DELETE", `/mcp/${mcpId}`),
2267
+ };
2268
+ /** Conversations, and what was said in them. */
2269
+ sessions = {
2270
+ list: () => this.request("GET", "/sessions"),
2271
+ messages: (sessionId) => this.request("GET", `/sessions/${sessionId}/messages`),
2272
+ };
2273
+ /** Scheduled work this project's end-users have created. */
2274
+ tasks = {
2275
+ list: () => this.request("GET", "/tasks"),
2276
+ };
2277
+ /** What the agent has written down: remembered facts and wiki pages. */
2278
+ wiki = {
2279
+ list: () => this.request("GET", "/wiki"),
2280
+ delete: (itemId) => this.request("DELETE", `/wiki/${itemId}`),
2281
+ };
2282
+ /** Live sandboxes, and what to do about one. */
2283
+ sandboxes = {
2284
+ list: () => this.request("GET", "/sandboxes"),
2285
+ action: (sessionId, action) => this.request("POST", `/sandboxes/${sessionId}/${action}`, {}),
2286
+ };
1983
2287
  /** Prepended to every request for this project, above anything a caller sends. */
1984
2288
  systemPrompt = {
1985
2289
  set: (systemPrompt) => this.request("PUT", "/system-prompt", { systemPrompt }),
@@ -1995,6 +2299,39 @@ export class OberikProject {
1995
2299
  delete: (toolId) => this.request("DELETE", `/webhook-tools/${toolId}`),
1996
2300
  };
1997
2301
  /** Server-side keys. A created one is returned once and never again. */
2302
+ /**
2303
+ * Checks on what goes into the model and what comes back.
2304
+ *
2305
+ * The enforcement has existed for a long time and there was no way to configure it — no
2306
+ * route, no dashboard section, no column — so a documentation page described switches
2307
+ * that could not be reached. `set` takes a partial: what you do not mention is left as it
2308
+ * is.
2309
+ */
2310
+ guardrails = {
2311
+ get: () => this.request("GET", "/guardrails"),
2312
+ // `request(method, path, body, form)` — the payload is the THIRD positional
2313
+ // argument. Wrapping it as `{ body: policy }` sent `{"body":{…}}`, every field of
2314
+ // which is optional, so the route validated it, applied nothing, and answered with
2315
+ // the unchanged policy — which reads exactly like success. Worse, `onViolation`
2316
+ // carries a non-null default, so it WAS written: a project running `flag` that
2317
+ // called `set` to turn groundedness on started refusing answers instead, silently.
2318
+ set: (policy) => this.request("PUT", "/guardrails", policy),
2319
+ };
2320
+ /**
2321
+ * Whether this project can actually answer a question yet.
2322
+ *
2323
+ * A new project has no models, so it can neither answer nor index anything — and the
2324
+ * flag that used to be the closest thing to this (`hasLlm`) meant "a LiteLLM key was
2325
+ * provisioned", which is true from the moment a project exists. Every unfinished step
2326
+ * names what it blocks and the one call that fixes it.
2327
+ *
2328
+ * Worth calling in a deploy check: a project that is not ready fails every request with
2329
+ * the provider's own error, which reads as your bug rather than as missing setup.
2330
+ */
2331
+ readiness = () => this.request("GET", "/readiness");
2332
+ /** The starting snippet and this project's endpoints — the same one the dashboard and the
2333
+ * SSH gateway show, so there is one of it rather than three. */
2334
+ connect = () => this.request("GET", "/connect");
1998
2335
  /**
1999
2336
  * Further project keys.
2000
2337
  *