Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions services/intake/src/nmp/intake/spans/ingest/atif_domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ class AtifStepBase(BaseModel):
is_copied_context: bool | None = None
extra: dict[str, Any] | None = None
llm_call_count: int | None = Field(default=None, ge=0)
observation: AtifObservation | None = None

model_config = ConfigDict(extra="forbid")

Expand Down Expand Up @@ -184,7 +185,6 @@ class AtifStepAgent(AtifStepBase):
reasoning_effort: str | float | None = None
reasoning_content: str | None = None
tool_calls: list[AtifToolCall] | None = None
observation: AtifObservation | None = None
metrics: AtifMetrics | None = None


Expand All @@ -209,10 +209,9 @@ def validate_atif_step_ids(steps: list[AtifStep]) -> None:
def validate_atif_tool_call_references(steps: list[AtifStep]) -> None:
"""Require unique calls and resolvable observation call references."""
for step in steps:
if not isinstance(step, AtifStepAgent):
continue
tool_call_ids: set[str] = set()
for tool_call in step.tool_calls or []:
tool_calls = step.tool_calls if isinstance(step, AtifStepAgent) else None
for tool_call in tool_calls or []:
if tool_call.tool_call_id in tool_call_ids:
raise ValueError(f"Duplicate tool_call_id '{tool_call.tool_call_id}' in step {step.step_id}")
tool_call_ids.add(tool_call.tool_call_id)
Expand All @@ -229,7 +228,7 @@ def validate_atif_tool_call_references(steps: list[AtifStep]) -> None:
def validate_atif_v17_subagent_ref_resolution_keys(steps: list[AtifStep]) -> None:
"""Require v1.7 subagent references to include a resolvable key."""
for step in steps:
if not isinstance(step, AtifStepAgent) or step.observation is None:
if step.observation is None:
continue
for result in step.observation.results:
for subagent_ref in result.subagent_trajectory_ref or []:
Expand Down
4 changes: 2 additions & 2 deletions services/intake/src/nmp/intake/spans/ingest/atif_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -844,8 +844,8 @@ def _step_tool_calls(step: AtifStep) -> list[AtifToolCall]:


def _step_observation(step: AtifStep) -> AtifObservation | None:
"""Return the observation attached to an agent step."""
return step.observation if isinstance(step, AtifStepAgent) else None
"""Return the observation attached to any step."""
return step.observation


def _observation_result_for_tool_call(step: AtifStep, tool_call_id: str) -> AtifObservationResult | None:
Expand Down
58 changes: 58 additions & 0 deletions services/intake/tests/test_atif_v17.py
Original file line number Diff line number Diff line change
Expand Up @@ -1040,3 +1040,61 @@ def test_atif_mapping_end_time_none_when_no_timing_exists_at_all() -> None:
for span in spans
if span.name in {"user-1", "bash"} or (span.name == "sample-agent" and span.kind == SpanKind.LLM)
)


def test_atif_v17_observation_on_nested_system_step() -> None:
"""Regression: DeepAgents emits an observation on a nested *system* step.

``observation`` previously lived only on ``AtifStepAgent``, so a system step
carrying one was rejected as an extra field and Intake returned HTTP 422,
storing nothing. It now lives on ``AtifStepBase``; every step type accepts it,
validators run on all step types, and the mapper reads observations anywhere.
"""
trajectory = AtifTrajectory.model_validate(
{
"schema_version": "ATIF-v1.7",
"session_id": "deepagents-session",
"trajectory_id": "root",
"agent": {"name": "orchestrator", "version": "1.0"},
"steps": [
{"step_id": 1, "source": "user", "message": "classify this email"},
{
"step_id": 2,
"source": "agent",
"message": "delegating to phishing-analyzer",
"tool_calls": [{"tool_call_id": "task-1", "function_name": "task"}],
"observation": {
"results": [{"source_call_id": "task-1", "subagent_trajectory_ref": [{"trajectory_id": "sub"}]}]
},
},
],
"subagent_trajectories": [
{
"schema_version": "ATIF-v1.7",
"trajectory_id": "sub",
"agent": {"name": "phishing-analyzer", "version": "1.0"},
"steps": [
{
"step_id": 1,
"source": "system",
"message": "You are a careful email phishing analyzer.",
"observation": {"results": [{"content": "is_likely_phishing: true"}]},
}
],
}
],
}
)

system_step = trajectory.subagent_trajectories[0].steps[0]
assert system_step.source == "system"
assert system_step.observation is not None
assert system_step.observation.results[0].content == "is_likely_phishing: true"

# The non-agent observation maps without error and produces spans.
spans = trajectory_to_spans(
workspace="default",
trajectory=trajectory,
ingested_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
)
assert spans
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
Loading