Ir para o conteúdo
mcpd
sem daemon
Código

Execuções

Uma execução é uma linha, um cursor, um diário de tentativas, um conjunto de prazos e um conjunto de suspensões. Esta página é o que você pode fazer com uma, e o que ela conta de volta.

Iniciar uma

Uma execução começa com um id de definição e uma entrada. O daemon valida a entrada contra o schema da definição quando ela tem um, fixa a maior versão gravada, cria a execução e o trace dela, e responde com a execução como ficou gravada. Daí em diante a execução anda sozinha: o daemon oferece as etapas dela aos workers, percorre seus sleeps e suas esperas, e escreve no diário o que vai acontecendo.

iniciar, responder, acompanhar

// Start a run from anywhere that holds an execute key, answer the wait from a
// webhook, then follow the journal to the end and read the run.
const run = await mcpd.runs.start({
  definitionId: 'support_ticket',
  input: { ticket: 'Q-42' },
})

await mcpd.events.publish({
  event: 'approved',
  key: 'Q-42',
  payload: { by: 'ops' },
  eventId: 'hook-1', // a repeat of this id is a duplicate, not a second event
})

for await (const event of mcpd.runs.watch(run.runId)) {
  console.log(event.seq, event.kind)
}
const detail = await mcpd.runs.get(run.runId)
console.log(detail.run.status, detail.run.output)

Mande um idempotency_key de qualquer chamador que possa ser repetido. Ele é único por projeto enquanto a execução estiver guardada, e uma repetição da mesma chamada devolve a mesma execução em vez de iniciar uma segunda. Um webhook que dispara duas vezes, ou um executor de jobs que reinicia, aí não custa nada.

The cursor

The daemon holds the run between steps: where it is, what each step answered, what it is waiting for and until when. A worker holds one step at a time, and only while it runs. During a wait no process is alive at all.

classify task your process draft agent your process approval wait_event the daemon alone send task · at_most_once your process retry

O diário

Toda mudança em uma execução é um evento com um número de sequência, a partir de zero e sem buracos: a execução começou, o cursor andou, uma etapa recebeu um lease, começou, terminou ou falhou, a execução esperou e retomou, a execução acabou. Cada um carrega um payload com o formato do seu tipo. O diário é a história da execução e é o que a interface desenha.

Leia-o em páginas a partir de after, ou segure aberto o endpoint de eventos com Accept: text/event-stream e receba cada evento assim que ele é confirmado. Um cliente que para e volta a começar passa o último número de sequência que viu e não perde nada. O watch de cada SDK faz isso por você e termina quando a execução termina, e o wait segue o diário até o fim e devolve a saída, ou uma falha tipada carregando a execução quando ela não deu certo.

Eventos e esperas

Uma etapa wait_event segura a execução até chegar um evento com o nome dela e com a chave dela já renderizada. O evento entra por uma porta só:

responder a uma espera

curl -X POST http://localhost:4317/v1/api/events \ -H 'content-type: application/json' \ -d '{"project":"support","event":"approved","key":"Q-42","payload":{"by":"ops"},"event_id":"hook-1"}'

A resposta é um de três desfechos. delivered: uma execução estava esperando e recebeu o evento na mesma transação, então quando você lê o recibo a execução já andou. parked: nenhuma execução estava esperando, e o evento fica guardado para uma espera que abrir depois e que disser que aceita eventos guardados. duplicate: o event_id já tinha sido visto e nada foi gravado. Mande um id seu de qualquer coisa que possa disparar duas vezes.

Uma espera que chega ao prazo faz o que o on_timeout dela diz: falha, e aí o on_error dela decide, ou segue adiante sem saída nenhuma. Mandar direto para uma execução, pela espera aberta dela, é POST /v1/api/runs/{run}/signal; ele recebe o mesmo corpo sem chave, porque a execução é o endereço.

Cancelar

POST /v1/api/runs/{run}/cancel encerra uma execução e toda execução filha abaixo dela. Uma etapa em voo não é interrompida no fio: o worker descobre no próximo heartbeat que a execução acabou, o contexto dele avisa o corpo, e o que o corpo mandar depois disso é recusado com run_terminal. Cancelar uma execução que já acabou não muda nada e devolve a execução como ela está.

Temporizadores e limites

Sleeps, prazos de espera, backoffs de repetição e vencimentos de lease são todos temporizadores que o daemon guarda. Eles disparam no relógio do daemon, uma vez por segundo, então um retry_after_ms de cem milissegundos é cumprido como pelo menos isso, e não exatamente isso. Um reinício do daemon não perde nenhum deles; são linhas, e a execução retoma a partir da linha. Nenhuma duração pode passar de 3650 dias, e um retry_after_ms mais longo vindo de um worker é recusado com um 400. Um sleep_until além do ano 2262 é tratado como um prazo que nunca vence.

O max_steps, o run_timeout_ms e o max_cost_usd de uma definição encerram a execução no teto, com o teto nomeado como o erro. O custo é a soma do que o uso relatado pelas etapas custou, pela mesma tabela que precifica spans, então uma etapa de agente que relata seus tokens é debitada do orçamento da execução no instante em que termina.

A execução dentro do seu trace

Toda execução também é um trace. O daemon abre um span raiz para a execução e um span por tentativa, com a entrada e a saída da etapa, e um corpo que registra os próprios spans pelo contexto da etapa os põe embaixo dele. Assim a chamada de modelo dentro de uma etapa de agente fica sob aquela etapa, sob aquela execução, e a busca, a apuração de custos e os avaliadores que servem para qualquer trace servem sem mudança para o trace de uma execução. Esses spans são escritos pelo mesmo caminho de ingestão que os spans de um SDK percorrem, então são amostrados, ocultados, precificados e encaminhados do mesmo jeito. Os tokens ficam nos spans de modelo que os gastaram; o span da etapa carrega a soma precificada nos atributos dele, então um total de trace conta cada token uma vez só. A execução carrega o trace_id dela, e a interface liga os dois.

O daemon escreve esses spans por um outbox, no relógio dele, depois que a linha que os criou está confirmada. Um trace lido um instante depois de uma etapa terminar pode ainda não trazer o span daquela etapa; o detalhe da própria execução sempre traz.

Ler as execuções de volta

GET /v1/api/runs lista as execuções de um projeto, da atualizada mais recentemente em diante, filtradas por status ou por definição. GET /v1/api/runs/{run} é a execução e tudo o que é durável nela: cada tentativa com o dono do lease, os tempos, o erro e o uso; os temporizadores ainda pendentes; as esperas ainda abertas. A tela Workflows da interface é este endpoint desenhado, com o diário ao vivo ao lado.

Os dois devolvem o que a execução recebeu, então os dois são registrados no log de auditoria, como list_runs e get_run, e também são registrados o diário (list_run_events, uma vez por requisição, inclusive um stream), o percurso de causa (get_step_cause) e um cancelamento (cancel_run). Uma leitura recusada é registrada contra o projeto dono da execução. As chamadas de worker não são registradas; veja Workers.

Armazenamento

As execuções vivem em um arquivo SQLite próprio, ao lado do armazenamento de traces: <data>.runs.<ext>, a menos que --runs-data diga outra coisa, e --runs-data off desliga a superfície de execuções por inteiro, caso em que todo endpoint de execução responde 503. Faça backup dele junto com o armazenamento de traces; o mcpd doctor o avalia como um grupo próprio.

O armazenamento de execuções guarda os payloads como eles foram enviados. A entrada, o estado, a saída e a correlação de uma execução, a entrada, a saída e o erro de cada etapa, o payload de cada evento e os payloads de span que ainda esperam ser escritos no armazenamento de traces são guardados sem ocultação, e o user_id na correlação de uma execução é guardado sem hash. Isso é de propósito: eles são o estado de trabalho do workflow, uma etapa posterior os lê de volta, e um valor que o ocultador tivesse reescrito mudaria o que a execução faz. Os spans que o daemon escreve no armazenamento de traces passam pela mesma ocultação e pelo mesmo hash de identificadores que um span enviado por um SDK, então o armazenamento de traces guarda a cópia ocultada. Trate o arquivo de execuções como um arquivo que guarda tudo o que as suas etapas receberam, e proteja-o e faça backup dele de acordo.

A retenção alcança o armazenamento de execuções. A passagem de hora em hora que corta o armazenamento de traces também apaga, na mesma janela por projeto, toda execução cuja raiz terminou antes do corte, com as execuções filhas dela e tudo o que está guardado para elas: tentativas, eventos, prazos, esperas, spans na fila e os eventos que essas execuções consumiram. Uma execução que não terminou nunca é apagada, por mais antiga que seja. As definições são mantidas.

O apagamento também o alcança. O DELETE /v1/api/subjects/{user_id} apaga toda execução terminada no projeto cuja correlação carrega aquele user_id, do jeito que você iniciou a execução com ele, com a árvore inteira de execuções filhas e tudo o que está guardado para elas, e informa a contagem como runs_erased. Uma execução dessa pessoa que não terminou fica inteira, é contada em runs_in_flight e é nomeada no note da resposta: cancele-a, ou deixe-a terminar, e mande o apagamento de novo. Um evento guardado para uma execução que ainda não chegou à espera dela é indexado pela chave de correlação dele, e não por uma pessoa, então o apagamento não o alcança; ele é apagado quando vence, depois de 24 horas. Veja Ocultação e retenção para o resto da resposta.

A seguir