@oberik/sdk 0.54.0 → 0.56.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
@@ -122,6 +122,28 @@ function renderCited(turn) {
122
122
  out.push({ text: content.slice(at), citations: [] });
123
123
  return out;
124
124
  }
125
+ const PROJECT_SESSION_QUERY_KEYS = [
126
+ "q", "model", "status", "userRef", "since", "until", "includeSubagents", "limit", "offset",
127
+ ];
128
+ /**
129
+ * Throw on an option key this library does not know.
130
+ *
131
+ * A dropped option is the worst of the three answers: `{ includeSubAgents: true }` — one
132
+ * capital away from the real name — returned a cheerful 200 with no delegates in it, which
133
+ * reads as "there are no delegates" rather than as "you spelled it wrong" (OBE-147). A
134
+ * typo in an options bag is not something a type system catches for a caller in plain JS,
135
+ * and it is exactly the caller who has no compiler to tell them.
136
+ */
137
+ function refuseUnknownKeys(opts, known, where) {
138
+ // `object` rather than `Record<string, unknown>`: an interface without an index
139
+ // signature is not assignable to the latter, so typing it that way makes the one call
140
+ // site fail to compile — which is exactly the caller this exists for.
141
+ const unknown = Object.keys(opts ?? {}).filter((k) => !known.includes(k));
142
+ if (unknown.length) {
143
+ throw new TypeError(`${where}: unknown option${unknown.length > 1 ? "s" : ""} ` +
144
+ `${unknown.map((k) => `'${k}'`).join(", ")}. Valid options: ${known.join(", ")}.`);
145
+ }
146
+ }
125
147
  /** Wrap the stream's `done` promise into a thenable handle: `await handle` yields
126
148
  * the ChatDone, and `.done`/`.cancel`/`.disconnect`/`.runId` remain available. */
127
149
  function makeStreamHandle(done, extra) {
@@ -426,13 +448,28 @@ class AgentApiError extends Error {
426
448
  * an intermediary, which is replaced by the same summary as `message`. Keeping four
427
449
  * kilobytes of someone else's markup here helped nobody and buried the status. */
428
450
  detail;
429
- constructor(status, detail) {
451
+ /** The run that failed, when the server said which.
452
+ *
453
+ * A failed `chat.send` used to carry neither this nor {@link sessionId}, so the reason
454
+ * the server stores for the turn — the only durable answer to "why did this fail" —
455
+ * could not be looked up by the person who hit it. `chat.stream` had both all along;
456
+ * this is the same handle on the blocking path (OBE-142). */
457
+ runId;
458
+ /** The conversation the failed turn belongs to, when the server said which.
459
+ *
460
+ * Present even when the turn failed before producing anything: the session exists from
461
+ * the moment the turn is prepared, and whatever it wrote is readable with
462
+ * `ai.chat.sessions.messages(err.sessionId)`. */
463
+ sessionId;
464
+ constructor(status, detail, ids) {
430
465
  const described = describeDetail(status, detail);
431
466
  super(described);
432
467
  this.name = "AgentApiError";
433
468
  this.status = status;
434
469
  this.detail =
435
470
  typeof detail === "string" && looksLikeHtml(detail) ? described : detail;
471
+ this.runId = ids?.runId;
472
+ this.sessionId = ids?.sessionId;
436
473
  }
437
474
  }
438
475
  exports.AgentApiError = AgentApiError;
@@ -1607,15 +1644,32 @@ class AgentFramework {
1607
1644
  }
1608
1645
  if (!res.ok) {
1609
1646
  let detail = await res.text();
1647
+ // Alongside `detail`, when the server said which turn this was about. Read here
1648
+ // because this is the one place an API error is built from a response — every route's
1649
+ // refusal passes through it, so a route that starts saying which run failed needs no
1650
+ // change on this side.
1651
+ let ids;
1610
1652
  try {
1611
- detail = JSON.parse(detail);
1612
- if (detail && typeof detail === "object" && "detail" in detail)
1613
- detail = detail.detail;
1653
+ const body = JSON.parse(detail);
1654
+ if (body && typeof body === "object") {
1655
+ const b = body;
1656
+ if ("detail" in b)
1657
+ detail = b.detail;
1658
+ else
1659
+ detail = body;
1660
+ const runId = typeof b.run_id === "string" ? b.run_id : undefined;
1661
+ const sessionId = typeof b.session_id === "string" ? b.session_id : undefined;
1662
+ if (runId || sessionId)
1663
+ ids = { runId, sessionId };
1664
+ }
1665
+ else {
1666
+ detail = body;
1667
+ }
1614
1668
  }
1615
1669
  catch {
1616
1670
  /* leave as text */
1617
1671
  }
1618
- throw new AgentApiError(res.status, detail);
1672
+ throw new AgentApiError(res.status, detail, ids);
1619
1673
  }
1620
1674
  return res;
1621
1675
  }
@@ -2658,7 +2712,13 @@ class AgentFramework {
2658
2712
  // This used to be a bare message, so a stream that died at minute
2659
2713
  // eighteen threw away the answer AND the only handle on where the rest
2660
2714
  // of it had been written.
2661
- throw new AgentStreamError("server: " + ev.data.detail, {
2715
+ // The server's sentence, unprefixed. This read `"server: " + detail`, so
2716
+ // every streamed failure reached the customer as
2717
+ // `server: the upstream endpoint answered 408 …` — a lowercase field name in
2718
+ // front of the first word they read, and the one difference between the two
2719
+ // paths' otherwise identical message (OBE-140). `AgentStreamError` already
2720
+ // says where it came from; the label said it again, worse.
2721
+ throw new AgentStreamError(ev.data.detail, {
2662
2722
  code: ev.data.code,
2663
2723
  // The status the same refusal carries on `POST /chat` — 429 for a spend
2664
2724
  // cap or a rate limit. Without it a streaming caller had no number to
@@ -3278,7 +3338,46 @@ class OberikProject {
3278
3338
  };
3279
3339
  /** Conversations, and what was said in them. */
3280
3340
  sessions = {
3281
- list: () => this.request("GET", "/sessions"),
3341
+ /**
3342
+ * Conversations on this project, newest first.
3343
+ *
3344
+ * A PAGE, and typed as one. This declared `Promise<unknown[]>` and returned
3345
+ * `{items, has_more, next_offset}`, so a TypeScript caller wrote `.length` and got
3346
+ * `undefined` — no type error, no exception, and a census that counted sessions
3347
+ * reported zero (OBE-147). The declared type is the envelope it has always been.
3348
+ *
3349
+ * It also forwarded nothing at all, which made its own paging unreachable: the
3350
+ * response carried `has_more` and `next_offset`, the two fields whose only purpose is
3351
+ * to be fed back in, and there was nowhere to feed them. Past 100 sessions the tail
3352
+ * was unreachable through this client.
3353
+ *
3354
+ * `includeSubagents` is the one that cost somebody an hour: a delegate's working
3355
+ * conversation is excluded by default (correct — a sidebar built from here grew a row
3356
+ * per delegate), and with nothing forwarded, the admin-scope client an operator audits
3357
+ * with was the one that could not ask for them.
3358
+ */
3359
+ list: async (opts = {}) => {
3360
+ // `async`, so a bad option REJECTS rather than throwing synchronously out of a
3361
+ // promise-returning method — `list({…}).catch(…)` would not have caught that.
3362
+ refuseUnknownKeys(opts, PROJECT_SESSION_QUERY_KEYS, "sessions.list");
3363
+ // Built the way `documents.list` two methods down builds its own: this client's
3364
+ // `request` takes a body, not a query, and passing `{query: …}` to it sends a JSON
3365
+ // body on a GET and no query string at all — which is quietly what "forwards
3366
+ // nothing" would have looked like if the query had been added the obvious way.
3367
+ const wire = {
3368
+ q: opts.q,
3369
+ model: opts.model,
3370
+ status: opts.status,
3371
+ user_ref: opts.userRef,
3372
+ since: opts.since,
3373
+ until: opts.until,
3374
+ include_subagents: opts.includeSubagents,
3375
+ limit: opts.limit,
3376
+ offset: opts.offset,
3377
+ };
3378
+ const qs = new URLSearchParams(Object.entries(wire).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)])).toString();
3379
+ return this.request("GET", `/sessions${qs ? `?${qs}` : ""}`);
3380
+ },
3282
3381
  messages: (sessionId) => this.request("GET", `/sessions/${sessionId}/messages`),
3283
3382
  };
3284
3383
  /** Scheduled work this project's end-users have created. */