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:
- arquivos obrigatórios, definidos pelo perfil da tarefa;
- 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:
- arquivos obrigatórios entram primeiro;
- caminhos alterados no PR entram depois, na ordem recebida;
- arquivos adicionais só entram quando uma ferramenta ou relação explícita justificar a leitura;
- 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údo | Política recomendada | Registro mínimo |
|---|---|---|
| Instrução ou política obrigatória | Falhar se estiver ausente ou não couber | caminho, tamanho, limite e motivo |
| Arquivo do PR em review, security ou release | Falhar se o perfil o classificar como obrigatório | caminho, papel e decisão |
| Fonte opcional em exploração | Descartar o item de menor prioridade | caminho, ranking e motivo |
| Histórico antigo de conversa | Compactar ou resumir | intervalo compactado e mecanismo |
| Resultado bruto de busca já consumido | Manter só referência ou síntese | origem 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, ounullquando 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:
- Execute com dois arquivos pequenos do PR e confira se ambos aparecem em
context.mde comoinjectno JSONL. - Defina
--byte-budget 1com um arquivo obrigatório; o processo deve sair com código 2 e registrarstatus: failed. - Repita com
--mode compacte um caminho opcional grande; o processo deve terminar com sucesso e registrardiscardpor orçamento. - 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.