좋은 하네스는 실패 로그를 많이 남기는 데서 끝나지 않습니다. 실패를 다음 실행에서 쓸 수 있는 힌트로 바꿔야 합니다.
1편에서는 Ctx2Skill을 개발 하네스의 운영 기억 관점으로 적용했고, 2편에서는 MemoryGraph가 검증된 compact rule만 받아야 한다고 정리했습니다. 이번 글의 주제는 그 중간 계층입니다. AWTL, 즉 Agent Work Trace Logging입니다.
AWTL의 목적은 로그 수집이 아닙니다. 목적은 실패를 action 단위로 귀속하고, 다음 attempt 전에 필요한 만큼만 재발 방지 힌트로 주입하는 것입니다.
핵심 요약
- AWTL은 agent 작업을 action/span/event 단위로 기록합니다.
- 실패한 judge event를 source action, artifact, memory read와 연결합니다.
- attribution 결과는 failed turn case로 압축됩니다.
- 다음 attempt 전에는 관련 case만 Failure Prevention Brief로 작게 주입합니다.
- replay scorecard는 stale, risky, blocked case를 걸러냅니다.
- MemoryGraph promotion은 replay 또는 human approval을 통과한 경우에만 허용합니다.
전체 흐름
AWTL은 trace에서 시작하지만 trace에서 끝나지 않습니다.
- Trace event 수집
- Failed judge event 탐지
- Failure attribution
- Failed turn case
- Failure Prevention Brief
- Replay scorecard
- Verified-only memory promotion
여기서 핵심은 “모든 로그를 다음 프롬프트에 붙인다”가 아닙니다. 현재 작업과 관련 있는 실패 사례만 골라, 작고 실행 가능한 힌트로 바꿉니다.
trace event의 최소 단위
최종 PR만 보면 실패 원인을 알기 어렵습니다. 실패는 보통 중간 action에서 이미 시작됩니다.
| 실패 시작점 | 최종 결과만 볼 때의 문제 | AWTL이 남겨야 하는 것 |
|---|---|---|
| 잘못된 파일 선택 | 왜 엉뚱한 파일을 고쳤는지 모름 | search/read action과 선택 근거 |
| 테스트 명령 오판 | 검증 누락만 보임 | 실행 명령, exit code, stderr |
| 에러 로그 오해 | 재수정 방향이 틀어짐 | observation과 다음 action 연결 |
| memory 오적용 | 기억이 원인인지 알 수 없음 | memory_read event와 action 연결 |
| verifier 실패 | 어떤 artifact가 실패했는지 모름 | judge_result와 artifact_refs |
따라서 trace는 최소한 다음 계층을 가져야 합니다.
Trace Sink: 재생 가능한 이벤트 스트림 만들기
Trace Sink는 에이전트가 실행 중에 남기는 일을 한 줄씩 받아서, 나중에 다시 읽을 수 있는 실행 로그로 저장하는 계층입니다. 여기서 중요한 것은 “많이 저장한다”가 아니라 “나중에 같은 기준으로 다시 분석할 수 있게 저장한다”입니다.
이 글에서 말하는 trace는 JSONL 파일에 가깝습니다. 한 줄에는 하나의 event가 들어갑니다.
{"type":"action","spanId":"s-42","tool":"shell","command":"npm test"}{"type":"observation","spanId":"s-42","exitCode":1,"summary":"contract test failed"}{"type":"judge_result","spanId":"s-42","status":"failed","reason":"contract mismatch"}
이런 파일이 있어야 나중에 “어떤 action 뒤에 어떤 observation이 왔고, judge가 왜 실패로 봤는지”를 다시 계산할 수 있습니다. 그래서 Trace Sink의 첫 번째 책임은 두 가지입니다.
| 책임 | 이유 |
|---|---|
| 정해진 trace 저장 위치만 쓴다 | 다른 프로젝트 로그나 임시 파일이 섞이면 실패 분석이 오염됩니다. |
| event 형식을 한 가지 canonical 형태로 맞춘다 | 실행마다 필드 이름과 구조가 달라지면 replay와 집계가 불가능합니다. |
아래 코드는 trace를 아무 디렉터리에나 쓰지 못하게 막는 경계입니다. EXPECTED_TRACE_ROOT가 하네스가 허용한 저장 위치이고, 그 밖의 경로가 들어오면 즉시 실패시킵니다.
function assertExpectedTraceRoot(traceRoot) { const resolvedRoot = normalizeAbsolutePath(traceRoot); const expectedRoot = normalizeAbsolutePath(EXPECTED_TRACE_ROOT); if (resolvedRoot !== expectedRoot) { throw new Error(`Invalid trace root: ${traceRoot}`); } return resolvedRoot;}
두 번째 문제는 깨진 JSONL line입니다. 실행 중간에 프로세스가 죽거나 파일 write가 끊기면 한 줄이 JSON으로 파싱되지 않을 수 있습니다. 이때 전체 trace를 버리면 정상 event까지 잃습니다. 그래서 깨진 line만 quarantine 파일로 분리하고, 정상 line은 계속 분석합니다.
quarantined.push(quarantineLine( quarantinePath, rawLine, reason, canonicalPath, index + 1,));
이 설계는 세 가지를 보장합니다.
| 보장 | 의미 |
|---|---|
| trace root 고정 | trace artifact가 예상 범위를 벗어나지 않습니다. |
| corrupt line 격리 | 한 줄 오류가 전체 run 분석을 망치지 않습니다. |
| materialized view 재생성 | canonical trace에서 파생 산출물을 다시 만들 수 있습니다. |
Harness Capture: action과 judge를 연결하기
judge 실패가 단순히 failed로만 남으면 재발 방지에 쓰기 어렵습니다. 어떤 action이 어떤 artifact를 만들었고, 어떤 verifier가 그 artifact를 보고 실패했는지 연결해야 합니다.
async function recordJudgeResult(details = {}) { return emit("judge_result", { judge_name: toText(details.judgeName, "phase-verifier"), result: toText(details.result, "warn"), lifecycle_event: "judge_result", source_action_id: actionId, artifact_refs: normalizeArtifactRefs(details.artifactRefs ?? [], repoRoot), detail: toText(details.detail, ""), });}
source_action_id와 artifact_refs가 핵심입니다. 이 둘이 없으면 실패를 다음 실행에서 재현하거나 방지하기 어렵습니다.
Failure Attribution: 원인과 승격 가능성 분리
Attribution은 실패한 judge event를 source action, artifact, memory read와 연결합니다. 동시에 이 실패가 memory promotion 대상인지도 분리해야 합니다.
return { failureEvent, failureTurnId, failedArtifactRefs, sourceActionIds, verifierActionId, touchedActionIds, memoryReadNodeIds, evidenceRefs, rootCauseSummary, verificationProbeCandidate, classification, failureTypeInfo, attributionHeuristics,};
중요한 것은 root cause 문장 하나가 아닙니다. classification입니다.
| classification | 기본 판정 |
|---|---|
agent_failure | failed turn case와 memory candidate 후보 |
verification_failure | replay probe 후보 |
environment_blocker | memory promotion 차단 |
flaky_blocker | 재현성 확인 전 차단 |
harness_blocker | 하네스 수정 backlog로 분리 |
환경 실패와 agent failure를 구분하지 않으면 장기 기억이 잘못됩니다. 예를 들어 브라우저 바이너리가 없어 e2e가 실패했는데 “이 저장소에서는 e2e를 생략한다”는 memory가 생기면 안 됩니다.
Failed Turn Case: 다음 실행용 compact case
Attribution 결과는 raw trace 전체가 아니라 compact case로 압축해야 합니다.
{ "schema_version": 1, "case_id": "case-demo-a", "turn_id": "turn-3-1", "failure_turn_id": "turn-3-1", "failure_event_id": "judge-17", "artifact_refs": ["artifact:build-output"], "memory_read_node_ids": ["memory:contract-rule"], "prevention_hint": "Closeout 전에 변경 artifact를 대상으로 같은 verifier를 다시 실행한다.", "applicability": ["contract_change", "public_api"], "evidence_refs": ["judge:contract-verifier"]}
검증도 엄격해야 합니다.
if (caseValue.turn_id !== caseValue.failure_turn_id) { errors.push("turn_id and failure_turn_id must match");} if (!Array.isArray(caseValue.artifact_refs) || caseValue.artifact_refs.length === 0) { errors.push("artifact_refs must be a non-empty array of strings");}
case는 다음 attempt에 영향을 줍니다. 따라서 artifact_refs, evidence_refs, applicability가 없는 case는 재발 방지 데이터로 쓰기 어렵습니다.
Failure Prevention Brief: 작게 주입하기
다음 attempt 전에 모든 failed case를 붙이면 안 됩니다. 현재 phase와 매칭되는 case만 제한적으로 넣어야 합니다.
const selectedCases = selectFailurePreventionCases(loaded.cases, context, options); if (selectedCases.length === 0) { return { status: "no-op", section: "", };}
brief는 짧아야 합니다.
Failure Prevention Brief- [high-confidence] Closeout 전에 실패했던 verifier를 변경 artifact 대상으로 다시 실행한다.- [scope: public_api] contract artifact와 verifier evidence를 같이 갱신한다.
좋은 brief는 세 가지 조건을 만족합니다.
| 조건 | 이유 |
|---|---|
| 현재 작업과 관련 있음 | 불필요한 memory noise를 줄입니다. |
| 실행 가능한 문장임 | 에이전트의 다음 action을 바꿉니다. |
| 근거가 있음 | 실패 로그와 verifier evidence로 추적됩니다. |
Replay Scorecard: 실패 기억의 유효성 관리
한 번 유효했던 failed case도 시간이 지나면 stale해질 수 있습니다. verifier가 바뀌거나, 코드 구조가 바뀌거나, 해당 실패가 더 이상 재현되지 않을 수 있습니다.
{ "schema_version": 1, "record_id": "replay-demo-a", "status": "passed", "decision": "allow_brief_and_promotion", "candidate_id": "memcand-demo-a", "case_id": "case-demo-a", "validated_by": "replay", "last_validated_at": "example-timestamp", "memory_graph_status": "candidate", "replay_status": "passed", "risk_level": "low", "applies_to": ["public_api"], "does_not_apply_to": ["internal_refactor"], "evidence_refs": ["judge:contract-verifier"]}
brief 주입 전에 scorecard로 제외 조건을 확인합니다.
export function isReplayScorecardExcluded(record = {}) { const status = normalizeStatus(record.status ?? record.result ?? record.outcome); return isReplayScorecardStaleOrRisky(record) || ["blocked", "skipped", "unavailable", "denied"].includes(status);}
이 필터가 있어야 오래된 실패 기억이 계속 프롬프트에 남는 문제를 막을 수 있습니다.
Memory promotion은 마지막 단계
AWTL의 결과가 곧바로 MemoryGraph로 들어가면 안 됩니다. promotion은 마지막 단계입니다.
if (!approval.approved && !replayOk) { reasons.push("replay or human approval is required before promotion");} if (isImportedOnlyCandidate(candidate)) { reasons.push("imported-only or trace-only candidate is blocked");} const shouldWrite = options.writeMemoryGraph === true && toText(options.autoPromote, "verified-only") === "verified-only";
이 세 조건이 만드는 정책은 명확합니다.
- replay 또는 human approval 없이는 승격하지 않습니다.
- imported-only 후보는 차단합니다.
- explicit write flag 없이는 MemoryGraph에 쓰지 않습니다.
- 자동 승격은 verified-only만 허용합니다.
실전 체크리스트
AWTL을 실제 하네스에 붙이기 전에는 다음 항목을 확인합니다.
AWTL 적용 체크리스트 [ ] event schema에 action, observation, judge_result, artifact_ref가 포함된다.[ ] trace event에는 run_id, attempt_id, turn/span/action 식별자가 있다.[ ] writer_seq 또는 ingest_seq로 정렬 가능성을 보장한다.[ ] trace root는 안전하게 고정하고 path traversal을 차단한다.[ ] corrupt JSONL line은 전체 trace를 망치지 않고 quarantine한다.[ ] judge_result는 source_action_id와 artifact_refs를 가진다.[ ] memory_read event를 attribution에 포함한다.[ ] failure attribution은 failure class를 분류한다.[ ] environment/flaky/harness failure는 기본 승격 차단한다.[ ] failed turn case는 compact metadata만 가진다.[ ] prevention brief는 현재 phase에 매칭되는 case만 제한적으로 주입한다.[ ] replay scorecard에서 stale/risky/blocked case를 제외한다.[ ] MemoryGraph promotion은 verified-only로 제한한다.
마무리
AWTL의 핵심은 로그를 잘 남기는 것이 아닙니다. 실패를 다음 실행의 힌트로 바꾸는 것입니다.
- 실패 로그
- failure attribution
- failed turn case
- prevention brief
- replay scorecard
- verified-only memory promotion
이 루프가 만들어지면 하네스는 단순 실행기가 아닙니다. 실행을 관찰하고, 실패를 해석하고, 검증된 지식만 장기 기억으로 승격하는 운영 시스템이 됩니다.

댓글
GitHub 계정으로 로그인하면 댓글을 남길 수 있습니다. 댓글은 GitHub Discussions를 통해 운영됩니다.