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

Workflows

Um workflow são dados que o daemon guarda e corpos que o daemon nunca vê. Esta página são os dados: o que uma definição pode dizer, e o que cada construção faz o daemon fazer.

A divisão do trabalho

Uma execução durável tem dois donos. O daemon é dono da execução entre as etapas: o cursor, o diário, o orçamento de repetições, os temporizadores e as esperas. O seu processo é dono de tudo o que há dentro de uma etapa: o modelo que você chama, o prompt que você manda, as ferramentas que você roda, as credenciais que você guarda. Nada disso é entregue ao daemon, e ele nunca roda um modelo no seu lugar.

Então um workflow chega em duas partes, vindas de um arquivo só. O grafo vai para o daemon como uma definição, um documento que o daemon percorre. Os corpos das etapas ficam no seu processo, na linguagem em que você os escreveu, para um worker servir. Nada precisa ser implantado no daemon, e um corpo pode mudar sem que a definição mude em 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

Escrever um

O builder produz as duas partes. document() é a definição, e cada chamada de step ou de agent deixa o corpo dela para trás, para o worker. Todo SDK tem o mesmo builder com as mesmas nove construções, então um workflow escrito em uma linguagem se lê igual em outra, e o daemon grava o mesmo documento seja qual for a que o escreveu.

um workflow de suporte

const support = workflow({ id: 'support_ticket' })
  .step('classify', async (ctx, input: { ticket: string }) => ({
    kind: input.ticket.startsWith('Q') ? 'question' : 'bug',
  }))
  .agent(
    'draft',
    async (ctx, input: { kind: string }) => {
      // Your own model call, recorded under this step in the run's trace. Its
      // usage adds up into the step's, which the daemon prices.
      return await ctx.withSpan('model_call', async (span) => {
        span.setModel('claude-sonnet-4', 'anthropic')
        span.setUsage({ inputTokens: 812, outputTokens: 37 })
        return `an answer to a ${input.kind}`
      })
    },
    { input: { kind: '${steps.classify.output.kind}' } },
  )
  .waitFor('approval', {
    event: 'approved',
    key: '${input.ticket}',
    timeoutMs: 7 * 24 * 3_600_000,
    when: when('steps.classify.output.kind', 'eq', 'bug'),
  })
  .step(
    'send',
    async (ctx, input: { text: string }) => {
      if (!(await deliver(input.text, ctx.idempotencyKey))) {
        throw new RetryableError('mail relay busy', { retryAfterMs: 2_000 })
      }
      return { sent: true }
    },
    {
      input: { text: '${steps.draft.output}' },
      effect: 'at_most_once',
      retry: { maxAttempts: 3, backoff: 'exponential', initialMs: 500 },
    },
  )
  .output({ sent: '${steps.send.output.sent}' })

Três coisas naquele exemplo são o formato de todo workflow. A entrada de uma etapa é um template que o daemon renderiza a cada tentativa, então draft lê o que classify respondeu sem que nenhum dos dois corpos saiba do outro. Uma guarda decide se um nó chega a rodar: uma pergunta segue como está, um bug espera por uma pessoa. E a última etapa é at_most_once com uma política de repetição, que é a combinação de que um efeito colateral precisa: o daemon repete uma falha passageira, e nunca roda um corpo duas vezes sem que lhe digam para isso.

As construções

Nove, e todo SDK tem as nove. Duas são reivindicáveis, o que quer dizer que um worker as serve; o resto é do próprio daemon.

ConstruçãoQuem rodaO que faz
task Um worker Trabalho reivindicável. O worker roda o código que registrou sob este id.
agent Um worker O mesmo no fio, gravado como um turno de agente: o seu próprio laço, com quantas chamadas de modelo forem, como uma etapa durável só. O trace e a interface o distinguem de uma task comum.
sleep O daemon Um atraso de tantos milissegundos. Nenhum processo fica vivo durante ele.
sleep_until O daemon Um atraso até um instante renderizado de um template, como uma data de cobrança que veio na entrada.
wait_event O daemon Uma pausa até chegar um evento com este nome e esta chave, ou até o prazo passar. A chave é renderizada por execução, então uma espera endereça um pedido e nenhum outro.
parallel Os dois Ramos nomeados, cada um uma etapa reivindicável, todos em disco antes de qualquer um ser oferecido. A execução segue adiante quando o último ramo acaba.
foreach Os dois Uma etapa reivindicável por elemento do array que está em um caminho, limitada por max_items e, se você disser, por quantas rodam ao mesmo tempo. As saídas voltam como um array na ordem dos itens.
loop Os dois Um laço limitado sobre uma etapa reivindicável. until é avaliado depois de cada iteração e pode encerrá-lo antes.
subworkflow O daemon Uma execução filha de outra definição, com diário próprio. A saída dela vira a saída desta etapa, e cancelar a execução mãe cancela a filha.

Um ramo, um corpo de foreach e um corpo de loop são uma etapa reivindicável cada. Um corpo que precisa de várias etapas é um subworkflow, o que mantém todo diário plano: uma execução é uma lista, nunca uma árvore de listas.

Templates e guardas

Um template é JSON com referências $${path} dentro das strings. O daemon o renderiza quando uma tentativa é criada, a partir do que a execução guarda naquele momento: input, a entrada da execução; steps.<id>, o que uma etapa anterior respondeu, com .output, .status e .error; run, o id e a correlação da própria execução; e, dentro de um foreach ou de um loop, item, index e iter.index. Uma referência que é a string inteira mantém o tipo, então "$${steps.classify.output}" entrega à etapa seguinte um objeto, e não uma string com um objeto dentro.

Uma guarda é um predicado sobre os mesmos caminhos: eq, ne, lt, lte, gt, gte, in, exists e matches, combinados com all, any e not. Uma guarda que dá falso grava o nó como skipped e a execução segue adiante. Um caminho que não existe torna toda comparação falsa em vez de levantar erro, porque uma execução que morre por um campo que falta é uma execução que ninguém consegue terminar.

O daemon valida o documento na hora de gravá-lo. Uma guarda que nomeia um caminho que nada pode produzir, um template que lê uma etapa declarada depois, um foreach sobre algo que não é um caminho, um id que não casa com ^[a-z][a-z0-9_]*$: cada um é recusado com um 400 que nomeia o campo. Campos desconhecidos também são recusados, então uma guarda escrita errado é um erro, e não uma execução que pula a guarda.

Toda duração tem um teto, além de um piso. run_timeout_ms, step_timeout_ms, o initial_ms e o max_ms de uma repetição, o ms de um sleep e o timeout_ms de uma espera valem cada um no máximo 315360000000, que são 3650 dias, e um valor mais longo é recusado com um 400 que nomeia o campo, o teto e o valor que você mandou. Um prazo mais distante do que isso não caberia no relógio em que o daemon o guarda.

Repetições, efeitos e falha

Um corpo que falha diz ao daemon de que jeito, com uma classe. retryable vale outra tentativa, e o daemon faz uma enquanto durar o orçamento de retry do nó, no backoff do nó, esperando pelo menos o retry_after_ms que o corpo pediu, que fica sujeito ao mesmo teto de 3650 dias que toda outra duração. fatal e invalid_input não são repetidos. unservable quer dizer que este worker não guarda a definição ou a etapa, e a tentativa volta para a fila para outro worker sem gastar o orçamento. Um corpo que levanta qualquer outra coisa é fatal.

max_attempts conta toda tentativa, inclusive a primeira, então o padrão de uma quer dizer nenhuma repetição. Esgotado o orçamento, o on_error decide: fail_run, o padrão, encerra a execução com a mensagem da etapa como erro dela; continue marca a etapa como falha e segue adiante, de modo que uma guarda posterior pode ler steps.<id>.status e desviar dela.

effect diz o que custaria um corpo duplicado. Sob at_least_once, o padrão, um lease perdido devolve a tentativa à fila e o corpo pode rodar de novo. Sob at_most_once o worker precisa mandar started antes do corpo, e uma tentativa já iniciada que perde o lease falha em vez de rodar outra vez. Use isso na etapa que manda o e-mail ou cobra o cartão, e dê àquele corpo o idempotency_key do lease para entregar ao terceiro: ele é estável entre as repetições da mesma etapa, então uma repetição depois de uma resposta perdida é reconhecida do outro lado também.

Versões

Uma definição é imutável depois de gravada. Mande o mesmo corpo de novo e você recebe a mesma versão de volta; mande um corpo diferente sob uma versão que já existe e o daemon recusa com um 409, porque senão uma execução que fixou aquela versão mudaria por baixo dela. Deixe a versão de fora e o daemon atribui o número seguinte, que é o que um worker faz ao subir. Uma execução fixa a versão com que começou e nunca a resolve de novo, então uma implantação que grava a versão 3 termina as execuções na versão 2 como elas foram escritas.

Limites

Uma definição pode declarar max_steps, run_timeout_ms e max_cost_usd. O daemon conta toda tentativa contra o primeiro, a idade da execução contra o segundo, e o uso precificado que as etapas relatam contra o terceiro, e encerra a execução como timed_out ou failed no teto, com o teto nomeado como o erro. Um loop que não converge e uma etapa de agente que não para de gastar são o mesmo problema para o daemon, e a mesma opção detém os dois.

O que o daemon guarda

A entrada de uma execução, o estado dela, a saída dela e a entrada e a saída de cada etapa ficam no armazenamento de execuções exatamente como foram enviados. Eles não são ocultados: são o próprio 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 uma execução escreve no armazenamento de traces são ocultados como qualquer outro span. A retenção e o apagamento também alcançam o armazenamento de execuções, para as execuções que terminaram. Execuções diz exatamente o que cada um apaga.

A seguir