Base comum dos SDKs JavaScript da Mitra: tipos e módulos de API que não dependem de ambiente. App não instala o Core direto. No browser, use @mitralab.io/platform-sdk; dentro de uma Server Function, use @mitralab.io/functions-sdk. Os dois já trazem o Core. Instale este pacote só para escrever um SDK ou adaptador da Mitra para outro runtime.
npm install @mitralab.io/sdk-coreNode 18 ou mais novo. Publicado em ESM e CommonJS, com tipos e sem dependências de runtime.
Os módulos de API não fazem HTTP: você entrega um Transport por serviço, que recebe o path e as opções e devolve o corpo já parseado. Autenticação, headers, timeout e erro HTTP ficam com ele. A exceção é o canal direto dos chats de agente (directChannel), que fala com o gateway por WebSocket ou fetch, fora do Transport.
import { createSdkCore, type Transport, type TransportRequestOptions } from "@mitralab.io/sdk-core"
const apiUrl = process.env.MITRA_API_URL!
const accessToken = process.env.MITRA_TOKEN!
const appId = process.env.MITRA_APP_ID!
function fetchTransport(service: string): Transport {
return {
async request<T>(path: string, options: TransportRequestOptions = {}): Promise<T> {
const url = new URL(`${apiUrl}/${service}${path}`)
for (const [key, value] of Object.entries(options.params ?? {})) {
for (const item of [value].flat()) {
if (item !== undefined) url.searchParams.append(key, String(item))
}
}
const headers = new Headers({ "Content-Type": "application/json", ...options.headers })
headers.set("Authorization", `Bearer ${accessToken}`)
headers.set("X-App-Id", appId)
const response = await fetch(url, {
method: options.method ?? "GET",
headers,
body: options.body === undefined ? null : JSON.stringify(options.body),
redirect: "error",
})
if (!response.ok) throw new Error(`Request failed with status ${response.status}`)
const text = await response.text()
return (text ? JSON.parse(text) : undefined) as T
},
}
}
const core = createSdkCore({
transports: {
auth: fetchTransport("iam"),
dataManager: fetchTransport("data-manager"),
functions: fetchTransport("functions"),
integration: fetchTransport("integration"),
},
getAppId: () => appId,
})
const { data: tasks, hasMore } = await core.entities.getTable("Task").list({ limit: 20 })entities: CRUD nas tabelas do app, porgetTable(nome)oucore.entities.<Tabela>.queries,customQueries,sqleschema: queries salvas, SQL parametrizado e estrutura das tabelas.functions,publicFunctions,functionsAdmineworkflows: executar e administrar Server Functions e workflows.integration,integrationAdmin,integrationResourceseintegrationTemplates: chamadas a APIs externas e a configuração delas.agentTasks, comcreateAgentTaskSessionManagerewithAgentTaskSessions: chats de agente comsend,sendAndWait,cancele eventos. O gerenciador recebetasks, oeventSourcedo seu SDK edirectChannel, com oapiUrldo gateway e, se quiser,WebSocketefetch.apps,context,agents,agentConnections,agentCredentials,members,imports,dataSourceseauth: o app em si, a configuração de agentes e os demais recursos.messenger:notifyavisa o usuário autenticado esendEmailmanda e-mail com o remetente do app.resolveApiKeyToken,readTokenAppIdetokenAuthorizesApp: contrato da troca de API key por token, para o SDK que autentica por chave.
| Opção | Obrigatória | Uso |
|---|---|---|
transports.auth, dataManager, functions, integration |
sim | um Transport para cada serviço |
transports.codeStudio, copilot, messenger |
não | sem eles falham com SdkCoreConfigurationError, antes de sair a requisição: apps (codeStudio); agentTasks, agentConnections, agentCredentials e agents.listModels (copilot); messenger. O resto de agents usa functions |
transports.publicFunctions |
não | transporte anônimo de publicFunctions, sem Authorization nem X-App-Id |
getAppId |
não | devolve o app fixado pelo seu SDK; usado por context |
functions.executeInvocationType |
não | sync ou async em functions.execute; sem valor, vale o padrão do servidor |
functions.emptyInput |
não | empty-object manda { "input": {} } quando não há input; omit-body não manda corpo |
errors |
não | SdkCoreErrorFactory para o seu SDK lançar as próprias classes no lugar de SdkCoreConfigurationError e SdkCoreResponseError |
| Erro | Quando | O que fazer |
|---|---|---|
SdkCoreConfigurationError |
entrada inválida: path vazio, deleteMany({}), transporte opcional ausente, API key ou app vazios |
corrija a chamada ou passe o transporte que falta |
SdkCoreResponseError (code INVALID_RESPONSE) |
resposta de sucesso fora do contrato | confira se o transporte devolve o corpo parseado; se sim, atualize o Core |
AgentTaskTurnError |
o turno do agente foi recusado ou terminou com erro; code traz o motivo, quando vem |
mostre o erro e deixe a pessoa mandar de novo |
erro do seu Transport |
falha HTTP ou de rede | trate no seu SDK; o Core repassa o erro sem mudar, exceto em agentCredentials.usage e agentConnections.usage, que devolvem null quando o erro traz code CREDENTIAL_USAGE_NOT_FOUND |
- Fixe o app no adaptador:
getAppIde o headerX-App-Idvêm do valor configurado no seu SDK, nunca de um argumento de quem chama. Aplique as credenciais depois dos headers deoptions, como no exemplo, para que eles não as substituam. - O Core não repete requisição nem renova token. Se o seu transporte repetir, repita só o que não grava.
publicFunctionsprecisa de um transporte sem credencial. O Core não usa o transporte autenticado no lugar dele.sendEmail({ to, subject, html, text?, cc?, bcc?, replyTo? })não temfrom: o Messenger usa o remetente do app do token. Por isso só o backend do app manda e-mail, com o token trocado pela API key ou o token da Server Function; token de usuário final do app recebe403.to,ccebccsão listas com um endereço por item. Os limites ficam no Messenger e chegam como erro do seuTransport:400acima do limite de destinatários dele somando os três campos e429acima da cota diária do app, com o headerRetry-Aftereretry_after_secondsno corpo.sendAndWaitcomtimeoutMsousignalsó para de esperar; o turno continua no servidor. Para interromper, chamecancel(). A sessão continua acompanhando esse turno: se o canal cair depois do prazo, ela ainda fecha o turno pelo histórico, como faz com umsend. Pelo histórico, o turno só fecha na linha que o termina: a do agente com o uso do turno, ou uma linhaERROR. As linhas que o Copilot grava com o turno rodando e a resposta atrasada da pergunta anterior não fecham nada. O stream do Copilot repete o turno em andamento a partir do que o histórico dele tem; o que a sessão já viu, pelosequenceda box, não sai de novo.cancel()chamado antes de a box aceitar o prompt espera esse aceite e só então manda o stop; a promise resolve depois disso. Se o prompt não chegar a virar turno, nada é enviado e o status não passa porcancelled. Mensagem mandada depois docancel(), com o stop ainda sem resposta, sai como qualquer outra; a box espera o turno parado terminar e dá a ela um turno próprio, e osendAndWaitdela resolve com esse turno. Turno parado que a sessão fecha pelo histórico termina comreasoninterrupted.- Toda mensagem sai na hora, com ou sem turno rodando; a sessão não guarda fila. Com turno rodando, a box coloca a mensagem dentro do turno em andamento (steering nativo do provedor), põe na fila do provedor para um turno próprio, ou dá a ela um turno próprio se o turno já acabou; o
sendAndWaitde cada mensagem resolve com o turno que a respondeu. No canal direto ela vai pelo socket semmodepor padrão, e o T3 decide como no composer dele: entra no turno em andamento quando o provedor aceita steer, ou espera na fila do T3.send(prompt, { mode: "steer" })pede o steer de propósito (o T3 recusa enquanto o turno ainda não está rodando) esend(prompt, { mode: "queue" })pede a fila;steerQueueItem(id)leva um item da fila para o turno em andamento eresumeQueue()retoma a fila que o Stop segurou. A mensagem leva também umclientMessageId, que a box devolve no frame que a aceita (stepStartoumessageQueued, este sem o prazo de 35 s). Osenddevolve esseclientMessageId(ouundefinedpara texto em branco, que não sai). Quando o T3 entrega a mensagem ao turno em andamento, a box marca o ponto com um framesteer, sem turno novo: a sessão emitesteeredcomtextemessageId, para o app mostrar a mensagem ali, e oturnEnddaquele turno responde a ela. Se o turno acabar antes, ela ganha turno próprio, que abre comstepStart. OclientMessageIdtambém volta noerrorque a recusa, que rejeita só aquela mensagem. Com o socket reconectando, ou sem canal direto, vai ao Copilot (POST /inputs) com o mesmoclientMessageId, que o Copilot guarda com a mensagem pendente enquanto a box liga e entrega à box, que o devolve igual. A mensagem mandada com turno rodando levaagentTypeereasoningEffortpara a box, que não troca o modelo do turno em andamento e os usa se a mensagem virar turno próprio; só quando vai ao Copilot com turno rodando eles são tirados, porque o Copilot reinicia a sessão viva para outro modelo. Recusa do Copilot com status de erro rejeita aquela mensagem e emiteerror. Falha no envio rejeita osendAndWaite emiteerror; mensagem que ainda não saiu quando a sessão fecha é rejeitada e não é enviada. A sessão não guarda fila própria:queueequeueChangemostram a fila do T3 que a box informa commessageQueued(só os que têmposition), de qualquer cliente do chat, comid(oclientMessageId),text,position,queueHeld,sequenceeturnId. A mensagem sai da fila quando um turno a pega (stepStartousteercom o mesmoclientMessageId), quando a box a tira da fila por umqueued-run.cancel(messageQueuedcomposition: nullecancelled: true, semstepFinish) ou quando ela termina antes de rodar; a saída sem turno rejeita só osendAndWaitdela e não mexe no turno em andamento. Como no T3, cancelar um item da fila só tira aquele item: nenhum turno termina por isso. Se ele era a mensagem que abriu o turno desta sessão, ainda esperando atrás do turno de outro cliente, a sessão segue a próxima mensagem dela na fila quando a box a começa, ou volta aidlese não sobrou nenhuma, semturnEnd. Frame de mensagem da fila nunca troca o turno que o canal acompanha para reconectar.removeQueueItem(id)mandaqueued-run.cancel,editQueueItem(id, text)mandaqueued-run.editeclearQueue()manda umqueued-run.cancelpor item; a fila muda quando a box responde, não antes. A recusa de um desses comandos chega comoerrorcomcommand(queued-run.cancelouqueued-run.edit): a sessão só emite oerror, a mensagem fica onde está e nenhum turno falha. Os três vão só pelo canal direto; sem ele sai umerrore nada é enviado. Depois do replay a box manda de novo a fila e os subagentes como estão, com osequenceda linha que os disse por último: o que a sessão já tem não sai de novo emqueueChangenem emsubagent, e numa abertura nova isso preenchequeueesubagents. - Subagentes: o evento
subagenttrazsubagentId,toolId(otoolIddotoolque o criou),status(os do T3:pending,running,waiting,completed,failed,cancelled,interrupted,idle; quando o T3 não manda status, fica o anterior),progress,startedAt,endedAt(epoch ms) echildThreadId;session.subagentsguarda o último estado de cada um. A conversa do subagente vem emsubagentDeltaesubagentTool, comparentToolId, e não abre nem fecha o turno do chat. Como na 0.2.13, o texto dela também entra emdeltaecontent, e as ferramentas também saem emtool, comparentToolId. O resultado de subagente em segundo plano que termina depois do turno dono chega comotoolde faseresultcom otoolIdda chamada, sem abrir turno nem mexer emstatusecontent.stopSubagent(subagentId)para só ele e deixa o turno seguir; vai só pelo canal direto, e sem ele sai umerrore nada é enviado. - Peças e ordem: cada evento que já existe leva, como campos opcionais, o que o frame da box trouxe:
sequence(a ordem do log da box),itemId(o id do item no T3),turnId(olifecycle.turnId) e, no texto e nas ferramentas de subagente,parentToolId. Odelta(e osubagentDelta) traz tambémitemDelta, o pedaço do item como a box mandou: igual aodelta, menos no primeiro pedaço de um item de texto que vem depois de outro, em que odeltacomeça com o separador de linha dupla que a 0.2.13 usava para juntar os dois. Quem desenha por item usaitemDelta. Osteeredtraz também oturnIdda mensagem (oiddo item dela no histórico),sequenceeclientMessageId; oturnEndtraz osequencee oturnIddostepFinish. O item dohistorytraz o que o Copilot gravou:sequence(o do primeiro pedaço da peça, igual ao do evento ao vivo),lastSequence(o do último pedaço, só quando a peça veio em mais de um),boxLog(qual log da box conta osequence; box que sobe sem o log recomeça),itemId,parentToolId,clientMessageIdna pergunta,subagentna ferramenta que criou o subagente ereasonna linha que fechou o turno. Osubagenttem o formato do framesubagentUpdateda box, sem conversão. Na ferramenta, oparentToolIdfica emtool.parentToolId. O histórico é o streaming gravado, uma linha por peça: oloadHistorypede todas as linhas comkinds=alle cada linha vira um item, como veio. LinhaREASONINGvira itemreasoning(raciocínio, comitemIde, no do subagente,parentToolId); linhaSUBAGENT_TEXTvira itemagentcomparentToolId; ferramenta de subagente é itemtoolcomtool.parentToolId; linhaAPPROVALvira itemapprovale linhaUSER_INPUT_REQUESTvira itemuserInputRequest, os dois com opayloaddo frame da box como veio (o mesmo que chega ao vivo como eventoraw). Ao vivo, os framesapprovaleuser_input_requestentram nohistorycomo esses mesmos itens, na posição em que vieram, igual ao recarregado. A recuperação de turno perdido também pedekinds=all, para ohistoryseguir com todas as peças, e lê a resposta só das linhas do próprio chat (TEXT,ERROReTOOL_USEsemparentToolId). Comkinds=all, a pergunta que ainda espera na fila não vem no histórico: ela aparece emqueueaté o turno dela começar. OturnStartaberto pelostepStartda box traz otexte oclientMessageIdda mensagem daquele turno, comsequence,itemIdeturnId: é ali que o app mostra a pergunta, também quando o histórico ainda não a tem (F5 logo depois do envio). Sem o parâmetro, o Copilot devolve só as linhas que a 0.2.13 lia. A SDK só repassa: não ordena, não agrupa e não deduz nada. Frame ou linha sem o campo deixa o campo de fora. - Turno e tempo de entrega: quem decide se a mensagem entra no turno em andamento ou ganha turno próprio é a box, não o tempo local da SDK. Um
stepStartcomturnIdsem turno rodando abre o turno com ocontentlimpo, e oacceptedsai no frame que nomeia a mensagem. Frames que chegam juntos num tick dão os mesmos eventos que frames espaçados. - Pergunta do agente no meio do turno: o T3 pode pedir uma resposta à pessoa. O pedido chega como evento
rawde tipouser_input_request, com o item do T3 nopayload(perguntas erequestId); o turno fica esperando, sem prazo da SDK. Responda comsession.respondUserInput(requestId, answers, attachmentsByQuestionId?), que vai direto para a box. Se a box recusar a resposta, sai umerrore o turno continua; com o canal direto fora (reconectando ou chat sem box), a resposta não é enviada e sai umerrordizendo isso, para o app mandar de novo depois dochannelConnected. - Portão de rollout da 0.3.0: ela só entra nas SDKs de app (
platform-sdke SDK de Functions) quando o ambiente tiver zero box antiga (template do forkexperimental). A box antiga trata a mensagem do meio do turno como início de turno novo, zera o uso e a identidade do turno, e nada disso aparece como erro. A SDK não distingue box antiga de box nova; o portão é o bump nas SDKs de app, que fixam a versão exata desta. - Abrir o chat no meio de uma run (F5): o
session({ taskId })carrega o histórico e pede à box o replay a partir de onde ele termina, o maiorlastSequence(ousequence, quando a linha não trazlastSequence) doboxLogmais novo, com o mesmo pedido que o redial já faz e antes de qualquer frame ao vivo. O que o Copilot ainda não gravou chega no replay, e nada do histórico vem duas vezes. No socket, a decisão é a mesma do redial, pelohelloda box: se oboxLogdo canal for outro que o do histórico (o Copilot sobe oboxLogquando a box troca de thread) ou olastSequencedohellofor menor que esse ponto, o log é outro e o replay começa do início dele. No HTTP, decide só peloboxLog, antes de abrir o stream. O cursor do canal começa no ponto do replay, então uma queda no meio dele pede de novo só o que faltou. Histórico semsequencenão pede replay, como antes. - O
open()do seueventSourcesó deve resolver depois que o stream estiver conectado, para nenhum evento do turno se perder.
npm ci
npm run checkO check roda format, lint, typecheck, testes, build, a conferência dos exports e do corpus de contrato em contracts/, e um smoke test do pacote. A publicação no npm sai do workflow Release, sempre antes dos SDKs que dependem do Core.