@noodleseed/assistant 1.14.0 → 1.15.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/index.cjs CHANGED
@@ -27276,6 +27276,24 @@ function hasToJsonProperty(value) {
27276
27276
  return false;
27277
27277
  }
27278
27278
 
27279
+ // src/session-source.ts
27280
+ var NOODLE_CLOUD_URL = "https://cloud.noodleseed.dev";
27281
+ var PUBLIC_SESSION_PATH = "/v1/assistant/public-sessions";
27282
+ function resolveSessionSource(options) {
27283
+ const { sessionEndpoint, embedId, serviceUrl } = options;
27284
+ if (Boolean(embedId) === Boolean(sessionEndpoint)) {
27285
+ throw new Error("pass either embedId or sessionEndpoint, not both and not neither");
27286
+ }
27287
+ if (embedId) {
27288
+ const base = (serviceUrl ?? NOODLE_CLOUD_URL).replace(/\/+$/, "");
27289
+ return { kind: "public", url: `${base}${PUBLIC_SESSION_PATH}`, embedId };
27290
+ }
27291
+ return { kind: "exchange", url: sessionEndpoint };
27292
+ }
27293
+ function sessionSourceKey(options) {
27294
+ return `${options.embedId ?? ""}|${options.serviceUrl ?? ""}|${options.sessionEndpoint ?? ""}`;
27295
+ }
27296
+
27279
27297
  // src/transport.ts
27280
27298
  var AssistantTransportError = class extends Error {
27281
27299
  code = "invalid_response";
@@ -27403,6 +27421,19 @@ function isAbortError2(error51) {
27403
27421
  }
27404
27422
 
27405
27423
  // src/client.ts
27424
+ async function refusalCode(response) {
27425
+ try {
27426
+ const body = await response.clone().json();
27427
+ const code = body.code;
27428
+ return typeof code === "string" ? code : void 0;
27429
+ } catch {
27430
+ return void 0;
27431
+ }
27432
+ }
27433
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27434
+ "daily_turn_budget_exhausted",
27435
+ "daily_session_budget_exhausted"
27436
+ ]);
27406
27437
  var AssistantClientError = class extends Error {
27407
27438
  detail;
27408
27439
  constructor(detail, message, options) {
@@ -27412,7 +27443,7 @@ var AssistantClientError = class extends Error {
27412
27443
  }
27413
27444
  };
27414
27445
  var DefaultAssistantClient = class {
27415
- #sessionEndpoint;
27446
+ #source;
27416
27447
  #fetch;
27417
27448
  #listeners = /* @__PURE__ */ new Set();
27418
27449
  #chat = new AssistantChatStateStore();
@@ -27424,8 +27455,7 @@ var DefaultAssistantClient = class {
27424
27455
  #active;
27425
27456
  #pendingAppInteractions = /* @__PURE__ */ new Map();
27426
27457
  constructor(options) {
27427
- if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
27428
- this.#sessionEndpoint = options.sessionEndpoint;
27458
+ this.#source = resolveSessionSource(options);
27429
27459
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
27430
27460
  this.#context = options.context ? { ...options.context } : void 0;
27431
27461
  this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
@@ -27641,11 +27671,14 @@ var DefaultAssistantClient = class {
27641
27671
  return;
27642
27672
  }
27643
27673
  if (!response.ok) {
27674
+ const serviceCode = await refusalCode(response);
27644
27675
  throw clientError(
27645
27676
  "turn_failed",
27646
27677
  `Assistant turn failed (${response.status})`,
27647
27678
  false,
27648
- response.status
27679
+ response.status,
27680
+ void 0,
27681
+ serviceCode
27649
27682
  );
27650
27683
  }
27651
27684
  await this.#consume(response);
@@ -27684,23 +27717,25 @@ var DefaultAssistantClient = class {
27684
27717
  }
27685
27718
  }
27686
27719
  async #createSession(signal) {
27687
- const response = await this.#request(
27688
- this.#sessionEndpoint,
27689
- {
27690
- method: "POST",
27691
- headers: { Accept: "application/json", "Content-Type": "application/json" },
27692
- body: JSON.stringify(this.#context ? { context: this.#context } : {}),
27693
- credentials: "same-origin",
27694
- signal
27695
- },
27696
- "session_failed"
27697
- );
27720
+ const source = this.#source;
27721
+ const response = await this.#requestSession(source, {
27722
+ method: "POST",
27723
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
27724
+ body: JSON.stringify(
27725
+ source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
27726
+ ),
27727
+ credentials: source.kind === "public" ? "omit" : "same-origin",
27728
+ signal
27729
+ });
27698
27730
  if (!response.ok) {
27731
+ const serviceCode = await refusalCode(response);
27699
27732
  throw clientError(
27700
27733
  "session_failed",
27701
27734
  `Assistant session failed (${response.status})`,
27702
- true,
27703
- response.status
27735
+ serviceCode === void 0 || !UNRETRYABLE_SERVICE_CODES.has(serviceCode),
27736
+ response.status,
27737
+ void 0,
27738
+ serviceCode
27704
27739
  );
27705
27740
  }
27706
27741
  let value;
@@ -27757,6 +27792,34 @@ var DefaultAssistantClient = class {
27757
27792
  );
27758
27793
  }
27759
27794
  }
27795
+ /**
27796
+ * The mint, with the one failure a public page hits that an in-app embed cannot.
27797
+ *
27798
+ * A page that loads the embed script and then blocks `connect-src` fails here as a bare network
27799
+ * rejection — no status, no body, nothing to read. "Assistant request failed" sends a developer
27800
+ * hunting through their own code; naming the directive and the origin ends the search. The message
27801
+ * covers a plain outage too, because from inside the browser the two are indistinguishable.
27802
+ */
27803
+ async #requestSession(source, init) {
27804
+ try {
27805
+ return await this.#fetch(source.url, init);
27806
+ } catch (error51) {
27807
+ if (init.signal?.aborted) throw error51;
27808
+ if (source.kind !== "public") {
27809
+ throw clientError("session_failed", "Assistant request failed", true, void 0, {
27810
+ cause: error51
27811
+ });
27812
+ }
27813
+ throw clientError(
27814
+ "session_failed",
27815
+ `Assistant could not reach ${new URL(source.url).origin}. If the page sets a Content-Security-Policy, allow that origin in connect-src (and in script-src and frame-src).`,
27816
+ true,
27817
+ void 0,
27818
+ { cause: error51 },
27819
+ "blocked_by_page"
27820
+ );
27821
+ }
27822
+ }
27760
27823
  async #request(input, init, failureCode) {
27761
27824
  try {
27762
27825
  return await this.#fetch(input, init);
@@ -27878,9 +27941,14 @@ async function readStableErrorCode(response) {
27878
27941
  if (!isRecord4(value) || typeof value.code !== "string") return void 0;
27879
27942
  return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
27880
27943
  }
27881
- function clientError(code, message, retryable, status, options) {
27944
+ function clientError(code, message, retryable, status, options, serviceCode) {
27882
27945
  return new AssistantClientError(
27883
- { code, ...status === void 0 ? {} : { status }, retryable },
27946
+ {
27947
+ code,
27948
+ ...status === void 0 ? {} : { status },
27949
+ retryable,
27950
+ ...serviceCode === void 0 ? {} : { serviceCode }
27951
+ },
27884
27952
  message,
27885
27953
  options
27886
27954
  );
@@ -31971,6 +32039,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
31971
32039
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
31972
32040
  #scroll = new AssistantElementScrollController();
31973
32041
  sessionEndpoint = "";
32042
+ /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
32043
+ embedId = "";
32044
+ /** Only for a dev or self-hosted service; the published snippet needs no origin. */
32045
+ serviceUrl = "";
31974
32046
  get appearance() {
31975
32047
  return this.#hostAppearance;
31976
32048
  }
@@ -32025,6 +32097,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32025
32097
  connectedCallback() {
32026
32098
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
32027
32099
  if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
32100
+ if (!this.embedId) this.embedId = this.getAttribute("embed-id") ?? "";
32101
+ if (!this.serviceUrl) this.serviceUrl = this.getAttribute("service-url") ?? "";
32028
32102
  this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
32029
32103
  this.#media?.addEventListener("change", this.#handleSystemTheme);
32030
32104
  document.addEventListener("keydown", this.#handleDocumentKeydown);
@@ -32196,26 +32270,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32196
32270
  }
32197
32271
  };
32198
32272
  #ensureClient() {
32199
- if (this.#client && this.#clientEndpoint === this.sessionEndpoint) return this.#client;
32273
+ const key = sessionSourceKey(this);
32274
+ if (this.#client && this.#clientEndpoint === key) return this.#client;
32200
32275
  this.#client?.abort();
32201
32276
  this.#unsubscribeClient?.();
32202
32277
  this.#sessionBootstrap = void 0;
32203
- const client = createAssistantClient({
32204
- sessionEndpoint: this.sessionEndpoint,
32278
+ const behavior = {
32205
32279
  ...this.#context ? { context: this.#context } : {},
32206
32280
  clientContext: browserClientContext,
32207
32281
  ...this.#pageContext ? { pageContext: this.#pageContext } : {},
32208
32282
  ...this.#modelContext ? { modelContext: this.#modelContext } : {}
32209
- });
32283
+ };
32284
+ const client = this.embedId ? createAssistantClient({
32285
+ embedId: this.embedId,
32286
+ ...this.serviceUrl ? { serviceUrl: this.serviceUrl } : {},
32287
+ ...behavior
32288
+ }) : createAssistantClient({ sessionEndpoint: this.sessionEndpoint, ...behavior });
32210
32289
  this.#client = client;
32211
- this.#clientEndpoint = this.sessionEndpoint;
32290
+ this.#clientEndpoint = key;
32212
32291
  this.#unsubscribeClient = client.subscribe((event) => {
32213
32292
  if (this.#client === client) this.#handleClientEvent(event);
32214
32293
  });
32215
32294
  return client;
32216
32295
  }
32217
32296
  #primeSession() {
32218
- if (!this.sessionEndpoint) return;
32297
+ if (!this.sessionEndpoint && !this.embedId) return;
32219
32298
  const client = this.#ensureClient();
32220
32299
  const generation = this.#conversationGeneration;
32221
32300
  this.#setSessionState("loading");
@@ -32339,23 +32418,30 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32339
32418
  }
32340
32419
  const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
32341
32420
  this.#appendRecoveryError(
32342
- detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable
32421
+ detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable,
32422
+ // A spent or switched-off daily budget offers no retry. Reconnecting cannot succeed, and every
32423
+ // open tab trying is exactly the load the cap was set to refuse — so the button goes, not just
32424
+ // the alarming words. A spent *session* keeps it: reconnect mints a fresh one, which is the fix.
32425
+ detail.retryable !== false || detail.serviceCode === void 0
32343
32426
  );
32344
32427
  this.#dispatchError(detail);
32345
32428
  }
32346
- #appendRecoveryError(message) {
32429
+ #appendRecoveryError(message, offerRetry = true) {
32347
32430
  if (!this.#messages) return;
32348
32431
  this.#messages.querySelector(".conversation-error")?.remove();
32349
32432
  const error51 = document.createElement("section");
32350
32433
  error51.className = "conversation-error";
32351
- error51.setAttribute("role", "alert");
32434
+ error51.setAttribute("role", offerRetry ? "alert" : "status");
32352
32435
  const text3 = document.createElement("p");
32353
32436
  text3.textContent = message;
32354
- const retry = document.createElement("button");
32355
- retry.type = "button";
32356
- retry.textContent = this.#appearance.labels.reconnect;
32357
- retry.addEventListener("click", () => this.reconnect());
32358
- error51.append(text3, retry);
32437
+ error51.append(text3);
32438
+ if (offerRetry) {
32439
+ const retry = document.createElement("button");
32440
+ retry.type = "button";
32441
+ retry.textContent = this.#appearance.labels.reconnect;
32442
+ retry.addEventListener("click", () => this.reconnect());
32443
+ error51.append(retry);
32444
+ }
32359
32445
  this.#messages.append(error51);
32360
32446
  this.#revealLatest();
32361
32447
  }