agent-apprenticeship 0.2.0 → 0.3.0

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 (83) hide show
  1. package/README.md +269 -3
  2. package/bin/agent-apprenticeship.js +1 -2
  3. package/package.json +4 -4
  4. package/pyproject.toml +10 -4
  5. package/schemas/environment_replay.schema.json +41 -0
  6. package/schemas/eval_package.schema.json +37 -0
  7. package/schemas/experience_compilation.schema.json +53 -0
  8. package/schemas/runtime_training.schema.json +33 -0
  9. package/schemas/source_evidence_map.schema.json +25 -0
  10. package/schemas/trace.schema.json +904 -0
  11. package/schemas/trace_step.schema.json +622 -0
  12. package/schemas/training_rows.schema.json +29 -0
  13. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/__init__.py +1 -1
  14. package/src/agent_apprenticeship/agent_env_exporter.py +53 -0
  15. package/src/agent_apprenticeship/agent_env_verify.py +32 -0
  16. package/src/agent_apprenticeship/atif.py +152 -0
  17. package/src/agent_apprenticeship/atif_models/LICENSE +201 -0
  18. package/src/agent_apprenticeship/atif_models/__init__.py +38 -0
  19. package/src/agent_apprenticeship/atif_models/agent.py +35 -0
  20. package/src/agent_apprenticeship/atif_models/content.py +159 -0
  21. package/src/agent_apprenticeship/atif_models/final_metrics.py +43 -0
  22. package/src/agent_apprenticeship/atif_models/metrics.py +47 -0
  23. package/src/agent_apprenticeship/atif_models/observation.py +19 -0
  24. package/src/agent_apprenticeship/atif_models/observation_result.py +45 -0
  25. package/src/agent_apprenticeship/atif_models/step.py +142 -0
  26. package/src/agent_apprenticeship/atif_models/subagent_trajectory_ref.py +88 -0
  27. package/src/agent_apprenticeship/atif_models/tool_call.py +34 -0
  28. package/src/agent_apprenticeship/atif_models/trajectory.py +196 -0
  29. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/batch_runner.py +1 -1
  30. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/bundle_exporter.py +8 -1
  31. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/certification.py +1 -1
  32. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/cli.py +26 -7
  33. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/codex_runner.py +17 -17
  34. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/config.py +2 -2
  35. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/ecosystem_r2.py +3 -1
  36. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/evaluator.py +8 -27
  37. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/grader.py +6 -5
  38. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/integration_status.py +1 -1
  39. package/src/agent_apprenticeship/io.py +104 -0
  40. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/learning.py +7 -1
  41. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/loop.py +5 -5
  42. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/loop_review.py +1 -1
  43. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/mentor_checkpoints.py +2 -2
  44. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/package_exporter.py +5 -3
  45. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/progress.py +1 -1
  46. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/public_run.py +1 -1
  47. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/public_sanitizer.py +2 -2
  48. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/release_exporter.py +7 -4
  49. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/rubric_generation.py +1 -1
  50. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/schemas.py +17 -4
  51. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/session_events.py +4 -4
  52. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/task_intake.py +1 -1
  53. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/tdo.py +5 -1
  54. package/src/agent_apprenticeship/trace_normalizer.py +103 -0
  55. package/src/agent_apprenticeship/trace_prompt.py +48 -0
  56. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/training_signals.py +1 -1
  57. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/validation.py +3 -3
  58. package/src/{agent_apprenticeship_trace → agent_apprenticeship}/verifier.py +6 -5
  59. package/docs/bioactive_evidence_mapping_schema.md +0 -113
  60. package/docs/dataset_schema.md +0 -160
  61. package/docs/local_usage.md +0 -191
  62. package/docs/scaling_to_1000_tasks.md +0 -3
  63. package/docs/trace_schema.md +0 -192
  64. package/docs/v0.2_release_notes.md +0 -61
  65. package/src/agent_apprenticeship_trace/io.py +0 -20
  66. package/src/agent_apprenticeship_trace/trace_normalizer.py +0 -1026
  67. package/src/agent_apprenticeship_trace/trace_prompt.py +0 -78
  68. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/actual_outputs_normalizer.py +0 -0
  69. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/apprentice_adapters.py +0 -0
  70. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/artifact_capture.py +0 -0
  71. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/artifact_previews.py +0 -0
  72. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/artifact_resolver.py +0 -0
  73. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/command_discovery.py +0 -0
  74. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/contract_diagnostics.py +0 -0
  75. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/contract_verifier.py +0 -0
  76. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/env.py +0 -0
  77. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/lesson_extractor.py +0 -0
  78. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/llm_output_normalizer.py +0 -0
  79. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/openai_structured.py +0 -0
  80. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/recipes.py +0 -0
  81. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/revision.py +0 -0
  82. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/role_runners.py +0 -0
  83. /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/version.py +0 -0
@@ -0,0 +1,43 @@
1
+ # Harbor ATIF models (Apache-2.0; see LICENSE).
2
+ # Source: harbor-framework/harbor@357c5e74020fc0890d69fb3a7db610896b9ce57d
3
+ # Modified: local imports; trajectory schema version pinned to v1.8.
4
+ """Final metrics model for ATIF trajectories."""
5
+
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class FinalMetrics(BaseModel):
12
+ """Aggregate statistics for the entire trajectory."""
13
+
14
+ total_prompt_tokens: int | None = Field(
15
+ default=None,
16
+ description="Sum of all prompt tokens across all steps, including cached tokens",
17
+ )
18
+ total_completion_tokens: int | None = Field(
19
+ default=None,
20
+ description="Sum of all completion tokens across all steps",
21
+ )
22
+ total_cached_tokens: int | None = Field(
23
+ default=None,
24
+ description="Sum of all cached tokens across all steps",
25
+ )
26
+ total_cost_usd: float | None = Field(
27
+ default=None,
28
+ description="Total real monetary cost for the entire trajectory, including cost for subagents, if any",
29
+ )
30
+ total_steps: int | None = Field(
31
+ default=None,
32
+ ge=0,
33
+ description=(
34
+ "Total number of steps. If not equivalent to the number of steps in the "
35
+ "trajectory, must be documented in the root-level notes field."
36
+ ),
37
+ )
38
+ extra: dict[str, Any] | None = Field(
39
+ default=None,
40
+ description="Custom aggregate metrics",
41
+ )
42
+
43
+ model_config = {"extra": "forbid"}
@@ -0,0 +1,47 @@
1
+ # Harbor ATIF models (Apache-2.0; see LICENSE).
2
+ # Source: harbor-framework/harbor@357c5e74020fc0890d69fb3a7db610896b9ce57d
3
+ # Modified: local imports; trajectory schema version pinned to v1.8.
4
+ """Metrics model for ATIF trajectories."""
5
+
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class Metrics(BaseModel):
12
+ """LLM operational and confidence data."""
13
+
14
+ prompt_tokens: int | None = Field(
15
+ default=None,
16
+ description="Total input tokens including cached and non-cached",
17
+ )
18
+ completion_tokens: int | None = Field(
19
+ default=None,
20
+ description="Total tokens generated by the LLM response",
21
+ )
22
+ cached_tokens: int | None = Field(
23
+ default=None,
24
+ description="Subset of prompt_tokens that were cache hits",
25
+ )
26
+ cost_usd: float | None = Field(
27
+ default=None,
28
+ description="Monetary cost of the API call in USD",
29
+ )
30
+ prompt_token_ids: list[int] | None = Field(
31
+ default=None,
32
+ description="Token IDs for prompt (input) tokens sent to the LLM, including chat history (if applicable)",
33
+ )
34
+ completion_token_ids: list[int] | None = Field(
35
+ default=None,
36
+ description="Token IDs for completion (response) tokens, enabling RL training without retokenization drift",
37
+ )
38
+ logprobs: list[float] | None = Field(
39
+ default=None,
40
+ description="Log probability assigned to each generated token",
41
+ )
42
+ extra: dict[str, Any] | None = Field(
43
+ default=None,
44
+ description="Other metrics",
45
+ )
46
+
47
+ model_config = {"extra": "forbid"}
@@ -0,0 +1,19 @@
1
+ # Harbor ATIF models (Apache-2.0; see LICENSE).
2
+ # Source: harbor-framework/harbor@357c5e74020fc0890d69fb3a7db610896b9ce57d
3
+ # Modified: local imports; trajectory schema version pinned to v1.8.
4
+ """Observation model for ATIF trajectories."""
5
+
6
+ from pydantic import BaseModel, Field
7
+
8
+ from agent_apprenticeship.atif_models.observation_result import ObservationResult
9
+
10
+
11
+ class Observation(BaseModel):
12
+ """Environment feedback/result after actions or system events."""
13
+
14
+ results: list[ObservationResult] = Field(
15
+ default=...,
16
+ description="Array of result objects from tool calls or actions",
17
+ )
18
+
19
+ model_config = {"extra": "forbid"}
@@ -0,0 +1,45 @@
1
+ # Harbor ATIF models (Apache-2.0; see LICENSE).
2
+ # Source: harbor-framework/harbor@357c5e74020fc0890d69fb3a7db610896b9ce57d
3
+ # Modified: local imports; trajectory schema version pinned to v1.8.
4
+ """Observation result model for ATIF trajectories."""
5
+
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+ from agent_apprenticeship.atif_models.content import ContentPart
11
+ from agent_apprenticeship.atif_models.subagent_trajectory_ref import SubagentTrajectoryRef
12
+
13
+
14
+ class ObservationResult(BaseModel):
15
+ """A single result within an observation."""
16
+
17
+ source_call_id: str | None = Field(
18
+ default=None,
19
+ description=(
20
+ "The `tool_call_id` from the _tool_calls_ array in _StepObject_ that this "
21
+ "result corresponds to. If null or omitted, the result comes from an "
22
+ "action that doesn't use the standard tool calling format (e.g., agent "
23
+ "actions without tool calls or system-initiated operations)."
24
+ ),
25
+ )
26
+ content: str | list[ContentPart] | None = Field(
27
+ default=None,
28
+ description=(
29
+ "The output or result from the tool execution. String for text-only "
30
+ "content, or array of ContentPart for multimodal content (added in ATIF-v1.6)."
31
+ ),
32
+ )
33
+ subagent_trajectory_ref: list[SubagentTrajectoryRef] | None = Field(
34
+ default=None,
35
+ description="Array of references to delegated subagent trajectories",
36
+ )
37
+ extra: dict[str, Any] | None = Field(
38
+ default=None,
39
+ description=(
40
+ "Custom observation-result-level metadata (e.g., confidence score, "
41
+ "retrieval score, source document ID). Added in ATIF-v1.7."
42
+ ),
43
+ )
44
+
45
+ model_config = {"extra": "forbid"}
@@ -0,0 +1,142 @@
1
+ # Harbor ATIF models (Apache-2.0; see LICENSE).
2
+ # Source: harbor-framework/harbor@357c5e74020fc0890d69fb3a7db610896b9ce57d
3
+ # Modified: local imports; trajectory schema version pinned to v1.8.
4
+ """Step model for ATIF trajectories."""
5
+
6
+ from datetime import datetime
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, Field, field_validator, model_validator
10
+
11
+ from agent_apprenticeship.atif_models.content import ContentPart
12
+ from agent_apprenticeship.atif_models.metrics import Metrics
13
+ from agent_apprenticeship.atif_models.observation import Observation
14
+ from agent_apprenticeship.atif_models.tool_call import ToolCall
15
+
16
+
17
+ class Step(BaseModel):
18
+ """A single step in the trajectory."""
19
+
20
+ step_id: int = Field(
21
+ default=...,
22
+ ge=1,
23
+ description="Ordinal index of the turn (starting from 1)",
24
+ )
25
+ timestamp: str | None = Field(
26
+ default=None,
27
+ description="ISO 8601 timestamp indicating when this step occurred",
28
+ )
29
+ source: Literal["system", "user", "agent"] = Field(
30
+ default=...,
31
+ description="The originator of this step",
32
+ )
33
+ model_name: str | None = Field(
34
+ default=None,
35
+ description=(
36
+ "The specific LLM model used for this turn. Omission implies the model "
37
+ "defined in the root-level agent config."
38
+ ),
39
+ )
40
+ reasoning_effort: str | float | None = Field(
41
+ default=None,
42
+ description="Qualitative or quantitative measure of effort",
43
+ )
44
+ message: str | list[ContentPart] = Field(
45
+ default=...,
46
+ description=(
47
+ "The dialogue message. String for text-only content, or array of "
48
+ "ContentPart for multimodal content (added in ATIF-v1.6)."
49
+ ),
50
+ )
51
+ reasoning_content: str | None = Field(
52
+ default=None,
53
+ description="The agent's explicit internal reasoning",
54
+ )
55
+ tool_calls: list[ToolCall] | None = Field(
56
+ default=None,
57
+ description="Array of structured objects for the agent's actions",
58
+ )
59
+ observation: Observation | None = Field(
60
+ default=None,
61
+ description="Environment feedback/result after actions or system events",
62
+ )
63
+ metrics: Metrics | None = Field(
64
+ default=None,
65
+ description="LLM operational and confidence data for this step",
66
+ )
67
+ is_copied_context: bool | None = Field(
68
+ default=None,
69
+ description=(
70
+ "Indicates whether this step was copied from a previous trajectory "
71
+ "for context (e.g., during continuation after summarization). "
72
+ "Steps marked as copied context should not be included in training data "
73
+ "as they represent previously-trained interactions. "
74
+ "Added in ATIF-v1.5."
75
+ ),
76
+ )
77
+ llm_call_count: int | None = Field(
78
+ default=None,
79
+ ge=0,
80
+ description=(
81
+ "Number of LLM inferences this step represents. When >1, metrics are "
82
+ "aggregated across multiple LLM calls. When 1, the step represents exactly "
83
+ "one inference. When 0 on a `source: 'agent'` step, the step represents a "
84
+ "deterministic (non-LLM) dispatch; `metrics` and `reasoning_content` MUST "
85
+ "be absent in that case. When null, the producer did not track this "
86
+ "(backward-compatible default). Added in ATIF-v1.7."
87
+ ),
88
+ )
89
+ extra: dict[str, Any] | None = Field(
90
+ default=None,
91
+ description="Custom step-level metadata",
92
+ )
93
+
94
+ model_config = {"extra": "forbid"}
95
+
96
+ @field_validator("timestamp")
97
+ @classmethod
98
+ def validate_timestamp(cls, v: str | None) -> str | None:
99
+ """Validate that timestamp is a valid ISO 8601 string."""
100
+ if v is not None:
101
+ try:
102
+ datetime.fromisoformat(v.replace("Z", "+00:00"))
103
+ except ValueError as e:
104
+ raise ValueError(f"Invalid ISO 8601 timestamp: {e}")
105
+ return v
106
+
107
+ @model_validator(mode="after")
108
+ def validate_agent_only_fields(self) -> "Step":
109
+ """Validate that certain fields are only present for agent steps."""
110
+ if self.source != "agent":
111
+ agent_only_fields = [
112
+ "model_name",
113
+ "reasoning_effort",
114
+ "reasoning_content",
115
+ "tool_calls",
116
+ "metrics",
117
+ ]
118
+ for field in agent_only_fields:
119
+ if getattr(self, field) is not None:
120
+ raise ValueError(
121
+ f"Field '{field}' is only applicable when source is 'agent', "
122
+ f"but source is '{self.source}'"
123
+ )
124
+ return self
125
+
126
+ @model_validator(mode="after")
127
+ def validate_llm_call_count_zero_fields(self) -> "Step":
128
+ """Enforce ATIF v1.7 no-LLM orchestration rule.
129
+
130
+ When ``llm_call_count == 0`` on a ``source: "agent"`` step, the step
131
+ represents a deterministic (non-LLM) dispatch. LLM-specific fields
132
+ (``metrics``, ``reasoning_content``) MUST be absent on such steps.
133
+ """
134
+ if self.llm_call_count == 0 and self.source == "agent":
135
+ llm_only_fields = ["metrics", "reasoning_content"]
136
+ for field in llm_only_fields:
137
+ if getattr(self, field) is not None:
138
+ raise ValueError(
139
+ f"Field '{field}' must be absent when llm_call_count is 0 "
140
+ f"(deterministic dispatch on a 'source: agent' step)"
141
+ )
142
+ return self
@@ -0,0 +1,88 @@
1
+ # Harbor ATIF models (Apache-2.0; see LICENSE).
2
+ # Source: harbor-framework/harbor@357c5e74020fc0890d69fb3a7db610896b9ce57d
3
+ # Modified: local imports; trajectory schema version pinned to v1.8.
4
+ """Subagent trajectory reference model for ATIF trajectories."""
5
+
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field, model_validator
9
+
10
+
11
+ class SubagentTrajectoryRef(BaseModel):
12
+ """Reference to a delegated subagent trajectory.
13
+
14
+ A subagent reference is resolved by one of two mechanisms:
15
+
16
+ 1. **Embedded form** — set `trajectory_id` to match the
17
+ `Trajectory.trajectory_id` of an entry in the parent's
18
+ `subagent_trajectories` array.
19
+ 2. **File-ref form** — set `trajectory_path` to the location
20
+ (file path, S3 URL, etc.) of an external trajectory file.
21
+
22
+ These two mechanisms are the only resolution keys. `session_id`, when
23
+ present on the ref, is **informational only**: it records the run
24
+ identity of the delegated subagent for debug / correlation / cross-
25
+ trajectory search purposes, and MUST NOT be used as a matching key
26
+ (it is run-scoped and MAY collide across siblings — see
27
+ `Trajectory.session_id`). A ref therefore MUST set at least one of
28
+ `trajectory_id` or `trajectory_path`; `session_id` alone is not a
29
+ resolvable reference.
30
+ """
31
+
32
+ trajectory_id: str | None = Field(
33
+ default=None,
34
+ description=(
35
+ "Canonical identifier of the delegated subagent trajectory. "
36
+ "Matches `Trajectory.trajectory_id` of an entry in the parent's "
37
+ "`subagent_trajectories` array and is the resolution key for "
38
+ "embedded references. Added in ATIF-v1.7 to provide a document-"
39
+ "unique matching key without overloading `session_id`."
40
+ ),
41
+ )
42
+ session_id: str | None = Field(
43
+ default=None,
44
+ description=(
45
+ "Run identity of the delegated subagent trajectory. Informational "
46
+ "only: recorded so consumers can correlate this ref back to the "
47
+ "subagent's run for debug / search / display purposes. Run-scoped "
48
+ "(see `Trajectory.session_id`) and therefore NOT a valid "
49
+ "resolution key — consumers MUST resolve via `trajectory_id` "
50
+ "(embedded) or `trajectory_path` (external file)."
51
+ ),
52
+ )
53
+ trajectory_path: str | None = Field(
54
+ default=None,
55
+ description=(
56
+ "Location of the complete subagent trajectory as an external "
57
+ "file (file path, S3 URL, database reference, etc.). Resolution "
58
+ "key for file-ref references. When both `trajectory_id` and "
59
+ "`trajectory_path` are set, consumers MAY choose either; "
60
+ "typically `trajectory_id` is preferred when the embedded "
61
+ "trajectory is available in-memory."
62
+ ),
63
+ )
64
+ extra: dict[str, Any] | None = Field(
65
+ default=None,
66
+ description="Custom metadata about the subagent execution",
67
+ )
68
+
69
+ model_config = {"extra": "forbid"}
70
+
71
+ @model_validator(mode="after")
72
+ def validate_is_resolvable(self) -> "SubagentTrajectoryRef":
73
+ """A ref must be resolvable: set `trajectory_id` or `trajectory_path`.
74
+
75
+ `session_id` alone is not sufficient because it is run-scoped and
76
+ MAY collide across siblings (see `Trajectory.session_id`), so it
77
+ cannot unambiguously identify which subagent trajectory a ref
78
+ points at.
79
+ """
80
+ if self.trajectory_id is None and self.trajectory_path is None:
81
+ raise ValueError(
82
+ "SubagentTrajectoryRef must be resolvable: set either "
83
+ "`trajectory_id` (for embedded references) or "
84
+ "`trajectory_path` (for external-file references). "
85
+ "`session_id` alone is not a resolution key — it is "
86
+ "run-scoped and may collide across siblings."
87
+ )
88
+ return self
@@ -0,0 +1,34 @@
1
+ # Harbor ATIF models (Apache-2.0; see LICENSE).
2
+ # Source: harbor-framework/harbor@357c5e74020fc0890d69fb3a7db610896b9ce57d
3
+ # Modified: local imports; trajectory schema version pinned to v1.8.
4
+ """Tool call model for ATIF trajectories."""
5
+
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class ToolCall(BaseModel):
12
+ """A tool call within a step."""
13
+
14
+ tool_call_id: str = Field(
15
+ default=...,
16
+ description="Unique identifier for this specific tool call",
17
+ )
18
+ function_name: str = Field(
19
+ default=...,
20
+ description="The name of the function or tool being invoked",
21
+ )
22
+ arguments: dict[str, Any] = Field(
23
+ default=...,
24
+ description="Arguments passed to the function (can be empty dict)",
25
+ )
26
+ extra: dict[str, Any] | None = Field(
27
+ default=None,
28
+ description=(
29
+ "Custom tool-call-level metadata (e.g., timeout, retry count, tool version). "
30
+ "Added in ATIF-v1.7."
31
+ ),
32
+ )
33
+
34
+ model_config = {"extra": "forbid"}
@@ -0,0 +1,196 @@
1
+ # Harbor ATIF models (Apache-2.0; see LICENSE).
2
+ # Source: harbor-framework/harbor@357c5e74020fc0890d69fb3a7db610896b9ce57d
3
+ # Modified: local imports; trajectory schema version pinned to v1.8.
4
+ """Trajectory model for ATIF (Agent Trajectory Interchange Format)."""
5
+
6
+ from typing import Any, Literal
7
+
8
+ from pydantic import BaseModel, Field, model_validator
9
+
10
+ from agent_apprenticeship.atif_models.agent import Agent
11
+ from agent_apprenticeship.atif_models.final_metrics import FinalMetrics
12
+ from agent_apprenticeship.atif_models.step import Step
13
+
14
+
15
+ class Trajectory(BaseModel):
16
+ """Agent Trajectory in ATIF (Agent Trajectory Interchange Format)."""
17
+
18
+ schema_version: Literal["ATIF-v1.8"] = "ATIF-v1.8"
19
+ session_id: str | None = Field(
20
+ default=None,
21
+ description=(
22
+ "Identifier for the agent run this trajectory belongs to. Scoped "
23
+ "to the run, NOT to an individual trajectory document: multiple "
24
+ "Trajectory objects MAY share the same `session_id` when they "
25
+ "represent the same logical run (e.g., a parent trajectory and "
26
+ "its embedded subagents, or a trajectory and its continuation "
27
+ "segments linked via `continued_trajectory_ref`). Therefore "
28
+ "`session_id`s within a parent's `subagent_trajectories` array "
29
+ "are NOT required to be unique. Use `trajectory_id` when a "
30
+ "per-trajectory-document unique identifier is required (e.g., "
31
+ "for `SubagentTrajectoryRef` resolution). Optional since "
32
+ "ATIF-v1.7; producers SHOULD set this on root trajectories for "
33
+ "run-level traceability, and MAY omit it on embedded subagents "
34
+ "that inherit the parent's run identity."
35
+ ),
36
+ )
37
+ trajectory_id: str | None = Field(
38
+ default=None,
39
+ description=(
40
+ "Canonical per-trajectory-document identifier, distinct from "
41
+ "`session_id`. Unlike `session_id` (which is run-scoped and MAY "
42
+ "be shared), `trajectory_id` uniquely identifies THIS trajectory "
43
+ "object. Used to resolve `SubagentTrajectoryRef` entries against "
44
+ "the root's `subagent_trajectories` array without overloading "
45
+ "`session_id`'s run-scoped semantics. Optional on standalone "
46
+ "trajectories, but REQUIRED on any trajectory embedded in a "
47
+ "parent's `subagent_trajectories` array. `trajectory_id`s within "
48
+ "a single parent's `subagent_trajectories` array MUST be unique. "
49
+ "Added in ATIF-v1.7."
50
+ ),
51
+ )
52
+ agent: Agent = Field(
53
+ default=...,
54
+ description="Object specifying the agent configuration",
55
+ )
56
+ steps: list[Step] = Field(
57
+ default=...,
58
+ min_length=1,
59
+ description="Array of step objects representing the complete interaction history",
60
+ )
61
+ notes: str | None = Field(
62
+ default=None,
63
+ description="Custom information, design notes, or explanations",
64
+ )
65
+ final_metrics: FinalMetrics | None = Field(
66
+ default=None,
67
+ description="Summary metrics for the entire trajectory",
68
+ )
69
+ continued_trajectory_ref: str | None = Field(
70
+ default=None,
71
+ description="Reference to the continuation trajectory file if this trajectory is continued in another file",
72
+ )
73
+ extra: dict[str, Any] | None = Field(
74
+ default=None,
75
+ description="Custom root-level metadata",
76
+ )
77
+ subagent_trajectories: list["Trajectory"] | None = Field(
78
+ default=None,
79
+ description=(
80
+ "Array of embedded subagent trajectories. Each element is a complete, "
81
+ "independently-valid ATIF Trajectory with its own schema_version, "
82
+ "agent, and step_id sequence starting at 1. Enables single-file "
83
+ "storage of multi-agent workflows: when a "
84
+ "SubagentTrajectoryRef.trajectory_path is null, consumers resolve "
85
+ "the reference by matching SubagentTrajectoryRef.trajectory_id "
86
+ "against Trajectory.trajectory_id of entries in this array. "
87
+ "Uniqueness rules: every embedded subagent MUST set `trajectory_id`, "
88
+ "and `trajectory_id`s within this array MUST be unique. "
89
+ "`session_id`, by contrast, is run-scoped and MAY collide across "
90
+ "siblings (or match the parent) when all trajectories belong to "
91
+ "the same logical run; embedded subagents MAY also omit "
92
+ "`session_id` entirely to inherit the parent's run identity. "
93
+ "Added in ATIF-v1.7."
94
+ ),
95
+ )
96
+
97
+ model_config = {"extra": "forbid"}
98
+
99
+ def to_json_dict(self, exclude_none: bool = True) -> dict[str, Any]:
100
+ """Export trajectory to a dictionary suitable for JSON serialization.
101
+
102
+ Args:
103
+ exclude_none: If True, exclude fields with None values from output.
104
+
105
+ Returns:
106
+ Dictionary representation of the trajectory.
107
+ """
108
+ return self.model_dump(exclude_none=exclude_none, mode="json")
109
+
110
+ @model_validator(mode="after")
111
+ def validate_step_ids(self) -> "Trajectory":
112
+ """Validate that step_ids are sequential starting from 1."""
113
+ for i, step in enumerate(self.steps):
114
+ expected_step_id = i + 1
115
+ if step.step_id != expected_step_id:
116
+ raise ValueError(
117
+ f"steps[{i}].step_id: expected {expected_step_id} "
118
+ f"(sequential from 1), got {step.step_id}"
119
+ )
120
+ return self
121
+
122
+ @model_validator(mode="after")
123
+ def validate_embedded_subagent_trajectory_ids(self) -> "Trajectory":
124
+ """Every embedded subagent must carry a unique, non-null `trajectory_id`.
125
+
126
+ Embedded subagents are resolved by matching
127
+ `SubagentTrajectoryRef.trajectory_id` against
128
+ `Trajectory.trajectory_id`, so the identifier must be present and
129
+ unique within a parent's `subagent_trajectories` array. Note that
130
+ no such constraint is placed on `session_id` — siblings MAY share
131
+ a `session_id` (or omit it entirely to inherit the parent's) when
132
+ they represent the same logical agent run.
133
+ """
134
+ if not self.subagent_trajectories:
135
+ return self
136
+ seen: set[str] = set()
137
+ for i, sub in enumerate(self.subagent_trajectories):
138
+ if sub.trajectory_id is None:
139
+ raise ValueError(
140
+ f"subagent_trajectories[{i}].trajectory_id is required "
141
+ f"for embedded subagents "
142
+ f"(agent.name={sub.agent.name!r}, "
143
+ f"session_id={sub.session_id!r})"
144
+ )
145
+ if sub.trajectory_id in seen:
146
+ raise ValueError(
147
+ f"subagent_trajectories[{i}].trajectory_id "
148
+ f"{sub.trajectory_id!r} is not unique within "
149
+ f"subagent_trajectories"
150
+ )
151
+ seen.add(sub.trajectory_id)
152
+ return self
153
+
154
+ @model_validator(mode="after")
155
+ def validate_tool_call_references(self) -> "Trajectory":
156
+ """Validate that observation source_call_ids reference valid tool_call_ids."""
157
+ for step in self.steps:
158
+ if step.observation is None:
159
+ continue
160
+
161
+ # Collect all tool_call_ids from this step
162
+ tool_call_ids = set()
163
+ if step.tool_calls:
164
+ tool_call_ids = {tc.tool_call_id for tc in step.tool_calls}
165
+
166
+ # Check that source_call_ids reference valid tool_call_ids
167
+ for result in step.observation.results:
168
+ if result.source_call_id is not None:
169
+ if result.source_call_id not in tool_call_ids:
170
+ raise ValueError(
171
+ f"Observation result references source_call_id "
172
+ f"'{result.source_call_id}' which is not found in "
173
+ f"step {step.step_id}'s tool_calls"
174
+ )
175
+ return self
176
+
177
+ def has_multimodal_content(self) -> bool:
178
+ """Check if any step contains multimodal content (images or audio).
179
+
180
+ Returns:
181
+ True if any step message or observation contains a media content
182
+ part. Audio counts since ATIF-v1.8.
183
+ """
184
+ media_types = {"image", "audio"}
185
+ for step in self.steps:
186
+ if isinstance(step.message, list):
187
+ for part in step.message:
188
+ if part.type in media_types:
189
+ return True
190
+ if step.observation:
191
+ for result in step.observation.results:
192
+ if isinstance(result.content, list):
193
+ for part in result.content:
194
+ if part.type in media_types:
195
+ return True
196
+ return False
@@ -3,7 +3,7 @@ import time, traceback, json
3
3
  from datetime import datetime, timezone
4
4
  from pathlib import Path
5
5
  from .schemas import RawTaskRecord
6
- from .io import read_jsonl, append_jsonl, write_json
6
+ from .io import read_workflow_jsonl as read_jsonl, append_jsonl, write_json
7
7
  from .loop import run_task
8
8
  from .release_exporter import create_release
9
9
  from .public_sanitizer import classify_provider_failure
@@ -1,4 +1,6 @@
1
1
  from __future__ import annotations
2
+ from .io import validate_trace_tree
3
+ from .agent_env_exporter import export_agent_env
2
4
 
3
5
  import shutil
4
6
  import tempfile
@@ -8,7 +10,7 @@ from pathlib import Path
8
10
  from typing import Any
9
11
 
10
12
  from .config import Settings, apprenticeship_mode_display, get_settings, normalize_apprenticeship_mode, normalize_mentor_mode
11
- from .io import append_jsonl, read_json, read_jsonl, write_json
13
+ from .io import append_jsonl, read_workflow_json as read_json, read_workflow_jsonl as read_jsonl, write_json
12
14
  from .public_sanitizer import sanitize_public_obj, sanitize_public_text
13
15
  from .release_exporter import create_release
14
16
  from .tdo import tdo_manifest_fields_from_report
@@ -461,11 +463,14 @@ def create_contribution_bundle(
461
463
  shutil.rmtree(bundle_root)
462
464
  bundle_root.mkdir(parents=True, exist_ok=True)
463
465
 
466
+ validate_trace_tree(run_root)
464
467
  if release_style:
465
468
  create_release(run_root, bundle_root)
466
469
  _copy_session_files(run_root, bundle_root, settings)
467
470
  _write_manifest(bundle_root, bundle_root, settings, include_internal_schema=True)
468
471
  _write_card(bundle_root)
472
+ validate_trace_tree(bundle_root)
473
+ export_agent_env(bundle_root)
469
474
  return bundle_root
470
475
 
471
476
  with tempfile.TemporaryDirectory(prefix="aa-release-for-bundle-") as tmp:
@@ -476,4 +481,6 @@ def create_contribution_bundle(
476
481
  _write_manifest(bundle_root, release_root, settings, include_internal_schema=include_debug)
477
482
  _write_card(bundle_root)
478
483
 
484
+ validate_trace_tree(bundle_root)
485
+ export_agent_env(bundle_root)
479
486
  return bundle_root
@@ -13,7 +13,7 @@ from .config import get_settings, init_settings, update_settings
13
13
  from .command_discovery import resolve_command, resolve_agent_command, gui_app_hint
14
14
  from .env import redact_secrets
15
15
  from .integration_status import record_certification_result
16
- from .io import read_json
16
+ from .io import read_workflow_json as read_json
17
17
  from .openai_structured import run_llm_smoke
18
18
  from .public_run import run_prompt_task
19
19
  from .recipes import MODEL_PROVIDER_RECIPES, WORKER_AGENT_RECIPES