@mastra/client-js 1.21.0-alpha.7 → 1.21.0-alpha.8

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,54 @@
1
1
  # @mastra/client-js
2
2
 
3
+ ## 1.21.0-alpha.8
4
+
5
+ ### Patch Changes
6
+
7
+ - Made optional memory response fields optional in server schemas and generated client types. ([#17070](https://github.com/mastra-ai/mastra/pull/17070))
8
+
9
+ - Add `StoredSkill.favorite()` and `StoredSkill.unfavorite()` methods, mirroring the existing `StoredAgent` favorite API. Both are idempotent and call `PUT`/`DELETE /api/stored/skills/:id/favorite`. ([#17101](https://github.com/mastra-ai/mastra/pull/17101))
10
+
11
+ - Agent Builder action routes (`/agent-builder/*`) are now registered automatically through the standard server route pipeline. Any adapter built on `@mastra/server` (Hono, Express, Fastify, Koa, etc.) serves the 15 `/agent-builder/*` endpoints without consumers wiring them manually. ([#17085](https://github.com/mastra-ai/mastra/pull/17085))
12
+
13
+ **Example**
14
+
15
+ ```ts
16
+ import { MastraClient } from '@mastra/client-js';
17
+
18
+ const client = new MastraClient({ baseUrl: 'http://localhost:4111' });
19
+
20
+ // `/agent-builder/*` routes are now reachable out-of-the-box
21
+ const actions = await client.getAgentBuilderActions();
22
+
23
+ const action = client.getAgentBuilderAction('generate-agent');
24
+ const { runId } = await action.createRun();
25
+ const result = await action.startAsync({ inputData: { prompt: 'Build me an agent' } }, runId);
26
+ ```
27
+
28
+ **Why**
29
+
30
+ Previously, `AGENT_BUILDER_ROUTES` was a type-only entry in the route registry to keep `@mastra/agent-builder` out of Cloudflare worker bundles. Consumers had to register the routes themselves to expose Agent Builder functionality. Lazy-loading of `@mastra/agent-builder` is preserved — handlers still resolve the workflow module on first request via dynamic `import()`, so Cloudflare bundles are unaffected.
31
+
32
+ **New EE permissions**
33
+
34
+ The following permissions are added to the EE registry. RBAC consumers with strict allowlists must grant these to retain access to builder action routes:
35
+ - `agent-builder:read`
36
+ - `agent-builder:write`
37
+ - `agent-builder:execute`
38
+
39
+ Two legacy stream routes (`STREAM_LEGACY_AGENT_BUILDER_ACTION_ROUTE`, `OBSERVE_STREAM_LEGACY_AGENT_BUILDER_ACTION_ROUTE`) are now registered through the standard pipeline as well.
40
+
41
+ - Improved agent thread subscription resilience by keeping server streams active during idle periods and allowing the JavaScript client to reconnect when subscription streams close or resubscribe requests fail. ([#17045](https://github.com/mastra-ai/mastra/pull/17045))
42
+
43
+ Enable automatic reconnection with `subscription.processDataStream({ onChunk: chunk => console.log(chunk), reconnect: true })`.
44
+
45
+ - Fixed `clientTools` being silently dropped — and never executed — on thread-backed chats. When a chat had a `threadId`, the React `useChat` hook routed messages through the new agent signals path but did not pass the `clientTools` map into the signal startup flow, so client-side tools were unavailable when the model requested them. ([#16540](https://github.com/mastra-ai/mastra/pull/16540))
46
+
47
+ The signals path now carries `clientTools` and other per-send stream options on `sendSignal`. When the subscribed stream finishes with `tool-calls`, the client executes matching local tools with observability support, emits tool result chunks, and posts a continuation with the assistant tool-call messages plus tool-result messages so the run resumes on the same thread with the same per-send options.
48
+
49
+ - Updated dependencies [[`c35b962`](https://github.com/mastra-ai/mastra/commit/c35b9625c7e854fcfdeee226a3338a750d0ff211), [`4084113`](https://github.com/mastra-ai/mastra/commit/408411370fc48a822e8b616b3b63f9409774e0e9)]:
50
+ - @mastra/core@1.37.0-alpha.8
51
+
3
52
  ## 1.21.0-alpha.7
4
53
 
5
54
  ### Patch 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.21.0-alpha.7"
6
+ version: "1.21.0-alpha.8"
7
7
  ---
8
8
 
9
9
  ## When to use
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.21.0-alpha.7",
2
+ "version": "1.21.0-alpha.8",
3
3
  "package": "@mastra/client-js",
4
4
  "exports": {
5
5
  "RequestContext": {
@@ -204,9 +204,30 @@ await subscription.processDataStream({
204
204
  onChunk: chunk => {
205
205
  console.log(chunk)
206
206
  },
207
+ reconnect: true,
207
208
  })
208
209
  ```
209
210
 
211
+ Use `reconnect: true` for long-lived subscriptions. The client resubscribes when the stream closes or a reconnect request fails, such as after a proxy idle timeout or a dropped network connection.
212
+
213
+ ## Keep custom SSE subscriptions alive
214
+
215
+ If you expose your own Server-Sent Events (SSE) endpoint for thread subscriptions, send periodic heartbeat frames while the stream is idle. This keeps browsers, proxies, and load balancers from closing the connection before the next signal or model chunk arrives.
216
+
217
+ The following example sends an SSE comment every 25 seconds:
218
+
219
+ ```typescript
220
+ const heartbeat = setInterval(() => {
221
+ controller.enqueue(encoder.encode(': keep-alive\n\n'))
222
+ }, 25_000)
223
+
224
+ request.signal.addEventListener('abort', () => {
225
+ clearInterval(heartbeat)
226
+ })
227
+ ```
228
+
229
+ Use heartbeats together with client-side reconnect logic. Heartbeats reduce idle disconnects, while reconnects recover when the network or runtime still closes the stream.
230
+
210
231
  ## Related
211
232
 
212
233
  - [`Agent.sendSignal()`](https://mastra.ai/reference/agents/agent)
@@ -228,18 +228,21 @@ const subscription = await agent.subscribeToThread({
228
228
  })
229
229
 
230
230
  await subscription.processDataStream({
231
- onChunk: async chunk => {
231
+ onChunk: chunk => {
232
232
  console.log(chunk)
233
233
  },
234
+ reconnect: true,
234
235
  })
235
236
  ```
236
237
 
237
- `subscribeToThread()` returns the underlying `Response` plus a `processDataStream()` helper. The helper reads the subscription stream until the connection closes or the request is aborted.
238
+ `subscribeToThread()` returns the underlying `Response` plus a `processDataStream()` helper. The helper reads the subscription stream until the connection closes or the request is aborted. Pass `reconnect: true` to resubscribe when the transport closes or a reconnect request fails, such as after a proxy idle timeout.
238
239
 
239
240
  **resourceId** (`string`): Resource ID for the memory thread.
240
241
 
241
242
  **threadId** (`string`): Thread ID to subscribe to.
242
243
 
244
+ **processDataStream().reconnect** (`boolean | { maxRetries?: number; delayMs?: number }`): Reconnects the subscription stream after it closes or a reconnect request fails. \`true\` retries indefinitely with a one-second delay.
245
+
243
246
  ### `streamUntilIdle()`
244
247
 
245
248
  Stream a response and keep the stream open until every [background task](https://mastra.ai/docs/agents/background-tasks) dispatched during the run completes. The server re-enters the agentic loop on each task completion so the LLM can react to results in the same call. Requires background tasks to be [enabled on the Mastra instance](https://mastra.ai/reference/configuration) and a memory thread; otherwise the call falls through to a plain `stream()`.
package/dist/index.cjs CHANGED
@@ -499,6 +499,24 @@ var BaseResource = class {
499
499
  };
500
500
 
501
501
  // src/resources/agent.ts
502
+ var SIGNAL_RUNTIME_OPTIONS_TTL_MS = 5 * 60 * 1e3;
503
+ var signalRuntimeOptionsByRunId = /* @__PURE__ */ new Map();
504
+ var latestSignalRuntimeOptionsByThread = /* @__PURE__ */ new Map();
505
+ var createSignalRuntimeOptionsEntry = (store, key, streamOptions) => {
506
+ const timeout = setTimeout(() => store.delete(key), SIGNAL_RUNTIME_OPTIONS_TTL_MS);
507
+ timeout.unref?.();
508
+ return { streamOptions, timeout };
509
+ };
510
+ var setSignalRuntimeOptionsEntry = (store, key, streamOptions) => {
511
+ const existing = store.get(key);
512
+ if (existing) clearTimeout(existing.timeout);
513
+ store.set(key, createSignalRuntimeOptionsEntry(store, key, streamOptions));
514
+ };
515
+ var deleteSignalRuntimeOptionsEntry = (store, key) => {
516
+ const existing = store.get(key);
517
+ if (existing) clearTimeout(existing.timeout);
518
+ store.delete(key);
519
+ };
502
520
  var noopClientToolObserve = {
503
521
  async span(_name, fn) {
504
522
  return fn();
@@ -696,6 +714,52 @@ var Agent = class extends BaseResource {
696
714
  const queryString = searchParams.toString();
697
715
  return queryString ? `${delimiter}${queryString}` : "";
698
716
  }
717
+ getSignalRuntimeRunKey(runId) {
718
+ return `${this.options.baseUrl}|${this.apiPrefix}|${this.agentId}|${runId}`;
719
+ }
720
+ getSignalRuntimeThreadKey({
721
+ resourceId,
722
+ threadId
723
+ }) {
724
+ if (!threadId) return void 0;
725
+ return `${this.options.baseUrl}|${this.apiPrefix}|${this.agentId}|${resourceId ?? ""}|${threadId}`;
726
+ }
727
+ setSignalRuntimeOptions({
728
+ runId,
729
+ resourceId,
730
+ threadId,
731
+ streamOptions
732
+ }) {
733
+ const threadKey = this.getSignalRuntimeThreadKey({ resourceId, threadId });
734
+ if (runId) {
735
+ setSignalRuntimeOptionsEntry(signalRuntimeOptionsByRunId, this.getSignalRuntimeRunKey(runId), streamOptions);
736
+ if (threadKey) deleteSignalRuntimeOptionsEntry(latestSignalRuntimeOptionsByThread, threadKey);
737
+ return;
738
+ }
739
+ if (threadKey) {
740
+ setSignalRuntimeOptionsEntry(latestSignalRuntimeOptionsByThread, threadKey, streamOptions);
741
+ }
742
+ }
743
+ getSignalRuntimeOptions({
744
+ runId,
745
+ resourceId,
746
+ threadId
747
+ }) {
748
+ if (runId) {
749
+ const runOptions = signalRuntimeOptionsByRunId.get(this.getSignalRuntimeRunKey(runId));
750
+ if (runOptions) return runOptions.streamOptions;
751
+ }
752
+ const threadKey = this.getSignalRuntimeThreadKey({ resourceId, threadId });
753
+ return threadKey ? latestSignalRuntimeOptionsByThread.get(threadKey)?.streamOptions : void 0;
754
+ }
755
+ deleteSignalRuntimeOptions(runId) {
756
+ if (!runId) return;
757
+ deleteSignalRuntimeOptionsEntry(signalRuntimeOptionsByRunId, this.getSignalRuntimeRunKey(runId));
758
+ }
759
+ deleteLatestSignalRuntimeOptions({ resourceId, threadId }) {
760
+ const threadKey = this.getSignalRuntimeThreadKey({ resourceId, threadId });
761
+ if (threadKey) deleteSignalRuntimeOptionsEntry(latestSignalRuntimeOptionsByThread, threadKey);
762
+ }
699
763
  /**
700
764
  * Retrieves details about the agent
701
765
  * @param requestContext - Optional request context to pass as query parameter
@@ -741,32 +805,227 @@ var Agent = class extends BaseResource {
741
805
  /**
742
806
  * @experimental Agent signals are experimental and may change in a future release.
743
807
  */
744
- sendSignal(params) {
745
- return this.request(`/agents/${this.agentId}/signals`, {
746
- method: "POST",
747
- body: params
748
- });
808
+ async sendSignal(params) {
809
+ const streamOptions = params.ifIdle?.streamOptions;
810
+ if (streamOptions) {
811
+ this.setSignalRuntimeOptions({
812
+ resourceId: params.resourceId,
813
+ threadId: params.threadId,
814
+ streamOptions
815
+ });
816
+ }
817
+ const body = params.ifIdle?.streamOptions ? {
818
+ ...params,
819
+ ifIdle: {
820
+ ...params.ifIdle,
821
+ streamOptions: {
822
+ ...params.ifIdle.streamOptions,
823
+ requestContext: parseClientRequestContext(params.ifIdle.streamOptions.requestContext),
824
+ clientTools: processClientTools(params.ifIdle.streamOptions.clientTools)
825
+ }
826
+ }
827
+ } : params;
828
+ let response;
829
+ try {
830
+ response = await this.request(`/agents/${this.agentId}/signals`, {
831
+ method: "POST",
832
+ body
833
+ });
834
+ } catch (error) {
835
+ if (streamOptions) {
836
+ this.deleteLatestSignalRuntimeOptions({ resourceId: params.resourceId, threadId: params.threadId });
837
+ }
838
+ throw error;
839
+ }
840
+ if (streamOptions) {
841
+ this.setSignalRuntimeOptions({
842
+ runId: response.runId,
843
+ resourceId: params.resourceId,
844
+ threadId: params.threadId,
845
+ streamOptions
846
+ });
847
+ }
848
+ return response;
749
849
  }
750
850
  /**
751
851
  * @experimental Agent signals are experimental and may change in a future release.
752
852
  */
753
853
  async subscribeToThread(params) {
754
- const streamResponse = await this.request(`/agents/${this.agentId}/threads/subscribe`, {
854
+ const { resourceId, threadId } = params;
855
+ const requestSubscription = () => this.request(`/agents/${this.agentId}/threads/subscribe`, {
755
856
  method: "POST",
756
- body: params,
857
+ body: { resourceId, threadId },
757
858
  stream: true
758
859
  });
860
+ const streamResponse = await requestSubscription();
759
861
  if (!streamResponse.body) {
760
862
  throw new Error("No response body");
761
863
  }
762
- streamResponse.processDataStream = async ({
763
- onChunk
764
- }) => {
765
- await processMastraStream({
766
- stream: streamResponse.body,
767
- onChunk,
768
- signal: this.options.abortSignal
769
- });
864
+ const agent = this;
865
+ 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) };
932
+ }
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;
941
+ }
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 {
970
+ }
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;
1002
+ }
1003
+ if (!reconnectOptions || this.options.abortSignal?.aborted || attempts >= reconnectOptions.maxRetries) {
1004
+ throw error;
1005
+ }
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) {
1013
+ return;
1014
+ }
1015
+ await new Promise((resolve) => setTimeout(resolve, reconnectOptions.delayMs));
1016
+ if (this.options.abortSignal?.aborted) {
1017
+ return;
1018
+ }
1019
+ try {
1020
+ response = await requestSubscription();
1021
+ break;
1022
+ } catch (error) {
1023
+ if (this.options.abortSignal?.aborted || attempts >= reconnectOptions.maxRetries) {
1024
+ throw error;
1025
+ }
1026
+ }
1027
+ }
1028
+ }
770
1029
  };
771
1030
  return streamResponse;
772
1031
  }