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
@@ -1,1026 +0,0 @@
1
- from __future__ import annotations
2
- import json, re
3
- from pathlib import Path
4
- from typing import Any
5
- from pydantic import BaseModel, Field
6
- from .schemas import AgentTrace, AgentTraceStep, ActualOutputs
7
- from .io import write_json
8
- from .artifact_resolver import normalize_artifact_ref
9
-
10
- CANONICAL_TOP = {'schema_version','trace_id','collection_id','run_id','package_id','bundle_id','prior_trace_id','trace_mode','task','task_id','task_family_id','attempt_id','attempt_kind','attempt_status','agent_tools','started_at','ended_at','system_prompt','system_prompt_hash','skills','memory','agent_config','environment_domain','actual_outputs_ref','input_artifact_refs','artifact_refs','deliverable_refs','output_summary','final_output_summary','learning','termination_reason','steps','actual_outputs','artifacts','metadata_json'}
11
- CANONICAL_STEP = set(AgentTraceStep.model_fields)
12
- VALID_OUTCOMES={'progress','neutral','blocked','failed','corrected','completed'}
13
- VALID_ACTIONS={'user_message','agent_step','output','error'}
14
- VALID_OPS={'plan','analyze','search','read','write','edit','execute','verify','download','install','ask_user','answer','select','grade','evaluate','revise','other'}
15
- VALID_CAUSAL_TYPES={'user_request','follow_up_user_request','answer_to_agent_question','execution_of_plan','dependency_on_tool_result','retry_after_failure','correction_response','approval_response','verification_of_prior_step','dependency_on_multiple_prior_steps','delegation_to_subagent','delegated_work','used_subagent_result','handoff_from_subagent','parallel_work','other'}
16
- CAUSAL_MAP={'sequential':'execution_of_plan','sequence':'execution_of_plan','previous_step':'execution_of_plan'}
17
- OP_MAP={
18
- 'inspect_workspace_files':'read','inspect_workspace':'read','read_policy':'read','read_input_csvs':'read','create_artifacts_directory':'execute','create_reconciliation_script':'write','run_initial_reconciliation':'execute','inspect_initial_outputs':'read','generate_reconciliation_artifacts':'execute','inspect_reconciled_payments':'read','validate_json_files':'verify','validate_artifacts':'verify','run':'execute','shell':'execute','bash':'execute','create_file':'write','write_file':'write','modify_file':'edit','read_file':'read','inspect':'read','think':'analyze','reason':'analyze','summarize':'analyze','finalize':'answer','final_response':'answer'}
19
- READ_OPS={'read','search'}; WRITE_OPS={'write','edit'}; EXEC_OPS={'execute','install','download'}; VERIFY_OPS={'verify'}
20
- VALID_ATTEMPT_KINDS={'baseline','revised','apprentice_without_lessons','apprentice_with_lessons','other'}
21
- VALID_TRACE_MODES={'live','retraced','hybrid'}
22
- VALID_TERMINATION_REASONS={'task_complete','verifier_passed','verifier_failed','max_iterations_reached','agent_blocked','timeout','error_unrecoverable','partial_then_stopped','provider_usage_limit','other'}
23
- VALID_ATTEMPT_STATUSES={'completed','failed','blocked','fallback','partial'}
24
- ENVIRONMENT_DOMAINS={'terminal','browser','computer_use','web','search','codebase','software','os','android','mcp','api','database','spreadsheet','file','multimodal','device','robotics','lab','industrial','physical','mixed','unknown'}
25
- INTERACTION_SURFACES={'api','mcp_tool','mcp_resource','mcp_prompt','browser','computer_use','cli','file','codebase','database','spreadsheet','search','cloud_console','saas_app','async_job','queue','webhook','scheduler','messaging','email','collaboration','document','pdf','table','multimodal','device','industrial','physical','mixed','other_tool','other_surface','unknown'}
26
- BASE_STEP_FIELDS={'step','turn','actor','action','operation','tool','environment_domain','execution_mode','parallel_group','observation','input','input_source','output','state_change','reasoning','caused_by','causal_type','causal_note','alternatives_considered','success','step_outcome','error_type','error_message','message_role','feedback_type','feedback_content','started_at','ended_at','retry_of','artifact_refs','metadata_json'}
27
- SURFACE_STEP_FIELDS=CANONICAL_STEP-BASE_STEP_FIELDS
28
- LIST_SURFACE_FIELDS={name for name in SURFACE_STEP_FIELDS if name.endswith('_refs') or name in {'modalities','connected_surfaces','depends_on_agent_outputs','formula_refs','pivot_refs','chart_refs','data_validation_refs','file_paths','selected_result_refs','citation_refs','attachment_refs','decision_refs','page_refs','section_refs','table_extract_refs','figure_refs','annotation_refs','safety_limit_refs','compliance_refs','cross_system_dependencies','handoff_refs','state_sync_refs','consistency_checks','evidence_refs','verification_refs','redaction_notes','missing_surface_fields'}}
29
- INT_SURFACE_FIELDS={'http_status','retry_count','row_count','affected_row_count','column_count','result_count','timeout_seconds'}
30
- FLOAT_SURFACE_FIELDS={'latency_ms'}
31
- BOOL_SURFACE_FIELDS={'operator_override','human_supervisor_present','emergency_stop','followup_required'}
32
-
33
- class TraceNormalizationContext(BaseModel):
34
- task_id: str
35
- attempt_id: str
36
- attempt_kind: str
37
- task: str
38
- actual_outputs: ActualOutputs | None = None
39
- trace_id_prefix: str = 'trace'
40
-
41
- class TraceNormalizationReport(BaseModel):
42
- task_id: str
43
- attempt_id: str
44
- attempt_kind: str
45
- raw_trace_ref: str | None = None
46
- normalized_trace_ref: str | None = None
47
- canonical_trace_ref: str | None = None
48
- trace_schema_valid: bool = False
49
- trace_normalized: bool = False
50
- trace_lossless: bool = True
51
- fallback_trace: bool = False
52
- raw_step_count: int = 0
53
- normalized_step_count: int = 0
54
- discarded_step_count: int = 0
55
- raw_trace_parse_error: bool = False
56
- trace_normalization_error: bool = False
57
- trace_normalization_partial: bool = False
58
- validation_errors: list[str] = Field(default_factory=list)
59
- warnings: list[str] = Field(default_factory=list)
60
- field_mappings: dict[str,str] = Field(default_factory=dict)
61
- input_dict_to_string_count: int = 0
62
- output_dict_to_string_count: int = 0
63
- causal_type_repair_count: int = 0
64
- operation_repair_count: int = 0
65
- action_repair_count: int = 0
66
- metadata_json: dict[str, Any] = Field(default_factory=dict)
67
-
68
- class NormalizationResult(BaseModel):
69
- normalized_trace: dict[str, Any] | None = None
70
- report: TraceNormalizationReport
71
- fallback_required: bool = False
72
- parse_error: str | None = None
73
-
74
- def _compact(v: Any, max_len: int=1200) -> str:
75
- if v is None: return ''
76
- if isinstance(v, str): return v[:max_len]
77
- try: s=json.dumps(v, sort_keys=True)
78
- except Exception: s=str(v)
79
- return s[:max_len]
80
-
81
- def _as_list(v: Any) -> list[Any]:
82
- if v is None: return []
83
- if isinstance(v, list): return v
84
- return [v]
85
-
86
- def _normalize_ref(ref: Any) -> str:
87
- value = normalize_artifact_ref(ref).replace("\\", "/").strip().lstrip("/")
88
- while value.startswith("./"):
89
- value = value[2:]
90
- return value
91
-
92
- def _normalize_ref_list(value: Any) -> list[str]:
93
- refs: list[str] = []
94
- for item in _as_list(value):
95
- ref = _normalize_ref(item)
96
- if ref and ref not in refs:
97
- refs.append(ref)
98
- return refs
99
-
100
- def _actual_ref(attempt_kind: str) -> str:
101
- return f"attempts/{attempt_kind}/actual_outputs.json"
102
-
103
- def _input_source(value: Any, meta: dict[str, Any]) -> dict[str, Any] | None:
104
- if isinstance(value, dict):
105
- return value
106
- if isinstance(value, str) and value.strip():
107
- return {"source": value.strip()}
108
- if value is not None:
109
- meta["original_input_source"] = value
110
- return None
111
-
112
- def _actor_to_string(value: Any, action: str, meta: dict[str, Any]) -> str:
113
- if action == "user_message":
114
- return "user"
115
- if isinstance(value, str) and value.strip():
116
- return value.strip()
117
- if isinstance(value, dict):
118
- meta["original_actor"] = value
119
- actor_kind = str(value.get("actor_kind") or value.get("kind") or "").strip()
120
- role = str(value.get("role") or value.get("actor_role") or value.get("name") or "").strip()
121
- agent_id = str(value.get("agent_id") or value.get("id") or "").strip()
122
- if actor_kind and role:
123
- return f"{actor_kind}:{role}"
124
- if actor_kind and agent_id:
125
- return f"{actor_kind}:{agent_id}"
126
- if actor_kind:
127
- return actor_kind
128
- if role:
129
- return role
130
- if agent_id:
131
- return f"agent:{agent_id}"
132
- elif value is not None:
133
- meta["original_actor"] = value
134
- return "agent:worker"
135
-
136
- def _infer_environment_domain(tool: Any, operation: Any = None, input_value: Any = None, output_value: Any = None) -> str:
137
- text = " ".join(str(x or "") for x in [tool, operation, input_value, output_value]).lower()
138
- if any(x in text for x in ["mcp__", "mcp ", "model context protocol"]):
139
- return "mcp"
140
- if any(x in text for x in ["robot", "robotics", "actuator", "telemetry", "sensor", "calibration", "emergency stop"]):
141
- return "robotics" if "robot" in text else "physical"
142
- if any(x in text for x in ["industrial", "manufacturing", "plc", "scada", "field device", "lab instrument"]):
143
- return "industrial"
144
- if any(x in text for x in ["browser", "playwright", "screenshot", "dom", "page.goto"]):
145
- return "browser"
146
- if any(x in text for x in ["computer_use", "desktop app", "ui automation", "accessibility tree"]):
147
- return "computer_use"
148
- if any(x in text for x in ["http://", "https://", "fetch", "curl ", "web "]):
149
- return "web"
150
- if any(x in text for x in ["search", "web_search"]):
151
- return "search"
152
- if any(x in text for x in ["sqlite", "postgres", "mysql", "sql ", "database", "db "]):
153
- return "database"
154
- if any(x in text for x in ["xlsx", "xls", "spreadsheet", "workbook", "csv"]):
155
- return "spreadsheet"
156
- if any(x in text for x in ["pdf", "document", "ocr", "transcript", "image", "video", "audio", "screenshot"]):
157
- return "multimodal"
158
- if any(x in text for x in ["android", "adb "]):
159
- return "android"
160
- if any(x in text for x in ["api", "json endpoint", "graphql", "rest "]):
161
- return "api"
162
- if any(x in text for x in ["apply_patch", "pytest", "compileall", "git ", "npm ", "node ", "python", "rg ", "grep ", "codebase"]):
163
- return "codebase"
164
- if any(x in text for x in ["bash", "zsh", "shell", "terminal", "subprocess", "command"]):
165
- return "terminal"
166
- if any(x in text for x in ["file_read", "file_write", "read_file", "write_file", "input/", "artifacts/"]):
167
- return "file"
168
- if any(x in text for x in ["os.", "filesystem", "mkdir", "cp ", "mv ", "rm "]):
169
- return "os"
170
- return "unknown"
171
-
172
-
173
- def _infer_interaction_surface(raw: dict[str, Any], tool: Any, operation: Any, input_value: Any, output_value: Any) -> str:
174
- explicit = raw.get("interaction_surface")
175
- if explicit in INTERACTION_SURFACES:
176
- return explicit
177
- text = " ".join(str(x or "") for x in [tool, operation, input_value, output_value, raw.get("surface_type"), raw.get("app_name")]).lower()
178
- if any(x in text for x in ["mcp__", "mcp tool", "mcp_tool"]):
179
- return "mcp_tool"
180
- if any(x in text for x in ["mcp resource", "mcp_resource"]):
181
- return "mcp_resource"
182
- if any(x in text for x in ["mcp prompt", "mcp_prompt"]):
183
- return "mcp_prompt"
184
- if any(x in text for x in ["curl ", "http ", "https://", "graphql", "rest api", "sdk", "api "]):
185
- return "api"
186
- if any(x in text for x in ["sql", "postgres", "mysql", "sqlite", "database", "db "]):
187
- return "database"
188
- if any(x in text for x in ["xlsx", "workbook", "spreadsheet", "sheet_name"]):
189
- return "spreadsheet"
190
- if any(x in text for x in ["csv", "table", "dataframe"]):
191
- return "table"
192
- if any(x in text for x in ["browser", "playwright", "dom", "page.", "url"]):
193
- return "browser"
194
- if any(x in text for x in ["desktop app", "computer_use", "accessibility tree", "screenshot"]):
195
- return "computer_use"
196
- if any(x in text for x in ["web_search", "search", "query"]):
197
- return "search"
198
- if any(x in text for x in ["cloud console", "aws console", "gcp console", "azure portal"]):
199
- return "cloud_console"
200
- if any(x in text for x in ["saas", "crm", "ticket", "tenant"]):
201
- return "saas_app"
202
- if any(x in text for x in ["webhook", "callback"]):
203
- return "webhook"
204
- if any(x in text for x in ["queue", "message_id", "dead letter"]):
205
- return "queue"
206
- if any(x in text for x in ["cron", "scheduler", "schedule"]):
207
- return "scheduler"
208
- if any(x in text for x in ["async job", "job_id", "polling"]):
209
- return "async_job"
210
- if any(x in text for x in ["email", "smtp", "inbox"]):
211
- return "email"
212
- if any(x in text for x in ["slack", "teams", "discord", "message", "thread"]):
213
- return "messaging"
214
- if any(x in text for x in ["comment", "collaboration", "shared doc"]):
215
- return "collaboration"
216
- if "pdf" in text:
217
- return "pdf"
218
- if any(x in text for x in ["document", "docx", "ocr", "report"]):
219
- return "document"
220
- if any(x in text for x in ["image", "video", "audio", "multimodal"]):
221
- return "multimodal"
222
- if any(x in text for x in ["robot", "industrial", "sensor", "actuator", "telemetry", "device"]):
223
- return "industrial" if "industrial" in text else "device"
224
- if any(x in text for x in ["apply_patch", "git ", "pytest", "npm ", "python", "node ", "rg ", "bash", "shell", "command"]):
225
- return "cli"
226
- if any(x in text for x in ["file_read", "file_write", "read_file", "write_file", "artifact", "input/"]):
227
- return "file"
228
- if any(x in text for x in ["codebase", "test", "build", "lint", "compile"]):
229
- return "codebase"
230
- return "unknown"
231
-
232
-
233
- def _surface_required_fields(surface: str) -> list[str]:
234
- return {
235
- "api": ["api_endpoint", "api_method", "http_status"],
236
- "mcp_tool": ["mcp_server", "mcp_tool_name", "mcp_result_ref"],
237
- "mcp_resource": ["mcp_server", "mcp_resource_uri", "mcp_result_ref"],
238
- "mcp_prompt": ["mcp_server", "mcp_prompt_name", "mcp_result_ref"],
239
- "database": ["database_type", "query_ref", "table_name"],
240
- "spreadsheet": ["workbook_ref", "sheet_name", "cell_range"],
241
- "table": ["table_ref", "row_count", "column_count"],
242
- "cli": ["command", "exit_code", "stdout_ref"],
243
- "file": ["file_path", "file_operation"],
244
- "codebase": ["command", "test_result_ref", "diff_ref"],
245
- "browser": ["visible_state_ref", "screenshot_ref", "element_selector", "action_type"],
246
- "computer_use": ["visible_state_ref", "screenshot_ref", "action_type"],
247
- "async_job": ["job_id", "job_status_after", "async_result_ref"],
248
- "queue": ["queue_name", "message_id", "delivery_status"],
249
- "webhook": ["webhook_id", "callback_status", "async_result_ref"],
250
- "scheduler": ["schedule_id", "trigger_type", "job_status_after"],
251
- "device": ["device_type", "telemetry_ref", "safety_state"],
252
- "industrial": ["physical_environment_type", "telemetry_ref", "safety_state"],
253
- "physical": ["physical_environment_type", "physical_state_after_ref", "safety_state"],
254
- }.get(surface, [])
255
-
256
-
257
- def _normalize_surface_value(field: str, value: Any, meta: dict[str, Any]) -> Any:
258
- if field in LIST_SURFACE_FIELDS:
259
- if field.endswith("_refs") or field in {"file_paths", "selected_result_refs", "citation_refs", "attachment_refs", "decision_refs", "page_refs", "section_refs", "table_extract_refs", "figure_refs", "annotation_refs", "safety_limit_refs", "compliance_refs", "handoff_refs", "state_sync_refs", "evidence_refs", "verification_refs"}:
260
- return _normalize_ref_list(value)
261
- return [str(v) for v in _as_list(value) if v not in (None, "")]
262
- if field.endswith("_ref"):
263
- return _normalize_ref(value)
264
- if field in INT_SURFACE_FIELDS:
265
- try:
266
- return int(value)
267
- except (TypeError, ValueError):
268
- meta.setdefault("surface_field_parse_warnings", []).append(f"{field}_not_integer")
269
- return None
270
- if field in FLOAT_SURFACE_FIELDS:
271
- try:
272
- return float(value)
273
- except (TypeError, ValueError):
274
- meta.setdefault("surface_field_parse_warnings", []).append(f"{field}_not_float")
275
- return None
276
- if field in BOOL_SURFACE_FIELDS:
277
- if isinstance(value, bool):
278
- return value
279
- if isinstance(value, str) and value.lower() in {"true", "false", "yes", "no", "1", "0"}:
280
- return value.lower() in {"true", "yes", "1"}
281
- return None
282
- return _string_or_none(value, meta, field)
283
-
284
-
285
- def _surface_completeness(step: dict[str, Any], meta: dict[str, Any]) -> tuple[str, list[str]]:
286
- surface = step.get("interaction_surface")
287
- if not surface or surface in {"unknown", "other_tool", "other_surface"}:
288
- return "not_applicable", []
289
- required = _surface_required_fields(surface)
290
- missing = [name for name in required if step.get(name) in (None, "", [], {})]
291
- if surface == "cli":
292
- if "command" in missing and step.get("input"):
293
- missing.remove("command")
294
- if "stdout_ref" in missing and step.get("output"):
295
- meta.setdefault("surface_evidence_substitutions", []).append("cli_stdout_ref_satisfied_by_output")
296
- missing.remove("stdout_ref")
297
- if "exit_code" in missing and step.get("success") is not None:
298
- meta.setdefault("surface_evidence_substitutions", []).append("cli_exit_code_satisfied_by_success_signal")
299
- missing.remove("exit_code")
300
- if surface == "file":
301
- if "file_path" in missing and (step.get("artifact_refs") or step.get("input")):
302
- missing.remove("file_path")
303
- if "file_operation" in missing and step.get("operation"):
304
- missing.remove("file_operation")
305
- if surface == "codebase":
306
- if "command" in missing and step.get("input"):
307
- missing.remove("command")
308
- if "test_result_ref" in missing and step.get("output"):
309
- meta.setdefault("surface_evidence_substitutions", []).append("codebase_test_result_ref_satisfied_by_output")
310
- missing.remove("test_result_ref")
311
- if "diff_ref" in missing and (step.get("artifact_refs") or step.get("state_change")):
312
- meta.setdefault("surface_evidence_substitutions", []).append("codebase_diff_ref_satisfied_by_artifact_or_state_change")
313
- missing.remove("diff_ref")
314
- if not required:
315
- return "partial" if step.get("output") or step.get("state_change") or step.get("artifact_refs") else "unavailable", []
316
- if not missing:
317
- return "complete", []
318
- if step.get("surface_capture_notes") or meta.get("surface_capture_unavailable_reason"):
319
- return "unavailable", missing
320
- return "partial", missing
321
-
322
- def _operation(raw_action: str | None, raw_op: str | None, meta: dict[str,Any], report: TraceNormalizationReport | None = None) -> str:
323
- val=str(raw_op or raw_action or '').strip()
324
- mapped=OP_MAP.get(val, val if val in VALID_OPS else 'other')
325
- if val and (val != mapped or val not in VALID_OPS):
326
- meta['original_operation']=val
327
- if report is not None:
328
- report.operation_repair_count += 1
329
- return mapped if mapped in VALID_OPS else 'other'
330
-
331
- def _action(raw_action: str | None, op: str, meta: dict[str,Any], report: TraceNormalizationReport | None = None) -> str:
332
- a=str(raw_action or '').strip()
333
- low=a.lower()
334
- if a:
335
- meta['original_action']=a
336
- if low in VALID_ACTIONS:
337
- return low
338
- if low in {'error','failure','exception'} or (op == 'other' and any(x in low for x in ['error','fail','exception'])):
339
- canonical='error'
340
- elif low in {'final_response','finalize'} or op == 'answer':
341
- canonical='output'
342
- else:
343
- canonical='agent_step'
344
- if a and report is not None:
345
- report.action_repair_count += 1
346
- return canonical
347
-
348
- def _string_or_none(v: Any, meta: dict[str, Any] | None = None, key: str | None = None) -> str | None:
349
- if v is None:
350
- return None
351
- if isinstance(v, str):
352
- return v
353
- if meta is not None and key is not None:
354
- meta[key]=v
355
- return _compact(v)
356
-
357
- def _output_to_string(v: Any) -> str | None:
358
- if v is None:
359
- return None
360
- if isinstance(v, str):
361
- return v
362
- if isinstance(v, dict):
363
- parts=[]
364
- for k in ['exit_code','stdout','stdout_summary','stderr','stderr_summary']:
365
- if k in v and v[k] not in (None, ''):
366
- parts.append(f'{k}: {_compact(v[k], 900)}')
367
- return '\n'.join(parts) if parts else _compact(v)
368
- return _compact(v)
369
-
370
- def _normalize_causal_type(v: Any, meta: dict[str, Any], report: TraceNormalizationReport | None = None) -> str | None:
371
- if v is None:
372
- return None
373
- raw=str(v).strip()
374
- mapped=CAUSAL_MAP.get(raw, raw if raw in VALID_CAUSAL_TYPES else 'other')
375
- if raw != mapped or raw not in VALID_CAUSAL_TYPES:
376
- meta['original_causal_type']=v
377
- if report is not None:
378
- report.causal_type_repair_count += 1
379
- return mapped
380
-
381
-
382
- def _command_tokens(command: str) -> list[str]:
383
- return [part.strip().lower() for part in command.replace('&&', '\n').replace(';', '\n').split('\n') if part.strip()]
384
-
385
- def _first_command_word(part: str) -> str:
386
- words=part.split()
387
- while words and ('=' in words[0] or words[0] in {'env','time','uv','poetry','pipenv','xargs','sudo'}):
388
- words=words[1:]
389
- return words[0] if words else ''
390
-
391
-
392
-
393
- def _has_phrase(text: str | None, phrase: str) -> bool:
394
- """Return true when a normalized phrase appears in text.
395
-
396
- Multi-word phrases use substring matching. Single-word phrases require
397
- token/word boundaries so generic language like "next move" does not
398
- accidentally become a file-write operation.
399
- """
400
- import re
401
-
402
- if not text or not phrase:
403
- return False
404
-
405
- text_l = str(text).lower()
406
- phrase_l = str(phrase).lower().strip()
407
- if not phrase_l:
408
- return False
409
-
410
- if " " in phrase_l:
411
- return phrase_l in text_l
412
-
413
- return re.search(r"(?<![a-z0-9_])" + re.escape(phrase_l) + r"(?![a-z0-9_])", text_l) is not None
414
-
415
-
416
- def _operation_from_phrase(value: str | None) -> str | None:
417
- """Infer canonical operation from descriptive action/operation/tool text.
418
-
419
- Canonical operations are intentionally small:
420
- read, write, execute, verify, other.
421
- Return None when intent is unclear so command-level inference can still run.
422
- """
423
- if not value:
424
- return None
425
-
426
- low = str(value).lower().replace("_", " ").replace("-", " ").strip()
427
- if not low or low == "other":
428
- return None
429
-
430
- # Avoid mapping generic reasoning language to file operations.
431
- if "next move" in low or low.startswith(("ponder ", "think ", "reason ")):
432
- return None
433
-
434
- if low in {"read", "write", "execute", "verify"}:
435
- return low
436
-
437
- verify_markers = (
438
- "validate", "validation", "verify", "verification", "check", "test",
439
- "pytest", "compileall", "json tool", "schema", "checksum", "hash",
440
- "inspect exported", "inspect workbook", "formula scan", "scan workbook",
441
- "render", "visual qa", "preview", "qa", "review output",
442
- "spreadsheet validation", "xlsx validation", "csv validation",
443
- "markdown validation", "artifact validation",
444
- "attempted spreadsheet library import", "checked local office",
445
- "package availability", "tool check",
446
- )
447
- if any(_has_phrase(low, marker) for marker in verify_markers):
448
- return "verify"
449
-
450
- write_markers = (
451
- "write", "create", "edit", "patch", "apply patch", "file creation",
452
- "file edit", "write artifact", "create artifact", "edit artifact",
453
- "mkdir", "copy", "cp ", "mv ", "builder script",
454
- "write final trace", "persist", "save", "generated artifacts",
455
- )
456
- if any(_has_phrase(low, marker) for marker in write_markers):
457
- return "write"
458
-
459
- execute_markers = (
460
- "execute", "run", "rerun", "re run", "executed", "script execution",
461
- "builder", "python", "python3", "node", "npm", "bash", "sh ",
462
- "calculation script", "link workspace", "environment setup",
463
- "symlink", "ln -s",
464
- )
465
- if any(_has_phrase(low, marker) for marker in execute_markers):
466
- return "execute"
467
-
468
- read_markers = (
469
- "read", "inspect", "inventory", "list", ("list" + "ing"), ("file " + "list" + "ing"),
470
- "file inspection", "read context", "inspect task", "inspect inputs",
471
- "inspect workspace", "load", "loaded", "sed", "cat", "head", "tail",
472
- "less", "ls", "find", "rg", "grep", "git status", "unzip",
473
- )
474
- if any(_has_phrase(low, marker) for marker in read_markers):
475
- return "read"
476
-
477
- return None
478
-
479
-
480
- def _operation_from_command(command: str | None, meta: dict[str, Any], report: TraceNormalizationReport | None = None) -> str | None:
481
- c=(command or '').strip()
482
- if not c:
483
- return None
484
- parts=_command_tokens(c) or [c.lower()]
485
- low='\n'.join(parts)
486
- firsts={_first_command_word(p) for p in parts}
487
- verify_markers=['json.tool','pytest','compileall','schema','validate','validation','validator',' check','check ','checksum','sha256sum','shasum','md5sum','openpyxl','xlsx validation','csv validation','json validation','markdown validation','test ']
488
- write_markers=['apply_patch','file edit','edit file','write artifact','write_artifact','create file','file creation','write','create','edit','patch','builder script','write final trace','persist','save']
489
- execute_markers=['execute','run','rerun','re run','executed','script execution','builder','python','python3','node','npm','bash','sh','calculation script','environment setup','symlink','ln -s']
490
- read_markers=['read','inspect','inventory','list',('list' + 'ing'),('file ' + 'list' + 'ing'),'file inspection','read context','inspect task','inspect inputs','inspect workspace','load','loaded','sed','cat','head','tail','less','ls','find','rg','grep','git status','unzip']
491
- if any(m in low for m in verify_markers) or any(f in {'pytest','py.test'} for f in firsts):
492
- return 'verify'
493
- if 'git status' in low:
494
- return 'read'
495
- if any(m in low for m in ['apply_patch','file edit','edit file','write artifact','write_artifact','create file','file creation']):
496
- return 'write'
497
- if any(f in {'sed','cat','head','tail','less','ls','find','rg','grep','file','pwd'} for f in firsts):
498
- return 'read'
499
- if any('unzip -l' in p or 'zipinfo' in p for p in parts) or ' inventory' in low or ('list' + 'ing') in low:
500
- return 'read'
501
- if any(f in {'mkdir','cp','mv','touch','tee'} for f in firsts):
502
- return 'write'
503
- if any(f in {'python','python3','node','npm','npx','bash','sh','zsh'} for f in firsts):
504
- return 'execute'
505
- if any(_has_phrase(low, m) for m in verify_markers):
506
- return 'verify'
507
- if any(_has_phrase(low, m) for m in write_markers):
508
- return 'write'
509
- if any(_has_phrase(low, m) for m in read_markers):
510
- return 'read'
511
- return None
512
-
513
- def _operation_from_step_context(raw_action: Any, raw_op: Any, tool: Any, command: Any, meta: dict[str, Any], report: TraceNormalizationReport | None = None) -> str | None:
514
- values=[raw_op, raw_action, tool, command]
515
- for value in values:
516
- op=_operation_from_phrase(str(value) if value is not None else None)
517
- if op:
518
- if report is not None:
519
- report.operation_repair_count += 1
520
- return op
521
- return None
522
-
523
- def infer_attempt_status(trace: dict[str, Any], report: dict[str, Any] | None = None) -> str:
524
- existing=trace.get('attempt_status')
525
- if existing in VALID_ATTEMPT_STATUSES:
526
- return existing
527
- termination=trace.get('termination_reason')
528
- md=trace.get('metadata_json') if isinstance(trace.get('metadata_json'), dict) else {}
529
- report=report or {}
530
- if termination == 'task_complete':
531
- return 'completed'
532
- if termination == 'agent_blocked':
533
- return 'blocked'
534
- actual=trace.get('actual_outputs') if isinstance(trace.get('actual_outputs'), dict) else {}
535
- if termination in {'timeout','error_unrecoverable','provider_usage_limit','verifier_failed'} or actual.get('status') in {'failed','timeout','error'}:
536
- return 'failed'
537
- if report.get('fallback_trace') or md.get('fallback_trace') or md.get('fallback_trace_created') or md.get('fallback_reason') or report.get('fallback_reason') or (isinstance(report.get('metadata_json'), dict) and report['metadata_json'].get('fallback_reason')):
538
- return 'fallback'
539
- steps=trace.get('steps') or []
540
- if any(s.get('step_outcome') == 'blocked' for s in steps if isinstance(s, dict)):
541
- return 'blocked'
542
- if any(s.get('step_outcome') == 'failed' or s.get('success') is False for s in steps if isinstance(s, dict)):
543
- return 'failed' if not any(s.get('success') is True for s in steps if isinstance(s, dict)) else 'partial'
544
- if steps:
545
- return 'partial'
546
- return 'failed'
547
-
548
- def normalize_trace_for_export(trace: dict[str, Any], report: dict[str, Any] | None = None) -> dict[str, Any]:
549
- out=dict(trace)
550
- steps=[]
551
- for s in out.get('steps') or []:
552
- if not isinstance(s, dict):
553
- steps.append(s); continue
554
- ns=dict(s)
555
- meta=dict(ns.get('metadata_json') or {}) if isinstance(ns.get('metadata_json'), dict) else {}
556
- command=ns.get('input') or ns.get('command') or meta.get('command')
557
- op=_operation_from_command(str(command) if command is not None else None, meta)
558
- if op and (not ns.get('operation') or ns.get('operation') == 'other'):
559
- ns['operation']=op
560
- if not ns.get("environment_domain"):
561
- ns["environment_domain"] = _infer_environment_domain(ns.get("tool"), ns.get("operation"), ns.get("input"), ns.get("output"))
562
- if ns.get("artifact_refs"):
563
- ns["artifact_refs"] = _normalize_ref_list(ns.get("artifact_refs"))
564
- ns['metadata_json']=meta
565
- steps.append(ns)
566
- out['steps']=steps
567
- actual = out.get("actual_outputs") if isinstance(out.get("actual_outputs"), dict) else {}
568
- attempt_kind = str(out.get("attempt_kind") or actual.get("attempt_kind") or "baseline")
569
- if actual:
570
- out.setdefault("actual_outputs_ref", _actual_ref(attempt_kind))
571
- out.setdefault("output_summary", actual.get("output_summary"))
572
- out.setdefault("final_output_summary", actual.get("output_summary"))
573
- out.setdefault("artifact_refs", _normalize_ref_list(actual.get("artifact_refs") or actual.get("files_created")))
574
- out.setdefault("deliverable_refs", _normalize_ref_list(actual.get("deliverable_refs")))
575
- out.setdefault("input_artifact_refs", _normalize_ref_list(actual.get("input_artifact_refs")))
576
- out['attempt_status']=infer_attempt_status(out, report)
577
- return out
578
-
579
- def _tool(command: str | None, raw: dict[str,Any], op: str) -> str | None:
580
- c=(command or '').strip()
581
- low=c.lower()
582
- if low.startswith('python ') or low.startswith('python3 ') or low == 'python' or low == 'python3': return 'python'
583
- if low.startswith('apply_patch') or low == 'apply_patch': return 'apply_patch'
584
- if c and any(tok in low for tok in ['sed','cat','ls','rg','find','pwd','mkdir','bash','zsh','sh ','python','touch','cp ','mv ','rm ']): return 'Bash'
585
- if op in READ_OPS: return 'file_read'
586
- if op in WRITE_OPS: return 'file_write'
587
- return raw.get('tool')
588
-
589
- def _derive_observation(step_num: int, op: str) -> str:
590
- if step_num == 1: return 'Attempt workspace and task inputs were available.'
591
- if op in READ_OPS: return 'Input file was available for inspection.'
592
- if op in EXEC_OPS or op in WRITE_OPS: return 'Prior inputs and task requirements were available.'
593
- if op in VERIFY_OPS: return 'Generated outputs were available for validation.'
594
- return 'Prior workflow context was available.'
595
-
596
- def _normalize_outcome(v: Any, meta: dict[str,Any]) -> str | None:
597
- if v is None: return None
598
- low=str(v).lower().strip()
599
- if low in VALID_OUTCOMES: return low
600
- meta['original_step_outcome']=v
601
- if low in {'ok','success','succeeded','done'}: return 'completed'
602
- if low in {'fail','failure','errored','error'}: return 'failed'
603
- if low in {'block','blocked'}: return 'blocked'
604
- return 'neutral'
605
-
606
-
607
- def _safe_int(v: Any, default: int, field: str, meta: dict[str, Any], report: TraceNormalizationReport | None = None) -> int:
608
- try:
609
- if isinstance(v, bool):
610
- raise ValueError('boolean is not numeric')
611
- if v is None or v == '':
612
- return default
613
- return int(v)
614
- except Exception:
615
- if v is not None:
616
- meta[f'original_{field}']=v
617
- warning=f'{field} value {v!r} was not numeric; defaulted to {default}'
618
- meta.setdefault('normalization_warnings', []).append(warning)
619
- if report is not None:
620
- report.warnings.append(warning)
621
- return default
622
-
623
- def _normalize_step(raw_step: Any, ordinal: int, used_steps: set[int], report: TraceNormalizationReport) -> dict[str,Any]:
624
- raw = raw_step if isinstance(raw_step, dict) else {'value': raw_step}
625
- meta=dict(raw.get('metadata_json') or {}) if isinstance(raw.get('metadata_json'), dict) else {}
626
- meta['raw_step']=raw
627
- unknown={k:v for k,v in raw.items() if k not in CANONICAL_STEP and k not in {'step_number','index','command','inputs','outputs','state_changes','artifacts','artifact_paths','decision_summary','stdout_summary','exit_code'}}
628
- if unknown: meta['original_fields']=unknown
629
- orig_step=raw.get('step', raw.get('step_number', raw.get('index')))
630
- step_num=_safe_int(orig_step, ordinal, 'step', meta, report)
631
- if step_num < 1 or step_num in used_steps:
632
- meta['original_step_number']=orig_step
633
- step_num=ordinal
634
- report.warnings.append(f'step {ordinal} renumbered from {orig_step!r}')
635
- used_steps.add(step_num)
636
- raw_action=raw.get('action')
637
- command=raw.get('command')
638
- inputs=raw.get('inputs')
639
- raw_input_probe=raw.get('input')
640
- commands_list = None
641
- if isinstance(raw_input_probe, dict):
642
- commands_list = raw_input_probe.get('commands')
643
- if commands_list is None and isinstance(inputs, dict):
644
- commands_list = inputs.get('commands')
645
- inferred_command = command or (raw_input_probe.get('cmd') if isinstance(raw_input_probe, dict) else None) or (raw_input_probe if isinstance(raw_input_probe, str) else None) or (inputs.get('cmd') if isinstance(inputs, dict) else None) or (inputs if isinstance(inputs, str) else None)
646
- if inferred_command is None and isinstance(commands_list, list):
647
- inferred_command = '\n'.join(str(c) for c in commands_list)
648
- op=_operation(str(raw_action) if raw_action is not None else None, raw.get('operation'), meta, report)
649
- ctx_op=_operation_from_step_context(raw_action, raw.get('operation'), raw.get('tool'), inferred_command, meta, report)
650
- cmd_op=_operation_from_command(str(inferred_command) if inferred_command is not None else None, meta, report) or ctx_op
651
- if cmd_op and (raw.get('operation') is None or op == 'other'):
652
- if op != cmd_op and report is not None:
653
- report.operation_repair_count += 1
654
- op=cmd_op
655
- action=_action(str(raw_action) if raw_action is not None else None, op, meta, report)
656
- if action == 'error' and raw.get('success') is True and not raw.get('error_type') and not raw.get('error_message'):
657
- action='agent_step'
658
-
659
- raw_input=raw.get('input')
660
- input_val: str | None
661
- if command is not None:
662
- input_val=str(command)
663
- meta['command']=command
664
- if inputs is not None: meta['inputs']=inputs
665
- if isinstance(raw_input, dict): meta.setdefault('input', raw_input); report.input_dict_to_string_count += 1
666
- elif isinstance(raw_input, dict):
667
- meta['inputs']=raw_input
668
- report.input_dict_to_string_count += 1
669
- input_val=str(raw_input.get('cmd')) if raw_input.get('cmd') is not None else _compact(raw_input)
670
- elif raw_input is not None:
671
- input_val=_string_or_none(raw_input, meta, 'input')
672
- elif inputs is not None:
673
- meta['inputs']=inputs
674
- if isinstance(inputs, dict) and inputs.get('cmd') is not None:
675
- input_val=str(inputs.get('cmd'))
676
- else:
677
- input_val=_compact(inputs)
678
- if isinstance(inputs, dict): report.input_dict_to_string_count += 1
679
- else:
680
- input_val=None
681
-
682
- raw_output=raw.get('output')
683
- outputs=raw.get('outputs')
684
- output_val: str | None = None
685
- if isinstance(raw_output, dict):
686
- meta['outputs']=raw_output
687
- output_val=_output_to_string(raw_output)
688
- report.output_dict_to_string_count += 1
689
- elif raw_output is not None:
690
- output_val=_string_or_none(raw_output, meta, 'output')
691
- if outputs is not None:
692
- meta['outputs']=outputs
693
- if isinstance(outputs, dict):
694
- report.output_dict_to_string_count += 1
695
- output_val=output_val or _output_to_string(outputs)
696
- if raw.get('stdout_summary') is not None:
697
- meta['stdout_summary']=raw.get('stdout_summary')
698
- output_val=output_val or str(raw.get('stdout_summary'))
699
-
700
- state_change=_string_or_none(raw.get('state_change'), meta, 'state_change')
701
- if raw.get('state_changes') is not None:
702
- sc=raw.get('state_changes'); meta['state_changes']=sc
703
- if isinstance(sc, list):
704
- state_change=state_change or ('; '.join(str(x) for x in sc) if sc else None)
705
- else:
706
- state_change=state_change or _compact(sc)
707
- artifact_refs=list(raw.get('artifact_refs') or [])
708
- if raw.get('artifacts') is not None:
709
- meta['original_artifacts']=raw.get('artifacts')
710
- artifact_refs.extend(str(x) for x in _as_list(raw.get('artifacts')))
711
- if raw.get('artifact_paths') is not None:
712
- meta['original_artifact_paths']=raw.get('artifact_paths')
713
- artifact_refs.extend(str(x) for x in _as_list(raw.get('artifact_paths')))
714
- artifact_refs = _normalize_ref_list(artifact_refs)
715
- if raw.get('exit_code') is not None: meta['exit_code']=raw.get('exit_code')
716
- if raw.get('decision_summary') is not None: meta['decision_summary']=raw.get('decision_summary')
717
-
718
- observation=_string_or_none(raw.get('observation'), meta, 'observation')
719
- if observation is None and action != 'user_message':
720
- observation=_derive_observation(step_num, op); meta['observation_derived']=True
721
- reasoning=None if action=='user_message' else _string_or_none(raw.get('reasoning') or raw.get('decision_summary'), meta, 'reasoning')
722
-
723
- caused=raw.get('caused_by')
724
- warnings=[]
725
- if caused is None:
726
- caused_by=None if ordinal == 1 else [ordinal-1]
727
- causal_type=None if ordinal == 1 else 'execution_of_plan'
728
- causal_note=None if ordinal == 1 else 'Sequential workflow step following the prior action.'
729
- else:
730
- caused_by=[int(x) for x in _as_list(caused) if isinstance(x, int) or str(x).isdigit()]
731
- valid=[x for x in caused_by if 1 <= x < step_num]
732
- if len(valid) != len(caused_by): warnings.append('invalid caused_by references removed')
733
- caused_by=valid or None
734
- causal_type=_normalize_causal_type(raw.get('causal_type') or ('execution_of_plan' if caused_by else None), meta, report)
735
- causal_note=_string_or_none(raw.get('causal_note'), meta, 'causal_note')
736
- if caused is None and causal_type is not None:
737
- causal_type=_normalize_causal_type(causal_type, meta, report)
738
- if warnings:
739
- meta['normalization_warnings']=warnings; report.warnings.extend(warnings)
740
-
741
- actor=_actor_to_string(raw.get('actor'), action, meta)
742
- raw_execution_mode = raw.get('execution_mode') if raw.get('execution_mode') in {'serial','parallel'} else None
743
- execution_mode = None if action == 'user_message' else (raw_execution_mode or 'serial')
744
- parallel_group = _string_or_none(raw.get('parallel_group'), meta, 'parallel_group') if execution_mode == 'parallel' else None
745
- tool = None if action=='user_message' else _tool(command if command is not None else input_val, raw, op)
746
- environment_domain = None if action == 'user_message' else (
747
- raw.get('environment_domain') if raw.get('environment_domain') in ENVIRONMENT_DOMAINS else _infer_environment_domain(tool, op, input_val, output_val)
748
- )
749
- interaction_surface = None if action == "user_message" else _infer_interaction_surface(raw, tool, op, input_val, output_val)
750
- feedback_type = raw.get('feedback_type') if raw.get('feedback_type') in {'correction','approval','clarification','new_instruction','other'} else None
751
- feedback_content = _string_or_none(raw.get('feedback_content'), meta, 'feedback_content')
752
- step={
753
- 'step':step_num,'turn':_safe_int(raw.get('turn'), 1, 'turn', meta, report),'actor':actor,'action':action,
754
- 'operation': None if action=='user_message' else op,'tool': tool,
755
- 'environment_domain': environment_domain,
756
- 'execution_mode':execution_mode,'parallel_group':parallel_group,'observation':None if action=='user_message' else observation,
757
- 'input':input_val,'input_source':_input_source(raw.get('input_source'), meta),'output':None if action=='user_message' else output_val,
758
- 'state_change':state_change,'reasoning':None if action=='user_message' else reasoning,
759
- 'caused_by':caused_by,'causal_type':causal_type,'causal_note':causal_note,'alternatives_considered':_string_or_none(raw.get('alternatives_considered'), meta, 'alternatives_considered'),
760
- 'success':None if action=='user_message' else raw.get('success') if isinstance(raw.get('success'), bool) else None,'step_outcome':None if action=='user_message' else _normalize_outcome(raw.get('step_outcome'), meta),
761
- 'error_type':_string_or_none(raw.get('error_type'), meta, 'error_type'),'error_message':_string_or_none(raw.get('error_message'), meta, 'error_message'),'message_role':raw.get('message_role') if action=='user_message' else None,
762
- 'feedback_type':feedback_type,'feedback_content':feedback_content,
763
- 'started_at':_string_or_none(raw.get('started_at'), meta, 'started_at'),'ended_at':_string_or_none(raw.get('ended_at'), meta, 'ended_at'),'retry_of':raw.get('retry_of') if isinstance(raw.get('retry_of'), int) else None,'artifact_refs':artifact_refs,'metadata_json':meta,
764
- }
765
- if action != "user_message":
766
- step["interaction_surface"] = interaction_surface
767
- if interaction_surface and interaction_surface != "unknown" and environment_domain == "unknown":
768
- if interaction_surface in {"cli"}:
769
- step["environment_domain"] = "terminal"
770
- elif interaction_surface in {"mcp_tool", "mcp_resource", "mcp_prompt"}:
771
- step["environment_domain"] = "mcp"
772
- elif interaction_surface in {"cloud_console", "saas_app"}:
773
- step["environment_domain"] = "software"
774
- elif interaction_surface in {"document", "pdf", "table"}:
775
- step["environment_domain"] = "multimodal" if interaction_surface in {"document", "pdf"} else "spreadsheet"
776
- elif interaction_surface in {"device", "industrial", "physical"}:
777
- step["environment_domain"] = "industrial" if interaction_surface == "industrial" else interaction_surface
778
- elif interaction_surface in ENVIRONMENT_DOMAINS:
779
- step["environment_domain"] = interaction_surface
780
- for field in SURFACE_STEP_FIELDS:
781
- if field in {"interaction_surface", "surface_capture_status", "missing_surface_fields", "surface_capture_notes"}:
782
- continue
783
- if field in raw:
784
- value = _normalize_surface_value(field, raw.get(field), meta)
785
- if value not in (None, "", [], {}):
786
- step[field] = value
787
- if command is not None and not step.get("command"):
788
- step["command"] = str(command)
789
- elif interaction_surface == "cli" and input_val and not step.get("command"):
790
- step["command"] = input_val
791
- if interaction_surface == "file":
792
- if not step.get("file_path") and artifact_refs:
793
- step["file_path"] = artifact_refs[0]
794
- if not step.get("file_operation") and op:
795
- step["file_operation"] = op
796
- if raw.get("exit_code") is not None and step.get("exit_code") is None:
797
- try:
798
- step["exit_code"] = int(raw.get("exit_code"))
799
- except (TypeError, ValueError):
800
- pass
801
- if raw.get("stdout_ref") is None and raw.get("stdout_summary") is not None:
802
- meta.setdefault("surface_stdout_summary", raw.get("stdout_summary"))
803
- if step.get("environment_domain") == "mixed" and not step.get("connected_surfaces") and interaction_surface:
804
- step["connected_surfaces"] = [interaction_surface]
805
- status, missing = _surface_completeness(step, meta)
806
- if step.get("surface_capture_status") is None:
807
- step["surface_capture_status"] = status
808
- if status in {"partial", "unavailable"} and missing and not step.get("surface_capture_notes"):
809
- step["surface_capture_notes"] = f"{interaction_surface} surface evidence is incomplete; missing fields were recorded without fabrication."
810
- if missing and not step.get("missing_surface_fields"):
811
- step["missing_surface_fields"] = missing
812
- meta.setdefault("trace_quality_warnings", []).append(f"{interaction_surface}_missing_surface_fields:{','.join(missing)}")
813
- if step.get("success") is True and not any(step.get(name) for name in ["output", "state_change", "artifact_refs", "evidence_refs", "raw_response_ref", "response_payload_ref", "screenshot_ref", "visible_state_ref", "telemetry_ref"]):
814
- meta.setdefault("trace_quality_warnings", []).append("successful_step_missing_output_state_or_evidence")
815
- if action == 'user_message':
816
- step.update({'operation':None,'tool':None,'environment_domain':None,'execution_mode':None,'observation':None,'reasoning':None,'success':None,'step_outcome':None,'output':None})
817
- return step
818
-
819
- def _model_dump(obj: Any) -> dict[str, Any]:
820
- if obj is None:
821
- return None
822
- if hasattr(obj, 'model_dump'):
823
- try:
824
- return obj.model_dump(mode='json')
825
- except TypeError:
826
- return obj.model_dump()
827
- return obj
828
-
829
- def _valid_actual_outputs(raw_actual: Any, context: TraceNormalizationContext, metadata: dict[str, Any]) -> dict[str, Any] | None:
830
- if context.actual_outputs is not None:
831
- if raw_actual is not None:
832
- metadata['original_actual_outputs']=raw_actual
833
- return _model_dump(context.actual_outputs)
834
- if isinstance(raw_actual, dict):
835
- try:
836
- return ActualOutputs.model_validate(raw_actual).model_dump()
837
- except Exception:
838
- metadata['original_actual_outputs']=raw_actual
839
- return None
840
- if raw_actual is not None:
841
- metadata['original_actual_outputs']=raw_actual
842
- return None
843
-
844
- def _canonical_attempt_kind(raw: dict[str, Any], context: TraceNormalizationContext, metadata: dict[str, Any]) -> str:
845
- val=raw.get('attempt_kind') or raw.get('attempt') or context.attempt_kind
846
- if val in VALID_ATTEMPT_KINDS:
847
- return val
848
- if val is not None:
849
- metadata['original_attempt_kind']=val
850
- return context.attempt_kind if context.attempt_kind in VALID_ATTEMPT_KINDS else 'other'
851
-
852
- def _canonical_trace_mode(raw: dict[str, Any], metadata: dict[str, Any]) -> str:
853
- val=raw.get('trace_mode') or 'live'
854
- if val in VALID_TRACE_MODES:
855
- return val
856
- metadata['original_trace_mode']=val
857
- return 'live'
858
-
859
- def _canonical_termination_reason(raw: dict[str, Any], success_like: bool, metadata: dict[str, Any]) -> str:
860
- val=raw.get('termination_reason')
861
- if val in VALID_TERMINATION_REASONS:
862
- return val
863
- if val is not None:
864
- metadata['original_termination_reason']=val
865
- return 'task_complete' if success_like else 'other'
866
-
867
- def _canonical_agent_tools(raw: dict[str, Any], metadata: dict[str, Any]) -> list[str]:
868
- val=raw.get('agent_tools')
869
- if isinstance(val, list) and all(isinstance(x, str) for x in val):
870
- return val
871
- if val is not None:
872
- metadata['original_agent_tools']=val
873
- return ['codex_cli','Bash','python','file_read','file_write','apply_patch']
874
-
875
- def _canonical_artifacts(raw: dict[str, Any], metadata: dict[str, Any]) -> list[dict[str, Any]]:
876
- val=raw.get('artifacts')
877
- if not val:
878
- return []
879
- metadata['original_artifacts']=val
880
- return []
881
-
882
- def _extract_raw_steps(raw: dict[str, Any]) -> tuple[list[Any], str | None]:
883
- for key in ['steps','trace_steps','trace','records','events','actions']:
884
- val=raw.get(key)
885
- if isinstance(val, list):
886
- return val, key
887
- if isinstance(val, dict):
888
- nested=val.get('steps') or val.get('events') or val.get('actions')
889
- if isinstance(nested, list):
890
- return nested, key
891
- return [], None
892
-
893
- def normalize_agent_trace(raw: dict[str,Any], context: TraceNormalizationContext) -> dict[str,Any]:
894
- report=TraceNormalizationReport(task_id=context.task_id, attempt_id=context.attempt_id, attempt_kind=context.attempt_kind)
895
- raw_steps, raw_step_source = _extract_raw_steps(raw)
896
- if not isinstance(raw_steps, list): raw_steps=[]; report.warnings.append('raw steps was not a list')
897
- metadata=dict(raw.get('metadata_json') or {}) if isinstance(raw.get('metadata_json'), dict) else {}
898
- if raw_step_source:
899
- metadata['raw_trace_step_source']=raw_step_source
900
- unknown_top={k:v for k,v in raw.items() if k not in CANONICAL_TOP and k not in {'attempt','role','schema_name'}}
901
- if unknown_top: metadata['original_fields']=unknown_top
902
- if raw.get('role') is not None: metadata['original_role']=raw.get('role')
903
- if raw.get('schema_name') is not None: metadata['schema_name']=raw.get('schema_name')
904
- used=set(); steps=[_normalize_step(s, i+1, used, report) for i,s in enumerate(raw_steps)]
905
- # Ensure final monotonic sequence without dropping; preserve original numbers already in metadata when changed.
906
- if [s['step'] for s in steps] != list(range(1,len(steps)+1)):
907
- for i,s in enumerate(steps, 1):
908
- s['metadata_json'].setdefault('original_step_number', s['step']); s['step']=i
909
- report.warnings.append('steps renumbered to canonical 1..N order')
910
- actual_outputs = _valid_actual_outputs(raw.get('actual_outputs'), context, metadata)
911
- success_like=bool(actual_outputs or raw.get('actual_outputs') or any(s.get('success') for s in steps))
912
- metadata.update({'raw_trace_preserved':True,'discarded_step_count':0,'trace_normalization_status':'normalized' if not report.warnings else 'partial'})
913
- attempt_kind = _canonical_attempt_kind(raw, context, metadata)
914
- raw_agent_config = raw.get('agent_config') if isinstance(raw.get('agent_config'), dict) else None
915
- actual_artifact_refs = _normalize_ref_list((actual_outputs or {}).get('artifact_refs') or (actual_outputs or {}).get('files_created'))
916
- actual_deliverable_refs = _normalize_ref_list((actual_outputs or {}).get('deliverable_refs'))
917
- input_artifact_refs = _normalize_ref_list(raw.get('input_artifact_refs') or (actual_outputs or {}).get('input_artifact_refs'))
918
- environment_domains = {
919
- s.get("environment_domain")
920
- for s in steps
921
- if s.get("environment_domain") and s.get("environment_domain") != "unknown"
922
- }
923
- top_environment_domain = raw.get("environment_domain") if raw.get("environment_domain") in ENVIRONMENT_DOMAINS else (
924
- environment_domains.pop() if len(environment_domains) == 1 else ("mixed" if environment_domains else "unknown")
925
- )
926
- missing_runtime_fields = []
927
- for field in ("run_id", "package_id", "bundle_id", "started_at", "ended_at", "agent_config"):
928
- if raw.get(field) in (None, "", {}):
929
- missing_runtime_fields.append(field)
930
- if missing_runtime_fields:
931
- metadata.setdefault("missing_runtime_fields", missing_runtime_fields)
932
- trace={
933
- 'schema_version': str(raw.get('schema_version') or 'aa-trace-v0.1'),
934
- 'trace_id': str(raw.get('trace_id') or f'{context.trace_id_prefix}_{context.attempt_id}_normalized'),
935
- 'collection_id': _string_or_none(raw.get('collection_id'), metadata, 'collection_id'),
936
- 'run_id': _string_or_none(raw.get('run_id'), metadata, 'run_id'),
937
- 'package_id': _string_or_none(raw.get('package_id'), metadata, 'package_id'),
938
- 'bundle_id': _string_or_none(raw.get('bundle_id'), metadata, 'bundle_id'),
939
- 'prior_trace_id': _string_or_none(raw.get('prior_trace_id'), metadata, 'prior_trace_id'), 'trace_mode': _canonical_trace_mode(raw, metadata),
940
- 'task': _string_or_none(raw.get('task'), metadata, 'task') or context.task, 'task_id': _string_or_none(raw.get('task_id'), metadata, 'task_id') or context.task_id, 'task_family_id': _string_or_none(raw.get('task_family_id'), metadata, 'task_family_id'),
941
- 'attempt_id': _string_or_none(raw.get('attempt_id'), metadata, 'attempt_id') or context.attempt_id, 'attempt_kind': attempt_kind,
942
- 'agent_tools': _canonical_agent_tools(raw, metadata),
943
- 'started_at': _string_or_none(raw.get('started_at'), metadata, 'started_at'), 'ended_at': _string_or_none(raw.get('ended_at'), metadata, 'ended_at'), 'system_prompt': _string_or_none(raw.get('system_prompt'), metadata, 'system_prompt'), 'system_prompt_hash': _string_or_none(raw.get('system_prompt_hash'), metadata, 'system_prompt_hash'), 'skills': raw.get('skills') if isinstance(raw.get('skills'), list) else None, 'memory': _string_or_none(raw.get('memory'), metadata, 'memory'), 'agent_config': raw_agent_config,
944
- 'environment_domain': top_environment_domain,
945
- 'actual_outputs_ref': _string_or_none(raw.get('actual_outputs_ref'), metadata, 'actual_outputs_ref') or _actual_ref(attempt_kind),
946
- 'input_artifact_refs': input_artifact_refs,
947
- 'artifact_refs': _normalize_ref_list(raw.get('artifact_refs')) or actual_artifact_refs,
948
- 'deliverable_refs': _normalize_ref_list(raw.get('deliverable_refs')) or actual_deliverable_refs,
949
- 'output_summary': _string_or_none(raw.get('output_summary'), metadata, 'output_summary') or (actual_outputs or {}).get('output_summary'),
950
- 'final_output_summary': _string_or_none(raw.get('final_output_summary'), metadata, 'final_output_summary') or (actual_outputs or {}).get('output_summary'),
951
- 'learning': _string_or_none(raw.get('learning'), metadata, 'learning'),
952
- 'termination_reason': _canonical_termination_reason(raw, success_like, metadata),
953
- 'steps': steps, 'actual_outputs': actual_outputs, 'artifacts': _canonical_artifacts(raw, metadata), 'metadata_json': metadata,
954
- }
955
- trace['attempt_status']=infer_attempt_status(trace)
956
- report.raw_step_count=len(raw_steps); report.normalized_step_count=len(steps); report.discarded_step_count=0
957
- valid, errors = _validate_normalized_trace(trace)
958
- report.trace_schema_valid=valid; trace['metadata_json']['schema_valid']=valid
959
- if not valid:
960
- report.trace_normalization_partial=True; report.validation_errors.extend(errors)
961
- report.trace_normalized=True; report.trace_lossless=(report.raw_step_count==report.normalized_step_count and report.discarded_step_count==0)
962
- return trace
963
-
964
- def _validation_error_messages(exc: Exception) -> list[str]:
965
- if hasattr(exc, 'errors'):
966
- try:
967
- return [_compact(e, 2000) for e in exc.errors()]
968
- except Exception:
969
- pass
970
- return [str(exc)]
971
-
972
- def _validate_normalized_trace(trace: dict[str, Any]) -> tuple[bool, list[str]]:
973
- try:
974
- AgentTrace.model_validate(trace)
975
- return True, []
976
- except Exception as exc:
977
- return False, _validation_error_messages(exc)
978
-
979
- def repair_agent_trace_file(raw_trace_path: Path, context: TraceNormalizationContext) -> NormalizationResult:
980
- report=TraceNormalizationReport(task_id=context.task_id, attempt_id=context.attempt_id, attempt_kind=context.attempt_kind, raw_trace_ref=raw_trace_path.name)
981
- try:
982
- text = raw_trace_path.read_text()
983
- try:
984
- raw=json.loads(text)
985
- except json.JSONDecodeError as strict_error:
986
- raw=json.loads(text, strict=False)
987
- report.warnings.append(f"raw trace parsed with permissive JSON control-character handling: {strict_error}")
988
- if isinstance(raw, list):
989
- raw={'steps': raw, 'metadata_json': {'raw_trace_shape': 'list', 'raw_trace_list': raw}}
990
- elif isinstance(raw, dict):
991
- raw_steps, source = _extract_raw_steps(raw)
992
- if source and source != 'steps':
993
- md=dict(raw.get('metadata_json') or {}) if isinstance(raw.get('metadata_json'), dict) else {}
994
- md.setdefault('raw_trace_shape', source)
995
- raw['metadata_json']=md
996
- raw['steps']=raw_steps
997
- else:
998
- raise ValueError('raw trace JSON was not an object or step list')
999
- except Exception as e:
1000
- report.raw_trace_parse_error=True; report.validation_errors.append(str(e))
1001
- return NormalizationResult(normalized_trace=None, report=report, fallback_required=True, parse_error=str(e))
1002
- trace=normalize_agent_trace(raw, context)
1003
- valid, errors = _validate_normalized_trace(trace)
1004
- raw_steps, _source = _extract_raw_steps(raw)
1005
- raw_step_count = len(raw_steps) if isinstance(raw_steps, list) else 0
1006
- normalized_step_count = len(trace.get('steps') or [])
1007
- comp=TraceNormalizationReport(task_id=context.task_id, attempt_id=context.attempt_id, attempt_kind=context.attempt_kind, raw_trace_ref=raw_trace_path.name, normalized_trace_ref='agent_trace.normalized.json', canonical_trace_ref='agent_trace.json', trace_schema_valid=valid, trace_normalized=True, trace_lossless=True, fallback_trace=False, raw_step_count=raw_step_count, normalized_step_count=normalized_step_count, discarded_step_count=0, trace_normalization_partial=(not valid) or trace.get('metadata_json',{}).get('trace_normalization_status')=='partial')
1008
- comp.warnings.extend(report.warnings)
1009
- # Re-run normalization into a temporary report to retain repair counters.
1010
- counter_report=TraceNormalizationReport(task_id=context.task_id, attempt_id=context.attempt_id, attempt_kind=context.attempt_kind)
1011
- used=set()
1012
- for i, step in enumerate(raw_steps if isinstance(raw_steps, list) else [], 1):
1013
- _normalize_step(step, i, used, counter_report)
1014
- comp.input_dict_to_string_count=counter_report.input_dict_to_string_count
1015
- comp.output_dict_to_string_count=counter_report.output_dict_to_string_count
1016
- comp.causal_type_repair_count=counter_report.causal_type_repair_count
1017
- comp.operation_repair_count=counter_report.operation_repair_count
1018
- comp.action_repair_count=counter_report.action_repair_count
1019
- comp.warnings.extend(counter_report.warnings)
1020
- comp.trace_lossless = comp.raw_step_count == comp.normalized_step_count and comp.discarded_step_count == 0
1021
- if not comp.trace_lossless: comp.validation_errors.append('normalized step count is less than raw step count')
1022
- if not valid:
1023
- comp.validation_errors.extend(errors)
1024
- trace.setdefault('metadata_json', {})['schema_valid']=valid
1025
- trace['metadata_json']['trace_normalization_status']='normalized' if valid and not comp.warnings else 'partial'
1026
- return NormalizationResult(normalized_trace=trace if valid else trace, report=comp, fallback_required=False)