agent-harness GitHub

Coding-agent loop observability v0

Status: proposed documentation contract, 2026-09-23. No exporter, SDK, native-agent qualification, measurement result, or merge-policy change is delivered here. Parent #299, child #303.

Classification: FACT paragraphs describe cited sources inspected on 2026-09-23; INFER is proposed engineering design dated 2026-09-23, including all unmarked contract tables, examples, and recommendations below. OVERSOLD labels claims that must not be treated as evidence. Access dates are not publication dates.

Boundary and ownership

Telemetry ≠ merge gate. Missing spans, disabled capture, exporter failures, coverage gaps, cost/latency observations, and LLM/vendor scores must never become merge-blocking CI conditions. Structural requirements describe captured records, not a requirement to produce telemetry before merging. A deterministic computation over telemetry does not make that telemetry eligible to gate.

Independently defined, applicable deterministic oracles may gate under existing repository policy. Their original results remain authoritative for the tested property. A telemetry event referencing a test result neither authenticates the execution nor proves a different revision. See review evidence and #304.

Owner Responsibility Excluded authority or duplication
agent-harness This canonical observation contract and future thin adapters No second harness, scheduler, approval service, or replacement receipt format
agent-hq Index evidence identity and descriptive coverage No correctness, qualification, merge or release promotion
Existing oracle/CI Original independently defined checks and exact tested scope Telemetry copies do not replace check results
Existing ops work #288 / #296 Logical-job/attempt accounting and denominators This document supplies observations, not another financial or job ledger
Later claude-config packaging Reference and explain this contract through existing skills Do not fork the schema or alter review doctrine; coordinate through #302
Optional vendors Render, aggregate or translate captured observations No backend or chat proxy is the sole evidence or release source of truth

FACT: agent-hq#10 specifies authority: none and gate_eligible: false, including for deterministic coverage. Do not import raw telemetry into that index as a new evidence protocol. Any later bridge must explicitly map an existing supported evidence schema. Policy Lab replays decisions; it is not a trajectory ledger. Product UX observe/judge remains in #281.

Operation vocabulary and parenting

FACT: The reviewed GenAI agent/client/event/metric documents are Development, not Stable. Use their names only where meanings match; this local format is OTel-shaped, not OTLP or a claim of stable GenAI compliance. S1, S2, S3, S4, S5

Choose one bounded user turn or explicit agent invocation as a loop. A standalone loop starts a trace; an invocation with genuine upstream context retains it. Multiple turns may share a genuine conversation ID without becoming one giant terminal-session span. Preserve native causal relationships across resumed turns.

Boundary Operation / kind Parent rule
Local invocation invoke_agent, INTERNAL Actual caller if observed; otherwise a known local root
Explicit planning plan, INTERNAL Invocation; omit when planning cannot be distinguished reliably
Model request API-appropriate operation such as chat, usually CLIENT Planning request under plan; other decisions under the active invocation; retain actual upstream nesting
Local agent-side tool execute_tool, INTERNAL Active causal invocation, not automatically the model span that requested it
Edit / test / lint / build execute_tool plus agent_harness.phase=edit or verify Same tool rule; the phase does not assert success
Delegated agent invoke_agent only if an invocation boundary is observed Actual launching operation; otherwise preserve native children without inventing an invocation span

FACT: Planning inference belongs beneath plan, while execution of its plan normally belongs beside it under the invocation. OTel advises omitting plan when instrumentation cannot identify it. A logical inference span includes automatic retries. S1, S2 Model-local inference can use INTERNAL; do not relabel remote calls as local merely because a coding CLI runs on a developer machine.

Names use operation plus a safe agent/model/tool name when available. Do not put prompts, commands, paths, PR titles or other arbitrary text into span names. Do not invent standard operations named edit or verify, or a workflow wrapper simply for presentation. Timeline arrows express order, not parentage.

Local record sketch

A future adapter may write bounded UTF-8 JSONL and a console projection using this vocabulary. No writer is implemented here. Console output is not assumed to be machine-readable JSONL. Unknown fields/versions must remain uninterpreted rather than acquiring a verdict. A future reader should reject malformed records from aggregation with a diagnostic, not stop the coding operation or a merge.

Field Type / units / presence Meaning and unavailable handling
record_kind span or event; required Completed observed interval or timestamped observation, respectively
name safe string; required Operation-based span name or documented event name
trace_id, span_id nonzero 32 / 16 lowercase hex digits on spans Preserve genuine context; generate only for a boundary actually instrumented, not to pretend an unseen native operation exists
parent_span_id 16 hex digits if known; null only for known root Omit when unknown and record that gap; an absent parent record may simply be outside the capture
span_kind INTERNAL / CLIENT for the boundaries above Span-only; do not confuse a vendor AI category with OTel span kind
start_time, end_time, duration_ms UTC RFC3339 strings and finite nonnegative milliseconds; completed spans Duration uses the source’s elapsed clock; disclose wall-clock-only estimates. Do not invent an end for interrupted operations
time UTC RFC3339 string; events Observation time, not an invented span start
status UNSET, OK, ERROR; spans Source operation status, never a descendant roll-up or merge verdict
attributes bounded object; required Only interpreted allowlisted metadata; no arbitrary vendor object passthrough
agent_harness.schema.version string 0; required attribute Version of this proposed local contract, independent of SDK/version numbers
agent_harness.source.name / .version safe source identifier / observed version Required name; omit unknown version. A document’s version is not an installed runtime measurement
agent_harness.content.capture off or redacted; required attribute Off by default; redacted means explicitly permitted fields after filtering, never unrestricted raw capture
agent_harness.unavailable optional object: field name to reason Reasons: not_exposed, not_observed, not_applicable, redacted, incomplete, invalid; never substitute zero

The agent_harness.* keys are inside attributes; object-valued extensions are local JSON only and would need an explicit translation for any future OTLP adapter. Events use trace/span IDs only when associated with a genuine known context. An uncorrelated source event can remain an event with an explicit gap. Partial native records are retained only as safe event observations, not fabricated completed spans. This is a sketch for an existing-harness adapter, not a new runnable schema validator, transport specification, evidence receipt or append-only attestation.

Correlation and observations

All source-dependent attributes below are optional when unavailable. Structural capture must not enable content to obtain a missing identifier. IDs are opaque correlation values, not authentication tokens; credentials are never identifiers.

Attribute Type / source / meaning
gen_ai.operation.name String matching the observed operation, on semantic spans
gen_ai.agent.name, gen_ai.tool.name Safe allowlisted name/category; omit unsafe raw names and record redaction
gen_ai.conversation.id Genuine native session/thread ID when safe; never a new UUID, trace ID or content hash fallback S3
gen_ai.tool.call.id Native model/tool-call ID, not tool name; correlate within producer/session scope S2, S3
gen_ai.response.id Actual completion ID, not a transport request-header ID S3
gen_ai.provider.name Observed provider identity; a gateway brand alone does not prove the upstream provider
gen_ai.request.model, gen_ai.response.model Requested and observed actual model respectively; never assume actual equals requested S2, S3
error.type Safe low-cardinality failure category on error, _OTHER when a failure is known but its type is unavailable; no full exception text
agent_harness.run.id Existing harness run identity only when associated; standalone native runs need not pretend a harness run exists
agent_harness.repo.id, agent_harness.repo.revision Safe stable repo identity and known full commit SHA; neither is a filesystem path
agent_harness.repo.dirty Boolean only when inspected; a revision without a cleanliness observation does not identify working-tree bytes
agent_harness.change.pr.number Positive integer only for a known associated PR; normal local runs may have none
agent_harness.oracle.id, agent_harness.evidence.id Existing oracle and evidence references, not a fresh acceptance authority
agent_harness.phase plan, tool, edit, verify when meaning is known; do not infer from a successful exit alone
agent_harness.outcome success, failure, cancelled, denied, unknown; source observation separate from span status

Keep run/session/repo/PR IDs out of metric label sets; they belong in records and drill-down references, not unbounded aggregate dimensions. Record identity below is distinct from logical invocation, attempt, session and delivered-job identity.

Source-record identity and conflict handling

This is the #314 design amendment, not native qualification. All new fields below are local attributes, not standard OTel attributes. Identity is an input claim, not authentication, and is never sufficient to establish capture completeness.

Attribute Type / presence Scope and mapping
agent_harness.source.record.identity native_span, producer_record or unavailable; new normalizers emit it Omission in an older v0 record means unavailable, never an inferred key
agent_harness.source.namespace Opaque string; needed for either keyed mode Stable namespace for the originating producer domain; preserved across re-exports and imports, distinct across unrelated producers
agent_harness.source.instance.id Opaque string; needed for producer_record Identifies the original producer lifetime, not the importer, collector, OS PID or conversation
agent_harness.source.record.id Opaque string; needed for producer_record Original event identity, unique within that instance and across its event streams
agent_harness.normalization.profile Opaque string; needed for qualified deduplication Origin normalization profile covering mapping plus privacy revision; compared as observation metadata, never part of the source key

These identity strings and source name are case-sensitive, nonempty ASCII tokens, 1 to 128 characters, matching [A-Za-z0-9][A-Za-z0-9_.:-]{0,127}. This syntax is not secret detection. Producers must supply safe opaque IDs; omit an unsafe ID. No paths, user/account names, credentials, body hashes or raw-content references are acceptable identity fallbacks. A capture may assign an opaque producer token once at the actual instrumented boundary and preserve it with every export; a reader must never mint a new per-import token to repair missing source identity.

Use exact tuples, not ambiguous delimiter concatenation:

Mode Source key Applicability
native_span (source.name, source.namespace, "span", "native_span", trace_id, span_id) Only completed native spans with genuine valid trace/span IDs
producer_record (source.name, source.namespace, "event", "producer_record", source.instance.id, source.record.id) Only events with established producer lifetime and original event identity
unavailable or an absent key component No key Retain the safe observation, report unavailable identity; never deduplicate by payload equality

The dotted names in tuples abbreviate the full agent_harness.* attribute names. A span’s optional instance metadata is not part of its native key. Do not apply a span key to its events: several different events may share a span and timestamp. A source restart must change the producer instance before a sequence can reset. For Claude log events, a qualified mapping may encode a native sequence as event:<decimal-sequence> only with a proven lifetime scope; other independently numbered streams need distinct prefixes. If no source lifetime can be established, the result is unavailable. Session IDs, provider request IDs and tool-call IDs do not repair that gap. The Claude mapping documents how a future adapter must supply these fields; it does not establish native support for an original lifetime token or claim the adapter is implemented.

The originating normalizer assigns agent_harness.normalization.profile to its reviewed mapping and privacy rules, changing it whenever those rules change. It must preserve that token on re-export; source application version and local schema version are not substitutes. Readers never add their own profile to old records. Missing profile information makes deduplication unqualified, reported as unavailable identity for counting even when the other key fields are present. Different supplied profiles under the same source key conflict even when every other safe field is identical. The token itself does not authenticate its meaning.

Within one declared metadata profile, compare the entire accepted, content-off normalized observation for a source key. Ignore JSON object-key order, not missing fields, changed status, timings, usage, normalization differences or source versions. Unsupported fields must be reported/refused before aggregation, not silently stripped until contradictory observations look equal. Do not hash raw records to manufacture identity. A later reader may use canonical safe-projection bytes for equality, but that representation is not the source key or a receipt.

Identical representations of a keyed observation are repeat exports. If any two representations under a key differ, mark the whole key group conflicting and exclude every member from consistent-observation aggregation. Retain safe variants for inspection when permitted; no last-writer, first-writer, success-wins, or “fill in missing usage” merge. A mapping/privacy-version mismatch is an unresolved representation conflict, not proof that the native operation itself was corrupt. Different keys remain distinct even when their redacted payloads or call IDs match.

Report received rows, keyed groups, consistent groups, duplicate rows within consistent groups, conflicting groups/rows and unavailable-identity rows separately. A deduplicated observation total is unavailable when any row lacks a key or any key conflicts; an empty capture does not establish complete coverage. Even a fully keyed input proves only the supplied-record population, never the number of tools/jobs, whole-run usage, billing, execution authenticity or readiness. Missing identity never invalidates independently recorded deterministic checks.

Authored controls, SYNTHETIC / NOT RUN; labels below are aliases, not captures:

Input variation Expected interpretation
Same span S re-imported, same namespace/trace/span and safe fields One consistent source group; one duplicate export row
Retry S2 has same tool-call ID but a different native span ID Two source groups, not one delivered job
Event sequence 7 in instances A and B after restart Distinct event keys; no collapse across sequence reset
Sequence 7 but no established original instance Unavailable identity, even if session ID and OS PID are present
Same provider/session/trace/span IDs in producer namespaces A and B Distinct source keys; namespaces must be preserved from origin
Two events on one span, same time, different original event IDs Two event keys, not a duplicate span
Three rows for S: failure, success, failure One conflicting key, all three rows excluded from consistent totals
Two identical redacted observations without original identity Two unavailable rows; deduplicated total unknown, not one or zero

The executable follow-through is separately scoped in #315. The offline identity inspector documents its accepted subset, resource bounds, executed synthetic controls and native-qualification limitations. It does not imply native capture or full v0 schema support.

Checkpoint events

Event Safe payload / interpretation
agent_harness.edit.applied Observed outcome and files-touched count when known; no diff or path list. A tool returning success is not proof the intended edit persisted
agent_harness.verify.result Oracle/evidence references, agent_harness.verify.result from pass/fail/error/not_run, original exit code if observed, tested repo/revision and dirty-state observations; no claim of overall correctness
agent_harness.request.attempt Request/attempt identity and attempt number when exposed, category/status and timing; no new logical inference count for each export
agent_harness.quota.observed Provider, native unit/window, value, observation time and reset time if exposed; never inferred authoritative headroom

A check that launches and reports failed assertions differs from a check that cannot launch. Preserve fail, error and not_run; don’t call every nonzero exit a product defect. Associate each verifier observation with the original evidence and tested scope. Dirty local work, an old head and a CI merge revision must not be silently relabelled as the current PR head. Parent OK/UNSET never erases a failed child; a recovered error remains in history.

Privacy and content opt-in

FACT: OTel content guidance makes messages/instructions and tool arguments and results non-default, sensitive fields. Native Claude content gates are useful mapping references, not a portable privacy policy or a guarantee of redaction. S2, S6

Data class Default / permitted treatment
User/model bodies, system instructions, tool I/O, source files, diffs Off. Explicit field-scoped debugging opt-in may retain a filtered bounded subset
Commands, environment values, absolute paths, raw exceptions, arbitrary tool/agent names Off. Prefer allowlisted categories; no automatic raw fallback
Credentials, auth headers, session access tokens, private keys, secret values Never emit, including with content opt-in
Reasoning text Not captured in v0; provider-supplied reasoning-token counts can be retained without text
Safe timing, operation/category, outcome, correlation and available usage Retained with content off; omit unsafe identifiers rather than claim metadata cannot be sensitive
Content hashes and body/file references Not a substitute for redaction; no raw-payload reference to auto-open, no secret hashing as anonymization

Opt-in must identify the run, allowed fields, destination, policy version and retention period before persistence. Never infer consent from another vendor switch. Filter before serialization, console output or disk writes; truncation alone is not redaction. A failed filter omits the affected field and emits a safe category when possible, without blocking the coding task or a merge. Do not promise perfect secret detection; omit classes that cannot be safely filtered.

Proposed local retention: bounded rotation and a 14-day maximum by default, with any debugging exception explicit. Storage limits/rotation implementation need a later reviewed slice; no deletion job or settings change is shipped here. Public PRs contain synthetic examples only. Local records can still carry linkable IDs; shipping them to a vendor is separate authorization, not implicit in local capture.

Usage, clocks and quota burn

FACT: The reviewed registry defines input/output, cache and reasoning token attributes. Cache counts are parts of input totals; reasoning is part of output. The reviewed registry has no standard cost attribute. S2, S3

Observation Field / units / interpretation
Input / output gen_ai.usage.input_tokens, gen_ai.usage.output_tokens: nonnegative integer tokens, source-defined billed counts where separately available
Cache read / write gen_ai.usage.cache_read.input_tokens, gen_ai.usage.cache_write.input_tokens: input subsets, not extra tokens to add again
Reasoning gen_ai.usage.reasoning.output_tokens: output subset, never inferred from text length
Latency duration_ms; custom agent_harness.latency.first_token_ms only for actual TTFT; use gen_ai.response.time_to_first_chunk in seconds only for that precise source meaning
Retries / rate limits agent_harness.request.attempt_count: observed integer, at least one for an attempted logical request; error category/HTTP status when available; unknown attempts remain unknown
Compaction gen_ai.conversation.compacted=true only when reliably detected; absence is not proof it did not happen S3
Monetary estimate agent_harness.cost.amount nonnegative finite decimal string, .currency explicit, .basis estimated or vendor_reported, .source and .pricing_as_of when estimated; optional observations, not invoices
Quota agent_harness.quota.value finite nonnegative number, .unit and .window strings, .observed_at and .reset_at UTC timestamps when known; preserve provider scope and whether value means used or remaining via .kind

Derive diagnostics from observed request/tool records, not a new metrics SDK: cache-read/input ratio, calls per loop, retries/rate limits, model share, and tokens per named deterministic oracle outcome. Unknown/zero denominators are undefined, not zero efficiency. State included/excluded runs and observed versus expected counts; absent source coverage is not a zero-cost run.

Count each usage observation once. Do not add native metrics, request events, request spans and parent summaries together. A successful final attempt may omit usage from failed attempts: report the partial observed total, not full spend. Keep unsuccessful/abandoned loops visible. A repeated cheap check passing is not another delivered job; #288/#296 owns logical-job outcomes and accounting.

Elapsed loop time is not the sum of parallel child durations. Separate observed model work, tool execution, permission waits and verifier work; unknown clocks stay unknown. TTFT, first chunk and first content are different clocks until a source mapping establishes equivalence. Compare weekly burn only within explicit provider quota windows. Token estimates do not establish subscription allowances or reset headroom. Provider billing remains accounting; these costs are advisory diagnosis.

Authored examples: SYNTHETIC / NOT RUN

These are structural sketches, not native captures or execution receipts. Labels A to G and R to V are diagram aliases, not literal OTel IDs. Times are authored relative milliseconds; all content is off. null parent means a known root here. No real repository, PR, model response or oracle execution is claimed.

One loop with explicit planning, edit and verification

Span Parent Operation Start..end ms Status / observation
A null invoke_agent example 0..900 UNSET; orchestration ended
B A plan example 0..200 UNSET; explicit planning boundary
C B chat example-model 10..190 UNSET; plan-producing model call
D A execute_tool read 210..260 UNSET; call call-read
E A execute_tool edit 270..300 UNSET; call call-edit, phase edit
F A execute_tool test 320..820 UNSET; call call-test, phase verify
G A chat example-model 830..890 UNSET; final summary request

E at 300: agent_harness.edit.applied, observed files touched 1. F at 820: agent_harness.verify.result, authored result pass, oracle example-unit-check, evidence example-evidence. Those invented example labels must never be indexed as observed evidence. C’s authored input 1000 includes cache read 600 and cache write 100; output 200 includes reasoning 80. The total is 1200, not 1980. G has unavailable usage (not_exposed), so a whole-loop total is unknown.

Retry, failure and interrupted coverage

Span Parent Operation Start..end ms Status / observation
R null invoke_agent example 0..1300 UNSET; no overall success assertion
S R chat example-model 10..900 UNSET after recovery; two observed request attempts
T R execute_tool edit 910..950 UNSET; observed edit outcome success
U R execute_tool test 960..1200 ERROR; known check failed, error.type=assertion_failure

S at 200: attempt 1 rate limited; S at 900: attempt 2 completed. Failed-attempt usage is unknown; the final response’s usage cannot establish total retry spend. U at 1200: verify event records fail with its original evidence identity. An interrupted later tool V has only a start observation at 1210 and no measured end; retain it as a safe event/gap, not an invented successful completed span. R ending UNSET neither clears U nor proves V completed. Missing capture remains advisory; the original deterministic verifier result is handled independently by existing policy.

Scout reconciliation and optional adapters

The accessible scout summary on #303 was compared; the original box-local OBSERVABILITY.md was not available here. This document clarifies arrows as order rather than mandatory nesting, estate verification as a tool phase rather than a new GenAI operation, and vendor content switches as mappings rather than canonical privacy rules. Compare the full scout locally before claiming its entire contents reconciled.

INFER: Document Claude Code native mapping first because its first-party guide exposes concrete coding-loop boundaries. Native schemas still need normalization, not blind copying. The separate mapping deliverable is tracked in #312. Muse, OpenCode and Codex-class adapters remain targets, not claimed qualified integrations. No all-agent feature parity is assumed.

Langfuse/LangSmith/Phoenix/OpenInference may be later projections; Helicone-class gateways can enrich model-plane observations. No backend is selected in v0. OVERSOLD: treating generic OTel ingestion, coding-agent-assisted setup, a vendor score, or a session tree as proof of complete uninstrumented local edit/verify coverage. Require concrete source-to-field mapping and loss accounting instead.

Landing plan and continuation

The table and first-week handoff below describe the initial docs-only wave, completed through #310/#313. The owner’s subsequent implementation request is tracked separately in #315, following the #314 identity amendment above; it does not authorize native telemetry activation or turn earlier examples into captures.

Rank Slice Done when
P0 This #303 contract plus README link Source-checked definitions, privacy/cost baseline and both NOT RUN examples reviewed; docs-only diff
P1 Separate Claude native mapping note, #312 Native field/unit/availability and loss tables distinguish tool counts, request IDs and token totals; no native qualification claim
P1 Future bounded local qualification proposal Human names safe synthetic task, runtime/version, capture route and comparison oracle; no runtime work is authorized by this table alone
P2 Later adapter implementation Separately reviewed existing-harness seam and failure-isolation tests; no merge-gating telemetry or new accounting system
P2 Packaging under #302 Canonical contract is settled; instructions link here, no second skill-policy home

First week: day 1 reconcile sources and core contract; day 2 review the two sketches; day 3 review privacy/unknown accounting; day 4 land native mapping documentation; day 5 reconcile issue checklists and record the next bounded proposal. This week produces docs PRs only, no SDK dependency or CI workflow change.

Local-agent handoff (difficulty M): inspect live main and #299/#303; revise this contract and the README link on docs/303-agent-loop-spans, keeping scope to the listed documentation. Review source semantics, examples, privacy and ownership, record actual checks and NOT RUN layers, and publish a draft while writing. Do not stack on or repair Verify reds in #295/#296/#298: they are not prerequisites for these docs. This PR’s own applicable checks and existing review policy still apply. No opentelemetry-sdk, CI changes, collector, SCITT/signing, UX #281 work, native activation, product PRs or skill packaging. Completion means reviewed documentation, not implemented observability or a qualified coding agent.

Dated primary sources

OTel references below are pinned to source revision 8ffdf568e1b4391a99adb081db16e8102e36918e (2026-09-22 commit), inspected 2026-09-23. S6 is a living first-party page inspected 2026-09-23; recheck feature/version claims before native qualification. The research input was the completed “v0 Observability Model for Coding-Agent Loops” brief dated 2026-09-23. Source review supports design choices; it is not a workstation experiment.

Ref Source Claim boundary
S1 GenAI agent/framework spans Development operation and planning relationships
S2 GenAI client/tool spans Logical inference, tools and content guidance
S3 GenAI attribute registry Correlation, token subsets, compaction; no standard cost field at reviewed revision
S4 GenAI metrics Development latency/usage vocabulary, not a required SDK
S5 GenAI events Development optional content event; not the required local envelope
S6 Claude Code monitoring Documented native surfaces, not proof they exist in an installed version/configuration