---
title: "AWTL: 실패 로그를 다음 실행 힌트로 바꾸기"
slug: "ctx2skill-harness-03-awtl-failure-brief"
canonicalUrl: "https://moonshotnotes.com/posts/ctx2skill-harness-03-awtl-failure-brief/"
sourceUrl: "https://moonshotnotes.com/posts/ctx2skill-harness-03-awtl-failure-brief/"
markdownUrl: "https://moonshotnotes.com/agent/posts/ctx2skill-harness-03-awtl-failure-brief.md"
language: "ko"
category: "Workflow"
updatedAt: "2026-05-08"
agentTokenEstimate: 2428
---

# AWTL: 실패 로그를 다음 실행 힌트로 바꾸기

Agent Work Trace Logging으로 action, judge result, failure attribution, failed turn case, replay scorecard를 연결해 실패를 재발 방지 힌트로 바꾸는 구조를 정리합니다.

## Agent metadata

- Source: https://moonshotnotes.com/posts/ctx2skill-harness-03-awtl-failure-brief/
- Markdown: https://moonshotnotes.com/agent/posts/ctx2skill-harness-03-awtl-failure-brief.md
- Language: ko
- Category: Workflow
- Tags: AI Agent, AWTL, Developer Harness, Failure Attribution, Replay Gate, MemoryGraph, Workflow
- Updated: 2026-05-08
- Estimated tokens: 2428

좋은 하네스는 실패 로그를 많이 남기는 데서 끝나지 않습니다. 실패를 다음 실행에서 쓸 수 있는 힌트로 바꿔야 합니다.

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에서 끝나지 않습니다.

```mermaid
%% AWTL은 실패 로그를 다음 실행 힌트와 검증된 memory promotion 후보로 바꾼다.
flowchart TD
  Trace["Trace event 수집"]
  Judge["Failed judge event 탐지"]
  Attribution["Failure attribution"]
  Case["Failed turn case"]
  Brief["Failure Prevention Brief"]
  Replay["Replay scorecard"]
  Promotion["Verified-only memory promotion"]

  Trace --> Judge --> Attribution --> Case --> Brief
  Case --> Replay
  Replay --> Brief
  Replay --> 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는 최소한 다음 계층을 가져야 합니다.

```mermaid
%% agent 작업은 task 전체가 아니라 run, turn, action, observation 단위로 관측되어야 한다.
flowchart TD
  Task["Task"]
  Run["Run"]
  Turn["Turn / Span"]
  Action["Action"]
  Observation["Observation"]
  Judge["judge_result"]

  Task --> Run --> Turn --> Action --> Observation --> Judge
```

## Trace Sink: 재생 가능한 이벤트 스트림 만들기

Trace Sink는 에이전트가 실행 중에 남기는 일을 한 줄씩 받아서, 나중에 다시 읽을 수 있는 실행 로그로 저장하는 계층입니다. 여기서 중요한 것은 “많이 저장한다”가 아니라 “나중에 같은 기준으로 다시 분석할 수 있게 저장한다”입니다.

이 글에서 말하는 trace는 JSONL 파일에 가깝습니다. 한 줄에는 하나의 event가 들어갑니다.

```js
{"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`가 하네스가 허용한 저장 위치이고, 그 밖의 경로가 들어오면 즉시 실패시킵니다.

```js
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은 계속 분석합니다.

```js
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를 보고 실패했는지 연결해야 합니다.

```js
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 대상인지도 분리해야 합니다.

```js
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로 압축해야 합니다.

```json
{
  "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"]
}
```

검증도 엄격해야 합니다.

```js
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만 제한적으로 넣어야 합니다.

```js
const selectedCases = selectFailurePreventionCases(loaded.cases, context, options);

if (selectedCases.length === 0) {
  return {
    status: "no-op",
    section: "",
  };
}
```

brief는 짧아야 합니다.

```text
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가 바뀌거나, 코드 구조가 바뀌거나, 해당 실패가 더 이상 재현되지 않을 수 있습니다.

```json
{
  "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로 제외 조건을 확인합니다.

```js
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은 마지막 단계입니다.

```js
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을 실제 하네스에 붙이기 전에는 다음 항목을 확인합니다.

```text
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의 핵심은 로그를 잘 남기는 것이 아닙니다. 실패를 다음 실행의 힌트로 바꾸는 것입니다.

```mermaid
%% 실패는 attribution과 replay를 거쳐 다음 실행 힌트 또는 verified memory 후보가 된다.
flowchart LR
  Log["실패 로그"]
  Attribution["failure attribution"]
  Case["failed turn case"]
  Brief["prevention brief"]
  Scorecard["replay scorecard"]
  Memory["verified-only memory promotion"]

  Log --> Attribution --> Case --> Brief
  Case --> Scorecard --> Memory
```

이 루프가 만들어지면 하네스는 단순 실행기가 아닙니다. 실행을 관찰하고, 실패를 해석하고, 검증된 지식만 장기 기억으로 승격하는 운영 시스템이 됩니다.
