Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions adapters/python/claude/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ output tokens.
Harbor accounting reads these counters before any ATIF trajectory. With the
input counters present, Harbor sets `n_input_tokens` to input plus cache reads
plus cache writes and `n_cache_tokens` to cache reads, from the same main-loop
scope as `n_output_tokens`. A Relay trajectory then fills only the counts the
result leaves unknown.
scope as `n_output_tokens`. Counts the result leaves unknown stay unknown in
Harbor; a Relay trajectory's totals are recorded separately and never fill them.
Comment thread
AnuradhaKaruppiah marked this conversation as resolved.

## Relay Observability

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ The task runner validates telemetry after the one-shot NeMo Fabric runtime shuts

## Invocation Accounting

The integration projects normalized `RunResult.usage` into Harbor's `AgentContext` without requiring NeMo Relay or ATIF. Harbor input counts include cache: inclusive NeMo Fabric input is copied unchanged, while exclusive input is combined with cache only when both counts are known. If input semantics are unknown, Harbor input remains unknown unless the adapter explicitly reports zero cache tokens. Legacy results remain valid, but their input count needs explicit cache semantics or an ATIF fallback to populate Harbor's inclusive count; the original values remain in result metadata. Unknown counts and costs remain `None`; estimates are not promoted to reported cost. ATIF metrics fill only missing fields and are never added to normalized usage. The same collection path applies to unsuccessful results that contain usage.
The integration projects normalized `RunResult.usage` into Harbor's `AgentContext` without requiring NeMo Relay or ATIF. Harbor input counts include cache: inclusive NeMo Fabric input is copied unchanged, while exclusive input is combined with cache only when both counts are known. If input semantics are unknown, Harbor input remains unknown unless the adapter explicitly reports zero cache tokens. Legacy results remain valid, but their input count needs explicit cache semantics to populate Harbor's inclusive count; the original values remain in result metadata. Unknown counts and costs remain `None` and reported zeros remain zero; estimates are not promoted to reported cost. When the runner exits without a NeMo Fabric result, the integration sets all four Harbor accounting fields to `None`. ATIF `final_metrics` never fill these fields, because their scope and cache conventions are not verified against the invocation. They are kept under `metadata.fabric.atif_final_metrics` with `source: atif` and `coverage: unverified`. The same collection path applies to unsuccessful results that contain usage.

**Reporting limitation:** Native Harbor trial and job totals sum available values and skip unknown fields. A displayed total can therefore omit usage or cost from steps or trials with unknown values. Do not interpret these aggregates as complete benchmark usage or expenditure when any contributing value is unknown.

## Host Admission And Task Execution

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@
HARBOR_ARTIFACT_ROOT = "/logs/agent/fabric-artifacts"
HARBOR_DEFAULT_WORKSPACE = "/testbed"
_DETAILED_TOKEN_USAGE_V1_KEY = "nemo_fabric.detailed_token_usage.v1"
# Evidence the bridge records beside the result, kept when result metadata is replaced.
_BRIDGE_EVIDENCE_KEYS = (
"atif_final_metrics",
"harbor_atif_validation",
"telemetry_validation",
)

try:
from harbor.agents.base import BaseAgent
Expand Down Expand Up @@ -614,6 +620,8 @@ def populate_context_post_run(self, context: AgentContext) -> None:

if self._result_path is not None:
populate_context_from_result(context, self._result_path)
else:
_clear_accounting(context)
populate_context_from_trajectory(context, self.logs_dir / "trajectory.json")
populate_context_from_telemetry_summary(
context,
Expand Down Expand Up @@ -884,26 +892,33 @@ def populate_context_from_result(
document = json.loads(path.read_text(encoding="utf-8"))
if isinstance(document, dict) and "runner_error" in document:
failure = FabricRunnerFailure.model_validate(document)
if context.metadata is None:
context.metadata = {}
context.metadata["fabric"] = failure.model_dump()
_replace_result_metadata(context, failure.model_dump())
_clear_accounting(context)
return failure
result = RunResult.from_mapping(document)
mapping = result.to_mapping()
if context.metadata is None:
context.metadata = {}
context.metadata["fabric"] = {
"status": mapping["status"],
"runtime_id": mapping["runtime_id"],
"invocation_id": mapping["invocation_id"],
"request_id": mapping["request_id"],
"harness": mapping["harness"],
"adapter_id": mapping.get("adapter_id"),
"artifacts": mapping["artifacts"],
"telemetry": mapping["telemetry"],
"error": mapping.get("error"),
"usage": mapping.get("usage"),
}
_replace_result_metadata(
context,
{
"status": mapping["status"],
"runtime_id": mapping["runtime_id"],
"invocation_id": mapping["invocation_id"],
"request_id": mapping["request_id"],
"harness": mapping["harness"],
"adapter_id": mapping.get("adapter_id"),
"artifacts": mapping["artifacts"],
"telemetry": mapping["telemetry"],
"error": mapping.get("error"),
"usage": mapping.get("usage"),
},
)
# Fabric results supply Harbor's accounting fields; an unknown dimension
# clears any value an earlier pass left in the context. ATIF metrics stay
# separate as evidence with unverified coverage.
input_tokens: int | None = None
cached_input_tokens: int | None = None
output_tokens: int | None = None
cost_usd: float | None = None
if result.usage is not None:
usage = result.usage
detailed_usage = _detailed_token_usage(usage)
Expand Down Expand Up @@ -959,19 +974,39 @@ def populate_context_from_result(
)
else:
output_tokens = usage.get("output_tokens")
if input_tokens is not None:
context.n_input_tokens = input_tokens
if cached_input_tokens is not None:
context.n_cache_tokens = cached_input_tokens
if output_tokens is not None:
context.n_output_tokens = output_tokens
if usage.get("cost_usd") is not None:
context.cost_usd = usage["cost_usd"]
cost_usd = usage.get("cost_usd")
context.n_input_tokens = input_tokens
context.n_cache_tokens = cached_input_tokens
context.n_output_tokens = output_tokens
context.cost_usd = cost_usd
return result


def _replace_result_metadata(context: AgentContext, fields: dict[str, Any]) -> None:
"""Replace one pass's result fields while keeping the bridge's own evidence."""

if context.metadata is None:
context.metadata = {}
previous = context.metadata.get("fabric")
evidence = (
{key: previous[key] for key in _BRIDGE_EVIDENCE_KEYS if key in previous}
if isinstance(previous, dict)
else {}
)
context.metadata["fabric"] = {**fields, **evidence}


def _clear_accounting(context: AgentContext) -> None:
"""Without a Fabric result, no source can vouch for Harbor's accounting fields."""

context.n_input_tokens = None
context.n_cache_tokens = None
context.n_output_tokens = None
context.cost_usd = None


def populate_context_from_trajectory(context: AgentContext, path: Path) -> None:
"""Validate canonical ATIF with Harbor and backfill usage fields when present."""
"""Validate canonical ATIF with Harbor and keep its totals as separate evidence."""

if not path.is_file():
return
Expand All @@ -986,15 +1021,23 @@ def populate_context_from_trajectory(context: AgentContext, path: Path) -> None:
metrics = trajectory.final_metrics
if metrics is None:
return
# These are two views of one invocation, not additive accounting sources.
if context.n_input_tokens is None:
context.n_input_tokens = metrics.total_prompt_tokens
if context.n_cache_tokens is None:
context.n_cache_tokens = metrics.total_cached_tokens
if context.n_output_tokens is None:
context.n_output_tokens = metrics.total_completion_tokens
if context.cost_usd is None:
context.cost_usd = metrics.total_cost_usd
# ATIF totals can cover a different scope (for example subagents) and cache
# convention than the invocation, so they never fill Harbor's accounting.
_record_atif_final_metrics(context, metrics.model_dump(mode="json"))


def _record_atif_final_metrics(
context: AgentContext, final_metrics: dict[str, Any]
) -> None:
if context.metadata is None:
context.metadata = {}
fabric = context.metadata.setdefault("fabric", {})
if isinstance(fabric, dict):
fabric["atif_final_metrics"] = {
"source": "atif",
"coverage": "unverified",
"final_metrics": final_metrics,
}


def _record_host_atif_validation(
Expand Down
15 changes: 11 additions & 4 deletions tests/integrations/test_harbor_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,10 +707,17 @@ async def download_file(self, _source, destination):
)

assert context.metadata["fabric"]["status"] == "succeeded"
assert context.n_input_tokens == 12
assert context.n_cache_tokens == 3
assert context.n_output_tokens == 4
assert context.cost_usd == 0.25
atif_metrics = context.metadata["fabric"]["atif_final_metrics"]
assert atif_metrics["source"] == "atif"
assert atif_metrics["coverage"] == "unverified"
assert atif_metrics["final_metrics"]["total_prompt_tokens"] == 12
assert atif_metrics["final_metrics"]["total_cached_tokens"] == 3
assert atif_metrics["final_metrics"]["total_completion_tokens"] == 4
assert atif_metrics["final_metrics"]["total_cost_usd"] == 0.25
assert context.n_input_tokens is None
assert context.n_cache_tokens is None
assert context.n_output_tokens is None
assert context.cost_usd is None


def test_harbor_023_options_schema_and_preflight():
Expand Down
Loading
Loading