@noodleseed/assistant 1.14.0 → 1.16.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
@@ -27092,6 +27092,11 @@ function toAssistantClientEvent(event) {
27092
27092
  return event;
27093
27093
  }
27094
27094
  break;
27095
+ case "auth_requested":
27096
+ if (hasString(value, "id") && hasString(value, "tool") && hasString(value, "continuation") && hasString(value, "expiresAt") && hasOptionalTurnId(value)) {
27097
+ return event;
27098
+ }
27099
+ break;
27095
27100
  case "interaction_resolved":
27096
27101
  if (hasString(value, "id") && (value.action === void 0 || isInteractionAction(value.action)) && hasOptionalTurnId(value)) {
27097
27102
  return event;
@@ -27276,6 +27281,24 @@ function hasToJsonProperty(value) {
27276
27281
  return false;
27277
27282
  }
27278
27283
 
27284
+ // src/session-source.ts
27285
+ var NOODLE_CLOUD_URL = "https://cloud.noodleseed.dev";
27286
+ var PUBLIC_SESSION_PATH = "/v1/assistant/public-sessions";
27287
+ function resolveSessionSource(options) {
27288
+ const { sessionEndpoint, embedId, serviceUrl } = options;
27289
+ if (Boolean(embedId) === Boolean(sessionEndpoint)) {
27290
+ throw new Error("pass either embedId or sessionEndpoint, not both and not neither");
27291
+ }
27292
+ if (embedId) {
27293
+ const base = (serviceUrl ?? NOODLE_CLOUD_URL).replace(/\/+$/, "");
27294
+ return { kind: "public", url: `${base}${PUBLIC_SESSION_PATH}`, embedId };
27295
+ }
27296
+ return { kind: "exchange", url: sessionEndpoint };
27297
+ }
27298
+ function sessionSourceKey(options) {
27299
+ return `${options.embedId ?? ""}|${options.serviceUrl ?? ""}|${options.sessionEndpoint ?? ""}`;
27300
+ }
27301
+
27279
27302
  // src/transport.ts
27280
27303
  var AssistantTransportError = class extends Error {
27281
27304
  code = "invalid_response";
@@ -27403,6 +27426,19 @@ function isAbortError2(error51) {
27403
27426
  }
27404
27427
 
27405
27428
  // src/client.ts
27429
+ async function refusalCode(response) {
27430
+ try {
27431
+ const body = await response.clone().json();
27432
+ const code = body.code;
27433
+ return typeof code === "string" ? code : void 0;
27434
+ } catch {
27435
+ return void 0;
27436
+ }
27437
+ }
27438
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
27439
+ "daily_turn_budget_exhausted",
27440
+ "daily_session_budget_exhausted"
27441
+ ]);
27406
27442
  var AssistantClientError = class extends Error {
27407
27443
  detail;
27408
27444
  constructor(detail, message, options) {
@@ -27412,7 +27448,7 @@ var AssistantClientError = class extends Error {
27412
27448
  }
27413
27449
  };
27414
27450
  var DefaultAssistantClient = class {
27415
- #sessionEndpoint;
27451
+ #source;
27416
27452
  #fetch;
27417
27453
  #listeners = /* @__PURE__ */ new Set();
27418
27454
  #chat = new AssistantChatStateStore();
@@ -27424,8 +27460,7 @@ var DefaultAssistantClient = class {
27424
27460
  #active;
27425
27461
  #pendingAppInteractions = /* @__PURE__ */ new Map();
27426
27462
  constructor(options) {
27427
- if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
27428
- this.#sessionEndpoint = options.sessionEndpoint;
27463
+ this.#source = resolveSessionSource(options);
27429
27464
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
27430
27465
  this.#context = options.context ? { ...options.context } : void 0;
27431
27466
  this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
@@ -27641,11 +27676,14 @@ var DefaultAssistantClient = class {
27641
27676
  return;
27642
27677
  }
27643
27678
  if (!response.ok) {
27679
+ const serviceCode = await refusalCode(response);
27644
27680
  throw clientError(
27645
27681
  "turn_failed",
27646
27682
  `Assistant turn failed (${response.status})`,
27647
27683
  false,
27648
- response.status
27684
+ response.status,
27685
+ void 0,
27686
+ serviceCode
27649
27687
  );
27650
27688
  }
27651
27689
  await this.#consume(response);
@@ -27684,23 +27722,25 @@ var DefaultAssistantClient = class {
27684
27722
  }
27685
27723
  }
27686
27724
  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
- );
27725
+ const source = this.#source;
27726
+ const response = await this.#requestSession(source, {
27727
+ method: "POST",
27728
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
27729
+ body: JSON.stringify(
27730
+ source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
27731
+ ),
27732
+ credentials: source.kind === "public" ? "omit" : "same-origin",
27733
+ signal
27734
+ });
27698
27735
  if (!response.ok) {
27736
+ const serviceCode = await refusalCode(response);
27699
27737
  throw clientError(
27700
27738
  "session_failed",
27701
27739
  `Assistant session failed (${response.status})`,
27702
- true,
27703
- response.status
27740
+ serviceCode === void 0 || !UNRETRYABLE_SERVICE_CODES.has(serviceCode),
27741
+ response.status,
27742
+ void 0,
27743
+ serviceCode
27704
27744
  );
27705
27745
  }
27706
27746
  let value;
@@ -27757,6 +27797,34 @@ var DefaultAssistantClient = class {
27757
27797
  );
27758
27798
  }
27759
27799
  }
27800
+ /**
27801
+ * The mint, with the one failure a public page hits that an in-app embed cannot.
27802
+ *
27803
+ * A page that loads the embed script and then blocks `connect-src` fails here as a bare network
27804
+ * rejection — no status, no body, nothing to read. "Assistant request failed" sends a developer
27805
+ * hunting through their own code; naming the directive and the origin ends the search. The message
27806
+ * covers a plain outage too, because from inside the browser the two are indistinguishable.
27807
+ */
27808
+ async #requestSession(source, init) {
27809
+ try {
27810
+ return await this.#fetch(source.url, init);
27811
+ } catch (error51) {
27812
+ if (init.signal?.aborted) throw error51;
27813
+ if (source.kind !== "public") {
27814
+ throw clientError("session_failed", "Assistant request failed", true, void 0, {
27815
+ cause: error51
27816
+ });
27817
+ }
27818
+ throw clientError(
27819
+ "session_failed",
27820
+ `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).`,
27821
+ true,
27822
+ void 0,
27823
+ { cause: error51 },
27824
+ "blocked_by_page"
27825
+ );
27826
+ }
27827
+ }
27760
27828
  async #request(input, init, failureCode) {
27761
27829
  try {
27762
27830
  return await this.#fetch(input, init);
@@ -27878,9 +27946,14 @@ async function readStableErrorCode(response) {
27878
27946
  if (!isRecord4(value) || typeof value.code !== "string") return void 0;
27879
27947
  return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
27880
27948
  }
27881
- function clientError(code, message, retryable, status, options) {
27949
+ function clientError(code, message, retryable, status, options, serviceCode) {
27882
27950
  return new AssistantClientError(
27883
- { code, ...status === void 0 ? {} : { status }, retryable },
27951
+ {
27952
+ code,
27953
+ ...status === void 0 ? {} : { status },
27954
+ retryable,
27955
+ ...serviceCode === void 0 ? {} : { serviceCode }
27956
+ },
27884
27957
  message,
27885
27958
  options
27886
27959
  );
@@ -30973,6 +31046,28 @@ function renderMarkdown(text3) {
30973
31046
  });
30974
31047
  }
30975
31048
 
31049
+ // src/sign-in-card.ts
31050
+ function createSignInCard(options) {
31051
+ const card = document.createElement("div");
31052
+ card.className = "noodle-assistant-card noodle-assistant-sign-in";
31053
+ card.dataset.tool = options.tool;
31054
+ card.setAttribute("role", "status");
31055
+ const heading = document.createElement("p");
31056
+ heading.className = "noodle-assistant-card-heading";
31057
+ heading.textContent = options.labels.heading;
31058
+ card.append(heading);
31059
+ const action = document.createElement("button");
31060
+ action.type = "button";
31061
+ action.className = "noodle-assistant-card-action";
31062
+ action.textContent = options.labels.action;
31063
+ action.addEventListener("click", () => {
31064
+ action.disabled = true;
31065
+ options.onSignIn();
31066
+ });
31067
+ card.append(action);
31068
+ return card;
31069
+ }
31070
+
30976
31071
  // src/element-event-controller.ts
30977
31072
  var AssistantElementEventController = class {
30978
31073
  #host;
@@ -31067,6 +31162,15 @@ var AssistantElementEventController = class {
31067
31162
  );
31068
31163
  return;
31069
31164
  }
31165
+ if (name21 === "auth_requested" && typeof data.continuation === "string") {
31166
+ this.#appendSignInRequest(
31167
+ String(data.id ?? ""),
31168
+ String(data.tool ?? "this"),
31169
+ data.continuation,
31170
+ String(data.expiresAt ?? "")
31171
+ );
31172
+ return;
31173
+ }
31070
31174
  if (name21 === "input_requested" && isRecord5(data.requestedSchema)) {
31071
31175
  this.#appendInputRequest(
31072
31176
  String(data.id ?? ""),
@@ -31152,6 +31256,40 @@ var AssistantElementEventController = class {
31152
31256
  })
31153
31257
  );
31154
31258
  }
31259
+ /**
31260
+ * The host application owns the login. This renders the prompt and raises
31261
+ * `assistant-sign-in-requested`; the page signs the visitor in however it already does, then its
31262
+ * backend spends the continuation. Nothing here talks to an identity provider.
31263
+ */
31264
+ #appendSignInRequest(id, tool, continuation, expiresAt) {
31265
+ const messages = this.#host.messages();
31266
+ if (!messages) return;
31267
+ this.#proposalCards.get(id)?.remove();
31268
+ const card = createSignInCard({
31269
+ tool,
31270
+ continuation,
31271
+ expiresAt,
31272
+ labels: {
31273
+ heading: "Sign in to continue",
31274
+ action: "Sign in"
31275
+ },
31276
+ onSignIn: () => this.#host.element.dispatchEvent(
31277
+ new CustomEvent("assistant-sign-in-requested", {
31278
+ detail: { id, tool, continuation, expiresAt },
31279
+ bubbles: true,
31280
+ composed: true
31281
+ })
31282
+ )
31283
+ });
31284
+ messages.append(card);
31285
+ this.#proposalCards.set(id, card);
31286
+ this.#host.revealLatest();
31287
+ this.#host.element.dispatchEvent(
31288
+ new CustomEvent("assistant-sign-in-required", {
31289
+ detail: { id, tool, continuation, expiresAt }
31290
+ })
31291
+ );
31292
+ }
31155
31293
  #appendInputRequest(id, message, requestedSchema) {
31156
31294
  const messages = this.#host.messages();
31157
31295
  if (!messages || !id) return;
@@ -31971,6 +32109,10 @@ var NoodleAssistantElement = class extends HTMLElementBase {
31971
32109
  #appearanceStyleSnapshot = /* @__PURE__ */ new Map();
31972
32110
  #scroll = new AssistantElementScrollController();
31973
32111
  sessionEndpoint = "";
32112
+ /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
32113
+ embedId = "";
32114
+ /** Only for a dev or self-hosted service; the published snippet needs no origin. */
32115
+ serviceUrl = "";
31974
32116
  get appearance() {
31975
32117
  return this.#hostAppearance;
31976
32118
  }
@@ -32025,6 +32167,8 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32025
32167
  connectedCallback() {
32026
32168
  if (!this.shadowRoot) this.attachShadow({ mode: "open" });
32027
32169
  if (!this.sessionEndpoint) this.sessionEndpoint = this.getAttribute("session-endpoint") ?? "";
32170
+ if (!this.embedId) this.embedId = this.getAttribute("embed-id") ?? "";
32171
+ if (!this.serviceUrl) this.serviceUrl = this.getAttribute("service-url") ?? "";
32028
32172
  this.#media = globalThis.matchMedia?.("(prefers-color-scheme: dark)");
32029
32173
  this.#media?.addEventListener("change", this.#handleSystemTheme);
32030
32174
  document.addEventListener("keydown", this.#handleDocumentKeydown);
@@ -32196,26 +32340,31 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32196
32340
  }
32197
32341
  };
32198
32342
  #ensureClient() {
32199
- if (this.#client && this.#clientEndpoint === this.sessionEndpoint) return this.#client;
32343
+ const key = sessionSourceKey(this);
32344
+ if (this.#client && this.#clientEndpoint === key) return this.#client;
32200
32345
  this.#client?.abort();
32201
32346
  this.#unsubscribeClient?.();
32202
32347
  this.#sessionBootstrap = void 0;
32203
- const client = createAssistantClient({
32204
- sessionEndpoint: this.sessionEndpoint,
32348
+ const behavior = {
32205
32349
  ...this.#context ? { context: this.#context } : {},
32206
32350
  clientContext: browserClientContext,
32207
32351
  ...this.#pageContext ? { pageContext: this.#pageContext } : {},
32208
32352
  ...this.#modelContext ? { modelContext: this.#modelContext } : {}
32209
- });
32353
+ };
32354
+ const client = this.embedId ? createAssistantClient({
32355
+ embedId: this.embedId,
32356
+ ...this.serviceUrl ? { serviceUrl: this.serviceUrl } : {},
32357
+ ...behavior
32358
+ }) : createAssistantClient({ sessionEndpoint: this.sessionEndpoint, ...behavior });
32210
32359
  this.#client = client;
32211
- this.#clientEndpoint = this.sessionEndpoint;
32360
+ this.#clientEndpoint = key;
32212
32361
  this.#unsubscribeClient = client.subscribe((event) => {
32213
32362
  if (this.#client === client) this.#handleClientEvent(event);
32214
32363
  });
32215
32364
  return client;
32216
32365
  }
32217
32366
  #primeSession() {
32218
- if (!this.sessionEndpoint) return;
32367
+ if (!this.sessionEndpoint && !this.embedId) return;
32219
32368
  const client = this.#ensureClient();
32220
32369
  const generation = this.#conversationGeneration;
32221
32370
  this.#setSessionState("loading");
@@ -32339,23 +32488,30 @@ var NoodleAssistantElement = class extends HTMLElementBase {
32339
32488
  }
32340
32489
  const detail = error51 instanceof AssistantClientError ? error51.detail : { code: fallbackCode, retryable: false };
32341
32490
  this.#appendRecoveryError(
32342
- detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable
32491
+ detail.code === "session_expired" ? this.#appearance.labels.sessionExpired : this.#appearance.labels.unavailable,
32492
+ // A spent or switched-off daily budget offers no retry. Reconnecting cannot succeed, and every
32493
+ // open tab trying is exactly the load the cap was set to refuse — so the button goes, not just
32494
+ // the alarming words. A spent *session* keeps it: reconnect mints a fresh one, which is the fix.
32495
+ detail.retryable !== false || detail.serviceCode === void 0
32343
32496
  );
32344
32497
  this.#dispatchError(detail);
32345
32498
  }
32346
- #appendRecoveryError(message) {
32499
+ #appendRecoveryError(message, offerRetry = true) {
32347
32500
  if (!this.#messages) return;
32348
32501
  this.#messages.querySelector(".conversation-error")?.remove();
32349
32502
  const error51 = document.createElement("section");
32350
32503
  error51.className = "conversation-error";
32351
- error51.setAttribute("role", "alert");
32504
+ error51.setAttribute("role", offerRetry ? "alert" : "status");
32352
32505
  const text3 = document.createElement("p");
32353
32506
  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);
32507
+ error51.append(text3);
32508
+ if (offerRetry) {
32509
+ const retry = document.createElement("button");
32510
+ retry.type = "button";
32511
+ retry.textContent = this.#appearance.labels.reconnect;
32512
+ retry.addEventListener("click", () => this.reconnect());
32513
+ error51.append(retry);
32514
+ }
32359
32515
  this.#messages.append(error51);
32360
32516
  this.#revealLatest();
32361
32517
  }