A good harness doesn't end with lots of failure logs. We need to turn the failure into a hint that can be used in the next run.
In Part 1, Ctx2Skill was applied from the operational memory perspective of the development harness, and in Part 2, it was summarized that MemoryGraph should only accept proven compact rules. The topic of this article is the middle layer.AWTL, or Agent Work Trace Logging.
The purpose of AWTL is not log collection. The goal is to attribute failures to the action unit and inject only as many recurrence prevention hints as necessary before the next attempt.
Key takeaways
- AWTL records agent actions in action/span/event units.
- Connect failed judge events with source actions, artifacts, and memory reads.
- The attribution results are compressed into failed turn cases.
- Before the next attempt, only relevant cases are injected into the Failure Prevention Brief.
- The replay scorecard filters out stale, risky, and blocked cases.
- MemoryGraph promotion is only allowed if it passes replay or human approval.
full flow
AWTL starts with a trace but does not end with a trace.
- Trace event collection
- Failed judge event detection
- Failure attribution
- Failed turn case
- Failure Prevention Brief
- Replay scorecard
- Verified-only memory promotion
The point here is not “Paste all logs to the next prompt”. Pick only the failures that are relevant to the task at hand and turn them into small, actionable hints.
Minimum unit of trace event
It is difficult to know the cause of failure by only looking at the final PR. Failure usually begins already in the middle of the action.
| failure starting point | Problems with only looking at the final result | What AWTL must leave behind |
|---|---|---|
| Wrong file selection | I don't know why I modified the wrong file | search/read action and selection basis |
| misjudgment of test command | Only verification missing is visible | Execution command, exit code, stderr |
| Error log misunderstanding | Re-edit direction is wrong | Connect observation and next action |
| memory misapplication | Don't know if memory is the cause | Connect memory_read event and action |
| verifier failed | Not sure which artifact failed | judge_result and artifact_refs |
Therefore, a trace must have at least the following layers:
Trace Sink: Creating a Replayable Event Stream
Trace Sink is a layer that receives the work left by the agent during execution line by line and saves it as an execution log that can be read again later. The important thing here is not “save a lot,” but “save so that it can be analyzed again with the same criteria later.”
The trace referred to in this article is closer to a JSONL file. One event is included in one line.
{"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"}
You must have this file so that you can later recalculate “what observation came after what action and why the judge considered it a failure.” So Trace Sink's first responsibility is twofold.
| responsibility | reason |
|---|---|
| Only use designated trace storage locations | Mixing other project logs or temporary files will pollute the failure analysis. |
| Set the event format to one canonical form. | If field names and structures change from run to run, replay and aggregation are impossible. |
The code below is a boundary that prevents trace from being written to any directory.EXPECTED_TRACE_ROOTis the storage location allowed by the harness, and if any other path comes in, it will fail immediately.
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;}
The second problem is a broken JSONL line. If the process dies mid-execution or file writing is interrupted, a single line may not be parsed as JSON. At this time, if you discard the entire trace, even normal events will be lost. Therefore, only broken lines are separated intoquarantinefiles, and normal lines are continued to be analyzed.
quarantined.push(quarantineLine( quarantinePath, rawLine, reason, canonicalPath, index + 1,));
This design ensures three things:
| guarantee | meaning |
|---|---|
| trace root fixed | Trace artifacts are not outside the expected range. |
| corrupt line isolation | A single line error won't ruin the entire run analysis. |
| Regenerate materialized view | Derived output can be recreated from the canonical trace. |
Harness Capture: Connecting action and judge
If the judge failure remains simplyfailed, it is difficult to use to prevent recurrence. You need to connect which action created which artifact and which verifier saw that artifact and failed.
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_idandartifact_refsare the key. Without these two, failures are difficult to reproduce or prevent in subsequent runs.
Failure Attribution: Separating cause from promotability
Attribution connects failed judge events with source actions, artifacts, and memory reads. At the same time, we need to isolate whether this failure is subject to memory promotion.
return { failureEvent, failureTurnId, failedArtifactRefs, sourceActionIds, verifierActionId, touchedActionIds, memoryReadNodeIds, evidenceRefs, rootCauseSummary, verificationProbeCandidate, classification, failureTypeInfo, attributionHeuristics,};
What is important is not the single root cause statement. This isclassification.
| classification | basic decision |
|---|---|
agent_failure | Failed turn case and memory candidate candidates |
verification_failure | replay probe candidate |
environment_blocker | Block memory promotion |
flaky_blocker | Block before checking reproducibility |
harness_blocker | Separated into harness modification backlog |
Long-term memory is faulty if we do not distinguish between environmental and agent failures. For example, if e2e fails because the browser binary is missing, you should not have memory saying “e2e is omitted in this repository.”
Failed Turn Case: Compact case for next run
Attribution results should be compressed into compact cases rather than the entire raw trace.
{ "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": "Before closeout, run the same verifier again for the changed artifact.", "applicability": ["contract_change", "public_api"], "evidence_refs": ["judge:contract-verifier"]}
Verification must also be rigorous.
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");}
The case affects the next attempt. Therefore, cases withoutartifact_refs,evidence_refs, andapplicabilityare difficult to use as recurrence prevention data.
Failure Prevention Brief: Inject Smallly
You should not add all failed cases before the next attempt. Only cases that match the current phase should be included.
const selectedCases = selectFailurePreventionCases(loaded.cases, context, options); if (selectedCases.length === 0) { return { status: "no-op", section: "", };}
The brief should be short.
Failure Prevention Brief- [high-confidence] Re-run the verifier that failed before Closeout as the target for the changed artifact.- [scope: public_api] Update contract artifact and verifier evidence together.
A good brief satisfies three conditions:
| condition | reason |
|---|---|
| Relevant to current task | Reduces unnecessary memory noise. |
| This is an executable statement | Change the agent's next action. |
| have a basis | Tracked with failure logs and verifier evidence. |
Replay Scorecard: Managing the effectiveness of failure memories.
Even a failed case that was once valid can become stale over time. The verifier may change, the code structure may change, or the failure may no longer be reproducible.
{ "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 Check exclusion conditions with scorecard before injection.
export function isReplayScorecardExcluded(record = {}) { const status = normalizeStatus(record.status ?? record.result ?? record.outcome); return isReplayScorecardStaleOrRisky(record) || ["blocked", "skipped", "unavailable", "denied"].includes(status);}
This filter prevents old failure memories from persisting in the prompt.
Memory promotion is the final step
The results from AWTL should not go directly into MemoryGraph. Promotion is the final step.
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";
The policy created by these three conditions is clear.
- No promotion without replay or human approval.
- Imported-only candidates are blocked.
- Do not write to MemoryGraph without an explicit write flag.
- Auto-promotion only allows verified-only.
Practical Checklist
Before attaching the AWTL to the actual harness, check the following items:
AWTL application checklist [ ] The event schema includes action, observation, judge_result, and artifact_ref.[ ] Trace event has run_id, attempt_id, and turn/span/action identifiers.[ ] Guarantees sortability with writer_seq or ingest_seq.[ ] The trace root is safely fixed and path traversal is blocked.[ ] Quarantine corrupt JSONL lines without ruining the entire trace.[ ] judge_result has source_action_id and artifact_refs.[ ] Include memory_read event in attribution.[ ] failure attribution classifies the failure class.[ ] environment/flaky/harness failure blocks default promotion.[ ] The failed turn case only has compact metadata.[ ] The prevention brief injects only cases that match the current phase.[ ] Exclude stale/risky/blocked cases from the replay scorecard.[ ] MemoryGraph promotion is limited to verified-only.
finish
The point of AWTL is not to keep good logs. It's about turning failure into a hint for the next move.
- failure log
- failure attribution
- failed turn case
- prevention brief
- replay scorecard
- verified-only memory promotion
Once this loop is created, the harness is not a simple executor. It becomes an operating system that observes execution, interprets failures, and promotes only verified knowledge to long-term memory.

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