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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,12 @@ Rules of the road:
`src/` imports nothing from either, and
`packages/atif-converter/tests/test_dev_only_imports_guard.py` fails on any
import, in any member. The ATIF data classes and the validator are harbor
0.23.0's, vendored under Apache-2.0 in `atif_converter.domain.atif` (each
0.24.0's, vendored under Apache-2.0 in `atif_converter.domain.atif` (each
file byte for byte upstream apart from its header and import path; ruff and
ty skip the directory so it stays that way). The raw-JSONL → `Trajectory`
conversion is ours (`atif_converter.domain.claude_code_conversion`,
`domain.codex_conversion`, ported from harbor and held to parity with
0.23.0). harbor survives in the tests
0.24.0). harbor survives in the tests
as two oracles: its private converters in
`packages/atif-converter/tests/harbor_oracle.py` (frozen goldens under
`tests/goldens/`, plus a live-corpus parity test), and
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,14 @@ member's `src/` imports either one.

**The ATIF models are vendored.** The data classes from `harbor.models.trajectories`
(RFC 0001) and `harbor.utils.trajectory_validator` live in
`atif_converter.domain.atif`, copied from harbor 0.23.0 under Apache-2.0. Each file
`atif_converter.domain.atif`, copied from harbor 0.24.0 under Apache-2.0. Each file
carries an attribution header and is otherwise upstream's file byte for byte, with
the import path rewritten; ruff and ty skip the directory so nobody reformats it
into ours. `UPSTREAM_VERSION` in its `__init__` records the release, and `meta.json`
stamps it as `harbor_version`. The conversion from a Claude Code session or a Codex
rollout to a `Trajectory` is ours, in `atif_converter.domain.claude_code_conversion`
and `atif_converter.domain.codex_conversion`, ported from harbor and held to parity
with harbor 0.23.0.
with harbor 0.24.0.

harbor itself is used in two places, both tests:

Expand Down
4 changes: 2 additions & 2 deletions docs/CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,10 @@ proofs are out of scope for the workspace.
## Converter (atif-converter owns the conversion; harbor's ATIF models are the contract)
0. src/ imports nothing from harbor (or litellm); both are dev dependencies.
The ATIF data classes (harbor.models.trajectories, RFC 0001) and
harbor.utils.trajectory_validator are vendored from harbor 0.23.0
harbor.utils.trajectory_validator are vendored from harbor 0.24.0
(Apache-2.0) as atif_converter.domain.atif and held to upstream by a
conformance test. The raw-JSONL -> Trajectory conversion for both agents is
ours, ported from harbor 0.23.0 and held to PARITY with it by an oracle in
ours, ported from harbor and held to PARITY with harbor 0.24.0 by an oracle in
atif-converter's tests: frozen goldens per synthetic fixture, plus a
live-corpus diff.
1. Side-file discovery: every *.jsonl under <session-stem>/ (including
Expand Down
4 changes: 2 additions & 2 deletions packages/atif-converter/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# atif-converter

Converts Claude Code session JSONL and Codex rollouts into ATIF trajectories
(converters ported from harbor and held to parity with harbor 0.23.0, on harbor's ATIF models vendored in
(converters ported from harbor and held to parity with harbor 0.24.0, on harbor's ATIF models vendored in
`atif_converter.domain.atif`), and owns the FIDELITY POLICY: the seven known upstream
conversion gaps are encoded as `atif_converter.domain.fidelity.FidelityGap`,
and every conversion is audited into a `LossReport` (raw-side census vs
converted output). harbor is a dev dependency only; the tests pin harbor 0.23.0
converted output). harbor is a dev dependency only; the tests pin harbor 0.24.0
behavior and hold the vendored models to it, so they are the drift alarm for a
harbor bump.

Expand Down
2 changes: 1 addition & 1 deletion packages/atif-converter/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ classifiers = [
"Typing :: Typed",
]
dependencies = [
# pydantic for the ATIF models vendored from harbor 0.23.0 under
# pydantic for the ATIF models vendored from harbor 0.24.0 under
# atif_converter.domain.atif; harbor itself and litellm are dev-only (the
# root's [dependency-groups] dev), kept for the parity oracle and the
# pricing identity test.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/__init__.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/__init__.py
# (Apache-2.0, Copyright the Harbor authors), with the provenance constants
# below added.

Expand Down Expand Up @@ -35,7 +35,7 @@
#: The distribution these files were vendored from.
UPSTREAM_DISTRIBUTION = "harbor"
#: The upstream release the files match; ``meta.json`` records it as ``harbor_version``.
UPSTREAM_VERSION = "0.23.0"
UPSTREAM_VERSION = "0.24.0"

__all__ = [
"UPSTREAM_DISTRIBUTION",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/agent.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/agent.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/content.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/content.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/final_metrics.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/final_metrics.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/metrics.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/metrics.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/observation.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/observation.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/observation_result.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/observation_result.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/step.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/step.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/subagent_trajectory_ref.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/subagent_trajectory_ref.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/tool_call.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/tool_call.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/models/trajectories/trajectory.py
# Vendored from harbor 0.24.0, src/harbor/models/trajectories/trajectory.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Vendored from harbor 0.23.0, src/harbor/utils/trajectory_validator.py
# Vendored from harbor 0.24.0, src/harbor/utils/trajectory_validator.py
# (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
# from these header lines and the import path, which reads
# atif_converter.domain.atif where upstream reads harbor.models.trajectories.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# Ported from harbor 0.23.0, src/harbor/agents/installed/claude_code.py
# Ported from harbor 0.24.0, src/harbor/agents/installed/claude_code.py
# (Apache-2.0, Copyright the Harbor authors), onto the ATIF models vendored in
# atif_converter.domain.atif, so atif-converter doesn't depend on harbor.

Expand Down Expand Up @@ -559,7 +559,8 @@ def _collect_events(
# Keep events in chronological order across the main chain and any
# subagent sidechains, so the first user step remains the instruction
# (downstream byte-identity checks rely on this) and step timestamps
# stay monotonic; sidechain steps are marked via `extra.is_sidechain`.
# stay monotonic; sidechain steps carry `extra.is_sidechain` and
# `extra.agent_id` so consumers can distinguish concurrent subagents.
deduped_raw_events.sort(key=lambda e: e.get("timestamp", ""))
return deduped_raw_events

Expand Down Expand Up @@ -595,6 +596,20 @@ def _agent_extra(events: list[dict[str, Any]]) -> dict[str, Any] | None:
return agent_extra or None


def _event_identity_extra(event: dict[str, Any]) -> dict[str, Any]:
"""Keep each Claude session event's agent identity on its ATIF step.

harbor 0.24.0's ``ClaudeCode._event_identity_extra`` (harbor #3434): every
step a session event becomes, the agent turn, a user message and a tool
result alike, carries ``is_sidechain`` and, when the event names one, the
``agentId`` of the subagent that wrote it.
"""
extra: dict[str, Any] = {"is_sidechain": event.get("isSidechain", False)}
if agent_id := event.get("agentId"):
extra["agent_id"] = agent_id
return extra


def _last_usage_by_msg_id(events: list[dict[str, Any]]) -> dict[str, Any]:
last_usage_by_msg_id: dict[str, Any] = {}
for ev in events:
Expand Down Expand Up @@ -638,20 +653,11 @@ def _normalize_assistant_event(
extra[key] = value
if event.get("id"):
extra["id"] = event["id"]
# DELIBERATE DIVERGENCE from harbor 0.22.0, which reads ``agent_id``: every
# transcript spells the field ``agentId`` (the key ``_agent_extra`` above
# already reads), so harbor's lookup never matched and no sidechain step
# carried its subagent's id. The snake_case spelling is still honored for
# a record that has it. The parity oracle names this divergence
# (``tests/harbor_oracle.py::DELIBERATE_DIVERGENCES``).
agent_id = event.get("agentId") or event.get("agent_id")
if agent_id:
extra["agent_id"] = agent_id
extra.update(_event_identity_extra(event))
if event.get("cwd"):
extra.setdefault("cwd", event["cwd"])
if event.get("userType") and event.get("userType") != "external":
extra["user_type"] = event["userType"]
extra["is_sidechain"] = event.get("isSidechain", False)

model_name = message.get("model") or state.default_model_name

Expand Down Expand Up @@ -777,7 +783,7 @@ def _normalize_tool_result_block(

extra_val = call_info.get("extra")
extra = extra_val if isinstance(extra_val, dict) else {}
extra["is_sidechain"] = event.get("isSidechain", False)
extra.update(_event_identity_extra(event))
if metadata:
extra.setdefault("tool_result_metadata", metadata)
if block.get("is_error") is not None:
Expand Down Expand Up @@ -805,7 +811,7 @@ def _normalize_user_event(
# first user step) hold; the strip is only the empty-skip filter.
text = content
if text.strip():
extra = {"is_sidechain": event.get("isSidechain", False)}
extra = _event_identity_extra(event)
state.normalized_events.append(
{
"kind": "message",
Expand Down Expand Up @@ -860,7 +866,7 @@ def _normalize_user_event(
"timestamp": timestamp,
"role": "user",
"text": text_message,
"extra": {"is_sidechain": event.get("isSidechain", False)},
"extra": _event_identity_extra(event),
}
)
return
Expand All @@ -876,7 +882,7 @@ def _normalize_user_event(
"timestamp": timestamp,
"role": "user",
"text": text,
"extra": {"is_sidechain": event.get("isSidechain", False)},
"extra": _event_identity_extra(event),
}
)

Expand Down
Loading
Loading