@mastra/client-js 1.22.0-alpha.5 → 1.22.0-alpha.6

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/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @mastra/client-js
2
2
 
3
+ ## 1.22.0-alpha.6
4
+
5
+ ### Patch Changes
6
+
7
+ - Separated thread subscription cleanup from active-run aborts so closing or switching a listener only unsubscribes that listener, while explicit cancel still aborts the active run. ([#17310](https://github.com/mastra-ai/mastra/pull/17310))
8
+
9
+ - Added subscription-native tool approval APIs so approving or declining a tool call resumes through the active thread subscription instead of requiring a separate continuation stream. New messages are queued while a tool approval is waiting, preventing overlapping runs from duplicating approval requests. ([#17311](https://github.com/mastra-ai/mastra/pull/17311))
10
+
11
+ ```ts
12
+ await agent.sendToolApproval({
13
+ resourceId: 'user-123',
14
+ threadId: 'thread-123',
15
+ toolCallId: 'tool-call-123',
16
+ approved: true,
17
+ });
18
+ ```
19
+
20
+ - Updated dependencies [[`19a8658`](https://github.com/mastra-ai/mastra/commit/19a86589c788ef48bb6c1b0612cc82a201857379), [`a659a77`](https://github.com/mastra-ai/mastra/commit/a659a779bdebe3a52a518c56d2260592d0240fe0), [`3332be9`](https://github.com/mastra-ai/mastra/commit/3332be9701ecd77aba840959d9a1d1ce7aef02d3)]:
21
+ - @mastra/core@1.38.0-alpha.6
22
+
3
23
  ## 1.22.0-alpha.5
4
24
 
5
25
  ### Minor Changes
@@ -3,7 +3,7 @@ name: mastra-client-js
3
3
  description: Documentation for @mastra/client-js. Use when working with @mastra/client-js APIs, configuration, or implementation.
4
4
  metadata:
5
5
  package: "@mastra/client-js"
6
- version: "1.22.0-alpha.5"
6
+ version: "1.22.0-alpha.6"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.22.0-alpha.5",
2
+ "version": "1.22.0-alpha.6",
3
3
  "package": "@mastra/client-js",
4
4
  "exports": {
5
5
  "RequestContext": {
@@ -233,9 +233,24 @@ Existing stored signal rows and older clients continue to load through the compa
233
233
 
234
234
  > **Note:** Visit [Agent signals reference](https://mastra.ai/reference/agents/agent) for the full message, signal, and subscription types.
235
235
 
236
+ ## Approve tool calls
237
+
238
+ When a subscribed run pauses for tool approval, approve or decline the tool call with the subscription-native methods. The call returns a JSON acknowledgement. The resumed chunks arrive through the existing thread subscription.
239
+
240
+ ```typescript
241
+ await agent.sendToolApproval({
242
+ resourceId: 'user_123',
243
+ threadId: 'thread_456',
244
+ toolCallId: 'tool-call_456',
245
+ approved: true,
246
+ })
247
+ ```
248
+
249
+ Pass `approved: false` to decline the same pending tool call. Use the older `approveToolCall()` and `declineToolCall()` methods only when you are rendering the separate continuation stream directly.
250
+
236
251
  ## Use HTTP routes
237
252
 
238
- If you call Mastra over HTTP directly, use `POST /api/agents/:agentId/send-message` for immediate messages and `POST /api/agents/:agentId/queue-message` for next-turn messages. See [Server routes reference](https://mastra.ai/reference/server/routes) for request and response schemas.
253
+ If you call Mastra over HTTP directly, use `POST /api/agents/:agentId/send-message` for immediate messages and `POST /api/agents/:agentId/queue-message` for next-turn messages. For subscription-native tool approval, use `POST /api/agents/:agentId/send-tool-approval`. See [Server routes reference](https://mastra.ai/reference/server/routes) for request and response schemas.
239
254
 
240
255
  ## Use the client SDK
241
256
 
@@ -294,4 +309,5 @@ Use heartbeats together with client-side reconnect logic. Heartbeats reduce idle
294
309
  - [`Agent.subscribeToThread()`](https://mastra.ai/reference/agents/agent)
295
310
  - [Server agent routes](https://mastra.ai/reference/server/routes)
296
311
  - [`client.getAgent().sendSignal()`](https://mastra.ai/reference/client-js/agents)
297
- - [`client.getAgent().subscribeToThread()`](https://mastra.ai/reference/client-js/agents)
312
+ - [`client.getAgent().subscribeToThread()`](https://mastra.ai/reference/client-js/agents)
313
+ - [`client.getAgent().sendToolApproval()`](https://mastra.ai/reference/client-js/agents)
@@ -326,7 +326,7 @@ response.processDataStream({
326
326
 
327
327
  ### `approveToolCall()`
328
328
 
329
- Approve a pending tool call that requires human confirmation:
329
+ Approve a pending tool call and return a continuation stream. Use this when you are rendering the resumed chunks from the approval response.
330
330
 
331
331
  ```typescript
332
332
  const response = await agent.approveToolCall({
@@ -341,9 +341,26 @@ response.processDataStream({
341
341
  })
342
342
  ```
343
343
 
344
+ ### `sendToolApproval()`
345
+
346
+ Approve or decline a pending tool call for a subscribed thread. Use this with `subscribeToThread()` when the resumed chunks should arrive through the existing thread subscription instead of a separate continuation stream.
347
+
348
+ ```typescript
349
+ const result = await agent.sendToolApproval({
350
+ resourceId: 'user-123',
351
+ threadId: 'thread-456',
352
+ toolCallId: 'tool-call-456',
353
+ approved: true,
354
+ })
355
+
356
+ console.log(result.accepted)
357
+ ```
358
+
359
+ Returns `{ accepted: true, runId: string, toolCallId?: string }`.
360
+
344
361
  ### `declineToolCall()`
345
362
 
346
- Decline a pending tool call that requires human confirmation:
363
+ Decline a pending tool call and return a continuation stream. Use this when you are rendering the resumed chunks from the decline response.
347
364
 
348
365
  ```typescript
349
366
  const response = await agent.declineToolCall({
package/dist/index.cjs CHANGED
@@ -862,173 +862,216 @@ var Agent = class extends BaseResource {
862
862
  throw new Error("No response body");
863
863
  }
864
864
  const agent = this;
865
+ streamResponse.abort = async () => (await agent.abortThread({ resourceId, threadId })).aborted;
866
+ let unsubscribed = false;
867
+ let processAbortController;
868
+ let processStarted = false;
869
+ streamResponse.unsubscribe = () => {
870
+ if (unsubscribed) return;
871
+ unsubscribed = true;
872
+ processAbortController?.abort();
873
+ if (!processStarted) {
874
+ void streamResponse.body?.cancel().catch(() => {
875
+ });
876
+ }
877
+ };
865
878
  streamResponse.processDataStream = async ({ onChunk, reconnect }) => {
866
- const pendingToolCallsByRunId = /* @__PURE__ */ new Map();
867
- const handleSubscribedChunk = async (chunk) => {
868
- if (chunk.type === "tool-call") {
869
- const payload = chunk.payload;
870
- const toolCallId = payload?.toolCallId;
871
- const toolName = payload?.toolName;
872
- const runId2 = chunk.runId;
873
- if (!toolCallId || !toolName || !runId2) return;
874
- const pendingToolCalls2 = pendingToolCallsByRunId.get(runId2) ?? [];
875
- pendingToolCalls2.push({ toolCallId, toolName, args: payload.args, observability: payload.observability });
876
- pendingToolCallsByRunId.set(runId2, pendingToolCalls2);
877
- return;
878
- }
879
- if (chunk.type !== "finish") return;
880
- const runId = chunk.runId;
881
- const finishPayload = chunk;
882
- if (!runId) return;
883
- if (finishPayload.payload?.stepResult?.reason !== "tool-calls") {
884
- agent.deleteSignalRuntimeOptions(runId);
885
- return;
886
- }
887
- const pendingToolCalls = pendingToolCallsByRunId.get(runId);
888
- pendingToolCallsByRunId.delete(runId);
889
- if (!pendingToolCalls?.length) {
890
- agent.deleteSignalRuntimeOptions(runId);
891
- return;
892
- }
893
- const activeRuntimeOptions = agent.getSignalRuntimeOptions({ runId, resourceId, threadId });
894
- const activeClientTools = activeRuntimeOptions?.clientTools;
895
- if (!activeClientTools) {
896
- agent.deleteSignalRuntimeOptions(runId);
897
- return;
898
- }
899
- const activeRequestContext = activeRuntimeOptions.requestContext;
900
- const processedClientTools = processClientTools(activeClientTools);
901
- const processedRequestContext = parseClientRequestContext(activeRequestContext);
902
- const toolResultMessages = [];
903
- for (const toolCall of pendingToolCalls) {
904
- const clientTool = activeClientTools[toolCall.toolName];
905
- if (!clientTool || typeof clientTool.execute !== "function") continue;
906
- let result;
907
- let observability;
908
- try {
909
- const execution = await executeClientToolWithObservability({
910
- clientTool,
911
- args: toolCall.args,
912
- toolName: toolCall.toolName,
913
- parentContext: toolCall.observability,
914
- executeContext: {
915
- requestContext: activeRequestContext,
916
- tracingContext: { currentSpan: void 0 },
917
- agent: {
918
- agentId: agent.agentId,
919
- messages: finishPayload.payload?.messages?.nonUser ?? [],
920
- toolCallId: toolCall.toolCallId,
921
- suspend: async () => {
922
- },
923
- threadId,
924
- resourceId
925
- }
926
- }
927
- });
928
- result = execution.result;
929
- observability = execution.observability;
930
- } catch (error) {
931
- result = { error: String(error) };
879
+ if (unsubscribed) return;
880
+ processStarted = true;
881
+ processAbortController = new AbortController();
882
+ const abortProcessStream = () => processAbortController?.abort();
883
+ const isClosed = () => unsubscribed || processAbortController?.signal.aborted || this.options.abortSignal?.aborted;
884
+ if (this.options.abortSignal?.aborted) {
885
+ abortProcessStream();
886
+ } else {
887
+ this.options.abortSignal?.addEventListener("abort", abortProcessStream, { once: true });
888
+ }
889
+ try {
890
+ const pendingToolCallsByRunId = /* @__PURE__ */ new Map();
891
+ const handleSubscribedChunk = async (chunk) => {
892
+ if (chunk.type === "tool-call") {
893
+ const payload = chunk.payload;
894
+ const toolCallId = payload?.toolCallId;
895
+ const toolName = payload?.toolName;
896
+ const runId2 = chunk.runId;
897
+ if (!toolCallId || !toolName || !runId2) return;
898
+ const pendingToolCalls2 = pendingToolCallsByRunId.get(runId2) ?? [];
899
+ pendingToolCalls2.push({ toolCallId, toolName, args: payload.args, observability: payload.observability });
900
+ pendingToolCallsByRunId.set(runId2, pendingToolCalls2);
901
+ return;
932
902
  }
933
- const toolResultContent = {
934
- type: "tool-result",
935
- toolCallId: toolCall.toolCallId,
936
- toolName: toolCall.toolName,
937
- result
938
- };
939
- if (observability) {
940
- toolResultContent.__mastraObservability = observability;
903
+ if (chunk.type !== "finish") return;
904
+ const runId = chunk.runId;
905
+ const finishPayload = chunk;
906
+ if (!runId) return;
907
+ if (finishPayload.payload?.stepResult?.reason !== "tool-calls") {
908
+ agent.deleteSignalRuntimeOptions(runId);
909
+ return;
941
910
  }
942
- await onChunk({
943
- type: "tool-result",
944
- runId,
945
- payload: toolResultContent
946
- });
947
- toolResultMessages.push({
948
- role: "tool",
949
- content: [toolResultContent]
950
- });
951
- }
952
- if (toolResultMessages.length === 0) {
953
- agent.deleteSignalRuntimeOptions(runId);
954
- return;
955
- }
956
- try {
957
- const continuation = await agent.streamUntilIdle(
958
- [...finishPayload.payload?.messages?.nonUser ?? [], ...toolResultMessages],
959
- {
960
- ...activeRuntimeOptions,
961
- runId: uuid.v4(),
962
- requestContext: processedRequestContext,
963
- memory: threadId ? { thread: threadId, resource: resourceId } : void 0,
964
- clientTools: processedClientTools
965
- }
966
- );
967
- try {
968
- void continuation.body?.cancel?.();
969
- } catch {
911
+ const pendingToolCalls = pendingToolCallsByRunId.get(runId);
912
+ pendingToolCallsByRunId.delete(runId);
913
+ if (!pendingToolCalls?.length) {
914
+ agent.deleteSignalRuntimeOptions(runId);
915
+ return;
970
916
  }
971
- } catch (error) {
972
- console.error("Error running client-tool continuation:", error);
973
- } finally {
974
- agent.deleteSignalRuntimeOptions(runId);
975
- }
976
- };
977
- const reconnectOptions = reconnect === true ? { maxRetries: Infinity, delayMs: 1e3 } : reconnect ? { maxRetries: reconnect.maxRetries ?? Infinity, delayMs: reconnect.delayMs ?? 1e3 } : null;
978
- let response = streamResponse;
979
- let attempts = 0;
980
- const onChunkErrorSentinel = /* @__PURE__ */ Symbol("onChunkErrorSentinel");
981
- const guardedOnChunk = async (chunk) => {
982
- try {
983
- await onChunk(chunk);
984
- await handleSubscribedChunk(chunk);
985
- } catch (cause) {
986
- throw { [onChunkErrorSentinel]: true, cause };
987
- }
988
- };
989
- while (true) {
990
- if (!response.body) {
991
- throw new Error("No response body");
992
- }
993
- try {
994
- await processMastraStream({
995
- stream: response.body,
996
- onChunk: guardedOnChunk,
997
- signal: this.options.abortSignal
998
- });
999
- } catch (error) {
1000
- if (typeof error === "object" && error !== null && error[onChunkErrorSentinel]) {
1001
- throw error.cause;
917
+ const activeRuntimeOptions = agent.getSignalRuntimeOptions({ runId, resourceId, threadId });
918
+ const activeClientTools = activeRuntimeOptions?.clientTools;
919
+ if (!activeClientTools) {
920
+ agent.deleteSignalRuntimeOptions(runId);
921
+ return;
1002
922
  }
1003
- if (!reconnectOptions || this.options.abortSignal?.aborted || attempts >= reconnectOptions.maxRetries) {
1004
- throw error;
923
+ const activeRequestContext = activeRuntimeOptions.requestContext;
924
+ const processedClientTools = processClientTools(activeClientTools);
925
+ const processedRequestContext = parseClientRequestContext(activeRequestContext);
926
+ const toolResultMessages = [];
927
+ for (const toolCall of pendingToolCalls) {
928
+ const clientTool = activeClientTools[toolCall.toolName];
929
+ if (!clientTool || typeof clientTool.execute !== "function") continue;
930
+ let result;
931
+ let observability;
932
+ try {
933
+ const execution = await executeClientToolWithObservability({
934
+ clientTool,
935
+ args: toolCall.args,
936
+ toolName: toolCall.toolName,
937
+ parentContext: toolCall.observability,
938
+ executeContext: {
939
+ requestContext: activeRequestContext,
940
+ tracingContext: { currentSpan: void 0 },
941
+ agent: {
942
+ agentId: agent.agentId,
943
+ messages: finishPayload.payload?.messages?.nonUser ?? [],
944
+ toolCallId: toolCall.toolCallId,
945
+ suspend: async () => {
946
+ },
947
+ threadId,
948
+ resourceId
949
+ }
950
+ }
951
+ });
952
+ result = execution.result;
953
+ observability = execution.observability;
954
+ } catch (error) {
955
+ result = { error: String(error) };
956
+ }
957
+ const toolResultContent = {
958
+ type: "tool-result",
959
+ toolCallId: toolCall.toolCallId,
960
+ toolName: toolCall.toolName,
961
+ result
962
+ };
963
+ if (observability) {
964
+ toolResultContent.__mastraObservability = observability;
965
+ }
966
+ await onChunk({
967
+ type: "tool-result",
968
+ runId,
969
+ payload: toolResultContent
970
+ });
971
+ toolResultMessages.push({
972
+ role: "tool",
973
+ content: [toolResultContent]
974
+ });
1005
975
  }
1006
- }
1007
- if (!reconnectOptions || this.options.abortSignal?.aborted || attempts >= reconnectOptions.maxRetries) {
1008
- return;
1009
- }
1010
- while (attempts < reconnectOptions.maxRetries) {
1011
- attempts++;
1012
- if (this.options.abortSignal?.aborted) {
976
+ if (toolResultMessages.length === 0) {
977
+ agent.deleteSignalRuntimeOptions(runId);
1013
978
  return;
1014
979
  }
1015
- await new Promise((resolve) => setTimeout(resolve, reconnectOptions.delayMs));
1016
- if (this.options.abortSignal?.aborted) {
1017
- return;
980
+ try {
981
+ const continuation = await agent.streamUntilIdle(
982
+ [...finishPayload.payload?.messages?.nonUser ?? [], ...toolResultMessages],
983
+ {
984
+ ...activeRuntimeOptions,
985
+ runId: uuid.v4(),
986
+ requestContext: processedRequestContext,
987
+ memory: threadId ? { thread: threadId, resource: resourceId } : void 0,
988
+ clientTools: processedClientTools
989
+ }
990
+ );
991
+ try {
992
+ void continuation.body?.cancel?.();
993
+ } catch {
994
+ }
995
+ } catch (error) {
996
+ console.error("Error running client-tool continuation:", error);
997
+ } finally {
998
+ agent.deleteSignalRuntimeOptions(runId);
1018
999
  }
1000
+ };
1001
+ const reconnectOptions = reconnect === true ? { maxRetries: Infinity, delayMs: 1e3 } : reconnect ? { maxRetries: reconnect.maxRetries ?? Infinity, delayMs: reconnect.delayMs ?? 1e3 } : null;
1002
+ let response = streamResponse;
1003
+ let attempts = 0;
1004
+ const onChunkErrorSentinel = /* @__PURE__ */ Symbol("onChunkErrorSentinel");
1005
+ const guardedOnChunk = async (chunk) => {
1019
1006
  try {
1020
- response = await requestSubscription();
1021
- break;
1007
+ await onChunk(chunk);
1008
+ await handleSubscribedChunk(chunk);
1009
+ } catch (cause) {
1010
+ throw { [onChunkErrorSentinel]: true, cause };
1011
+ }
1012
+ };
1013
+ while (true) {
1014
+ if (!response.body) {
1015
+ throw new Error("No response body");
1016
+ }
1017
+ try {
1018
+ await processMastraStream({
1019
+ stream: response.body,
1020
+ onChunk: guardedOnChunk,
1021
+ signal: processAbortController.signal
1022
+ });
1022
1023
  } catch (error) {
1023
- if (this.options.abortSignal?.aborted || attempts >= reconnectOptions.maxRetries) {
1024
+ if (typeof error === "object" && error !== null && error[onChunkErrorSentinel]) {
1025
+ throw error.cause;
1026
+ }
1027
+ if (!reconnectOptions || isClosed() || attempts >= reconnectOptions.maxRetries) {
1028
+ if (isClosed()) return;
1024
1029
  throw error;
1025
1030
  }
1026
1031
  }
1032
+ if (!reconnectOptions || isClosed() || attempts >= reconnectOptions.maxRetries) {
1033
+ return;
1034
+ }
1035
+ while (attempts < reconnectOptions.maxRetries) {
1036
+ attempts++;
1037
+ if (isClosed()) {
1038
+ return;
1039
+ }
1040
+ await new Promise((resolve) => setTimeout(resolve, reconnectOptions.delayMs));
1041
+ if (isClosed()) {
1042
+ return;
1043
+ }
1044
+ try {
1045
+ response = await requestSubscription();
1046
+ break;
1047
+ } catch (error) {
1048
+ if (isClosed() || attempts >= reconnectOptions.maxRetries) {
1049
+ if (isClosed()) return;
1050
+ throw error;
1051
+ }
1052
+ }
1053
+ }
1054
+ }
1055
+ } finally {
1056
+ this.options.abortSignal?.removeEventListener("abort", abortProcessStream);
1057
+ if (processAbortController?.signal.aborted) {
1058
+ unsubscribed = true;
1027
1059
  }
1060
+ processAbortController = void 0;
1028
1061
  }
1029
1062
  };
1030
1063
  return streamResponse;
1031
1064
  }
1065
+ /**
1066
+ * @experimental Agent signals are experimental and may change in a future release.
1067
+ */
1068
+ async abortThread(params) {
1069
+ const { resourceId, threadId } = params;
1070
+ return this.request(`/agents/${this.agentId}/threads/abort`, {
1071
+ method: "POST",
1072
+ body: { resourceId, threadId }
1073
+ });
1074
+ }
1032
1075
  /**
1033
1076
  * Clones this agent to a new stored agent in the database
1034
1077
  * @param params - Clone parameters including optional newId, newName, metadata, authorId, and requestContext
@@ -2220,6 +2263,16 @@ var Agent = class extends BaseResource {
2220
2263
  };
2221
2264
  return streamResponse;
2222
2265
  }
2266
+ async sendToolApproval(params) {
2267
+ const { requestContext, ...rest } = params;
2268
+ return this.request(
2269
+ `/agents/${this.agentId}/send-tool-approval`,
2270
+ {
2271
+ method: "POST",
2272
+ body: { ...rest, requestContext: parseClientRequestContext(requestContext) }
2273
+ }
2274
+ );
2275
+ }
2223
2276
  async declineToolCall(params) {
2224
2277
  const { requestContext, ...rest } = params;
2225
2278
  const processedParams = { ...rest, requestContext: parseClientRequestContext(requestContext) };