@oberik/sdk 0.2.0 → 0.3.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
@@ -110,6 +110,75 @@ function uiToolSchemas(ui) {
110
110
  },
111
111
  }));
112
112
  }
113
+ /**
114
+ * Fold a later turn's payload into the running one.
115
+ *
116
+ * A pause is a new server turn. When the agent stops for a client tool, a question or an
117
+ * approval, resuming starts a *fresh* request, and the server builds its `done` from that
118
+ * turn alone — so the citations from the retrieval it did before the pause simply are not
119
+ * in the payload the loop finally returns.
120
+ *
121
+ * Which is what happened: an app that combined retrieval with its own tools — the app the
122
+ * docs tell you to build — got `citations: 0` and `sources: 0` on every turn where a tool
123
+ * ran, and an empty `ui` for anything drawn before the pause. Meanwhile both were streamed
124
+ * live and both rendered, so it worked on screen and vanished from the object. The docs
125
+ * promise the opposite in as many words: "the `done` payload carries the final state of
126
+ * everything above, so a client that ignored the incremental frames still ends up with the
127
+ * whole turn."
128
+ *
129
+ * Three kinds of field, and getting the kind wrong is its own bug:
130
+ * - accumulated: prose and lists the turn produced across all its segments
131
+ * - deduped: retrieval, which is very likely to repeat across segments
132
+ * - last-wins: what the *server* recomputes each time and is authoritative about
133
+ */
134
+ function mergeDone(prev, next) {
135
+ if (!prev)
136
+ return next;
137
+ const byKey = (items, key) => {
138
+ const seen = new Set();
139
+ const out = [];
140
+ for (const it of items) {
141
+ const k = key(it);
142
+ if (seen.has(k))
143
+ continue;
144
+ seen.add(k);
145
+ out.push(it);
146
+ }
147
+ return out;
148
+ };
149
+ return {
150
+ ...next,
151
+ // Accumulated: everything the agent said and drew, in order.
152
+ content: [prev.content, next.content].filter(Boolean).join(""),
153
+ // Claims accumulate, and the later segment's offsets shift by however much prose came
154
+ // before it. Getting this wrong points a footnote at the wrong sentence, which is worse
155
+ // than having no footnote.
156
+ claims: [
157
+ ...(prev.claims ?? []),
158
+ ...(next.claims ?? []).map((claim) => ({
159
+ ...claim,
160
+ start: claim.start == null ? null : claim.start + prev.content.length,
161
+ end: claim.end == null ? null : claim.end + prev.content.length,
162
+ })),
163
+ ],
164
+ attribution: prev.attribution === "per-claim" || next.attribution === "per-claim"
165
+ ? "per-claim"
166
+ : prev.attribution === "retrieval-only" || next.attribution === "retrieval-only"
167
+ ? "retrieval-only"
168
+ : "none",
169
+ reasoning: [prev.reasoning ?? "", next.reasoning ?? ""].filter(Boolean).join("") || undefined,
170
+ ui: [...(prev.ui ?? []), ...(next.ui ?? [])],
171
+ guard_flags: [...new Set([...(prev.guard_flags ?? []), ...(next.guard_flags ?? [])])],
172
+ // Deduped: the same chunk retrieved twice is one citation, and an attachment carried
173
+ // forward across a pause is one file.
174
+ citations: byKey([...(prev.citations ?? []), ...(next.citations ?? [])], (c) => `${c.document_id}:${c.chunk_index}`),
175
+ sources: byKey([...(prev.sources ?? []), ...(next.sources ?? [])], (s) => String(s.url ?? `${s.document_id}:${s.chunk_index ?? ""}`)),
176
+ attachments: byKey([...(prev.attachments ?? []), ...(next.attachments ?? [])], (a) => String(a.id ?? a.s3_key ?? a.filename)),
177
+ // Last-wins: the server recomputes these per turn and is right about them. `todos` is
178
+ // the whole current plan, not a delta; `subagents` is every subagent of the
179
+ // conversation; `context` describes the window as it is now.
180
+ };
181
+ }
113
182
  /** Run each pending client tool via its handler; failures become error results
114
183
  * (not thrown) so one bad tool doesn't abort the whole turn. */
115
184
  async function executeToolCalls(calls, tools) {
@@ -1213,6 +1282,35 @@ class AgentFramework {
1213
1282
  get: (id) => this.request("GET", `/tasks/${id}`),
1214
1283
  cancel: (id) => this.request("POST", `/tasks/${id}/cancel`),
1215
1284
  };
1285
+ /**
1286
+ * Turns that start because something happened somewhere else.
1287
+ *
1288
+ * A trigger is a URL you give another system — a ticket tracker, a CI job, a Zapier step
1289
+ * — and the path is the credential, so it runs as a fixed subject chosen at creation.
1290
+ * These were the only routes in the API reference with no SDK method: every integration
1291
+ * hand-wrote `fetch` for them.
1292
+ *
1293
+ * The URL comes back absolute and is not a secret we can show once: the whole point is
1294
+ * that someone else's configuration holds it. `secret` IS shown once — with it, the
1295
+ * sender signs the body and the URL stops being a bearer token.
1296
+ */
1297
+ triggers = {
1298
+ list: () => this.request("GET", "/triggers"),
1299
+ /** `prompt` may interpolate the event: `"A ticket arrived: {{ body.title }}"`. */
1300
+ create: (opts) => this.request("POST", "/triggers", {
1301
+ body: {
1302
+ prompt: opts.prompt,
1303
+ name: opts.name,
1304
+ system_prompt: opts.systemPrompt,
1305
+ session_id: opts.sessionId,
1306
+ signed: opts.signed ?? false,
1307
+ },
1308
+ }),
1309
+ delete: (triggerId) => this.request("DELETE", `/triggers/${triggerId}`),
1310
+ /** A new URL, with the old one alive for 24 hours — so telling the other system its new
1311
+ * address is not an outage. */
1312
+ rotate: (triggerId) => this.request("POST", `/triggers/${triggerId}/rotate`),
1313
+ };
1216
1314
  /**
1217
1315
  * What the agent wrote down — remembered facts and wiki pages.
1218
1316
  *
@@ -1306,6 +1404,10 @@ class AgentFramework {
1306
1404
  signal: opts.signal,
1307
1405
  });
1308
1406
  this.renderUi(resp.ui, ui);
1407
+ // What the caller gets back: every segment folded together. Each pause starts a new
1408
+ // server turn, so the last segment's payload knows nothing about the retrieval that
1409
+ // happened before it.
1410
+ let merged = resp;
1309
1411
  let rounds = 0;
1310
1412
  // Three ways a turn pauses: work for the client to run, a question for the user to
1311
1413
  // answer, or permission to ask for. All resume the same way, and the round budget
@@ -1315,7 +1417,7 @@ class AgentFramework {
1315
1417
  const hasQuestions = onQuestion != null && (resp.questions?.length ?? 0) > 0;
1316
1418
  const hasApprovals = onApproval != null && (resp.approvals?.length ?? 0) > 0;
1317
1419
  if (!hasTools && !hasQuestions && !hasApprovals)
1318
- return resp;
1420
+ return merged;
1319
1421
  if (rounds++ >= maxRounds)
1320
1422
  throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
1321
1423
  const next = {
@@ -1331,6 +1433,7 @@ class AgentFramework {
1331
1433
  next.approval_decisions = await collectDecisions(resp.approvals, onApproval);
1332
1434
  resp = await this.request("POST", "/chat", { body: next, signal: opts.signal });
1333
1435
  this.renderUi(resp.ui, ui);
1436
+ merged = mergeDone(merged, resp);
1334
1437
  }
1335
1438
  }
1336
1439
  /** Wrap startStream so a `done` carrying `requires_action` auto-executes the
@@ -1349,17 +1452,19 @@ class AgentFramework {
1349
1452
  const done = (async () => {
1350
1453
  const uiSchemas = ui.size ? uiToolSchemas(ui) : undefined;
1351
1454
  let reqBody = { ...body, client_tools: schemas, ui_tools: uiSchemas };
1455
+ let merged;
1352
1456
  let rounds = 0;
1353
1457
  for (;;) {
1354
1458
  current = this.startStream(reqBody, handlers);
1355
1459
  const d = await current.done;
1460
+ merged = mergeDone(merged, d);
1356
1461
  const hasTools = d.requires_action && d.tool_calls.length > 0;
1357
1462
  const hasQuestions = onQuestion != null && (d.questions?.length ?? 0) > 0;
1358
1463
  const hasApprovals = onApproval != null && (d.approvals?.length ?? 0) > 0;
1359
1464
  if (!hasTools && !hasQuestions && !hasApprovals)
1360
- return d;
1465
+ return merged;
1361
1466
  if (stopped)
1362
- return d;
1467
+ return merged;
1363
1468
  if (rounds++ >= maxRounds)
1364
1469
  throw new AgentStreamError(`agent pause loop exceeded maxToolRounds (${maxRounds})`);
1365
1470
  reqBody = { session_id: d.session_id, client_tools: schemas, ui_tools: uiSchemas };
@@ -1979,6 +2084,18 @@ class OberikProject {
1979
2084
  };
1980
2085
  /** Documents owned by the project rather than by any one end-user: the corpus you
1981
2086
  * curate and your users only read. */
2087
+ /**
2088
+ * The corpus you curate.
2089
+ *
2090
+ * Uploads here are stored `tenant`-visible — readable by every end-user of the project —
2091
+ * which is what "a corpus you curate, that users only read" means. That is the default
2092
+ * and the only option, deliberately: a project key is not a person, so there is no
2093
+ * per-user subtree for it to write into. Per-user documents go through the data-plane
2094
+ * client with an end-user token, where the subject IS the owner.
2095
+ *
2096
+ * The docs used to show `visibility: "tenant"` being passed here, which was neither
2097
+ * accepted nor needed — a recipe that worked by luck rather than by expression.
2098
+ */
1982
2099
  documents = {
1983
2100
  list: () => this.request("GET", "/documents"),
1984
2101
  upload: (file, opts = {}) => {
@@ -1989,6 +2106,95 @@ class OberikProject {
1989
2106
  },
1990
2107
  delete: (documentId) => this.request("DELETE", `/documents/${documentId}`),
1991
2108
  };
2109
+ /**
2110
+ * The models this project runs on, and the retrieval it uses.
2111
+ *
2112
+ * These had no methods at all: `project-api.md` documents them in a table and every
2113
+ * integration hand-rolled `fetch` for half its setup. `providers.add` is the one that
2114
+ * matters most — it is the step a new project cannot answer a question without, and it
2115
+ * finishes the rest of the setup itself (see `derived` in the response).
2116
+ *
2117
+ * `providers.catalog` is deliberately absent: it is not project-scoped, so it does not
2118
+ * belong on a client whose every path hangs off one project.
2119
+ */
2120
+ providers = {
2121
+ list: () => this.request("GET", "/providers"),
2122
+ /** Name a chat model AND an embedding model: the first lets the agent answer, the
2123
+ * second lets it index. The response's `derived` says what was set for you. */
2124
+ add: (opts) => this.request("POST", "/providers", opts),
2125
+ edit: (credId, opts) => this.request("PATCH", `/providers/${credId}`, opts),
2126
+ /** Re-read the provider's catalog: a model registered before its price was published
2127
+ * bills nothing, so the usage cap never trips. */
2128
+ refresh: (credId) => this.request("POST", `/providers/${credId}/refresh`, {}),
2129
+ remove: (credId) => this.request("DELETE", `/providers/${credId}`),
2130
+ };
2131
+ /** The model used when a request does not name one. */
2132
+ defaultModel = {
2133
+ set: (model) => this.request("POST", "/default-model", { model }),
2134
+ };
2135
+ /** Embedding and rerank overrides. Set for you when you register an embedding model, so
2136
+ * this is for changing it rather than for getting started. */
2137
+ retrieval = {
2138
+ set: (opts) => this.request("PUT", "/retrieval", opts),
2139
+ /** How many floats a model returns, measured by embedding one word. No provider
2140
+ * publishes it, and a wrong one fails at the first ingest rather than here. */
2141
+ probe: (model) => this.request("POST", "/retrieval/probe", { model }),
2142
+ };
2143
+ /** How documents are read: the built-in parser, or a vision model you choose. */
2144
+ documentProcessor = {
2145
+ set: (opts) => this.request("PUT", "/document-processor", opts),
2146
+ };
2147
+ /** What happens when a conversation outgrows the model's window. */
2148
+ context = {
2149
+ get: () => this.request("GET", "/context"),
2150
+ set: (opts) => this.request("PUT", "/context", opts),
2151
+ };
2152
+ /** Spend and rate caps on this project's LLM key. Enforced by the biller, so they hold
2153
+ * even when the usage views cannot be read. */
2154
+ limits = {
2155
+ get: () => this.request("GET", "/limits"),
2156
+ set: (opts) => this.request("PUT", "/limits", opts),
2157
+ };
2158
+ /** Which models a delegate may run on, and how many may run at once. Without this the
2159
+ * subagents capability stays unavailable however it is granted. */
2160
+ subagents = {
2161
+ set: (opts) => this.request("PUT", "/subagents", opts),
2162
+ };
2163
+ /** Procedures you publish as Agent Plugins, and what your end-users have added. */
2164
+ skills = {
2165
+ list: () => this.request("GET", "/skills"),
2166
+ upload: (zip, filename) => {
2167
+ const form = new FormData();
2168
+ form.append("file", zip, filename ?? zip.name ?? "skill.zip");
2169
+ return this.request("POST", "/skills", undefined, form);
2170
+ },
2171
+ delete: (pluginId) => this.request("DELETE", `/skills/${pluginId}`),
2172
+ };
2173
+ /** MCP servers whose tools join this project's catalog. */
2174
+ mcp = {
2175
+ list: () => this.request("GET", "/mcp"),
2176
+ add: (opts) => this.request("POST", "/mcp", opts),
2177
+ remove: (mcpId) => this.request("DELETE", `/mcp/${mcpId}`),
2178
+ };
2179
+ /** Conversations, and what was said in them. */
2180
+ sessions = {
2181
+ list: () => this.request("GET", "/sessions"),
2182
+ messages: (sessionId) => this.request("GET", `/sessions/${sessionId}/messages`),
2183
+ };
2184
+ /** Scheduled work this project's end-users have created. */
2185
+ tasks = {
2186
+ list: () => this.request("GET", "/tasks"),
2187
+ };
2188
+ /** What the agent has written down: remembered facts and wiki pages. */
2189
+ wiki = {
2190
+ list: () => this.request("GET", "/wiki"),
2191
+ delete: (itemId) => this.request("DELETE", `/wiki/${itemId}`),
2192
+ };
2193
+ /** Live sandboxes, and what to do about one. */
2194
+ sandboxes = {
2195
+ list: () => this.request("GET", "/sandboxes"),
2196
+ action: (sessionId, action) => this.request("POST", `/sandboxes/${sessionId}/${action}`, {}),
2197
+ };
1992
2198
  /** Prepended to every request for this project, above anything a caller sends. */
1993
2199
  systemPrompt = {
1994
2200
  set: (systemPrompt) => this.request("PUT", "/system-prompt", { systemPrompt }),
@@ -2004,6 +2210,33 @@ class OberikProject {
2004
2210
  delete: (toolId) => this.request("DELETE", `/webhook-tools/${toolId}`),
2005
2211
  };
2006
2212
  /** Server-side keys. A created one is returned once and never again. */
2213
+ /**
2214
+ * Checks on what goes into the model and what comes back.
2215
+ *
2216
+ * The enforcement has existed for a long time and there was no way to configure it — no
2217
+ * route, no dashboard section, no column — so a documentation page described switches
2218
+ * that could not be reached. `set` takes a partial: what you do not mention is left as it
2219
+ * is.
2220
+ */
2221
+ guardrails = {
2222
+ get: () => this.request("GET", "/guardrails"),
2223
+ set: (policy) => this.request("PUT", "/guardrails", { body: policy }),
2224
+ };
2225
+ /**
2226
+ * Whether this project can actually answer a question yet.
2227
+ *
2228
+ * A new project has no models, so it can neither answer nor index anything — and the
2229
+ * flag that used to be the closest thing to this (`hasLlm`) meant "a LiteLLM key was
2230
+ * provisioned", which is true from the moment a project exists. Every unfinished step
2231
+ * names what it blocks and the one call that fixes it.
2232
+ *
2233
+ * Worth calling in a deploy check: a project that is not ready fails every request with
2234
+ * the provider's own error, which reads as your bug rather than as missing setup.
2235
+ */
2236
+ readiness = () => this.request("GET", "/readiness");
2237
+ /** The starting snippet and this project's endpoints — the same one the dashboard and the
2238
+ * SSH gateway show, so there is one of it rather than three. */
2239
+ connect = () => this.request("GET", "/connect");
2007
2240
  /**
2008
2241
  * Further project keys.
2009
2242
  *