@noodleseed/assistant 1.13.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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { d as AssistantViewAvailableDetail, f as AssistantClientEvent, a as AssistantContext, e as AssistantModelContextUpdate, A as AssistantPageContext, h as AssistantInteractionResponse } from './client-BKIeuSVS.cjs';
2
- export { g as AssistantErrorDetail, i as AssistantEvent, j as AssistantViewAvailableEvent } from './client-BKIeuSVS.cjs';
1
+ import { d as AssistantViewAvailableDetail, f as AssistantClientEvent, a as AssistantContext, e as AssistantModelContextUpdate, A as AssistantPageContext, h as AssistantInteractionResponse } from './client-B_l59rWg.cjs';
2
+ export { g as AssistantErrorDetail, i as AssistantEvent, j as AssistantViewAvailableEvent } from './client-B_l59rWg.cjs';
3
3
  import './appearance-Cc-eqSbp.cjs';
4
4
  import 'ai';
5
5
 
@@ -58,6 +58,10 @@ declare class NoodleAssistantElement extends HTMLElementBase {
58
58
  #private;
59
59
  static readonly observedAttributes: string[];
60
60
  sessionEndpoint: string;
61
+ /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
62
+ embedId: string;
63
+ /** Only for a dev or self-hosted service; the published snippet needs no origin. */
64
+ serviceUrl: string;
61
65
  get appearance(): AssistantAppearance | undefined;
62
66
  set appearance(value: AssistantAppearance | undefined);
63
67
  connectedCallback(): void;
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { d as AssistantViewAvailableDetail, f as AssistantClientEvent, a as AssistantContext, e as AssistantModelContextUpdate, A as AssistantPageContext, h as AssistantInteractionResponse } from './client-BccAX_om.js';
2
- export { g as AssistantErrorDetail, i as AssistantEvent, j as AssistantViewAvailableEvent } from './client-BccAX_om.js';
1
+ import { d as AssistantViewAvailableDetail, f as AssistantClientEvent, a as AssistantContext, e as AssistantModelContextUpdate, A as AssistantPageContext, h as AssistantInteractionResponse } from './client-MnLfiyo2.js';
2
+ export { g as AssistantErrorDetail, i as AssistantEvent, j as AssistantViewAvailableEvent } from './client-MnLfiyo2.js';
3
3
  import './appearance-Cc-eqSbp.js';
4
4
  import 'ai';
5
5
 
@@ -58,6 +58,10 @@ declare class NoodleAssistantElement extends HTMLElementBase {
58
58
  #private;
59
59
  static readonly observedAttributes: string[];
60
60
  sessionEndpoint: string;
61
+ /** Public mount: the non-secret embed id `noodle deploy` printed. Mutually exclusive with the above. */
62
+ embedId: string;
63
+ /** Only for a dev or self-hosted service; the published snippet needs no origin. */
64
+ serviceUrl: string;
61
65
  get appearance(): AssistantAppearance | undefined;
62
66
  set appearance(value: AssistantAppearance | undefined);
63
67
  connectedCallback(): void;
package/dist/index.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  ASSISTANT_TAG_NAME,
3
3
  NoodleAssistantElement,
4
4
  registerNoodleAssistant
5
- } from "./chunk-ZPELSHUP.js";
6
- import "./chunk-KRB2SXFD.js";
5
+ } from "./chunk-SUHNNWKP.js";
6
+ import "./chunk-RRJIK473.js";
7
7
  import "./chunk-3YYTUJPJ.js";
8
8
  import "./chunk-5FUTL2UF.js";
9
9
 
@@ -22644,7 +22644,7 @@ var AssistantChatStateStore = class {
22644
22644
  this.#writeText(event.data.delta);
22645
22645
  return;
22646
22646
  case "tool_proposed":
22647
- this.#writeConfirmation(event.data);
22647
+ this.#writeStandaloneData(() => this.#writeConfirmation(event.data));
22648
22648
  return;
22649
22649
  case "interaction_proposed":
22650
22650
  this.#writeData("confirmation", event.data.id, {
@@ -22653,10 +22653,12 @@ var AssistantChatStateStore = class {
22653
22653
  });
22654
22654
  return;
22655
22655
  case "input_requested":
22656
- this.#writeData("input-request", event.data.id, {
22657
- ...event.data,
22658
- status: "pending"
22659
- });
22656
+ this.#writeStandaloneData(
22657
+ () => this.#writeData("input-request", event.data.id, {
22658
+ ...event.data,
22659
+ status: "pending"
22660
+ })
22661
+ );
22660
22662
  return;
22661
22663
  case "tool_completed":
22662
22664
  this.#writeData("tool-result", event.data.id, event.data);
@@ -22763,6 +22765,12 @@ var AssistantChatStateStore = class {
22763
22765
  #writeConfirmation(data) {
22764
22766
  this.#writeData("confirmation", data.id, { ...data, status: "pending" });
22765
22767
  }
22768
+ #writeStandaloneData(write) {
22769
+ const standalone = this.#operation === void 0;
22770
+ if (standalone) this.#startOperation();
22771
+ write();
22772
+ if (standalone) this.#closeOperation("ready");
22773
+ }
22766
22774
  #writeData(name21, id, data) {
22767
22775
  const operation = this.#operation;
22768
22776
  if (!operation || operation.closed) return;
@@ -23095,6 +23103,21 @@ function hasToJsonProperty(value) {
23095
23103
  return false;
23096
23104
  }
23097
23105
 
23106
+ // src/session-source.ts
23107
+ var NOODLE_CLOUD_URL = "https://cloud.noodleseed.dev";
23108
+ var PUBLIC_SESSION_PATH = "/v1/assistant/public-sessions";
23109
+ function resolveSessionSource(options) {
23110
+ const { sessionEndpoint, embedId, serviceUrl } = options;
23111
+ if (Boolean(embedId) === Boolean(sessionEndpoint)) {
23112
+ throw new Error("pass either embedId or sessionEndpoint, not both and not neither");
23113
+ }
23114
+ if (embedId) {
23115
+ const base = (serviceUrl ?? NOODLE_CLOUD_URL).replace(/\/+$/, "");
23116
+ return { kind: "public", url: `${base}${PUBLIC_SESSION_PATH}`, embedId };
23117
+ }
23118
+ return { kind: "exchange", url: sessionEndpoint };
23119
+ }
23120
+
23098
23121
  // src/transport.ts
23099
23122
  var AssistantTransportError = class extends Error {
23100
23123
  code = "invalid_response";
@@ -23222,6 +23245,19 @@ function isAbortError2(error51) {
23222
23245
  }
23223
23246
 
23224
23247
  // src/client.ts
23248
+ async function refusalCode(response) {
23249
+ try {
23250
+ const body = await response.clone().json();
23251
+ const code = body.code;
23252
+ return typeof code === "string" ? code : void 0;
23253
+ } catch {
23254
+ return void 0;
23255
+ }
23256
+ }
23257
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
23258
+ "daily_turn_budget_exhausted",
23259
+ "daily_session_budget_exhausted"
23260
+ ]);
23225
23261
  var AssistantClientError = class extends Error {
23226
23262
  detail;
23227
23263
  constructor(detail, message, options) {
@@ -23231,7 +23267,7 @@ var AssistantClientError = class extends Error {
23231
23267
  }
23232
23268
  };
23233
23269
  var DefaultAssistantClient = class {
23234
- #sessionEndpoint;
23270
+ #source;
23235
23271
  #fetch;
23236
23272
  #listeners = /* @__PURE__ */ new Set();
23237
23273
  #chat = new AssistantChatStateStore();
@@ -23241,9 +23277,9 @@ var DefaultAssistantClient = class {
23241
23277
  #context;
23242
23278
  #modelContext;
23243
23279
  #active;
23280
+ #pendingAppInteractions = /* @__PURE__ */ new Map();
23244
23281
  constructor(options) {
23245
- if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
23246
- this.#sessionEndpoint = options.sessionEndpoint;
23282
+ this.#source = resolveSessionSource(options);
23247
23283
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
23248
23284
  this.#context = options.context ? { ...options.context } : void 0;
23249
23285
  this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
@@ -23282,6 +23318,7 @@ var DefaultAssistantClient = class {
23282
23318
  }
23283
23319
  await this.#singleFlight(async (signal) => {
23284
23320
  await this.#runChatOperation(signal, async () => {
23321
+ const pendingApp = this.#pendingAppInteractions.get(id);
23285
23322
  const session = this.#session;
23286
23323
  if (!session) {
23287
23324
  throw clientError("confirmation_expired", "assistant session is not active", false);
@@ -23301,32 +23338,67 @@ var DefaultAssistantClient = class {
23301
23338
  ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
23302
23339
  } : { id };
23303
23340
  this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
23304
- const response = await this.#request(
23305
- endpoint,
23306
- {
23307
- method: "POST",
23308
- headers: authorizedHeaders(session.token, "text/event-stream"),
23309
- body: JSON.stringify(body),
23310
- signal
23311
- },
23312
- "confirmation_failed"
23313
- );
23314
- if (response.status === 401) {
23315
- this.#session = void 0;
23316
- this.#emit({ event: "session_expired", data: {} });
23317
- }
23318
- if (!response.ok) {
23319
- const serverCode = await readStableErrorCode(response);
23320
- const expired = response.status === 401 || response.status === 409;
23321
- throw clientError(
23322
- serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
23323
- `Assistant interaction failed (${response.status})`,
23324
- false,
23325
- response.status
23341
+ try {
23342
+ const response = await this.#request(
23343
+ endpoint,
23344
+ {
23345
+ method: "POST",
23346
+ headers: authorizedHeaders(session.token, "text/event-stream"),
23347
+ body: JSON.stringify(body),
23348
+ signal
23349
+ },
23350
+ "confirmation_failed"
23326
23351
  );
23352
+ if (response.status === 401) {
23353
+ this.#session = void 0;
23354
+ this.#emit({ event: "session_expired", data: {} });
23355
+ }
23356
+ if (!response.ok) {
23357
+ const serverCode = await readStableErrorCode(response);
23358
+ const expired = response.status === 401 || response.status === 409;
23359
+ throw clientError(
23360
+ serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
23361
+ `Assistant interaction failed (${response.status})`,
23362
+ false,
23363
+ response.status
23364
+ );
23365
+ }
23366
+ let completedResult;
23367
+ let nextInteractionId;
23368
+ await this.#consume(response, (event) => {
23369
+ if (event.event === "tool_completed" && event.data.id === id) {
23370
+ completedResult = event.data.result;
23371
+ }
23372
+ if ((event.event === "tool_proposed" || event.event === "input_requested") && event.data.id !== id) {
23373
+ nextInteractionId = event.data.id;
23374
+ }
23375
+ });
23376
+ this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
23377
+ if (pendingApp) {
23378
+ this.#pendingAppInteractions.delete(id);
23379
+ if (nextInteractionId) {
23380
+ this.#pendingAppInteractions.set(nextInteractionId, pendingApp);
23381
+ } else if (resolution.action !== "accept") {
23382
+ pendingApp.resolve(appInteractionStopped(resolution.action));
23383
+ } else if (completedResult !== void 0) {
23384
+ pendingApp.resolve(appToolResult(completedResult));
23385
+ } else {
23386
+ pendingApp.reject(
23387
+ clientError(
23388
+ "invalid_response",
23389
+ "Assistant app interaction completed without a tool result",
23390
+ false
23391
+ )
23392
+ );
23393
+ }
23394
+ }
23395
+ } catch (error51) {
23396
+ if (pendingApp && this.#pendingAppInteractions.get(id) === pendingApp) {
23397
+ this.#pendingAppInteractions.delete(id);
23398
+ pendingApp.reject(error51);
23399
+ }
23400
+ throw error51;
23327
23401
  }
23328
- await this.#consume(response);
23329
- this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
23330
23402
  });
23331
23403
  });
23332
23404
  }
@@ -23367,12 +23439,29 @@ var DefaultAssistantClient = class {
23367
23439
  response.status
23368
23440
  );
23369
23441
  }
23370
- return response.json();
23442
+ const value = await response.json();
23443
+ const interaction = parseAppInteraction(value);
23444
+ if (!interaction) return value;
23445
+ return new Promise((resolve2, reject) => {
23446
+ const id = interaction.data.id;
23447
+ const replaced = this.#pendingAppInteractions.get(id);
23448
+ if (replaced) {
23449
+ replaced.reject(
23450
+ clientError("invalid_response", "Assistant returned a duplicate app interaction", false)
23451
+ );
23452
+ }
23453
+ this.#pendingAppInteractions.set(id, { resolve: resolve2, reject });
23454
+ this.#emit(interaction);
23455
+ void this.#chat.flush();
23456
+ });
23371
23457
  }
23372
23458
  appSandboxUrl() {
23373
23459
  return this.#session?.endpoints.sandbox;
23374
23460
  }
23375
23461
  resetSession() {
23462
+ this.#rejectPendingAppInteractions(
23463
+ clientError("confirmation_expired", "assistant session is not active", false)
23464
+ );
23376
23465
  this.#session = void 0;
23377
23466
  this.#emit({ event: "session_reset", data: {} });
23378
23467
  }
@@ -23406,11 +23495,14 @@ var DefaultAssistantClient = class {
23406
23495
  return;
23407
23496
  }
23408
23497
  if (!response.ok) {
23498
+ const serviceCode = await refusalCode(response);
23409
23499
  throw clientError(
23410
23500
  "turn_failed",
23411
23501
  `Assistant turn failed (${response.status})`,
23412
23502
  false,
23413
- response.status
23503
+ response.status,
23504
+ void 0,
23505
+ serviceCode
23414
23506
  );
23415
23507
  }
23416
23508
  await this.#consume(response);
@@ -23449,23 +23541,25 @@ var DefaultAssistantClient = class {
23449
23541
  }
23450
23542
  }
23451
23543
  async #createSession(signal) {
23452
- const response = await this.#request(
23453
- this.#sessionEndpoint,
23454
- {
23455
- method: "POST",
23456
- headers: { Accept: "application/json", "Content-Type": "application/json" },
23457
- body: JSON.stringify(this.#context ? { context: this.#context } : {}),
23458
- credentials: "same-origin",
23459
- signal
23460
- },
23461
- "session_failed"
23462
- );
23544
+ const source = this.#source;
23545
+ const response = await this.#requestSession(source, {
23546
+ method: "POST",
23547
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
23548
+ body: JSON.stringify(
23549
+ source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
23550
+ ),
23551
+ credentials: source.kind === "public" ? "omit" : "same-origin",
23552
+ signal
23553
+ });
23463
23554
  if (!response.ok) {
23555
+ const serviceCode = await refusalCode(response);
23464
23556
  throw clientError(
23465
23557
  "session_failed",
23466
23558
  `Assistant session failed (${response.status})`,
23467
- true,
23468
- response.status
23559
+ serviceCode === void 0 || !UNRETRYABLE_SERVICE_CODES.has(serviceCode),
23560
+ response.status,
23561
+ void 0,
23562
+ serviceCode
23469
23563
  );
23470
23564
  }
23471
23565
  let value;
@@ -23493,12 +23587,13 @@ var DefaultAssistantClient = class {
23493
23587
  });
23494
23588
  return session;
23495
23589
  }
23496
- async #consume(response) {
23590
+ async #consume(response, observe) {
23497
23591
  let streamError;
23498
23592
  try {
23499
23593
  await consumeAssistantEvents(response, (event) => {
23500
23594
  const clientEvent = toAssistantClientEvent(event);
23501
23595
  this.#emit(clientEvent);
23596
+ observe?.(clientEvent);
23502
23597
  if (clientEvent.event !== "error" || typeof clientEvent.data.code !== "string") return;
23503
23598
  streamError = {
23504
23599
  code: clientEvent.data.code,
@@ -23521,6 +23616,34 @@ var DefaultAssistantClient = class {
23521
23616
  );
23522
23617
  }
23523
23618
  }
23619
+ /**
23620
+ * The mint, with the one failure a public page hits that an in-app embed cannot.
23621
+ *
23622
+ * A page that loads the embed script and then blocks `connect-src` fails here as a bare network
23623
+ * rejection — no status, no body, nothing to read. "Assistant request failed" sends a developer
23624
+ * hunting through their own code; naming the directive and the origin ends the search. The message
23625
+ * covers a plain outage too, because from inside the browser the two are indistinguishable.
23626
+ */
23627
+ async #requestSession(source, init) {
23628
+ try {
23629
+ return await this.#fetch(source.url, init);
23630
+ } catch (error51) {
23631
+ if (init.signal?.aborted) throw error51;
23632
+ if (source.kind !== "public") {
23633
+ throw clientError("session_failed", "Assistant request failed", true, void 0, {
23634
+ cause: error51
23635
+ });
23636
+ }
23637
+ throw clientError(
23638
+ "session_failed",
23639
+ `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).`,
23640
+ true,
23641
+ void 0,
23642
+ { cause: error51 },
23643
+ "blocked_by_page"
23644
+ );
23645
+ }
23646
+ }
23524
23647
  async #request(input, init, failureCode) {
23525
23648
  try {
23526
23649
  return await this.#fetch(input, init);
@@ -23567,6 +23690,10 @@ var DefaultAssistantClient = class {
23567
23690
  }
23568
23691
  }
23569
23692
  }
23693
+ #rejectPendingAppInteractions(error51) {
23694
+ for (const pending of this.#pendingAppInteractions.values()) pending.reject(error51);
23695
+ this.#pendingAppInteractions.clear();
23696
+ }
23570
23697
  };
23571
23698
  function createAssistantClient(options) {
23572
23699
  return new DefaultAssistantClient(options);
@@ -23578,6 +23705,30 @@ function authorizedHeaders(token, accept) {
23578
23705
  Accept: accept
23579
23706
  };
23580
23707
  }
23708
+ function parseAppInteraction(value) {
23709
+ if (!isRecord4(value) || !isRecord4(value.interaction)) return void 0;
23710
+ const event = value.interaction.event;
23711
+ const data = value.interaction.data;
23712
+ if (event !== "tool_proposed" && event !== "input_requested" || !isRecord4(data)) {
23713
+ return void 0;
23714
+ }
23715
+ const parsed = toAssistantClientEvent({ event, data });
23716
+ return parsed.event === "tool_proposed" || parsed.event === "input_requested" ? parsed : void 0;
23717
+ }
23718
+ function appToolResult(result) {
23719
+ const text2 = typeof result === "string" ? result : JSON.stringify(result);
23720
+ return result !== null && typeof result === "object" && !Array.isArray(result) ? {
23721
+ content: [{ type: "text", text: text2 }],
23722
+ structuredContent: result,
23723
+ isError: false
23724
+ } : { content: [{ type: "text", text: text2 }], isError: false };
23725
+ }
23726
+ function appInteractionStopped(action) {
23727
+ return {
23728
+ content: [{ type: "text", text: `interaction_${action}` }],
23729
+ isError: true
23730
+ };
23731
+ }
23581
23732
  function parseSession(value) {
23582
23733
  if (!isRecord4(value) || !isRecord4(value.endpoints)) {
23583
23734
  throw clientError("session_failed", "Assistant session response is invalid", true);
@@ -23614,9 +23765,14 @@ async function readStableErrorCode(response) {
23614
23765
  if (!isRecord4(value) || typeof value.code !== "string") return void 0;
23615
23766
  return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
23616
23767
  }
23617
- function clientError(code, message, retryable, status, options) {
23768
+ function clientError(code, message, retryable, status, options, serviceCode) {
23618
23769
  return new AssistantClientError(
23619
- { code, ...status === void 0 ? {} : { status }, retryable },
23770
+ {
23771
+ code,
23772
+ ...status === void 0 ? {} : { status },
23773
+ retryable,
23774
+ ...serviceCode === void 0 ? {} : { serviceCode }
23775
+ },
23620
23776
  message,
23621
23777
  options
23622
23778
  );
@@ -23626,16 +23782,23 @@ function clientError(code, message, retryable, status, options) {
23626
23782
  var useCommittedLayoutEffect = typeof window === "undefined" ? import_react.useEffect : import_react.useLayoutEffect;
23627
23783
  function useNoodleAssistant(options) {
23628
23784
  const optionsRef = (0, import_react.useRef)(options);
23629
- const { fetch: injectedFetch, principalKey, sessionEndpoint } = options;
23785
+ const { fetch: injectedFetch, principalKey, sessionEndpoint, embedId, serviceUrl } = options;
23630
23786
  const client = (0, import_react.useMemo)(() => {
23631
23787
  if (!principalKey.trim()) throw new Error("principalKey is required");
23632
- return createAssistantClient({
23633
- sessionEndpoint,
23788
+ const behavior = {
23634
23789
  ...injectedFetch ? { fetch: injectedFetch } : {},
23635
23790
  clientContext: () => readProvider(optionsRef.current.clientContext),
23636
23791
  pageContext: () => readProvider(optionsRef.current.pageContext)
23792
+ };
23793
+ return embedId !== void 0 ? createAssistantClient({
23794
+ embedId,
23795
+ ...serviceUrl !== void 0 ? { serviceUrl } : {},
23796
+ ...behavior
23797
+ }) : createAssistantClient({
23798
+ sessionEndpoint: sessionEndpoint ?? "",
23799
+ ...behavior
23637
23800
  });
23638
- }, [injectedFetch, principalKey, sessionEndpoint]);
23801
+ }, [injectedFetch, principalKey, sessionEndpoint, embedId, serviceUrl]);
23639
23802
  const store = (0, import_react.useMemo)(() => createAssistantReactStore(client), [client]);
23640
23803
  const state = (0, import_react.useSyncExternalStore)(store.subscribe, store.getSnapshot, store.getSnapshot);
23641
23804
  const synchronized = (0, import_react.useRef)(void 0);