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) # 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.
run = mcpd.runs.start(support, input={"ticket": "Q-42"})
# A repeat of this event id is a duplicate, not a second event.
mcpd.events.publish("approved", "Q-42", payload={"by": "ops"}, event_id="hook-1")
for event in mcpd.runs.watch(run.run_id):
print(event.seq, event.kind)
detail = mcpd.runs.get(run.run_id)
print(detail.run.status, detail.run.output) // 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.
let start = StartRun::new("support_ticket").input(json!({ "ticket": "Q-42" }));
let run = mcpd.start_run(start).await?;
// A repeat of this event id is a duplicate, not a second event.
let payload = json!({ "by": "ops" });
let approval = PublishEvent::new("approved", "Q-42").payload(payload).event_id("hook-1");
mcpd.publish_event(approval).await?;
mcpd.watch_run(&run.run_id, -1, |e| println!("{} {:?}", e.seq, e.kind)).await?;
let detail = mcpd.get_run(&run.run_id).await?;
println!("{:?} {:?}", detail.run.status, detail.run.output); // 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.
run, err := m.StartRun(ctx, mcpd.StartRun{
Definition: "support_ticket",
Input: map[string]any{"ticket": "Q-42"},
})
if err != nil {
panic(err)
}
// A repeat of this event id is a duplicate, not a second event.
_, err = m.PublishEvent(ctx, mcpd.PublishEvent{
Event: "approved",
Key: "Q-42",
Payload: map[string]any{"by": "ops"},
EventID: "hook-1",
})
if err != nil {
panic(err)
}
_ = m.WatchRun(ctx, run.RunID, -1, func(e mcpd.RunEvent) error {
fmt.Println(e.Seq, e.Kind)
return nil
})
detail, _ := m.GetRun(ctx, run.RunID)
fmt.Println(detail.Run.Status, string(detail.Run.Output)) // 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.
val run = mcpd.startRun("support_ticket", input = mapOf("ticket" to "Q-42"))
// A repeat of this event id is a duplicate, not a second event.
mcpd.publishEvent("approved", "Q-42", payload = mapOf("by" to "ops"), eventId = "hook-1")
mcpd.watchRun(run.runId) { event -> println("${event.seq} ${event.kind}") }
val detail = mcpd.getRun(run.runId)
println("${detail.run.status} ${detail.run.output}") # 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.
{:ok, run} = Mcpd.start_run(mcpd, support, input: %{"ticket" => "Q-42"})
# A repeat of this event id is a duplicate, not a second event.
{:ok, _} =
Mcpd.publish_event(mcpd, "approved", "Q-42",
payload: %{"by" => "ops"},
event_id: "hook-1"
)
mcpd |> Mcpd.watch_run(run.run_id) |> Enum.each(&IO.inspect({&1.seq, &1.kind}))
{:ok, detail} = Mcpd.get_run(mcpd, run.run_id)
IO.inspect({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.
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
- O que uma definição pode dizer: Workflows.
- O processo que serve as etapas: Workers.
- Todos os endpoints de execução, gerados a partir da especificação: API HTTP, execuções.