Todos os artigos

// Knowledge.log — 技術記事

Memória de projeto para coding agents: quem monta o contexto

Monte o contexto de um coding agent por tarefa, com orçamento em bytes e tokens, descarte auditável e falha segura para arquivos obrigatórios.

O coding agent recebe uma correção pequena, pergunta onde ficam os testes, lê metade do repositório e, dois turnos depois, pergunta onde ficam os testes outra vez. O problema não é necessariamente falta de uma “memória melhor” no modelo. Em muitos casos, o harness montou um contexto ruim: incluiu informação demais, deixou uma regra necessária de fora ou não registrou o que entregou ao modelo.

A divisão de responsabilidade precisa ficar clara. O modelo interpreta o contexto. O harness — a aplicação que coordena modelo, ferramentas, instruções e histórico — decide o que entra nele. É ali que o time deve implementar carregamento por tarefa, orçamento, descarte e auditoria.

Um AGENTS.md ajuda a manter convenções estáveis perto do código, mas não escolhe sozinho os arquivos de uma correção nem garante que todos couberam na janela. Ele é uma entrada do loader, não o loader inteiro.

Memória de projeto não é despejar o repositório no prompt

Para uma tarefa em payments/refund.py, o agente talvez precise de:

  • instruções curtas do projeto;
  • o arquivo alterado e os testes relacionados;
  • contratos importados diretamente por esse código;
  • a descrição e o diff do PR.

Ele provavelmente não precisa dos exemplos antigos, da documentação de outro serviço e de todos os arquivos gerados. Colocar tudo “por segurança” troca uma decisão explícita por ruído caro. O diretório inteiro entrou; a compreensão, mais modestamente, não assinou presença.

Um loader por tarefa começa com dois conjuntos:

  1. arquivos obrigatórios, definidos pelo perfil da tarefa;
  2. caminhos de foco, obtidos do PR ou informados pelo operador.

Para uma revisão de segurança ou release, a ausência de uma política obrigatória deve interromper o trabalho. Para exploração inicial ou rascunho, arquivos opcionais podem ser descartados quando o orçamento terminar. A busca por dependências pode acontecer depois, sob demanda, em vez de antecipar qualquer arquivo que talvez venha a ser útil.

Esse desenho aparece de formas diferentes nas ferramentas atuais. O Aider repo map envia um subconjunto ranqueado de assinaturas dentro de um orçamento, não o corpo completo de todo o projeto. Cursor separa regras do projeto e ferramentas de busca no contexto do Agent. Já as instruções do GitHub Copilot podem ter escopo por caminho, conforme a referência de customização. Não existe um único algoritmo universal de descoberta; existe a responsabilidade comum do harness de montar a entrada.

Tamanho e relevância são decisões diferentes

Tamanho é mensurável. Relevância é uma política que o time precisa declarar.

Para tamanho, registre pelo menos bytes UTF-8. Se o modelo pertence a uma família coberta pelo tokenizer usado no runtime, registre também tokens. O tiktoken 0.14.0 oferece o200k_base para modelos OpenAI compatíveis. Essa contagem não deve ser reaproveitada como se fosse a medida oficial de qualquer outro provedor.

No exemplo abaixo, o orçamento de bytes funciona apenas com a biblioteca padrão do Python 3.14.4. O orçamento de tokens é opcional e, quando ativado, exige tiktoken==0.14.0. O programa não usa uma divisão aproximada por quatro como gate: uma estimativa conveniente continua sendo uma estimativa, ainda que use uma gravata e participe da reunião.

Para relevância, uma regra simples e verificável costuma ser melhor do que um “score inteligente” sem origem:

  1. arquivos obrigatórios entram primeiro;
  2. caminhos alterados no PR entram depois, na ordem recebida;
  3. arquivos adicionais só entram quando uma ferramenta ou relação explícita justificar a leitura;
  4. cada decisão recebe um motivo no log.

É possível evoluir isso com globs por tarefa, imports diretos ou um mapa de símbolos. O ponto é preservar a explicação: required-policy, pr-path, dependency-of:X. Sem esse motivo, fica difícil saber se o loader ajudou ou apenas reorganizou o acaso.

Um packer executável com orçamento e auditoria

O script a seguir recebe a raiz do repositório, arquivos obrigatórios e caminhos do PR. Ele aplica simultaneamente um limite em bytes e, quando configurado, outro em tokens. Cada execução acrescenta uma linha JSON a um arquivo de auditoria.

#!/usr/bin/env python3
"""Empacota arquivos explícitos de um repositório para um coding agent."""

from __future__ import annotations

import argparse
import json
import sys
from dataclasses import dataclass, asdict
from pathlib import Path


@dataclass
class Decision:
    path: str
    role: str
    bytes: int
    tokens: int | None
    action: str
    reason: str


def parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser()
    p.add_argument("root", type=Path)
    p.add_argument("--pr-path", action="append", default=[], dest="pr_paths",
                   help="caminho alterado no PR; repita a opção")
    p.add_argument("--required", action="append", default=[])
    p.add_argument("--byte-budget", type=int, default=32_768)
    p.add_argument("--token-budget", type=int)
    p.add_argument("--mode", choices=("fail-closed", "compact"), required=True)
    p.add_argument(
        "--audit", type=Path, default=Path(".harness/context-audit.jsonl")
    )
    return p


def token_counter(enabled: bool):
    if not enabled:
        return None
    try:
        import tiktoken
    except ImportError as exc:
        raise SystemExit(
            "--token-budget exige: python -m pip install tiktoken==0.14.0"
        ) from exc
    encoding = tiktoken.get_encoding("o200k_base")
    return lambda text: len(encoding.encode(text))


def safe_path(root: Path, relative: str) -> Path:
    candidate = (root / relative).resolve()
    if not candidate.is_relative_to(root):
        raise ValueError(f"caminho fora do repositório: {relative}")
    return candidate


def append_audit(path: Path, record: dict) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    with path.open("a", encoding="utf-8") as stream:
        stream.write(json.dumps(record, ensure_ascii=False) + "\n")


def main() -> int:
    args = parser().parse_args()
    root = args.root.resolve()
    if not root.is_dir():
        raise SystemExit(f"raiz inválida: {root}")
    if args.byte_budget <= 0 or (
        args.token_budget is not None and args.token_budget <= 0
    ):
        raise SystemExit("orçamentos devem ser positivos")

    count_tokens = token_counter(args.token_budget is not None)
    queue = [(path, "required") for path in args.required]
    queue.extend((path, "pr-path") for path in args.pr_paths)

    used_bytes = 0
    used_tokens = 0
    packed: list[str] = []
    decisions: list[Decision] = []
    seen: set[str] = set()
    failure: str | None = None

    for relative, role in queue:
        if relative in seen:
            continue
        seen.add(relative)
        try:
            path = safe_path(root, relative)
        except ValueError as exc:
            failure = str(exc)
            decisions.append(Decision(relative, role, 0, None, "reject", failure))
            break

        if not path.is_file():
            reason = "required-missing" if role == "required" else "not-a-file"
            decisions.append(Decision(relative, role, 0, None, "discard", reason))
            if role == "required":
                failure = f"arquivo obrigatório ausente: {relative}"
                break
            continue

        raw = path.read_bytes()
        text = raw.decode("utf-8", errors="replace")
        tokens = count_tokens(text) if count_tokens else None
        exceeds_bytes = used_bytes + len(raw) > args.byte_budget
        exceeds_tokens = (
            args.token_budget is not None
            and tokens is not None
            and used_tokens + tokens > args.token_budget
        )

        if exceeds_bytes or exceeds_tokens:
            limit = "bytes+tokens" if exceeds_bytes and exceeds_tokens else (
                "bytes" if exceeds_bytes else "tokens"
            )
            decisions.append(
                Decision(relative, role, len(raw), tokens, "discard", f"budget:{limit}")
            )
            if role == "required" or args.mode == "fail-closed":
                failure = f"fail-closed: {relative} excede o orçamento de {limit}"
                break
            continue

        used_bytes += len(raw)
        used_tokens += tokens or 0
        packed.append(f"## {relative}\n{text}")
        decisions.append(
            Decision(relative, role, len(raw), tokens, "inject", "budget-ok")
        )

    record = {
        "schema": 1,
        "mode": args.mode,
        "byte_budget": args.byte_budget,
        "token_budget": args.token_budget,
        "used_bytes": used_bytes,
        "used_tokens": used_tokens if count_tokens else None,
        "status": "failed" if failure else "packed",
        "failure": failure,
        "items": [asdict(item) for item in decisions],
    }
    audit_path = args.audit if args.audit.is_absolute() else root / args.audit
    append_audit(audit_path, record)

    if failure:
        print(failure, file=sys.stderr)
        return 2

    sys.stdout.write("\n\n".join(packed))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

Uma execução só com a biblioteca padrão pode usar o limite em bytes:

python pack_context.py ./meu-repo \
  --mode fail-closed \
  --byte-budget 32768 \
  --required AGENTS.md \
  --required pyproject.toml \
  --pr-path src/payments/refund.py \
  --pr-path tests/payments/test_refund.py \
  > context.md

Para aplicar também um limite de tokens:

python -m pip install tiktoken==0.14.0
python pack_context.py ./meu-repo \
  --mode compact \
  --byte-budget 65536 \
  --token-budget 8000 \
  --required AGENTS.md \
  --pr-path src/payments/refund.py \
  --pr-path tests/payments/test_refund.py \
  > context.md

O packer não percorre a árvore inteira, não segue um caminho para fora da raiz e não esconde uma falha obrigatória. Mesmo quando aborta, grava status: failed e a decisão que provocou a saída. O log, portanto, registra tanto o contexto entregue quanto o contexto recusado.

Em um harness real, o texto de stdout segue para o modelo. O arquivo .harness/context-audit.jsonl segue para a telemetria interna e deve ficar fora do prompt. Auditoria que consome o próprio orçamento seria um pequeno triunfo da burocracia sobre a engenharia.

Compactar e falhar são políticas para conteúdos diferentes

“Compact” não deve significar “corte qualquer coisa até caber”. A decisão depende do papel do conteúdo.

ConteúdoPolítica recomendadaRegistro mínimo
Instrução ou política obrigatóriaFalhar se estiver ausente ou não coubercaminho, tamanho, limite e motivo
Arquivo do PR em review, security ou releaseFalhar se o perfil o classificar como obrigatóriocaminho, papel e decisão
Fonte opcional em exploraçãoDescartar o item de menor prioridadecaminho, ranking e motivo
Histórico antigo de conversaCompactar ou resumirintervalo compactado e mecanismo
Resultado bruto de busca já consumidoManter só referência ou sínteseorigem e artefato preservado

A compactação da OpenAI trata o histórico de uma conversa longa. Ela não é memória de projeto e não prova que uma política descartada continuou disponível. Fonte do repositório e histórico conversacional precisam de tratamentos separados.

Também convém separar a política do time do comportamento de uma ferramenta específica. No Codex, project_doc_max_bytes limita a cadeia de documentos do projeto em bytes; o padrão documentado é 32 KiB. Segundo o guia de AGENTS.md do Codex, a descoberta para quando o tamanho combinado atinge esse limite. Isso é uma parada silenciosa do carregamento do produto, não um orçamento de tokens nem a política fail-closed do exemplo.

Se o nosso harness decide abortar porque AGENTS.md ou um arquivo obrigatório do PR não coube, essa garantia é nossa. Ela deve aparecer no código, no status de saída e no JSONL. A configuração de um fornecedor não deve receber crédito por uma proteção que não implementou.

Onde entra o AGENTS.md

O AGENTS.md é uma convenção aberta para fornecer ao agente comandos, estrutura do projeto e orientações locais. Ele não exige um schema fixo, e arquivos mais próximos podem especializar instruções para subprojetos conforme o suporte de cada ferramenta.

No loader, ele pode ser uma fatia obrigatória e curta do contexto. O restante continua sob responsabilidade do harness:

  • selecionar arquivos por tarefa e pelos caminhos do PR;
  • medir bytes e tokens com o contador correto;
  • escolher entre descarte e interrupção;
  • registrar tudo o que entrou, ficou de fora ou estava ausente.

Isso também evita pedir ao AGENTS.md que faça trabalho de CI, revisão ou controle de acesso. Para essa separação entre orientação e enforcement, vale a leitura sobre gates antes do primeiro PR. Aqui, basta manter o arquivo como convenção complementar, não como banco de memória nem motor de busca.

Medir packing sem fabricar uma nota de qualidade

O JSONL do packer responde perguntas operacionais: quantos bytes foram injetados, quais arquivos foram descartados e por que a execução falhou. Ele não demonstra, sozinho, que a resposta do agente melhorou.

Para observar a relação entre packing e resultado, o harness pode produzir um segundo evento local por tarefa:

{"schema":1,"repo":"checkout","task":"bugfix","pr":142,"injected_bytes":18420,"injected_tokens":4100,"discarded_files":2,"repeat_questions":1,"same_task_rereads":3,"review_rounds":2}

Os campos precisam de definições operacionais:

  • repeat_questions: perguntas sobre fatos já presentes nos arquivos injetados naquele turno;
  • same_task_rereads: novas leituras do mesmo caminho durante a tarefa;
  • review_rounds: rodadas de revisão registradas no PR até merge ou encerramento;
  • injected_tokens: contagem do tokenizer compatível, ou null quando ela não existe.

Compare tarefas do mesmo repositório e do mesmo tipo. Registre o dado bruto e procure casos para inspecionar, como muitos tokens com perguntas repetidas ou poucos tokens com releituras frequentes. Não transforme correlação local em percentual de melhoria, e não use custo como sinônimo de qualidade do packing. Teto financeiro é outro controle, discutido em token budget e circuit breaker.

A telemetria serve para orientar mudanças no perfil: promover um arquivo a obrigatório, remover uma regra sempre carregada ou ajustar o orçamento. Sem um experimento controlado, ela não autoriza afirmar que “8 mil tokens melhoram PRs”. Autoriza afirmar quais 8 mil tokens entraram. Já é uma informação consideravelmente mais útil.

Verificação e limitações

Antes de ligar o packer a um agente, rode quatro verificações locais:

  1. Execute com dois arquivos pequenos do PR e confira se ambos aparecem em context.md e como inject no JSONL.
  2. Defina --byte-budget 1 com um arquivo obrigatório; o processo deve sair com código 2 e registrar status: failed.
  3. Repita com --mode compact e um caminho opcional grande; o processo deve terminar com sucesso e registrar discard por orçamento.
  4. Tente ../fora-do-repo; a execução deve rejeitar o caminho e não ler o arquivo.

Há limites deliberados. O exemplo não descobre imports, não resume arquivos grandes, não calcula um score semântico e preserva a ordem informada pelo PR. tiktoken só mede famílias compatíveis; outros modelos exigem o contador indicado pelo respectivo provedor. O JSONL pode conter nomes de arquivos sensíveis, então precisa da mesma política de retenção e acesso aplicada aos metadados de engenharia. Por fim, compactar um arquivo significa descartá-lo inteiro no exemplo; uma síntese automática exigiria proveniência, avaliação e um registro separado do texto resumido.

Quando a DevDojo adotaria esse loader

A DevDojo adotaria o loader quando as tarefas já tiverem caminhos de PR confiáveis, houver convenções obrigatórias pequenas e os logs mostrarem releituras ou perguntas repetidas sobre fatos que deveriam ter entrado. Começaríamos em modo de observação, depois ativaríamos fail-closed para review, security e release, mantendo compact apenas para exploração e rascunho.

Recuaríamos se os perfis exigissem manutenção maior que o problema, se tarefas legítimas passassem a falhar por caminhos gerados ou se o log mostrasse que a maior parte das leituras úteis acontece fora do conjunto previsto. Nesse caso, o próximo passo não seria aumentar o prompt às cegas: seria simplificar os perfis ou voltar à busca sob demanda.

O teste inicial cabe em um PR real: passe os arquivos alterados ao script, force um overflow em cada modo e leia a linha de auditoria antes de iniciar o modelo. Se o time consegue explicar cada item injetado e cada descarte, a memória de projeto deixou de ser uma esperança no prompt e virou uma decisão de engenharia.

ai-agentsdevexpython

// Continue.training — 次のステップ

Conhecimento só conta quando vira prática.

Volte ao artigo, execute os exemplos e compartilhe o que aprendeu.

Explorar mais artigos