Flymmatik: flows de IA que se leem como receitas — a DSL e o porquê do Elixir

O desenho do Flymmatik: uma superfície mnemônica para flows de IA declarativos, os princípios que a seguram, e por que o runtime é Elixir/OTP.

Arte abstrata: barras laranjas arredondadas indentadas como linhas de uma receita descem à esquerda; um fluxo em gradiente azul-teal abre em três círculos laranjas paralelos que convergem e terminam num ponto em gradiente, como um ponto final. Ao fundo, uma árvore de supervisão esmaecida.
Uma receita que se lê de cima a baixo: passos, um trecho em paralelo, e o ponto final. Ao fundo, quem supervisiona.

O Flymmatik é um runtime pequeno, em Elixir, para flows de IA declarativos. Um flow é um diretório com um YAML de mesmo nome; o runtime valida a entrada, interpola variáveis, executa os steps em ordem e devolve, como JSON, apenas as chaves de contexto que o flow declarou como saída. Isso é tudo — e o resto deste texto é sobre por que “isso é tudo” foi a decisão de design mais difícil de manter.

O nome vem do tema da mosca: um aceno à Grace Hopper e ao vocabulário cotidiano de “bug” que deu nome ao debugging — a mosca não é a mariposa histórica do log dela, mas mora no mesmo dicionário. A intenção está no tema: sair do procedimento manual para a automação, sem esconder onde a máquina decide.

O problema

Fluxos de IA em produção viram, quase sempre, código imperativo espalhado: um script chama um modelo, cola o resultado num prompt, chama outro, trata erro pela metade, e seis meses depois ninguém sabe onde a IA decide e onde ela apenas formata. As bibliotecas de orquestração atacam isso adicionando abstração — classes, grafos, callbacks — e o fluxo fica mais difícil de ler, não menos.

A aposta do Flymmatik é a oposta: o fluxo é um documento. Uma receita que se lê de cima a baixo, versionável, diffável, auditável — e executável. Se a tese de que documento vem antes de código vale para projetos, vale para o próprio artefato que descreve um processo de IA.

A superfície mnemônica

Todo primitivo do parser é um mnemônico de 3 ou 4 letras: FLOW, DECL, INPS, BANK, STEP, USE, WITH, RETS, OUTS, e adiante. Maiúsculas ou minúsculas, tanto faz — o parser é case-insensitive; a convenção visual é caixa alta, porque em caixa alta os mnemônicos funcionam como marcas de seção num texto corrido: o olho acha STEP numa página de YAML como acha um título.

Um trecho — condensado, mas fiel — do bncc-map, o flow que atende o bncc.click em produção:

FLOW:
  DECL: bncc-map
  DESC: BNCC Click retrieval flow

  INPS:
    - briefing
    - ano
    - disciplina
    # ...

  STEP:
    - DECL: retrieve_habilidades
      PAR: 3                     # três buscas, em paralelo
      STEP:
        - DECL: search_nucleo
          USE: bncc              # a coleção no PaveDB
          WITH:
            filters: {Bloco_STEAM: "1", Bloco_PERF: "1"}
          RETS: nucleo
        - DECL: search_transversal
          USE: bncc
          WITH:
            filters: {Disciplina: "!{{disciplina}}"}
          RETS: transversal
        # ...

    # Embedding aproxima habilidades irmãs (EF67LP08 vs EF89LP08);
    # o juiz LLM reordena o núcleo contra a aula descrita.
    - DECL: rerank_nucleo
      USE: rerank
      WITH:
        from: nucleo
        query: "{{briefing}}"
        # Curto de propósito: juiz frio/lento -> ERR: skip devolve
        # a ordem do embedding em vez de deixar a professora esperando.
        timeout_ms: 25000
      ERR: skip
      RETS: nucleo

Repare no que a página conta sozinha: onde há paralelismo (PAR), onde a IA decide (rerank) e o que acontece quando ela falha (ERR: skip, com o porquê no comentário). O fluxo é o documento — inclusive os seus compromissos operacionais.

Por que mnemônicos, em vez de palavras completas e “amigáveis”? Três motivos práticos:

  1. Densidade de leitura. Uma receita boa tem ingredientes e passos, não parágrafos. Com mnemônicos curtos, a estrutura ocupa pouco: o grosso da página são os prompts, os campos e as condições, que é o que alguém abre o arquivo para ler.
  2. Vocabulário fechado. Um conjunto pequeno e estranho o suficiente para não colidir com o inglês corrente força o autor a aprender o vocabulário uma vez — e força o projeto a mantê-lo canônico. O glossário do Flymmatik abre com a regra: se o termo não está lá, não é canônico. O parser rejeita sintaxe legada; a spec é o contrato, não uma sugestão.
  3. Herança honesta. Mnemônicos de poucas letras são a estética de uma era em que cada token custava — assembly, COBOL, telex. Num runtime cujo tema é a Hopper, a homenagem é funcional: brevidade como disciplina, não como nostalgia.

Princípios que seguram o desenho

  • Flow é função pura. Entra um dicionário, sai um JSON com as chaves selecionadas em RETS/OUTS. Sem estado escondido entre runs. O que o flow sabe está no contexto; o que o flow entrega está declarado.
  • IA explícita. LLM aparece somente em nós de decisão (if/ switch dirigidos por modelo, com schema booleano ou enum forçado) e em nós de geração (gen, com JSON schema). Nunca embutida em “mágica” de biblioteca. Quem lê o flow sabe exatamente onde a máquina opina.
  • Canais são externos. Telegram, WhatsApp, Google Chat, REST, web, ou uma API compatível com OpenAI — um canal adapta um transporte ao runtime; o flow não sabe de onde veio a mensagem. Um listener por protocolo, com despacho por caminho canônico /<flow>/<adapter>.
  • Providers plugáveis. Retrieval, memória e backends de LLM são trocáveis. O retrieval de referência é o PaveDB — que grava e reproduz queries, então um flow inteiro pode ser reexecutado com as provas de cada busca.
  • Zero dependências onde der. O runtime anda com a biblioteca padrão. Cada dependência é uma decisão, não um reflexo.
  • Spec forward-compatible. Um step pode existir “spec only”: declarado na especificação, rejeitado em execução até a implementação chegar. O vocabulário anda na frente do motor, nunca atrás.

Papéis: kernel, callable, channel, resource

Um flow (ou módulo de código) exerce papéis num grafo de invocação: o kernel conduz o caso; callables são chamáveis (subflows inclusive); channels falam com o mundo; resources servem capacidades (modelo, retrieval, memória). Um mesmo flow pode exercer mais de um papel. Essa separação é o que permite trocar um canal de Telegram por web sem tocar no kernel — e o que mantém o caso de uso fora do motor, erro que já enterrou uma geração anterior deste projeto.

O conjunto de steps do v0

Deliberadamente pequeno: gen (geração com schema e usage), extract (campos tipados → schema estrito), if e switch (determinísticos ou LLM-driven), each (map com contexto filho), parallel (fan-out/ fan-in de steps independentes), ask (espera resposta de um canal), template (EEx), emit (evento na trilha do run), rerank (reordenação por índice, sem reescrever conteúdo), e erro por step com três políticas: halt, skip, default. Cada run produz um trace JSON com ids de run e de step. Distribuição em arquivo único .fmtk (pack/unpack), e um adaptador MCP para plugar o runtime em ferramentas que falam esse protocolo.

Por que Elixir

A resposta curta: porque o step ask existe.

Um flow conversacional espera. Espera minutos ou dias, no meio do passo 7 de 12, por uma resposta que chega por um canal qualquer. Em quase todo runtime, isso vira uma máquina de estados serializada num banco — o fluxo é fatiado em “antes” e “depois” da espera, e a leitura de cima a baixo morre.

Na BEAM, cada run é um processo. ask é um processo em espera seletiva: o flow simplesmente para na linha em que está até a mensagem chegar, e o texto da receita continua correspondendo à execução. Milhares de conversas paradas custam milhares de processos — ou seja, quase nada.

O resto do OTP vem junto e é tudo coisa que um runtime de flows precisa e normalmente reimplementa mal: supervisão (um run que morre, morre sozinho; o supervisor reergue servidores e canais sem derrubar vizinhos), isolamento (um flow em loop não trava o runtime), registro de conversas (o despacho canal→run é uma tabela de processos, não um banco), e pattern matching, que faz parser e dispatch de mnemônicos ficarem do tamanho que merecem. A biblioteca padrão rica sustenta a política de zero dependências.

Elixir não foi escolhido por moda — fluxo conversacional de longa duração é literalmente o problema que a plataforma de telecom que originou a BEAM resolvia: muitas sessões simultâneas, longas, que não podem derrubar umas às outras.

Direção

O próximo movimento da spec é fazer os steps declararem classes de capacidade (extrair, redigir, julgar) em vez de modelos, e carregarem critérios de aceitação como campo de primeira classe — schema e checagens sobre a saída. Com contrato tipado e aceitação declarada, o runtime pode provar que um step foi bem atendido, e a escolha de modelo vira detalhe de execução, não decisão do autor. A superfície de autoria não cresce com isso: quem escreve receita continua escrevendo receita.

O Flymmatik é AGPL — leia, audite, rode o seu. A versão gerenciada entra no Flowlexi Cloud, ao lado do PaveDB, quando sair do preview. Receitas que se leem como receitas, rodando como relógio — com as provas guardadas.