Pular para o conteúdo

Arquitetura de armazenamento de blobs e artefatos

Este documento descreve como o coding-agent armazena payloads grandes/binários fora do JSONL de sessão, como a saída truncada de ferramentas é persistida e como as URLs internas (artifact://, agent://) resolvem de volta para os dados armazenados.

O runtime utiliza dois mecanismos de persistência diferentes para diferentes formatos de dados:

  • Blobs endereçados por conteúdo (blob:sha256:<hash>): armazenamento global orientado a binários, usado para externalizar payloads base64 de imagens grandes das entradas de sessão persistidas.
  • Artefatos com escopo de sessão (arquivos sob <arquivoDeSessão-sem-.jsonl>/): arquivos de texto por sessão usados para saídas completas de ferramentas e saídas de subagentes.

Eles são intencionalmente separados:

  • o armazenamento de blobs otimiza a deduplicação e referências estáveis por hash de conteúdo,
  • o armazenamento de artefatos otimiza ferramentas de sessão append-only e recuperação por humanos/ferramentas através de IDs locais.

SessionManager constrói BlobStore(getBlobsDir()), então os arquivos de blob ficam em um diretório global compartilhado de blobs (não em uma pasta de sessão).

Nomenclatura de arquivos de blob:

  • caminho do arquivo: <blobsDir>/<sha256-hex>
  • sem extensão
  • string de referência armazenada nas entradas: blob:sha256:<sha256-hex>

Implicações:

  • o mesmo conteúdo binário entre sessões resolve para o mesmo hash/caminho,
  • escritas são idempotentes no nível do conteúdo,
  • blobs podem sobreviver a qualquer arquivo de sessão individual.

ArtifactManager deriva o diretório de artefatos a partir do caminho do arquivo de sessão:

  • arquivo de sessão: .../<timestamp>_<sessionId>.jsonl
  • diretório de artefatos: .../<timestamp>_<sessionId>/ (remove .jsonl)

Os tipos de artefatos compartilham este diretório:

  • arquivos de saída de ferramenta truncada: <numericId>.<toolType>.log (para artifact://)
  • arquivos de saída de subagente: <outputId>.md (para agent://)

BlobStore.put() computa SHA-256 sobre os bytes binários brutos e retorna:

  • hash: digest hexadecimal,
  • path: <blobsDir>/<hash>,
  • ref: blob:sha256:<hash>.

Nenhum contador local de sessão é utilizado.

IDs de artefato: inteiro monotônico local à sessão

Seção intitulada “IDs de artefato: inteiro monotônico local à sessão”

ArtifactManager escaneia os arquivos de artefato *.log existentes no primeiro uso para encontrar o ID numérico máximo existente e define nextId = max + 1.

Comportamento de alocação:

  • formato do arquivo: {id}.{toolType}.log
  • IDs são strings sequenciais ("0", "1", …)
  • a retomada não sobrescreve artefatos existentes porque o escaneamento acontece antes da alocação.

Se o diretório de artefatos estiver ausente, o escaneamento retorna lista vazia e a alocação começa do 0.

AgentOutputManager aloca IDs para saídas de subagentes como <index>-<requestedId> (opcionalmente aninhado sob prefixo pai, por exemplo, 0-Parent.1-Child). Ele escaneia arquivos .md existentes na inicialização para continuar a partir do próximo índice na retomada.

1) Caminho de reescrita na persistência de entradas de sessão

Seção intitulada “1) Caminho de reescrita na persistência de entradas de sessão”

Antes que as entradas de sessão sejam escritas (#rewriteFile / persistência incremental), SessionManager chama prepareEntryForPersistence() (via truncateForPersistence).

Comportamentos-chave:

  1. Truncamento de strings grandes: strings superdimensionadas são cortadas e sufixadas com "[Session persistence truncated large content]".
  2. Remoção de campos transientes: partialJson e jsonlEvents são removidos das entradas persistidas.
  3. Externalização de imagens para blobs:
    • aplica-se apenas a blocos de imagem em arrays content,
    • apenas quando data não é já uma referência de blob,
    • apenas quando o comprimento do base64 é pelo menos o limite (BLOB_EXTERNALIZE_THRESHOLD = 1024),
    • substitui base64 inline por blob:sha256:<hash>.

Isso mantém o JSONL de sessão compacto enquanto preserva a recuperabilidade.

2) Caminho de reidratação no carregamento de sessão

Seção intitulada “2) Caminho de reidratação no carregamento de sessão”

Ao abrir uma sessão (setSessionFile), após as migrações, SessionManager executa resolveBlobRefsInEntries().

Para cada bloco de imagem de message/custom-message com blob:sha256:<hash>:

  • lê os bytes do blob a partir do armazenamento de blobs,
  • converte os bytes de volta para base64,
  • modifica a entrada em memória para inline base64 para consumidores em tempo de execução.

Se o blob estiver ausente:

  • resolveImageData() registra um aviso,
  • retorna a string de referência original sem alteração,
  • o carregamento continua (sem crash).

3) Caminho de despejo/truncamento de saída de ferramenta

Seção intitulada “3) Caminho de despejo/truncamento de saída de ferramenta”

OutputSink alimenta a saída em streaming no bash/python/ssh e executores relacionados.

Comportamento:

  1. Cada chunk é sanitizado e adicionado ao buffer de cauda em memória.
  2. Quando os bytes em memória excedem o limite de despejo (DEFAULT_MAX_BYTES, 50KB), o sink marca a saída como truncada.
  3. Se um caminho de artefato está disponível, o sink abre um escritor de arquivo e escreve:
    • o conteúdo já em buffer uma vez,
    • todos os chunks subsequentes.
  4. O buffer em memória é sempre aparado para a janela de cauda para exibição.
  5. dump() retorna um resumo incluindo artifactId apenas quando o file sink foi criado com sucesso.

Efeito prático:

  • UI/retorno de ferramenta mostra a cauda truncada,
  • a saída completa é preservada no arquivo de artefato e referenciada como artifact://<id>.

Se a criação do file sink falhar (erro de I/O, caminho ausente, etc.), o sink silenciosamente faz fallback para truncamento somente em memória; a saída completa não é persistida.

blob:sha256:<hash> é uma referência de persistência dentro dos payloads de entradas de sessão, não um esquema de URL interno tratado pelo roteador. A resolução é feita pelo SessionManager durante o carregamento da sessão.

Tratado pelo ArtifactProtocolHandler:

  • requer diretório de artefatos de sessão ativo,
  • o ID deve ser numérico,
  • resolve por correspondência do prefixo do nome de arquivo <id>.,
  • retorna texto bruto (text/plain) do arquivo .log correspondente,
  • quando ausente, o erro inclui lista de IDs de artefatos disponíveis.

Comportamento com diretório ausente:

  • se o diretório de artefatos não existir, lança No artifacts directory found.

Tratado pelo AgentProtocolHandler sobre <artifactsDir>/<id>.md:

  • a forma simples retorna texto markdown,
  • as formas /path ou ?q= realizam extração JSON,
  • extração por path e query não podem ser combinadas,
  • se extração é solicitada, o conteúdo do arquivo deve ser parseado como JSON.

Comportamento com diretório ausente:

  • lança No artifacts directory found.

Comportamento com saída ausente:

  • lança Not found: <id> com IDs disponíveis dos arquivos .md existentes.

Integração com a ferramenta read:

  • read suporta paginação com offset/limit para leituras de URL interna sem extração,
  • rejeita offset/limit quando extração agent:// é utilizada.
  • ArtifactManager escaneia arquivos {id}.*.log existentes na primeira alocação e continua a numeração.
  • AgentOutputManager escaneia IDs de saída .md existentes e continua a numeração.
  • SessionManager reidrata referências de blob para base64 no carregamento.

SessionManager.fork() cria um novo arquivo de sessão com novo ID de sessão e link parentSession, então retorna os caminhos de arquivo antigo/novo. A cópia de artefatos é tratada pelo AgentSession.fork():

  • tenta cópia recursiva do diretório de artefatos antigo para o novo diretório de artefatos,
  • diretório antigo ausente é tolerado,
  • erros de cópia que não são ENOENT são registrados como avisos e o fork ainda é concluído.

Implicações de ID após o fork:

  • se a cópia foi bem-sucedida, os contadores de artefatos na nova sessão continuam após o ID máximo copiado,
  • se a cópia falhou/foi ignorada, os IDs de artefatos da nova sessão começam do 0.

Implicações de blob após o fork:

  • blobs são globais e endereçados por conteúdo, então nenhuma cópia de diretório de blobs é necessária.

SessionManager.moveTo() renomeia tanto o arquivo de sessão quanto o diretório de artefatos para o novo diretório de sessão padrão, com lógica de rollback se uma etapa posterior falhar. Isso preserva a identidade dos artefatos enquanto realoca o escopo da sessão.

CasoComportamento
Arquivo de blob ausente durante reidrataçãoAvisa e mantém a string de referência blob:sha256: em memória
Blob read ENOENT via BlobStore.getRetorna null
Diretório de artefatos ausente (ArtifactManager.listFiles)Retorna lista vazia (alocação pode começar do zero)
Diretório de artefatos ausente (artifact:// / agent://)Lança explicitamente No artifacts directory found
ID de artefato não encontradoLança com listagem de IDs disponíveis
Falha na inicialização do escritor de artefato do OutputSinkContinua com truncamento somente de cauda (sem artefato de saída completa)
Sem arquivo de sessão (alguns caminhos de tarefa)Ferramenta Task faz fallback para diretório de artefatos temporário para saídas de subagente

Externalização de blob binário vs artefatos de saída de texto

Seção intitulada “Externalização de blob binário vs artefatos de saída de texto”
  • Externalização de blob é para payloads de imagens binárias dentro do conteúdo de entradas de sessão persistidas; substitui base64 inline no JSONL por referências de conteúdo estáveis.
  • Artefatos são arquivos de texto simples para saída de execução e saída de subagente; são endereçáveis por IDs locais à sessão através de URLs internas.

Os dois sistemas se intersectam apenas indiretamente (ambos reduzem o inchaço do JSONL de sessão) mas possuem caminhos diferentes de identidade, tempo de vida e recuperação.