atif 1.7.0a0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,218 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ # Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ # poetry.lock
109
+ # poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ # pdm.lock
116
+ # pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ # pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # Redis
135
+ *.rdb
136
+ *.aof
137
+ *.pid
138
+
139
+ # RabbitMQ
140
+ mnesia/
141
+ rabbitmq/
142
+ rabbitmq-data/
143
+
144
+ # ActiveMQ
145
+ activemq-data/
146
+
147
+ # SageMath parsed files
148
+ *.sage.py
149
+
150
+ # Environments
151
+ .env
152
+ .envrc
153
+ .venv
154
+ env/
155
+ venv/
156
+ ENV/
157
+ env.bak/
158
+ venv.bak/
159
+
160
+ # Spyder project settings
161
+ .spyderproject
162
+ .spyproject
163
+
164
+ # Rope project settings
165
+ .ropeproject
166
+
167
+ # mkdocs documentation
168
+ /site
169
+
170
+ # mypy
171
+ .mypy_cache/
172
+ .dmypy.json
173
+ dmypy.json
174
+
175
+ # Pyre type checker
176
+ .pyre/
177
+
178
+ # pytype static type analyzer
179
+ .pytype/
180
+
181
+ # Cython debug symbols
182
+ cython_debug/
183
+
184
+ # PyCharm
185
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
186
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
187
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
188
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
189
+ # .idea/
190
+
191
+ # Abstra
192
+ # Abstra is an AI-powered process automation framework.
193
+ # Ignore directories containing user credentials, local state, and settings.
194
+ # Learn more at https://abstra.io/docs
195
+ .abstra/
196
+
197
+ # Visual Studio Code
198
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
199
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
200
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
201
+ # you could uncomment the following to ignore the entire vscode folder
202
+ # .vscode/
203
+ # Temporary file for partial code execution
204
+ tempCodeRunnerFile.py
205
+
206
+ # Ruff stuff:
207
+ .ruff_cache/
208
+
209
+ # PyPI configuration file
210
+ .pypirc
211
+
212
+ # Marimo
213
+ marimo/_static/
214
+ marimo/_lsp/
215
+ __marimo__/
216
+
217
+ # Streamlit
218
+ .streamlit/secrets.toml
atif-1.7.0a0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 jian
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
atif-1.7.0a0/PKG-INFO ADDED
@@ -0,0 +1,86 @@
1
+ Metadata-Version: 2.4
2
+ Name: atif
3
+ Version: 1.7.0a0
4
+ Summary: Pydantic models for the Agent Trajectory Interchange Format (ATIF v1.7).
5
+ Project-URL: Homepage, https://github.com/bolu61/python-atif
6
+ Project-URL: Repository, https://github.com/bolu61/python-atif
7
+ Project-URL: Issues, https://github.com/bolu61/python-atif/issues
8
+ Project-URL: Specification, https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md
9
+ Author-email: bolu61 <bolu61@zjc.dev>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: agent,atif,harbor,llm,pydantic,schema,trajectory
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Programming Language :: Python :: 3.14
22
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: pydantic>=2.9
27
+ Description-Content-Type: text/markdown
28
+
29
+ # atif
30
+
31
+ Pydantic models for the [Agent Trajectory Interchange Format (ATIF)][spec], a
32
+ standardized JSON schema for logging the complete interaction history of
33
+ autonomous LLM agents — user messages, agent responses, tool calls, observations,
34
+ metrics, and embedded subagent trajectories. Implements ATIF v1.7.
35
+
36
+ ## Install
37
+
38
+ ```bash
39
+ pip install atif
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ from atif import Trajectory
46
+
47
+ data = {
48
+ "schema_version": "ATIF-v1.7",
49
+ "agent": {"name": "my-agent", "version": "1.0.0", "model_name": "claude-sonnet-4-6"},
50
+ "steps": [
51
+ {"step_id": 1, "source": "user", "message": "What time is it?"},
52
+ {
53
+ "step_id": 2,
54
+ "source": "agent",
55
+ "message": "Let me check.",
56
+ "tool_calls": [
57
+ {"tool_call_id": "c1", "function_name": "now", "arguments": {}}
58
+ ],
59
+ "observation": {
60
+ "results": [{"source_call_id": "c1", "content": "2026-05-16T12:00:00Z"}]
61
+ },
62
+ "metrics": {"prompt_tokens": 120, "completion_tokens": 18},
63
+ },
64
+ ],
65
+ }
66
+
67
+ trajectory = Trajectory.model_validate(data)
68
+ print(trajectory.steps[1].tool_calls[0].function_name) # "now"
69
+ print(trajectory.to_json_dict(exclude_none=True))
70
+ ```
71
+
72
+ Validation enforces the ATIF rules: sequential `step_id`s, ISO 8601 timestamps,
73
+ agent-only fields gated on `source == "agent"`, tool-call / observation
74
+ correlation, embedded-subagent `trajectory_id` uniqueness, and
75
+ `ContentPart` text/image XOR.
76
+
77
+ ## Versioning
78
+
79
+ `atif`'s `MAJOR.MINOR` tracks the ATIF RFC version it implements
80
+ (`1.7.x` ⇔ ATIF v1.7); `PATCH` is reserved for library-only fixes. Because
81
+ the ATIF RFC occasionally introduces breaking changes on a MINOR bump
82
+ (e.g. v1.7's `SubagentTrajectoryRef` resolution change), a MINOR bump of
83
+ this library may be breaking too — pin to `atif~=1.7.0` if you need to
84
+ stay on a single ATIF version.
85
+
86
+ [spec]: https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md
atif-1.7.0a0/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # atif
2
+
3
+ Pydantic models for the [Agent Trajectory Interchange Format (ATIF)][spec], a
4
+ standardized JSON schema for logging the complete interaction history of
5
+ autonomous LLM agents — user messages, agent responses, tool calls, observations,
6
+ metrics, and embedded subagent trajectories. Implements ATIF v1.7.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pip install atif
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from atif import Trajectory
18
+
19
+ data = {
20
+ "schema_version": "ATIF-v1.7",
21
+ "agent": {"name": "my-agent", "version": "1.0.0", "model_name": "claude-sonnet-4-6"},
22
+ "steps": [
23
+ {"step_id": 1, "source": "user", "message": "What time is it?"},
24
+ {
25
+ "step_id": 2,
26
+ "source": "agent",
27
+ "message": "Let me check.",
28
+ "tool_calls": [
29
+ {"tool_call_id": "c1", "function_name": "now", "arguments": {}}
30
+ ],
31
+ "observation": {
32
+ "results": [{"source_call_id": "c1", "content": "2026-05-16T12:00:00Z"}]
33
+ },
34
+ "metrics": {"prompt_tokens": 120, "completion_tokens": 18},
35
+ },
36
+ ],
37
+ }
38
+
39
+ trajectory = Trajectory.model_validate(data)
40
+ print(trajectory.steps[1].tool_calls[0].function_name) # "now"
41
+ print(trajectory.to_json_dict(exclude_none=True))
42
+ ```
43
+
44
+ Validation enforces the ATIF rules: sequential `step_id`s, ISO 8601 timestamps,
45
+ agent-only fields gated on `source == "agent"`, tool-call / observation
46
+ correlation, embedded-subagent `trajectory_id` uniqueness, and
47
+ `ContentPart` text/image XOR.
48
+
49
+ ## Versioning
50
+
51
+ `atif`'s `MAJOR.MINOR` tracks the ATIF RFC version it implements
52
+ (`1.7.x` ⇔ ATIF v1.7); `PATCH` is reserved for library-only fixes. Because
53
+ the ATIF RFC occasionally introduces breaking changes on a MINOR bump
54
+ (e.g. v1.7's `SubagentTrajectoryRef` resolution change), a MINOR bump of
55
+ this library may be breaking too — pin to `atif~=1.7.0` if you need to
56
+ stay on a single ATIF version.
57
+
58
+ [spec]: https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md
@@ -0,0 +1,33 @@
1
+ """Pydantic models for the Agent Trajectory Interchange Format (ATIF v1.7).
2
+
3
+ Reference: https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md
4
+ """
5
+
6
+ from .agent import Agent
7
+ from .content import ContentPart, ContentPartType, ImageMediaType, ImageSource
8
+ from .final_metrics import FinalMetrics
9
+ from .metrics import Metrics
10
+ from .observation import Observation
11
+ from .observation_result import ObservationResult
12
+ from .step import ReasoningEffort, Step, StepSource
13
+ from .subagent_trajectory_ref import SubagentTrajectoryRef
14
+ from .tool_call import ToolCall
15
+ from .trajectory import Trajectory
16
+
17
+ __all__ = [
18
+ "Agent",
19
+ "ContentPart",
20
+ "ContentPartType",
21
+ "FinalMetrics",
22
+ "ImageMediaType",
23
+ "ImageSource",
24
+ "Metrics",
25
+ "Observation",
26
+ "ObservationResult",
27
+ "ReasoningEffort",
28
+ "Step",
29
+ "StepSource",
30
+ "SubagentTrajectoryRef",
31
+ "ToolCall",
32
+ "Trajectory",
33
+ ]
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class Agent(BaseModel):
9
+ """Agent system that produced the trajectory (ATIF `AgentSchema`)."""
10
+
11
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
12
+
13
+ name: str = Field(..., description="Name of the agent system, e.g. 'openhands'.")
14
+ version: str = Field(..., description="Version identifier of the agent system.")
15
+ model_name: str | None = Field(
16
+ default=None,
17
+ description="Default LLM model for this trajectory; step-level model_name overrides.",
18
+ )
19
+ tool_definitions: list[dict[str, Any]] | None = Field(
20
+ default=None,
21
+ description="OpenAI-style function/tool definitions available to the agent.",
22
+ )
23
+ extra: dict[str, Any] | None = Field(
24
+ default=None,
25
+ description="Custom agent configuration not covered by the core schema.",
26
+ )
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
6
+
7
+ ImageMediaType = Literal["image/jpeg", "image/png", "image/gif", "image/webp"]
8
+ ContentPartType = Literal["text", "image"]
9
+
10
+
11
+ class ImageSource(BaseModel):
12
+ """Reference to an image stored alongside the trajectory (ATIF v1.6+)."""
13
+
14
+ model_config = ConfigDict(extra="forbid")
15
+
16
+ media_type: ImageMediaType = Field(..., description="MIME type of the image.")
17
+ path: str = Field(
18
+ ...,
19
+ description="Relative/absolute file path or URL pointing at the image.",
20
+ )
21
+
22
+
23
+ class ContentPart(BaseModel):
24
+ """One element of a multimodal `message` or `content` array (ATIF v1.6+)."""
25
+
26
+ model_config = ConfigDict(extra="forbid")
27
+
28
+ type: ContentPartType
29
+ text: str | None = None
30
+ source: ImageSource | None = None
31
+
32
+ @model_validator(mode="after")
33
+ def _check_payload(self) -> "ContentPart":
34
+ if self.type == "text":
35
+ if self.text is None:
36
+ raise ValueError("ContentPart of type 'text' requires `text`.")
37
+ if self.source is not None:
38
+ raise ValueError("ContentPart of type 'text' must omit `source`.")
39
+ else:
40
+ if self.source is None:
41
+ raise ValueError("ContentPart of type 'image' requires `source`.")
42
+ if self.text is not None:
43
+ raise ValueError("ContentPart of type 'image' must omit `text`.")
44
+ return self
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class FinalMetrics(BaseModel):
9
+ """Aggregate metrics for a whole trajectory (ATIF `FinalMetricsSchema`)."""
10
+
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ total_prompt_tokens: int | None = Field(
14
+ default=None,
15
+ description="Sum of all `prompt_tokens` across steps (cached + non-cached).",
16
+ )
17
+ total_completion_tokens: int | None = Field(
18
+ default=None, description="Sum of all `completion_tokens` across steps."
19
+ )
20
+ total_cached_tokens: int | None = Field(
21
+ default=None,
22
+ description="Sum of all `cached_tokens` (subset of `total_prompt_tokens`).",
23
+ )
24
+ total_cost_usd: float | None = Field(
25
+ default=None, description="Total monetary cost across the trajectory in USD."
26
+ )
27
+ total_steps: int | None = Field(
28
+ default=None,
29
+ description=(
30
+ "Total step count. MAY diverge from `len(steps)` when explained in `notes`."
31
+ ),
32
+ )
33
+ extra: dict[str, Any] | None = Field(
34
+ default=None, description="Custom aggregate metrics."
35
+ )
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class Metrics(BaseModel):
9
+ """Per-step LLM metrics (ATIF `MetricsSchema`)."""
10
+
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ prompt_tokens: int | None = Field(
14
+ default=None,
15
+ description=(
16
+ "All input tokens for this turn, including both cached and non-cached. "
17
+ "`cached_tokens` is a subset of this count."
18
+ ),
19
+ )
20
+ completion_tokens: int | None = Field(
21
+ default=None,
22
+ description="Tokens generated by the LLM response (reasoning + tool calls).",
23
+ )
24
+ cached_tokens: int | None = Field(
25
+ default=None,
26
+ description="Subset of `prompt_tokens` served from prompt cache.",
27
+ )
28
+ cost_usd: float | None = Field(
29
+ default=None, description="Monetary cost of this step in USD."
30
+ )
31
+ prompt_token_ids: list[int] | None = Field(
32
+ default=None,
33
+ description="Integer token IDs for the prompt (v1.4+).",
34
+ )
35
+ completion_token_ids: list[int] | None = Field(
36
+ default=None,
37
+ description="Integer token IDs for the completion (v1.3+).",
38
+ )
39
+ logprobs: list[float] | None = Field(
40
+ default=None,
41
+ description="Per-completion-token log probabilities; aligns with `completion_token_ids`.",
42
+ )
43
+ extra: dict[str, Any] | None = Field(
44
+ default=None,
45
+ description="Provider-specific or experimental metrics (e.g. reasoning_tokens).",
46
+ )
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ from pydantic import BaseModel, ConfigDict, Field
4
+
5
+ from .observation_result import ObservationResult
6
+
7
+
8
+ class Observation(BaseModel):
9
+ """Environment/system feedback after a step (ATIF `ObservationSchema`)."""
10
+
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ results: list[ObservationResult] = Field(
14
+ ...,
15
+ description="One entry per tool call, action, or system-event result.",
16
+ )
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from .content import ContentPart
8
+ from .subagent_trajectory_ref import SubagentTrajectoryRef
9
+
10
+
11
+ class ObservationResult(BaseModel):
12
+ """Single observation result (ATIF `ObservationResultSchema`).
13
+
14
+ `content` MAY be omitted when `subagent_trajectory_ref` is present.
15
+ """
16
+
17
+ model_config = ConfigDict(extra="forbid")
18
+
19
+ source_call_id: str | None = Field(
20
+ default=None,
21
+ description=(
22
+ "The `ToolCall.tool_call_id` this result corresponds to. "
23
+ "If null/omitted the result comes from a non-tool action or system event."
24
+ ),
25
+ )
26
+ content: str | list[ContentPart] | None = Field(
27
+ default=None,
28
+ description="Tool/action output; string or multimodal `ContentPart` array (v1.6+).",
29
+ )
30
+ subagent_trajectory_ref: list[SubagentTrajectoryRef] | None = Field(
31
+ default=None,
32
+ description="Refs to delegated subagent trajectories; use a singleton array for one.",
33
+ )
34
+ extra: dict[str, Any] | None = Field(
35
+ default=None,
36
+ description="Custom result-level metadata (e.g. retrieval_score, source_doc_id).",
37
+ )
File without changes
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import datetime
4
+ from typing import Any, Literal
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
7
+
8
+ from .content import ContentPart
9
+ from .metrics import Metrics
10
+ from .observation import Observation
11
+ from .tool_call import ToolCall
12
+
13
+ StepSource = Literal["system", "user", "agent"]
14
+ ReasoningEffort = str | float
15
+
16
+
17
+ class Step(BaseModel):
18
+ """A single turn in a trajectory (ATIF `StepObject`).
19
+
20
+ Represents a system prompt, a user message, or one complete agent turn
21
+ (LLM inference, action execution, observation receipt).
22
+ """
23
+
24
+ model_config = ConfigDict(extra="forbid")
25
+
26
+ step_id: int = Field(..., ge=1, description="Ordinal index of the turn (starting at 1).")
27
+ timestamp: str | None = Field(
28
+ default=None,
29
+ description="ISO 8601 timestamp, e.g. '2025-10-16T14:30:00Z'.",
30
+ )
31
+ source: StepSource = Field(..., description="Originator of this step.")
32
+ model_name: str | None = Field(
33
+ default=None,
34
+ description="Specific LLM used. Agent-only; falls back to `Trajectory.agent.model_name`.",
35
+ )
36
+ reasoning_effort: ReasoningEffort | None = Field(
37
+ default=None,
38
+ description="Qualitative/quantitative effort score. Agent-only.",
39
+ )
40
+ message: str | list[ContentPart] = Field(
41
+ ...,
42
+ description=(
43
+ "Dialogue message. String for text, or `ContentPart` array for multimodal (v1.6+)."
44
+ ),
45
+ )
46
+ reasoning_content: str | None = Field(
47
+ default=None,
48
+ description="Explicit internal reasoning. Agent-only.",
49
+ )
50
+ tool_calls: list[ToolCall] | None = Field(
51
+ default=None,
52
+ description="Structured actions for this turn. Agent-only.",
53
+ )
54
+ observation: Observation | None = Field(
55
+ default=None,
56
+ description=(
57
+ "Environment / system feedback. For agent steps, results from tool calls or "
58
+ "non-tool actions. For system steps, results from infrastructure events."
59
+ ),
60
+ )
61
+ metrics: Metrics | None = Field(
62
+ default=None, description="LLM operational/confidence data. Agent-only."
63
+ )
64
+ extra: dict[str, Any] | None = Field(
65
+ default=None, description="Custom step-level metadata."
66
+ )
67
+ llm_call_count: int | None = Field(
68
+ default=None,
69
+ ge=0,
70
+ description=(
71
+ "Number of LLM inferences for this step. `0` on an agent step signals a "
72
+ "deterministic (non-LLM) dispatch (metrics and reasoning_content MUST be absent). "
73
+ "`>1` means metrics are aggregated."
74
+ ),
75
+ )
76
+ is_copied_context: bool | None = Field(
77
+ default=None,
78
+ description=(
79
+ "True if this step was copied from a prior trajectory for context. "
80
+ "Consumers MUST filter such steps out of SFT training data."
81
+ ),
82
+ )
83
+
84
+ @field_validator("timestamp")
85
+ @classmethod
86
+ def _validate_timestamp(cls, v: str | None) -> str | None:
87
+ if v is None:
88
+ return v
89
+ try:
90
+ datetime.fromisoformat(v.replace("Z", "+00:00"))
91
+ except ValueError as exc:
92
+ raise ValueError(f"timestamp must be ISO 8601, got {v!r}") from exc
93
+ return v
94
+
95
+ @model_validator(mode="after")
96
+ def _check_source_constraints(self) -> "Step":
97
+ agent_only = {
98
+ "model_name": self.model_name,
99
+ "reasoning_effort": self.reasoning_effort,
100
+ "reasoning_content": self.reasoning_content,
101
+ "tool_calls": self.tool_calls,
102
+ "metrics": self.metrics,
103
+ }
104
+ if self.source != "agent":
105
+ for name, value in agent_only.items():
106
+ if value is not None:
107
+ raise ValueError(
108
+ f"Field `{name}` is only valid on `source='agent'` steps "
109
+ f"(this step has source={self.source!r})."
110
+ )
111
+
112
+ if self.source == "agent" and self.llm_call_count == 0:
113
+ if self.metrics is not None:
114
+ raise ValueError(
115
+ "When `llm_call_count == 0` on an agent step, `metrics` MUST be absent."
116
+ )
117
+ if self.reasoning_content is not None:
118
+ raise ValueError(
119
+ "When `llm_call_count == 0` on an agent step, "
120
+ "`reasoning_content` MUST be absent."
121
+ )
122
+
123
+ if self.observation is not None and self.tool_calls is not None:
124
+ tool_call_ids = {tc.tool_call_id for tc in self.tool_calls}
125
+ for result in self.observation.results:
126
+ if (
127
+ result.source_call_id is not None
128
+ and result.source_call_id not in tool_call_ids
129
+ ):
130
+ raise ValueError(
131
+ f"observation.results[*].source_call_id={result.source_call_id!r} "
132
+ f"does not match any tool_call in step {self.step_id}."
133
+ )
134
+ return self
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
6
+
7
+
8
+ class SubagentTrajectoryRef(BaseModel):
9
+ """Reference to a delegated subagent trajectory (ATIF `SubagentTrajectoryRefSchema`).
10
+
11
+ Two resolution mechanisms (at least one MUST be set):
12
+ - Embedded: `trajectory_id` matches an entry in the parent's `subagent_trajectories`.
13
+ - File-ref: `trajectory_path` points at an external trajectory file.
14
+
15
+ `session_id` is run-scoped and **informational only** — not a valid resolution key.
16
+ """
17
+
18
+ model_config = ConfigDict(extra="forbid")
19
+
20
+ trajectory_id: str | None = Field(
21
+ default=None,
22
+ description="Canonical id for embedded refs; matches `Trajectory.trajectory_id`.",
23
+ )
24
+ trajectory_path: str | None = Field(
25
+ default=None,
26
+ description="External location of the subagent trajectory (path, URL, DB ref).",
27
+ )
28
+ session_id: str | None = Field(
29
+ default=None,
30
+ description="Informational run identity; NOT a valid resolution key.",
31
+ )
32
+ extra: dict[str, Any] | None = Field(
33
+ default=None,
34
+ description="Custom metadata about the subagent execution.",
35
+ )
36
+
37
+ @model_validator(mode="after")
38
+ def _require_resolvable(self) -> "SubagentTrajectoryRef":
39
+ if self.trajectory_id is None and self.trajectory_path is None:
40
+ raise ValueError(
41
+ "SubagentTrajectoryRef must set at least one of "
42
+ "`trajectory_id` (embedded) or `trajectory_path` (file-ref)."
43
+ )
44
+ return self
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+
8
+ class ToolCall(BaseModel):
9
+ """A single function/tool invocation in an agent step (ATIF `ToolCallSchema`)."""
10
+
11
+ model_config = ConfigDict(extra="forbid")
12
+
13
+ tool_call_id: str = Field(
14
+ ...,
15
+ description="Unique identifier; correlated with `ObservationResult.source_call_id`.",
16
+ )
17
+ function_name: str = Field(..., description="Name of the function/tool invoked.")
18
+ arguments: dict[str, Any] = Field(
19
+ ...,
20
+ description="JSON object of arguments; MAY be empty (`{}`) when no args are needed.",
21
+ )
22
+ extra: dict[str, Any] | None = Field(
23
+ default=None,
24
+ description="Custom tool-call metadata (e.g. timeout, retry count, tool version).",
25
+ )
@@ -0,0 +1,111 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
6
+
7
+ from .agent import Agent
8
+ from .content import ContentPart
9
+ from .final_metrics import FinalMetrics
10
+ from .step import Step
11
+
12
+
13
+ class Trajectory(BaseModel):
14
+ """Root ATIF trajectory document (ATIF `Trajectory`).
15
+
16
+ A trajectory is a sequence of interactions between a user and an agent,
17
+ including the agent's internal reasoning, actions, and observations.
18
+ """
19
+
20
+ model_config = ConfigDict(extra="forbid")
21
+
22
+ schema_version: str = Field(
23
+ ...,
24
+ description="ATIF compatibility tag, e.g. 'ATIF-v1.7'.",
25
+ )
26
+ session_id: str | None = Field(
27
+ default=None,
28
+ description=(
29
+ "Run-scoped identifier. MAY be shared across siblings or continuation segments. "
30
+ "Not a valid resolution key for subagent refs."
31
+ ),
32
+ )
33
+ trajectory_id: str | None = Field(
34
+ default=None,
35
+ description=(
36
+ "Per-document identifier (v1.7+). REQUIRED on embedded subagents and unique "
37
+ "within a parent's `subagent_trajectories`."
38
+ ),
39
+ )
40
+ agent: Agent = Field(..., description="Agent system configuration.")
41
+ steps: list[Step] = Field(
42
+ ..., description="Complete interaction history in turn order."
43
+ )
44
+ notes: str | None = Field(
45
+ default=None,
46
+ description="Developer notes / explanations for format discrepancies.",
47
+ )
48
+ final_metrics: FinalMetrics | None = Field(
49
+ default=None, description="Aggregate metrics for the whole trajectory."
50
+ )
51
+ continued_trajectory_ref: str | None = Field(
52
+ default=None,
53
+ description="Pointer to a continuation trajectory file (e.g. post-summarization).",
54
+ )
55
+ extra: dict[str, Any] | None = Field(
56
+ default=None, description="Custom root-level metadata."
57
+ )
58
+ subagent_trajectories: list["Trajectory"] | None = Field(
59
+ default=None,
60
+ description=(
61
+ "Embedded subagent trajectories (v1.7+). Each entry MUST set `trajectory_id` "
62
+ "and ids MUST be unique within this array."
63
+ ),
64
+ )
65
+
66
+ @model_validator(mode="after")
67
+ def _validate_steps_and_subagents(self) -> "Trajectory":
68
+ for index, step in enumerate(self.steps, start=1):
69
+ if step.step_id != index:
70
+ raise ValueError(
71
+ f"steps[{index - 1}].step_id={step.step_id} is not sequential; "
72
+ f"expected {index}."
73
+ )
74
+
75
+ if self.subagent_trajectories:
76
+ seen: set[str] = set()
77
+ for i, sub in enumerate(self.subagent_trajectories):
78
+ if sub.trajectory_id is None:
79
+ raise ValueError(
80
+ f"subagent_trajectories[{i}] must set `trajectory_id` "
81
+ "when embedded in a parent trajectory."
82
+ )
83
+ if sub.trajectory_id in seen:
84
+ raise ValueError(
85
+ f"Duplicate trajectory_id={sub.trajectory_id!r} in "
86
+ "`subagent_trajectories`."
87
+ )
88
+ seen.add(sub.trajectory_id)
89
+ return self
90
+
91
+ def has_multimodal_content(self) -> bool:
92
+ """True if any step message or observation content uses `ContentPart` arrays."""
93
+ for step in self.steps:
94
+ if isinstance(step.message, list):
95
+ return True
96
+ if step.observation is not None:
97
+ for result in step.observation.results:
98
+ if isinstance(result.content, list):
99
+ return True
100
+ if self.subagent_trajectories:
101
+ return any(s.has_multimodal_content() for s in self.subagent_trajectories)
102
+ return False
103
+
104
+ def to_json_dict(self, exclude_none: bool = True) -> dict[str, Any]:
105
+ """Dump to a JSON-serialisable dict, dropping unset fields by default."""
106
+ return self.model_dump(mode="json", exclude_none=exclude_none)
107
+
108
+
109
+ Trajectory.model_rebuild()
110
+ # Re-export so callers can `from atif.trajectory import ContentPart` if they want.
111
+ __all__ = ["Trajectory", "ContentPart"]
@@ -0,0 +1,72 @@
1
+ [project]
2
+ name = "atif"
3
+ dynamic = ["version"]
4
+ description = "Pydantic models for the Agent Trajectory Interchange Format (ATIF v1.7)."
5
+ readme = "README.md"
6
+ requires-python = ">=3.11"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [
10
+ { name = "bolu61", email = "bolu61@zjc.dev" },
11
+ ]
12
+ keywords = ["atif", "agent", "trajectory", "llm", "pydantic", "harbor", "schema"]
13
+ classifiers = [
14
+ "Development Status :: 3 - Alpha",
15
+ "Intended Audience :: Developers",
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3",
18
+ "Programming Language :: Python :: 3 :: Only",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Programming Language :: Python :: 3.14",
23
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
24
+ "Topic :: Software Development :: Libraries :: Python Modules",
25
+ "Typing :: Typed",
26
+ ]
27
+ dependencies = [
28
+ "pydantic>=2.9",
29
+ ]
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/bolu61/python-atif"
33
+ Repository = "https://github.com/bolu61/python-atif"
34
+ Issues = "https://github.com/bolu61/python-atif/issues"
35
+ Specification = "https://github.com/harbor-framework/harbor/blob/main/rfcs/0001-trajectory-format.md"
36
+
37
+ [build-system]
38
+ requires = ["hatchling", "hatch-vcs"]
39
+ build-backend = "hatchling.build"
40
+
41
+ [tool.hatch.version]
42
+ source = "vcs"
43
+
44
+ [tool.hatch.build.targets.wheel]
45
+ packages = ["atif"]
46
+
47
+ [tool.hatch.build.targets.sdist]
48
+ include = ["atif", "README.md", "LICENSE", "pyproject.toml"]
49
+
50
+ [dependency-groups]
51
+ dev = [
52
+ "pytest>=8",
53
+ "pytest-cov>=5",
54
+ ]
55
+
56
+ [tool.pytest.ini_options]
57
+ testpaths = ["tests"]
58
+ addopts = ["--cov=atif", "--cov-report=term-missing"]
59
+
60
+ [tool.coverage.run]
61
+ source = ["atif"]
62
+ branch = true
63
+ omit = ["atif/_version.py"]
64
+
65
+ [tool.coverage.report]
66
+ show_missing = true
67
+ skip_covered = false
68
+ exclude_lines = [
69
+ "pragma: no cover",
70
+ "raise NotImplementedError",
71
+ "if TYPE_CHECKING:",
72
+ ]