mitra-interactions-sdk 1.0.60-beta.48 → 1.0.60-beta.49

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
@@ -263,7 +263,7 @@ const result2 = await listRecordsMitra({
263
263
 
264
264
  ## Agent Chat (copilot)
265
265
 
266
- Chat com o agente de IA embarcado. **REST-first, tudo pela BFF no `baseURL` normal** (`/agentAiShortcut/*` — herda `fetchWithRefresh` e CORS): chats (`createChat`/`listChats`/`readChat`/`renameChat`/`deleteChat`), histórico (`listChatMessages`), credenciais e modelos. `projectId` obrigatório em todas (opcional na chamada se veio do `configureSdkMitra`). **Só o prompt é streaming** — um WebSocket por task em `wss://{origin}/copilot/ws/tasks/{taskId}?token=JWT`, aberto pela SDK quando a conversa começa (WS não passa por preflight de CORS).
266
+ Chat com o agente de IA embarcado. **REST-first, tudo pela BFF no `baseURL` normal** (`/agentAiShortcut/*` — herda `fetchWithRefresh` e CORS): chats (`createChat`/`listChats`/`readChat`/`renameChat`/`deleteChat`), histórico (`listChatMessages`), credenciais e modelos. `projectId` obrigatório em todas (opcional na chamada se veio do `configureSdkMitra`). **Só o prompt é streaming** — um WebSocket por task em `wss://{origin}/copilot/ws/tasks/{taskId}?token=JWT`, aberto pela SDK quando a conversa começa (WS não passa por preflight de CORS). Chats que nascem no **box T3** têm canal direto. A decisão é do copilot por app e vem no campo `runtime` da task (`RUNNER` ou `T3`), que a SDK lê no `createChat`/`readChat`: **só quando ele é `T3`** a SDK chama `POST /copilot/api/v1/tasks/{id}/channel` antes de conectar e abre o WebSocket na URL devolvida, que aponta ao box do agente pelo gateway com `grant` e `ticket` curtos na query. O envelope de eventos é o mesmo; só a URL muda. Esse POST responde na hora: `200` com `status: "ready"` e a URL, ou `202` com `status: "booting"` enquanto o box ainda sobe — nesse caso a SDK repete a cada 2s até 120s, teto que cobre o boot frio de box T3, que leva até ~2 min. Chat do runner, copilot antigo (que não devolve `runtime`), box que não subiu dentro do teto ou falha nessa chamada seguem pelo WebSocket do copilot, byte a byte como hoje. Cada reconexão pede um canal novo.
267
267
 
268
268
  Precisa de `token` e `baseURL` configurados (o login já deixa pronto). O transporte do prompt aceita `transport: 'ws' | 'http'` — `'ws'` é o default; `'http'` é **funcional**: `POST /copilot/api/v1/tasks/{id}/inputs` + SSE em `/events` via fetch, com a MESMA paridade de eventos do WS (pra ambientes onde WebSocket não rola, ex.: serverless).
269
269
 
package/dist/index.d.mts CHANGED
@@ -651,8 +651,8 @@ declare function refreshTokenSilently(authUrl: string, _projectId: number | stri
651
651
  * Mitra Interactions SDK — Agent Chat (via BFF)
652
652
  *
653
653
  * REST-first, tudo pela BFF no baseURL normal (`/agentAiShortcut/*`) — herda
654
- * fetchWithRefresh e o CORS do BFF. Envelope `{ status, result }`; listas vêm
655
- * desembrulhadas do Page (result é o array direto).
654
+ * fetchWithRefresh e a troca de token server-side. Envelope `{ status, result }`;
655
+ * listas vêm desembrulhadas do Page (result é o array direto).
656
656
  *
657
657
  * SÓ o prompt é streaming: WebSocket por task em
658
658
  * `wss://{origin}/copilot/ws/tasks/{taskId}?token=JWT` (não passa por
@@ -694,7 +694,8 @@ declare function getAgentTaskMitra(options: GetAgentTaskOptions): AgentTaskSessi
694
694
  * Mitra Interactions SDK — Agent Credentials (via BFF)
695
695
  *
696
696
  * Função ÚNICA `manageAgentCredentialMitra`, toda HTTP pela BFF no baseURL
697
- * normal (`/agentAiShortcut/*`) — herda fetchWithRefresh e o CORS do BFF:
697
+ * normal (`/agentAiShortcut/*`) — herda fetchWithRefresh e a troca de token
698
+ * server-side:
698
699
  * - list_providers → GET /agentAiShortcut/listProviders?projectId=
699
700
  * - list_models → GET /agentAiShortcut/listModels?projectId=&agentId= (agentId opcional)
700
701
  * - save → POST /agentAiShortcut/saveCredential {projectId, provider, key}
package/dist/index.d.ts CHANGED
@@ -651,8 +651,8 @@ declare function refreshTokenSilently(authUrl: string, _projectId: number | stri
651
651
  * Mitra Interactions SDK — Agent Chat (via BFF)
652
652
  *
653
653
  * REST-first, tudo pela BFF no baseURL normal (`/agentAiShortcut/*`) — herda
654
- * fetchWithRefresh e o CORS do BFF. Envelope `{ status, result }`; listas vêm
655
- * desembrulhadas do Page (result é o array direto).
654
+ * fetchWithRefresh e a troca de token server-side. Envelope `{ status, result }`;
655
+ * listas vêm desembrulhadas do Page (result é o array direto).
656
656
  *
657
657
  * SÓ o prompt é streaming: WebSocket por task em
658
658
  * `wss://{origin}/copilot/ws/tasks/{taskId}?token=JWT` (não passa por
@@ -694,7 +694,8 @@ declare function getAgentTaskMitra(options: GetAgentTaskOptions): AgentTaskSessi
694
694
  * Mitra Interactions SDK — Agent Credentials (via BFF)
695
695
  *
696
696
  * Função ÚNICA `manageAgentCredentialMitra`, toda HTTP pela BFF no baseURL
697
- * normal (`/agentAiShortcut/*`) — herda fetchWithRefresh e o CORS do BFF:
697
+ * normal (`/agentAiShortcut/*`) — herda fetchWithRefresh e a troca de token
698
+ * server-side:
698
699
  * - list_providers → GET /agentAiShortcut/listProviders?projectId=
699
700
  * - list_models → GET /agentAiShortcut/listModels?projectId=&agentId= (agentId opcional)
700
701
  * - save → POST /agentAiShortcut/saveCredential {projectId, provider, key}
package/dist/index.js CHANGED
@@ -498,6 +498,8 @@ var httpTenant = {
498
498
  var QUEUE_MAX = 10;
499
499
  var DEFAULT_AGENT_TYPE = "ANTHROPIC_CLAUDE_OPUS";
500
500
  var CANCEL_SAFETY_MS = 1e4;
501
+ var WS_SILENCE_MS = 6e4;
502
+ var WS_RECONNECT_DELAYS_MS = [1e3, 2e3, 4e3, 8e3, 16e3, 3e4];
501
503
  var queueSeq = 0;
502
504
  var AgentTaskSession = class {
503
505
  constructor(init) {
@@ -512,7 +514,24 @@ var AgentTaskSession = class {
512
514
  // ── Transporte ────────────────────────
513
515
  this._ws = null;
514
516
  this._wsConnectPromise = null;
517
+ /** Metadados do chat existente (o `runtime` entre eles). Quem escolhe transporte espera
518
+ * por ela: sem o runtime a session nem pergunta pelo canal do box e cai no WS do copilot,
519
+ * que numa conversa entregue por HTTP não tem como levar a mensagem ao agente. */
520
+ this._metadataReady = null;
515
521
  this._creatingTask = false;
522
+ /** O socket atual é o do box (canal direto), que sabe repetir o log por `sequence`. */
523
+ this._wsIsDirect = false;
524
+ /** Já houve socket aberto nesta session: do segundo em diante existe buraco a cobrir. */
525
+ this._wsEverOpened = false;
526
+ /** Onde o log do box estava quando esta session entrou. Congelado no primeiro socket: numa
527
+ * reconexão o canal já conta o que a session perdeu, e adotar o número de lá pularia
528
+ * justamente esses eventos. */
529
+ this._joinSequence = 0;
530
+ /** Maior `sequence` já vista nesta session: de onde o box repete depois de uma queda. */
531
+ this._lastSequence = 0;
532
+ this._wsSilenceTimer = null;
533
+ this._wsReconnectTimer = null;
534
+ this._wsReconnectAttempt = 0;
516
535
  // ── Cancel ────────────────────────────
517
536
  this._cancelTimer = null;
518
537
  // ── Transporte HTTP (SSE por fetch; o WS continua o default) ───────────
@@ -564,11 +583,15 @@ var AgentTaskSession = class {
564
583
  }
565
584
  // ── Abertura de chat existente ─────────
566
585
  async _openExisting(taskId) {
567
- try {
586
+ this._metadataReady = (async () => {
568
587
  const t = await readChatTask(taskId, this._projectId);
569
588
  this._task = toAgentChat(t);
589
+ this._runtime = t.runtime;
570
590
  this._agentType = this._task.agentType;
571
591
  if (this._status === "opening") this._setStatus("idle");
592
+ })();
593
+ try {
594
+ await this._metadataReady;
572
595
  if (this._transport === "http") await this._ensureSse();
573
596
  else await this._ensureWs();
574
597
  } catch (err) {
@@ -625,6 +648,7 @@ var AgentTaskSession = class {
625
648
  autonomous: this._autonomous
626
649
  });
627
650
  this._task = toAgentChat(t);
651
+ this._runtime = t.runtime;
628
652
  this._taskId = this._task.id;
629
653
  registerSession(this._taskId, this);
630
654
  this._emit("taskCreated", { task: this._task });
@@ -698,6 +722,7 @@ var AgentTaskSession = class {
698
722
  clearTimeout(this._cancelTimer);
699
723
  this._cancelTimer = null;
700
724
  }
725
+ this._clearWsTimers();
701
726
  if (this._ws) {
702
727
  try {
703
728
  this._ws.close();
@@ -860,13 +885,15 @@ var AgentTaskSession = class {
860
885
  }
861
886
  this._wsConnectPromise = (async () => {
862
887
  try {
888
+ if (this._metadataReady) await this._metadataReady.catch(() => {
889
+ });
863
890
  if (isTokenExpiring()) await tryRefreshToken();
864
891
  try {
865
892
  return await this._connectWs(this._taskId);
866
893
  } catch (err) {
867
894
  const refreshed = await tryRefreshToken();
868
- if (!refreshed) throw err;
869
- return await this._connectWs(this._taskId);
895
+ if (!refreshed && this._runtime !== "T3") throw err;
896
+ return await this._connectWs(this._taskId, false);
870
897
  }
871
898
  } finally {
872
899
  this._wsConnectPromise = null;
@@ -874,8 +901,14 @@ var AgentTaskSession = class {
874
901
  })();
875
902
  return this._wsConnectPromise;
876
903
  }
877
- _connectWs(taskId) {
878
- const url = getTaskWsUrl(taskId);
904
+ /** `allowDirect` false força o WS do copilot: é como a reconexão degrada um box que não abriu. */
905
+ async _connectWs(taskId, allowDirect = true) {
906
+ var _a;
907
+ const direct = allowDirect && this._runtime === "T3" ? await requestDirectChannel(taskId, () => this._status === "closed") : null;
908
+ if (this._status === "closed") {
909
+ throw new Error("AgentTaskSession: session fechada durante a conex\xE3o.");
910
+ }
911
+ const url = (_a = direct == null ? void 0 : direct.wsUrl) != null ? _a : getTaskWsUrl(taskId);
879
912
  return new Promise((resolve, reject) => {
880
913
  const socket = new WebSocket(url);
881
914
  let opened = false;
@@ -891,7 +924,28 @@ var AgentTaskSession = class {
891
924
  socket.onopen = () => {
892
925
  opened = true;
893
926
  clearTimeout(timer);
927
+ if (this._status === "closed") {
928
+ try {
929
+ socket.close();
930
+ } catch (e) {
931
+ }
932
+ reject(new Error("AgentTaskSession: session fechada durante a conex\xE3o."));
933
+ return;
934
+ }
894
935
  this._ws = socket;
936
+ this._wsIsDirect = direct !== null;
937
+ this._wsReconnectAttempt = 0;
938
+ this._armSilenceWatchdog(socket);
939
+ const reconexao = this._wsEverOpened;
940
+ if (!reconexao && direct) this._joinSequence = direct.lastSequence;
941
+ this._wsEverOpened = true;
942
+ if (this._wsIsDirect && reconexao) {
943
+ const fromSequence = Math.max(this._lastSequence, this._joinSequence);
944
+ try {
945
+ socket.send(JSON.stringify({ type: "replay", fromSequence }));
946
+ } catch (e) {
947
+ }
948
+ }
895
949
  resolve(socket);
896
950
  };
897
951
  socket.onerror = () => {
@@ -901,9 +955,13 @@ var AgentTaskSession = class {
901
955
  }
902
956
  };
903
957
  socket.onclose = () => {
904
- if (this._ws === socket) this._ws = null;
958
+ if (this._ws !== socket) return;
959
+ this._ws = null;
960
+ this._clearSilenceWatchdog();
961
+ if (this._status !== "closed") this._scheduleReconnect();
905
962
  };
906
963
  socket.onmessage = (evt) => {
964
+ if (this._ws === socket) this._armSilenceWatchdog(socket);
907
965
  let msg;
908
966
  try {
909
967
  msg = JSON.parse(String(evt.data));
@@ -914,13 +972,68 @@ var AgentTaskSession = class {
914
972
  };
915
973
  });
916
974
  }
917
- // ── Eventos do stream (WsOutboundMessage { type, payload, timestamp }) ──
975
+ // ── Vida do socket: watchdog de silêncio e reconexão ──────────────────
976
+ _armSilenceWatchdog(socket) {
977
+ this._clearSilenceWatchdog();
978
+ this._wsSilenceTimer = setTimeout(() => {
979
+ this._wsSilenceTimer = null;
980
+ if (this._ws !== socket) return;
981
+ try {
982
+ socket.close();
983
+ } catch (e) {
984
+ }
985
+ if (this._ws === socket) {
986
+ this._ws = null;
987
+ if (this._status !== "closed") this._scheduleReconnect();
988
+ }
989
+ }, WS_SILENCE_MS);
990
+ }
991
+ _clearSilenceWatchdog() {
992
+ if (this._wsSilenceTimer) {
993
+ clearTimeout(this._wsSilenceTimer);
994
+ this._wsSilenceTimer = null;
995
+ }
996
+ }
997
+ _clearWsTimers() {
998
+ this._clearSilenceWatchdog();
999
+ if (this._wsReconnectTimer) {
1000
+ clearTimeout(this._wsReconnectTimer);
1001
+ this._wsReconnectTimer = null;
1002
+ }
1003
+ }
1004
+ _scheduleReconnect() {
1005
+ if (this._wsReconnectTimer || this._wsConnectPromise) return;
1006
+ const delay = WS_RECONNECT_DELAYS_MS[this._wsReconnectAttempt];
1007
+ if (delay === void 0) {
1008
+ this._wsReconnectAttempt = 0;
1009
+ this._emit("error", { code: "CONNECTION_LOST", error: "Conex\xE3o com o agente perdida; a resposta segue no hist\xF3rico." });
1010
+ if (this._status === "streaming" || this._status === "cancelled") this._setStatus("idle");
1011
+ return;
1012
+ }
1013
+ this._wsReconnectAttempt += 1;
1014
+ this._wsReconnectTimer = setTimeout(() => {
1015
+ this._wsReconnectTimer = null;
1016
+ if (this._status === "closed") return;
1017
+ void this._ensureWs().catch(() => {
1018
+ if (this._status !== "closed") this._scheduleReconnect();
1019
+ });
1020
+ }, delay);
1021
+ }
1022
+ // ── Eventos do stream (WsOutboundMessage { type, payload, timestamp, sequence? }) ──
918
1023
  _handleWsEvent(msg) {
919
1024
  var _a, _b, _c, _d, _e, _f, _g, _h;
920
1025
  if (!msg || typeof msg.type !== "string" || !msg.type) return;
1026
+ if (typeof msg.sequence === "number" && msg.sequence > this._lastSequence) {
1027
+ this._lastSequence = msg.sequence;
1028
+ }
921
1029
  const { type, payload } = msg;
922
1030
  switch (type) {
923
- case "textDelta": {
1031
+ // `textChunk` é a forma que o texto do agente tem no log do box: ele transmite ao vivo
1032
+ // em `textDelta` e dobra os deltas em chunks numerados ao gravar. Uma repetição depois
1033
+ // de queda devolve chunks, então tratá-los como texto é o que faz a resposta recuperada
1034
+ // aparecer; sem este caso ela chega no socket e é descartada em silêncio.
1035
+ case "textDelta":
1036
+ case "textChunk": {
924
1037
  const text = (_a = payload == null ? void 0 : payload.text) != null ? _a : "";
925
1038
  if (this._status !== "streaming" && this._status !== "cancelled") this._setStatus("streaming");
926
1039
  this._content += text;
@@ -1099,6 +1212,59 @@ function getRawToken() {
1099
1212
  }
1100
1213
  return stripBearer(config.token);
1101
1214
  }
1215
+ var CHANNEL_BOOT_RETRY_MS = 2e3;
1216
+ var CHANNEL_BOOT_TIMEOUT_MS = 12e4;
1217
+ function sleep(ms) {
1218
+ return new Promise((resolve) => setTimeout(resolve, ms));
1219
+ }
1220
+ function isGatewayWsUrl(wsUrl) {
1221
+ const expected = getGatewayOrigin().replace(/^http/, "ws");
1222
+ try {
1223
+ return new URL(wsUrl).origin === expected;
1224
+ } catch (e) {
1225
+ return false;
1226
+ }
1227
+ }
1228
+ async function requestDirectChannel(taskId, isCancelled) {
1229
+ var _a;
1230
+ if (!hasAppScope(getRawToken())) return null;
1231
+ const url = `${getGatewayOrigin()}/copilot/api/v1/tasks/${encodeURIComponent(taskId)}/channel`;
1232
+ const doPost = async () => fetch(url, {
1233
+ method: "POST",
1234
+ headers: { Authorization: `Bearer ${getRawToken()}` }
1235
+ });
1236
+ const deadline = Date.now() + CHANNEL_BOOT_TIMEOUT_MS;
1237
+ for (; ; ) {
1238
+ if (isCancelled == null ? void 0 : isCancelled()) return null;
1239
+ let response;
1240
+ try {
1241
+ response = await doPost();
1242
+ if (response.status === 401 && await tryRefreshToken()) response = await doPost();
1243
+ } catch (e) {
1244
+ return null;
1245
+ }
1246
+ if (response.status === 202) {
1247
+ if (Date.now() >= deadline) return null;
1248
+ await sleep(CHANNEL_BOOT_RETRY_MS);
1249
+ continue;
1250
+ }
1251
+ if (response.status !== 200) return null;
1252
+ let body;
1253
+ try {
1254
+ body = await response.json();
1255
+ } catch (e) {
1256
+ return null;
1257
+ }
1258
+ const channel = body;
1259
+ if (!channel || typeof channel.wsUrl !== "string" || !channel.wsUrl) return null;
1260
+ if (!isGatewayWsUrl(channel.wsUrl)) return null;
1261
+ return {
1262
+ wsUrl: channel.wsUrl,
1263
+ expiresAt: String((_a = channel.expiresAt) != null ? _a : ""),
1264
+ lastSequence: typeof channel.lastSequence === "number" ? channel.lastSequence : 0
1265
+ };
1266
+ }
1267
+ }
1102
1268
  function toAgentChat(t) {
1103
1269
  return {
1104
1270
  id: t.id,