@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/README.md CHANGED
@@ -305,6 +305,11 @@ Do not also key an ancestor by the whole view object or a callback. Pass the emb
305
305
  resolved `"light"` or `"dark"` theme; later changes are published through MCP Apps host context without
306
306
  replacing the iframe.
307
307
 
308
+ The App document owns its action intent: it calls standard `tools/call` after connecting and never relies on
309
+ native form navigation. If that call needs input or confirmation, the same `client` publishes the normal
310
+ pending interaction part for either managed or customer-owned rendering. The original App call stays pending
311
+ and receives its MCP result after `client.respond(...)`; applications must not retry or translate the click.
312
+
308
313
  Outside React, subscribe to the DOM-free AI SDK `UIMessage` state without registering a custom element or
309
314
  touching browser storage:
310
315
 
@@ -8108,7 +8108,7 @@ var AssistantChatStateStore = class {
8108
8108
  this.#writeText(event.data.delta);
8109
8109
  return;
8110
8110
  case "tool_proposed":
8111
- this.#writeConfirmation(event.data);
8111
+ this.#writeStandaloneData(() => this.#writeConfirmation(event.data));
8112
8112
  return;
8113
8113
  case "interaction_proposed":
8114
8114
  this.#writeData("confirmation", event.data.id, {
@@ -8117,10 +8117,12 @@ var AssistantChatStateStore = class {
8117
8117
  });
8118
8118
  return;
8119
8119
  case "input_requested":
8120
- this.#writeData("input-request", event.data.id, {
8121
- ...event.data,
8122
- status: "pending"
8123
- });
8120
+ this.#writeStandaloneData(
8121
+ () => this.#writeData("input-request", event.data.id, {
8122
+ ...event.data,
8123
+ status: "pending"
8124
+ })
8125
+ );
8124
8126
  return;
8125
8127
  case "tool_completed":
8126
8128
  this.#writeData("tool-result", event.data.id, event.data);
@@ -8227,6 +8229,12 @@ var AssistantChatStateStore = class {
8227
8229
  #writeConfirmation(data) {
8228
8230
  this.#writeData("confirmation", data.id, { ...data, status: "pending" });
8229
8231
  }
8232
+ #writeStandaloneData(write) {
8233
+ const standalone = this.#operation === void 0;
8234
+ if (standalone) this.#startOperation();
8235
+ write();
8236
+ if (standalone) this.#closeOperation("ready");
8237
+ }
8230
8238
  #writeData(name21, id, data) {
8231
8239
  const operation = this.#operation;
8232
8240
  if (!operation || operation.closed) return;
@@ -8559,6 +8567,24 @@ function hasToJsonProperty(value) {
8559
8567
  return false;
8560
8568
  }
8561
8569
 
8570
+ // src/session-source.ts
8571
+ var NOODLE_CLOUD_URL = "https://cloud.noodleseed.dev";
8572
+ var PUBLIC_SESSION_PATH = "/v1/assistant/public-sessions";
8573
+ function resolveSessionSource(options) {
8574
+ const { sessionEndpoint, embedId, serviceUrl } = options;
8575
+ if (Boolean(embedId) === Boolean(sessionEndpoint)) {
8576
+ throw new Error("pass either embedId or sessionEndpoint, not both and not neither");
8577
+ }
8578
+ if (embedId) {
8579
+ const base = (serviceUrl ?? NOODLE_CLOUD_URL).replace(/\/+$/, "");
8580
+ return { kind: "public", url: `${base}${PUBLIC_SESSION_PATH}`, embedId };
8581
+ }
8582
+ return { kind: "exchange", url: sessionEndpoint };
8583
+ }
8584
+ function sessionSourceKey(options) {
8585
+ return `${options.embedId ?? ""}|${options.serviceUrl ?? ""}|${options.sessionEndpoint ?? ""}`;
8586
+ }
8587
+
8562
8588
  // src/transport.ts
8563
8589
  var AssistantTransportError = class extends Error {
8564
8590
  code = "invalid_response";
@@ -8686,6 +8712,19 @@ function isAbortError2(error) {
8686
8712
  }
8687
8713
 
8688
8714
  // src/client.ts
8715
+ async function refusalCode(response) {
8716
+ try {
8717
+ const body = await response.clone().json();
8718
+ const code = body.code;
8719
+ return typeof code === "string" ? code : void 0;
8720
+ } catch {
8721
+ return void 0;
8722
+ }
8723
+ }
8724
+ var UNRETRYABLE_SERVICE_CODES = /* @__PURE__ */ new Set([
8725
+ "daily_turn_budget_exhausted",
8726
+ "daily_session_budget_exhausted"
8727
+ ]);
8689
8728
  var AssistantClientError = class extends Error {
8690
8729
  detail;
8691
8730
  constructor(detail, message, options) {
@@ -8695,7 +8734,7 @@ var AssistantClientError = class extends Error {
8695
8734
  }
8696
8735
  };
8697
8736
  var DefaultAssistantClient = class {
8698
- #sessionEndpoint;
8737
+ #source;
8699
8738
  #fetch;
8700
8739
  #listeners = /* @__PURE__ */ new Set();
8701
8740
  #chat = new AssistantChatStateStore();
@@ -8705,9 +8744,9 @@ var DefaultAssistantClient = class {
8705
8744
  #context;
8706
8745
  #modelContext;
8707
8746
  #active;
8747
+ #pendingAppInteractions = /* @__PURE__ */ new Map();
8708
8748
  constructor(options) {
8709
- if (!options.sessionEndpoint) throw new Error("sessionEndpoint is required");
8710
- this.#sessionEndpoint = options.sessionEndpoint;
8749
+ this.#source = resolveSessionSource(options);
8711
8750
  this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
8712
8751
  this.#context = options.context ? { ...options.context } : void 0;
8713
8752
  this.#modelContext = options.modelContext ? copyAssistantModelContext(options.modelContext) : void 0;
@@ -8746,6 +8785,7 @@ var DefaultAssistantClient = class {
8746
8785
  }
8747
8786
  await this.#singleFlight(async (signal) => {
8748
8787
  await this.#runChatOperation(signal, async () => {
8788
+ const pendingApp = this.#pendingAppInteractions.get(id);
8749
8789
  const session = this.#session;
8750
8790
  if (!session) {
8751
8791
  throw clientError("confirmation_expired", "assistant session is not active", false);
@@ -8765,32 +8805,67 @@ var DefaultAssistantClient = class {
8765
8805
  ...resolution.action === "accept" && resolution.content !== void 0 ? { content: resolution.content } : {}
8766
8806
  } : { id };
8767
8807
  this.#emit({ event: "interaction_started", data: { id, action: resolution.action } });
8768
- const response = await this.#request(
8769
- endpoint,
8770
- {
8771
- method: "POST",
8772
- headers: authorizedHeaders(session.token, "text/event-stream"),
8773
- body: JSON.stringify(body),
8774
- signal
8775
- },
8776
- "confirmation_failed"
8777
- );
8778
- if (response.status === 401) {
8779
- this.#session = void 0;
8780
- this.#emit({ event: "session_expired", data: {} });
8781
- }
8782
- if (!response.ok) {
8783
- const serverCode = await readStableErrorCode(response);
8784
- const expired = response.status === 401 || response.status === 409;
8785
- throw clientError(
8786
- serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
8787
- `Assistant interaction failed (${response.status})`,
8788
- false,
8789
- response.status
8808
+ try {
8809
+ const response = await this.#request(
8810
+ endpoint,
8811
+ {
8812
+ method: "POST",
8813
+ headers: authorizedHeaders(session.token, "text/event-stream"),
8814
+ body: JSON.stringify(body),
8815
+ signal
8816
+ },
8817
+ "confirmation_failed"
8790
8818
  );
8819
+ if (response.status === 401) {
8820
+ this.#session = void 0;
8821
+ this.#emit({ event: "session_expired", data: {} });
8822
+ }
8823
+ if (!response.ok) {
8824
+ const serverCode = await readStableErrorCode(response);
8825
+ const expired = response.status === 401 || response.status === 409;
8826
+ throw clientError(
8827
+ serverCode ?? (expired ? "confirmation_expired" : "confirmation_failed"),
8828
+ `Assistant interaction failed (${response.status})`,
8829
+ false,
8830
+ response.status
8831
+ );
8832
+ }
8833
+ let completedResult;
8834
+ let nextInteractionId;
8835
+ await this.#consume(response, (event) => {
8836
+ if (event.event === "tool_completed" && event.data.id === id) {
8837
+ completedResult = event.data.result;
8838
+ }
8839
+ if ((event.event === "tool_proposed" || event.event === "input_requested") && event.data.id !== id) {
8840
+ nextInteractionId = event.data.id;
8841
+ }
8842
+ });
8843
+ this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
8844
+ if (pendingApp) {
8845
+ this.#pendingAppInteractions.delete(id);
8846
+ if (nextInteractionId) {
8847
+ this.#pendingAppInteractions.set(nextInteractionId, pendingApp);
8848
+ } else if (resolution.action !== "accept") {
8849
+ pendingApp.resolve(appInteractionStopped(resolution.action));
8850
+ } else if (completedResult !== void 0) {
8851
+ pendingApp.resolve(appToolResult(completedResult));
8852
+ } else {
8853
+ pendingApp.reject(
8854
+ clientError(
8855
+ "invalid_response",
8856
+ "Assistant app interaction completed without a tool result",
8857
+ false
8858
+ )
8859
+ );
8860
+ }
8861
+ }
8862
+ } catch (error) {
8863
+ if (pendingApp && this.#pendingAppInteractions.get(id) === pendingApp) {
8864
+ this.#pendingAppInteractions.delete(id);
8865
+ pendingApp.reject(error);
8866
+ }
8867
+ throw error;
8791
8868
  }
8792
- await this.#consume(response);
8793
- this.#emit({ event: "interaction_completed", data: { id, action: resolution.action } });
8794
8869
  });
8795
8870
  });
8796
8871
  }
@@ -8831,12 +8906,29 @@ var DefaultAssistantClient = class {
8831
8906
  response.status
8832
8907
  );
8833
8908
  }
8834
- return response.json();
8909
+ const value = await response.json();
8910
+ const interaction = parseAppInteraction(value);
8911
+ if (!interaction) return value;
8912
+ return new Promise((resolve2, reject) => {
8913
+ const id = interaction.data.id;
8914
+ const replaced = this.#pendingAppInteractions.get(id);
8915
+ if (replaced) {
8916
+ replaced.reject(
8917
+ clientError("invalid_response", "Assistant returned a duplicate app interaction", false)
8918
+ );
8919
+ }
8920
+ this.#pendingAppInteractions.set(id, { resolve: resolve2, reject });
8921
+ this.#emit(interaction);
8922
+ void this.#chat.flush();
8923
+ });
8835
8924
  }
8836
8925
  appSandboxUrl() {
8837
8926
  return this.#session?.endpoints.sandbox;
8838
8927
  }
8839
8928
  resetSession() {
8929
+ this.#rejectPendingAppInteractions(
8930
+ clientError("confirmation_expired", "assistant session is not active", false)
8931
+ );
8840
8932
  this.#session = void 0;
8841
8933
  this.#emit({ event: "session_reset", data: {} });
8842
8934
  }
@@ -8870,11 +8962,14 @@ var DefaultAssistantClient = class {
8870
8962
  return;
8871
8963
  }
8872
8964
  if (!response.ok) {
8965
+ const serviceCode = await refusalCode(response);
8873
8966
  throw clientError(
8874
8967
  "turn_failed",
8875
8968
  `Assistant turn failed (${response.status})`,
8876
8969
  false,
8877
- response.status
8970
+ response.status,
8971
+ void 0,
8972
+ serviceCode
8878
8973
  );
8879
8974
  }
8880
8975
  await this.#consume(response);
@@ -8913,23 +9008,25 @@ var DefaultAssistantClient = class {
8913
9008
  }
8914
9009
  }
8915
9010
  async #createSession(signal) {
8916
- const response = await this.#request(
8917
- this.#sessionEndpoint,
8918
- {
8919
- method: "POST",
8920
- headers: { Accept: "application/json", "Content-Type": "application/json" },
8921
- body: JSON.stringify(this.#context ? { context: this.#context } : {}),
8922
- credentials: "same-origin",
8923
- signal
8924
- },
8925
- "session_failed"
8926
- );
9011
+ const source = this.#source;
9012
+ const response = await this.#requestSession(source, {
9013
+ method: "POST",
9014
+ headers: { Accept: "application/json", "Content-Type": "application/json" },
9015
+ body: JSON.stringify(
9016
+ source.kind === "public" ? { embedId: source.embedId } : this.#context ? { context: this.#context } : {}
9017
+ ),
9018
+ credentials: source.kind === "public" ? "omit" : "same-origin",
9019
+ signal
9020
+ });
8927
9021
  if (!response.ok) {
9022
+ const serviceCode = await refusalCode(response);
8928
9023
  throw clientError(
8929
9024
  "session_failed",
8930
9025
  `Assistant session failed (${response.status})`,
8931
- true,
8932
- response.status
9026
+ serviceCode === void 0 || !UNRETRYABLE_SERVICE_CODES.has(serviceCode),
9027
+ response.status,
9028
+ void 0,
9029
+ serviceCode
8933
9030
  );
8934
9031
  }
8935
9032
  let value;
@@ -8957,12 +9054,13 @@ var DefaultAssistantClient = class {
8957
9054
  });
8958
9055
  return session;
8959
9056
  }
8960
- async #consume(response) {
9057
+ async #consume(response, observe) {
8961
9058
  let streamError;
8962
9059
  try {
8963
9060
  await consumeAssistantEvents(response, (event) => {
8964
9061
  const clientEvent = toAssistantClientEvent(event);
8965
9062
  this.#emit(clientEvent);
9063
+ observe?.(clientEvent);
8966
9064
  if (clientEvent.event !== "error" || typeof clientEvent.data.code !== "string") return;
8967
9065
  streamError = {
8968
9066
  code: clientEvent.data.code,
@@ -8985,6 +9083,34 @@ var DefaultAssistantClient = class {
8985
9083
  );
8986
9084
  }
8987
9085
  }
9086
+ /**
9087
+ * The mint, with the one failure a public page hits that an in-app embed cannot.
9088
+ *
9089
+ * A page that loads the embed script and then blocks `connect-src` fails here as a bare network
9090
+ * rejection — no status, no body, nothing to read. "Assistant request failed" sends a developer
9091
+ * hunting through their own code; naming the directive and the origin ends the search. The message
9092
+ * covers a plain outage too, because from inside the browser the two are indistinguishable.
9093
+ */
9094
+ async #requestSession(source, init) {
9095
+ try {
9096
+ return await this.#fetch(source.url, init);
9097
+ } catch (error) {
9098
+ if (init.signal?.aborted) throw error;
9099
+ if (source.kind !== "public") {
9100
+ throw clientError("session_failed", "Assistant request failed", true, void 0, {
9101
+ cause: error
9102
+ });
9103
+ }
9104
+ throw clientError(
9105
+ "session_failed",
9106
+ `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).`,
9107
+ true,
9108
+ void 0,
9109
+ { cause: error },
9110
+ "blocked_by_page"
9111
+ );
9112
+ }
9113
+ }
8988
9114
  async #request(input, init, failureCode) {
8989
9115
  try {
8990
9116
  return await this.#fetch(input, init);
@@ -9031,6 +9157,10 @@ var DefaultAssistantClient = class {
9031
9157
  }
9032
9158
  }
9033
9159
  }
9160
+ #rejectPendingAppInteractions(error) {
9161
+ for (const pending of this.#pendingAppInteractions.values()) pending.reject(error);
9162
+ this.#pendingAppInteractions.clear();
9163
+ }
9034
9164
  };
9035
9165
  function createAssistantClient(options) {
9036
9166
  return new DefaultAssistantClient(options);
@@ -9042,6 +9172,30 @@ function authorizedHeaders(token, accept) {
9042
9172
  Accept: accept
9043
9173
  };
9044
9174
  }
9175
+ function parseAppInteraction(value) {
9176
+ if (!isRecord4(value) || !isRecord4(value.interaction)) return void 0;
9177
+ const event = value.interaction.event;
9178
+ const data = value.interaction.data;
9179
+ if (event !== "tool_proposed" && event !== "input_requested" || !isRecord4(data)) {
9180
+ return void 0;
9181
+ }
9182
+ const parsed = toAssistantClientEvent({ event, data });
9183
+ return parsed.event === "tool_proposed" || parsed.event === "input_requested" ? parsed : void 0;
9184
+ }
9185
+ function appToolResult(result) {
9186
+ const text2 = typeof result === "string" ? result : JSON.stringify(result);
9187
+ return result !== null && typeof result === "object" && !Array.isArray(result) ? {
9188
+ content: [{ type: "text", text: text2 }],
9189
+ structuredContent: result,
9190
+ isError: false
9191
+ } : { content: [{ type: "text", text: text2 }], isError: false };
9192
+ }
9193
+ function appInteractionStopped(action) {
9194
+ return {
9195
+ content: [{ type: "text", text: `interaction_${action}` }],
9196
+ isError: true
9197
+ };
9198
+ }
9045
9199
  function parseSession(value) {
9046
9200
  if (!isRecord4(value) || !isRecord4(value.endpoints)) {
9047
9201
  throw clientError("session_failed", "Assistant session response is invalid", true);
@@ -9078,9 +9232,14 @@ async function readStableErrorCode(response) {
9078
9232
  if (!isRecord4(value) || typeof value.code !== "string") return void 0;
9079
9233
  return /^[A-Za-z0-9_.-]{1,64}$/.test(value.code) ? value.code : void 0;
9080
9234
  }
9081
- function clientError(code, message, retryable, status, options) {
9235
+ function clientError(code, message, retryable, status, options, serviceCode) {
9082
9236
  return new AssistantClientError(
9083
- { code, ...status === void 0 ? {} : { status }, retryable },
9237
+ {
9238
+ code,
9239
+ ...status === void 0 ? {} : { status },
9240
+ retryable,
9241
+ ...serviceCode === void 0 ? {} : { serviceCode }
9242
+ },
9084
9243
  message,
9085
9244
  options
9086
9245
  );
@@ -9088,7 +9247,8 @@ function clientError(code, message, retryable, status, options) {
9088
9247
 
9089
9248
  export {
9090
9249
  copyAssistantModelContext,
9250
+ sessionSourceKey,
9091
9251
  AssistantClientError,
9092
9252
  createAssistantClient
9093
9253
  };
9094
- //# sourceMappingURL=chunk-KRB2SXFD.js.map
9254
+ //# sourceMappingURL=chunk-RRJIK473.js.map