atif-sql 0.2.0__py3-none-any.whl

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.
Files changed (164) hide show
  1. atif_analytics/__init__.py +9 -0
  2. atif_analytics/application/__init__.py +3 -0
  3. atif_analytics/application/analyze.py +158 -0
  4. atif_analytics/application/prompts.py +750 -0
  5. atif_analytics/application/use_cases/__init__.py +3 -0
  6. atif_analytics/application/use_cases/_shared.py +264 -0
  7. atif_analytics/application/use_cases/classify.py +430 -0
  8. atif_analytics/application/use_cases/conflicts.py +448 -0
  9. atif_analytics/application/use_cases/friction.py +634 -0
  10. atif_analytics/application/use_cases/perceived.py +451 -0
  11. atif_analytics/domain/__init__.py +3 -0
  12. atif_analytics/domain/authorship.py +166 -0
  13. atif_analytics/domain/config.py +30 -0
  14. atif_analytics/domain/costs.py +50 -0
  15. atif_analytics/domain/friction.py +117 -0
  16. atif_analytics/domain/layout.py +94 -0
  17. atif_analytics/domain/models.py +385 -0
  18. atif_analytics/domain/ports.py +100 -0
  19. atif_analytics/domain/transcript.py +359 -0
  20. atif_analytics/infrastructure/__init__.py +3 -0
  21. atif_analytics/infrastructure/corpus_reader.py +546 -0
  22. atif_analytics/infrastructure/parquet_cache.py +186 -0
  23. atif_analytics/infrastructure/settings.py +114 -0
  24. atif_analytics/infrastructure/sqlite_state/__init__.py +3 -0
  25. atif_analytics/infrastructure/sqlite_state/checkpointer.py +326 -0
  26. atif_analytics/infrastructure/sqlite_state/retry_queue.py +296 -0
  27. atif_analytics/py.typed +0 -0
  28. atif_cli/__init__.py +3 -0
  29. atif_cli/__main__.py +8 -0
  30. atif_cli/app.py +2459 -0
  31. atif_cli/converter_adapter.py +151 -0
  32. atif_cli/corpus.py +409 -0
  33. atif_cli/cron.py +311 -0
  34. atif_cli/duck_errors.py +97 -0
  35. atif_cli/embed_lake.py +101 -0
  36. atif_cli/errors.py +89 -0
  37. atif_cli/lake.py +373 -0
  38. atif_cli/lake_sessions.py +270 -0
  39. atif_cli/output.py +297 -0
  40. atif_cli/py.typed +0 -0
  41. atif_converter/__init__.py +3 -0
  42. atif_converter/application/__init__.py +3 -0
  43. atif_converter/application/convert_and_audit.py +294 -0
  44. atif_converter/application/convert_codex.py +210 -0
  45. atif_converter/domain/__init__.py +9 -0
  46. atif_converter/domain/agents.py +40 -0
  47. atif_converter/domain/atif/__init__.py +55 -0
  48. atif_converter/domain/atif/agent.py +37 -0
  49. atif_converter/domain/atif/content.py +161 -0
  50. atif_converter/domain/atif/final_metrics.py +45 -0
  51. atif_converter/domain/atif/metrics.py +49 -0
  52. atif_converter/domain/atif/observation.py +21 -0
  53. atif_converter/domain/atif/observation_result.py +47 -0
  54. atif_converter/domain/atif/step.py +144 -0
  55. atif_converter/domain/atif/subagent_trajectory_ref.py +90 -0
  56. atif_converter/domain/atif/tool_call.py +36 -0
  57. atif_converter/domain/atif/trajectory.py +211 -0
  58. atif_converter/domain/atif/trajectory_validator.py +299 -0
  59. atif_converter/domain/blobs.py +510 -0
  60. atif_converter/domain/claude_code_conversion.py +1077 -0
  61. atif_converter/domain/codex_conversion.py +1036 -0
  62. atif_converter/domain/codex_edges.py +192 -0
  63. atif_converter/domain/codex_enrichment.py +419 -0
  64. atif_converter/domain/codex_fidelity.py +124 -0
  65. atif_converter/domain/codex_nested_calls.py +165 -0
  66. atif_converter/domain/edges.py +113 -0
  67. atif_converter/domain/enrichment.py +412 -0
  68. atif_converter/domain/errors.py +60 -0
  69. atif_converter/domain/fidelity.py +171 -0
  70. atif_converter/domain/model_prices.json +8538 -0
  71. atif_converter/domain/pricing.py +790 -0
  72. atif_converter/domain/result_signals.py +774 -0
  73. atif_converter/domain/schema_version.py +90 -0
  74. atif_converter/domain/session_events.py +408 -0
  75. atif_converter/infrastructure/__init__.py +3 -0
  76. atif_converter/infrastructure/census.py +89 -0
  77. atif_converter/infrastructure/claude_code_converter.py +127 -0
  78. atif_converter/infrastructure/codex_adapter.py +132 -0
  79. atif_converter/infrastructure/codex_census.py +100 -0
  80. atif_converter/infrastructure/codex_converter.py +82 -0
  81. atif_converter/infrastructure/harbor_adapter.py +187 -0
  82. atif_converter/infrastructure/raw_records.py +426 -0
  83. atif_converter/infrastructure/source_archive.py +154 -0
  84. atif_converter/py.typed +0 -0
  85. atif_corpus/__init__.py +13 -0
  86. atif_corpus/application/__init__.py +7 -0
  87. atif_corpus/application/materialize.py +1826 -0
  88. atif_corpus/domain/__init__.py +9 -0
  89. atif_corpus/domain/agents.py +33 -0
  90. atif_corpus/domain/generation.py +34 -0
  91. atif_corpus/domain/layout.py +168 -0
  92. atif_corpus/domain/ports.py +175 -0
  93. atif_corpus/domain/session_id.py +68 -0
  94. atif_corpus/domain/sessions.py +228 -0
  95. atif_corpus/domain/slug.py +72 -0
  96. atif_corpus/domain/source_layout.py +143 -0
  97. atif_corpus/domain/watermark.py +76 -0
  98. atif_corpus/infrastructure/__init__.py +7 -0
  99. atif_corpus/infrastructure/atomic.py +266 -0
  100. atif_corpus/infrastructure/compress_artifacts.py +161 -0
  101. atif_corpus/infrastructure/fake_converter.py +174 -0
  102. atif_corpus/infrastructure/scanner.py +316 -0
  103. atif_corpus/infrastructure/settings.py +130 -0
  104. atif_corpus/infrastructure/source_archive.py +202 -0
  105. atif_corpus/py.typed +0 -0
  106. atif_duck/__init__.py +43 -0
  107. atif_duck/domain/__init__.py +32 -0
  108. atif_duck/domain/artifacts.py +66 -0
  109. atif_duck/domain/authorship.py +165 -0
  110. atif_duck/domain/catalog.py +626 -0
  111. atif_duck/domain/columnar.py +149 -0
  112. atif_duck/domain/embedding_guard.py +88 -0
  113. atif_duck/domain/examples.py +265 -0
  114. atif_duck/domain/lake.py +427 -0
  115. atif_duck/domain/lake_sessions.py +97 -0
  116. atif_duck/domain/raw_readers.py +140 -0
  117. atif_duck/domain/session_id.py +69 -0
  118. atif_duck/domain/sql_literal.py +50 -0
  119. atif_duck/infrastructure/__init__.py +34 -0
  120. atif_duck/infrastructure/analytics.py +469 -0
  121. atif_duck/infrastructure/authorship.py +139 -0
  122. atif_duck/infrastructure/columnar.py +641 -0
  123. atif_duck/infrastructure/lake.py +1383 -0
  124. atif_duck/infrastructure/lake_sessions.py +194 -0
  125. atif_duck/infrastructure/lake_settings.py +63 -0
  126. atif_duck/infrastructure/lake_steps.py +225 -0
  127. atif_duck/infrastructure/projections.py +221 -0
  128. atif_duck/infrastructure/registry.py +2194 -0
  129. atif_duck/infrastructure/stored_artifacts.py +115 -0
  130. atif_duck/py.typed +0 -0
  131. atif_embed/__init__.py +12 -0
  132. atif_embed/application/__init__.py +3 -0
  133. atif_embed/application/embed.py +293 -0
  134. atif_embed/application/prune.py +80 -0
  135. atif_embed/domain/__init__.py +3 -0
  136. atif_embed/domain/discovery.py +70 -0
  137. atif_embed/domain/embedding_guard.py +83 -0
  138. atif_embed/domain/errors.py +94 -0
  139. atif_embed/domain/ports.py +223 -0
  140. atif_embed/domain/sql_literal.py +49 -0
  141. atif_embed/domain/text_stamp.py +71 -0
  142. atif_embed/infrastructure/__init__.py +3 -0
  143. atif_embed/infrastructure/cohere_bedrock.py +433 -0
  144. atif_embed/infrastructure/corpus_text_rows.py +273 -0
  145. atif_embed/infrastructure/gemma_local.py +355 -0
  146. atif_embed/infrastructure/lake_text_rows.py +203 -0
  147. atif_embed/infrastructure/lance_store.py +440 -0
  148. atif_embed/infrastructure/providers.py +30 -0
  149. atif_embed/infrastructure/settings.py +154 -0
  150. atif_embed/py.typed +0 -0
  151. atif_models/__init__.py +14 -0
  152. atif_models/domain/__init__.py +9 -0
  153. atif_models/domain/ports.py +143 -0
  154. atif_models/domain/registry.py +151 -0
  155. atif_models/domain/schema.py +109 -0
  156. atif_models/infrastructure/__init__.py +8 -0
  157. atif_models/infrastructure/openai_bedrock.py +314 -0
  158. atif_models/infrastructure/settings.py +85 -0
  159. atif_models/py.typed +0 -0
  160. atif_sql-0.2.0.dist-info/METADATA +272 -0
  161. atif_sql-0.2.0.dist-info/RECORD +164 -0
  162. atif_sql-0.2.0.dist-info/WHEEL +4 -0
  163. atif_sql-0.2.0.dist-info/entry_points.txt +2 -0
  164. atif_sql-0.2.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,144 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Vendored from harbor 0.24.0, src/harbor/models/trajectories/step.py
3
+ # (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
4
+ # from these header lines and the import path, which reads
5
+ # atif_converter.domain.atif where upstream reads harbor.models.trajectories.
6
+ """Step model for ATIF trajectories."""
7
+
8
+ from datetime import datetime
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import BaseModel, Field, field_validator, model_validator
12
+
13
+ from atif_converter.domain.atif.content import ContentPart
14
+ from atif_converter.domain.atif.metrics import Metrics
15
+ from atif_converter.domain.atif.observation import Observation
16
+ from atif_converter.domain.atif.tool_call import ToolCall
17
+
18
+
19
+ class Step(BaseModel):
20
+ """A single step in the trajectory."""
21
+
22
+ step_id: int = Field(
23
+ default=...,
24
+ ge=1,
25
+ description="Ordinal index of the turn (starting from 1)",
26
+ )
27
+ timestamp: str | None = Field(
28
+ default=None,
29
+ description="ISO 8601 timestamp indicating when this step occurred",
30
+ )
31
+ source: Literal["system", "user", "agent"] = Field(
32
+ default=...,
33
+ description="The originator of this step",
34
+ )
35
+ model_name: str | None = Field(
36
+ default=None,
37
+ description=(
38
+ "The specific LLM model used for this turn. Omission implies the model "
39
+ "defined in the root-level agent config."
40
+ ),
41
+ )
42
+ reasoning_effort: str | float | None = Field(
43
+ default=None,
44
+ description="Qualitative or quantitative measure of effort",
45
+ )
46
+ message: str | list[ContentPart] = Field(
47
+ default=...,
48
+ description=(
49
+ "The dialogue message. String for text-only content, or array of "
50
+ "ContentPart for multimodal content (added in ATIF-v1.6)."
51
+ ),
52
+ )
53
+ reasoning_content: str | None = Field(
54
+ default=None,
55
+ description="The agent's explicit internal reasoning",
56
+ )
57
+ tool_calls: list[ToolCall] | None = Field(
58
+ default=None,
59
+ description="Array of structured objects for the agent's actions",
60
+ )
61
+ observation: Observation | None = Field(
62
+ default=None,
63
+ description="Environment feedback/result after actions or system events",
64
+ )
65
+ metrics: Metrics | None = Field(
66
+ default=None,
67
+ description="LLM operational and confidence data for this step",
68
+ )
69
+ is_copied_context: bool | None = Field(
70
+ default=None,
71
+ description=(
72
+ "Indicates whether this step was copied from a previous trajectory "
73
+ "for context (e.g., during continuation after summarization). "
74
+ "Steps marked as copied context should not be included in training data "
75
+ "as they represent previously-trained interactions. "
76
+ "Added in ATIF-v1.5."
77
+ ),
78
+ )
79
+ llm_call_count: int | None = Field(
80
+ default=None,
81
+ ge=0,
82
+ description=(
83
+ "Number of LLM inferences this step represents. When >1, metrics are "
84
+ "aggregated across multiple LLM calls. When 1, the step represents exactly "
85
+ "one inference. When 0 on a `source: 'agent'` step, the step represents a "
86
+ "deterministic (non-LLM) dispatch; `metrics` and `reasoning_content` MUST "
87
+ "be absent in that case. When null, the producer did not track this "
88
+ "(backward-compatible default). Added in ATIF-v1.7."
89
+ ),
90
+ )
91
+ extra: dict[str, Any] | None = Field(
92
+ default=None,
93
+ description="Custom step-level metadata",
94
+ )
95
+
96
+ model_config = {"extra": "forbid"}
97
+
98
+ @field_validator("timestamp")
99
+ @classmethod
100
+ def validate_timestamp(cls, v: str | None) -> str | None:
101
+ """Validate that timestamp is a valid ISO 8601 string."""
102
+ if v is not None:
103
+ try:
104
+ datetime.fromisoformat(v.replace("Z", "+00:00"))
105
+ except ValueError as e:
106
+ raise ValueError(f"Invalid ISO 8601 timestamp: {e}")
107
+ return v
108
+
109
+ @model_validator(mode="after")
110
+ def validate_agent_only_fields(self) -> "Step":
111
+ """Validate that certain fields are only present for agent steps."""
112
+ if self.source != "agent":
113
+ agent_only_fields = [
114
+ "model_name",
115
+ "reasoning_effort",
116
+ "reasoning_content",
117
+ "tool_calls",
118
+ "metrics",
119
+ ]
120
+ for field in agent_only_fields:
121
+ if getattr(self, field) is not None:
122
+ raise ValueError(
123
+ f"Field '{field}' is only applicable when source is 'agent', "
124
+ f"but source is '{self.source}'"
125
+ )
126
+ return self
127
+
128
+ @model_validator(mode="after")
129
+ def validate_llm_call_count_zero_fields(self) -> "Step":
130
+ """Enforce ATIF v1.7 no-LLM orchestration rule.
131
+
132
+ When ``llm_call_count == 0`` on a ``source: "agent"`` step, the step
133
+ represents a deterministic (non-LLM) dispatch. LLM-specific fields
134
+ (``metrics``, ``reasoning_content``) MUST be absent on such steps.
135
+ """
136
+ if self.llm_call_count == 0 and self.source == "agent":
137
+ llm_only_fields = ["metrics", "reasoning_content"]
138
+ for field in llm_only_fields:
139
+ if getattr(self, field) is not None:
140
+ raise ValueError(
141
+ f"Field '{field}' must be absent when llm_call_count is 0 "
142
+ f"(deterministic dispatch on a 'source: agent' step)"
143
+ )
144
+ return self
@@ -0,0 +1,90 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Vendored from harbor 0.24.0, src/harbor/models/trajectories/subagent_trajectory_ref.py
3
+ # (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
4
+ # from these header lines and the import path, which reads
5
+ # atif_converter.domain.atif where upstream reads harbor.models.trajectories.
6
+ """Subagent trajectory reference model for ATIF trajectories."""
7
+
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, Field, model_validator
11
+
12
+
13
+ class SubagentTrajectoryRef(BaseModel):
14
+ """Reference to a delegated subagent trajectory.
15
+
16
+ A subagent reference is resolved by one of two mechanisms:
17
+
18
+ 1. **Embedded form** — set `trajectory_id` to match the
19
+ `Trajectory.trajectory_id` of an entry in the parent's
20
+ `subagent_trajectories` array.
21
+ 2. **File-ref form** — set `trajectory_path` to the location
22
+ (file path, S3 URL, etc.) of an external trajectory file.
23
+
24
+ These two mechanisms are the only resolution keys. `session_id`, when
25
+ present on the ref, is **informational only**: it records the run
26
+ identity of the delegated subagent for debug / correlation / cross-
27
+ trajectory search purposes, and MUST NOT be used as a matching key
28
+ (it is run-scoped and MAY collide across siblings — see
29
+ `Trajectory.session_id`). A ref therefore MUST set at least one of
30
+ `trajectory_id` or `trajectory_path`; `session_id` alone is not a
31
+ resolvable reference.
32
+ """
33
+
34
+ trajectory_id: str | None = Field(
35
+ default=None,
36
+ description=(
37
+ "Canonical identifier of the delegated subagent trajectory. "
38
+ "Matches `Trajectory.trajectory_id` of an entry in the parent's "
39
+ "`subagent_trajectories` array and is the resolution key for "
40
+ "embedded references. Added in ATIF-v1.7 to provide a document-"
41
+ "unique matching key without overloading `session_id`."
42
+ ),
43
+ )
44
+ session_id: str | None = Field(
45
+ default=None,
46
+ description=(
47
+ "Run identity of the delegated subagent trajectory. Informational "
48
+ "only: recorded so consumers can correlate this ref back to the "
49
+ "subagent's run for debug / search / display purposes. Run-scoped "
50
+ "(see `Trajectory.session_id`) and therefore NOT a valid "
51
+ "resolution key — consumers MUST resolve via `trajectory_id` "
52
+ "(embedded) or `trajectory_path` (external file)."
53
+ ),
54
+ )
55
+ trajectory_path: str | None = Field(
56
+ default=None,
57
+ description=(
58
+ "Location of the complete subagent trajectory as an external "
59
+ "file (file path, S3 URL, database reference, etc.). Resolution "
60
+ "key for file-ref references. When both `trajectory_id` and "
61
+ "`trajectory_path` are set, consumers MAY choose either; "
62
+ "typically `trajectory_id` is preferred when the embedded "
63
+ "trajectory is available in-memory."
64
+ ),
65
+ )
66
+ extra: dict[str, Any] | None = Field(
67
+ default=None,
68
+ description="Custom metadata about the subagent execution",
69
+ )
70
+
71
+ model_config = {"extra": "forbid"}
72
+
73
+ @model_validator(mode="after")
74
+ def validate_is_resolvable(self) -> "SubagentTrajectoryRef":
75
+ """A ref must be resolvable: set `trajectory_id` or `trajectory_path`.
76
+
77
+ `session_id` alone is not sufficient because it is run-scoped and
78
+ MAY collide across siblings (see `Trajectory.session_id`), so it
79
+ cannot unambiguously identify which subagent trajectory a ref
80
+ points at.
81
+ """
82
+ if self.trajectory_id is None and self.trajectory_path is None:
83
+ raise ValueError(
84
+ "SubagentTrajectoryRef must be resolvable: set either "
85
+ "`trajectory_id` (for embedded references) or "
86
+ "`trajectory_path` (for external-file references). "
87
+ "`session_id` alone is not a resolution key — it is "
88
+ "run-scoped and may collide across siblings."
89
+ )
90
+ return self
@@ -0,0 +1,36 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Vendored from harbor 0.24.0, src/harbor/models/trajectories/tool_call.py
3
+ # (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
4
+ # from these header lines and the import path, which reads
5
+ # atif_converter.domain.atif where upstream reads harbor.models.trajectories.
6
+ """Tool call model for ATIF trajectories."""
7
+
8
+ from typing import Any
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+
13
+ class ToolCall(BaseModel):
14
+ """A tool call within a step."""
15
+
16
+ tool_call_id: str = Field(
17
+ default=...,
18
+ description="Unique identifier for this specific tool call",
19
+ )
20
+ function_name: str = Field(
21
+ default=...,
22
+ description="The name of the function or tool being invoked",
23
+ )
24
+ arguments: dict[str, Any] = Field(
25
+ default=...,
26
+ description="Arguments passed to the function (can be empty dict)",
27
+ )
28
+ extra: dict[str, Any] | None = Field(
29
+ default=None,
30
+ description=(
31
+ "Custom tool-call-level metadata (e.g., timeout, retry count, tool version). "
32
+ "Added in ATIF-v1.7."
33
+ ),
34
+ )
35
+
36
+ model_config = {"extra": "forbid"}
@@ -0,0 +1,211 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ # Vendored from harbor 0.24.0, src/harbor/models/trajectories/trajectory.py
3
+ # (Apache-2.0, Copyright the Harbor authors). Byte for byte upstream apart
4
+ # from these header lines and the import path, which reads
5
+ # atif_converter.domain.atif where upstream reads harbor.models.trajectories.
6
+ """Trajectory model for ATIF (Agent Trajectory Interchange Format)."""
7
+
8
+ from typing import Any, Literal
9
+
10
+ from pydantic import BaseModel, Field, model_validator
11
+
12
+ from atif_converter.domain.atif.agent import Agent
13
+ from atif_converter.domain.atif.final_metrics import FinalMetrics
14
+ from atif_converter.domain.atif.step import Step
15
+
16
+
17
+ class Trajectory(BaseModel):
18
+ """Agent Trajectory in ATIF (Agent Trajectory Interchange Format)."""
19
+
20
+ schema_version: Literal[
21
+ "ATIF-v1.0",
22
+ "ATIF-v1.1",
23
+ "ATIF-v1.2",
24
+ "ATIF-v1.3",
25
+ "ATIF-v1.4",
26
+ "ATIF-v1.5",
27
+ "ATIF-v1.6",
28
+ "ATIF-v1.7",
29
+ "ATIF-v1.8",
30
+ ] = Field(
31
+ default="ATIF-v1.8",
32
+ description="String defining ATIF compatibility",
33
+ )
34
+ session_id: str | None = Field(
35
+ default=None,
36
+ description=(
37
+ "Identifier for the agent run this trajectory belongs to. Scoped "
38
+ "to the run, NOT to an individual trajectory document: multiple "
39
+ "Trajectory objects MAY share the same `session_id` when they "
40
+ "represent the same logical run (e.g., a parent trajectory and "
41
+ "its embedded subagents, or a trajectory and its continuation "
42
+ "segments linked via `continued_trajectory_ref`). Therefore "
43
+ "`session_id`s within a parent's `subagent_trajectories` array "
44
+ "are NOT required to be unique. Use `trajectory_id` when a "
45
+ "per-trajectory-document unique identifier is required (e.g., "
46
+ "for `SubagentTrajectoryRef` resolution). Optional since "
47
+ "ATIF-v1.7; producers SHOULD set this on root trajectories for "
48
+ "run-level traceability, and MAY omit it on embedded subagents "
49
+ "that inherit the parent's run identity."
50
+ ),
51
+ )
52
+ trajectory_id: str | None = Field(
53
+ default=None,
54
+ description=(
55
+ "Canonical per-trajectory-document identifier, distinct from "
56
+ "`session_id`. Unlike `session_id` (which is run-scoped and MAY "
57
+ "be shared), `trajectory_id` uniquely identifies THIS trajectory "
58
+ "object. Used to resolve `SubagentTrajectoryRef` entries against "
59
+ "the root's `subagent_trajectories` array without overloading "
60
+ "`session_id`'s run-scoped semantics. Optional on standalone "
61
+ "trajectories, but REQUIRED on any trajectory embedded in a "
62
+ "parent's `subagent_trajectories` array. `trajectory_id`s within "
63
+ "a single parent's `subagent_trajectories` array MUST be unique. "
64
+ "Added in ATIF-v1.7."
65
+ ),
66
+ )
67
+ agent: Agent = Field(
68
+ default=...,
69
+ description="Object specifying the agent configuration",
70
+ )
71
+ steps: list[Step] = Field(
72
+ default=...,
73
+ min_length=1,
74
+ description="Array of step objects representing the complete interaction history",
75
+ )
76
+ notes: str | None = Field(
77
+ default=None,
78
+ description="Custom information, design notes, or explanations",
79
+ )
80
+ final_metrics: FinalMetrics | None = Field(
81
+ default=None,
82
+ description="Summary metrics for the entire trajectory",
83
+ )
84
+ continued_trajectory_ref: str | None = Field(
85
+ default=None,
86
+ description="Reference to the continuation trajectory file if this trajectory is continued in another file",
87
+ )
88
+ extra: dict[str, Any] | None = Field(
89
+ default=None,
90
+ description="Custom root-level metadata",
91
+ )
92
+ subagent_trajectories: list["Trajectory"] | None = Field(
93
+ default=None,
94
+ description=(
95
+ "Array of embedded subagent trajectories. Each element is a complete, "
96
+ "independently-valid ATIF Trajectory with its own schema_version, "
97
+ "agent, and step_id sequence starting at 1. Enables single-file "
98
+ "storage of multi-agent workflows: when a "
99
+ "SubagentTrajectoryRef.trajectory_path is null, consumers resolve "
100
+ "the reference by matching SubagentTrajectoryRef.trajectory_id "
101
+ "against Trajectory.trajectory_id of entries in this array. "
102
+ "Uniqueness rules: every embedded subagent MUST set `trajectory_id`, "
103
+ "and `trajectory_id`s within this array MUST be unique. "
104
+ "`session_id`, by contrast, is run-scoped and MAY collide across "
105
+ "siblings (or match the parent) when all trajectories belong to "
106
+ "the same logical run; embedded subagents MAY also omit "
107
+ "`session_id` entirely to inherit the parent's run identity. "
108
+ "Added in ATIF-v1.7."
109
+ ),
110
+ )
111
+
112
+ model_config = {"extra": "forbid"}
113
+
114
+ def to_json_dict(self, exclude_none: bool = True) -> dict[str, Any]:
115
+ """Export trajectory to a dictionary suitable for JSON serialization.
116
+
117
+ Args:
118
+ exclude_none: If True, exclude fields with None values from output.
119
+
120
+ Returns:
121
+ Dictionary representation of the trajectory.
122
+ """
123
+ return self.model_dump(exclude_none=exclude_none, mode="json")
124
+
125
+ @model_validator(mode="after")
126
+ def validate_step_ids(self) -> "Trajectory":
127
+ """Validate that step_ids are sequential starting from 1."""
128
+ for i, step in enumerate(self.steps):
129
+ expected_step_id = i + 1
130
+ if step.step_id != expected_step_id:
131
+ raise ValueError(
132
+ f"steps[{i}].step_id: expected {expected_step_id} "
133
+ f"(sequential from 1), got {step.step_id}"
134
+ )
135
+ return self
136
+
137
+ @model_validator(mode="after")
138
+ def validate_embedded_subagent_trajectory_ids(self) -> "Trajectory":
139
+ """Every embedded subagent must carry a unique, non-null `trajectory_id`.
140
+
141
+ Embedded subagents are resolved by matching
142
+ `SubagentTrajectoryRef.trajectory_id` against
143
+ `Trajectory.trajectory_id`, so the identifier must be present and
144
+ unique within a parent's `subagent_trajectories` array. Note that
145
+ no such constraint is placed on `session_id` — siblings MAY share
146
+ a `session_id` (or omit it entirely to inherit the parent's) when
147
+ they represent the same logical agent run.
148
+ """
149
+ if not self.subagent_trajectories:
150
+ return self
151
+ seen: set[str] = set()
152
+ for i, sub in enumerate(self.subagent_trajectories):
153
+ if sub.trajectory_id is None:
154
+ raise ValueError(
155
+ f"subagent_trajectories[{i}].trajectory_id is required "
156
+ f"for embedded subagents "
157
+ f"(agent.name={sub.agent.name!r}, "
158
+ f"session_id={sub.session_id!r})"
159
+ )
160
+ if sub.trajectory_id in seen:
161
+ raise ValueError(
162
+ f"subagent_trajectories[{i}].trajectory_id "
163
+ f"{sub.trajectory_id!r} is not unique within "
164
+ f"subagent_trajectories"
165
+ )
166
+ seen.add(sub.trajectory_id)
167
+ return self
168
+
169
+ @model_validator(mode="after")
170
+ def validate_tool_call_references(self) -> "Trajectory":
171
+ """Validate that observation source_call_ids reference valid tool_call_ids."""
172
+ for step in self.steps:
173
+ if step.observation is None:
174
+ continue
175
+
176
+ # Collect all tool_call_ids from this step
177
+ tool_call_ids = set()
178
+ if step.tool_calls:
179
+ tool_call_ids = {tc.tool_call_id for tc in step.tool_calls}
180
+
181
+ # Check that source_call_ids reference valid tool_call_ids
182
+ for result in step.observation.results:
183
+ if result.source_call_id is not None:
184
+ if result.source_call_id not in tool_call_ids:
185
+ raise ValueError(
186
+ f"Observation result references source_call_id "
187
+ f"'{result.source_call_id}' which is not found in "
188
+ f"step {step.step_id}'s tool_calls"
189
+ )
190
+ return self
191
+
192
+ def has_multimodal_content(self) -> bool:
193
+ """Check if any step contains multimodal content (images or audio).
194
+
195
+ Returns:
196
+ True if any step message or observation contains a media content
197
+ part. Audio counts since ATIF-v1.8.
198
+ """
199
+ media_types = {"image", "audio"}
200
+ for step in self.steps:
201
+ if isinstance(step.message, list):
202
+ for part in step.message:
203
+ if part.type in media_types:
204
+ return True
205
+ if step.observation:
206
+ for result in step.observation.results:
207
+ if isinstance(result.content, list):
208
+ for part in result.content:
209
+ if part.type in media_types:
210
+ return True
211
+ return False