agent-apprenticeship 0.2.1 → 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.
- package/README.md +261 -1
- package/bin/agent-apprenticeship.js +1 -2
- package/package.json +4 -4
- package/pyproject.toml +10 -4
- package/schemas/environment_replay.schema.json +41 -0
- package/schemas/eval_package.schema.json +37 -0
- package/schemas/experience_compilation.schema.json +53 -0
- package/schemas/runtime_training.schema.json +33 -0
- package/schemas/source_evidence_map.schema.json +25 -0
- package/schemas/trace.schema.json +904 -0
- package/schemas/trace_step.schema.json +622 -0
- package/schemas/training_rows.schema.json +29 -0
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/__init__.py +1 -1
- package/src/agent_apprenticeship/agent_env_exporter.py +53 -0
- package/src/agent_apprenticeship/agent_env_verify.py +32 -0
- package/src/agent_apprenticeship/atif.py +152 -0
- package/src/agent_apprenticeship/atif_models/LICENSE +201 -0
- package/src/agent_apprenticeship/atif_models/__init__.py +38 -0
- package/src/agent_apprenticeship/atif_models/agent.py +35 -0
- package/src/agent_apprenticeship/atif_models/content.py +159 -0
- package/src/agent_apprenticeship/atif_models/final_metrics.py +43 -0
- package/src/agent_apprenticeship/atif_models/metrics.py +47 -0
- package/src/agent_apprenticeship/atif_models/observation.py +19 -0
- package/src/agent_apprenticeship/atif_models/observation_result.py +45 -0
- package/src/agent_apprenticeship/atif_models/step.py +142 -0
- package/src/agent_apprenticeship/atif_models/subagent_trajectory_ref.py +88 -0
- package/src/agent_apprenticeship/atif_models/tool_call.py +34 -0
- package/src/agent_apprenticeship/atif_models/trajectory.py +196 -0
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/batch_runner.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/bundle_exporter.py +8 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/certification.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/cli.py +26 -7
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/codex_runner.py +17 -17
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/config.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/ecosystem_r2.py +3 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/evaluator.py +8 -27
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/grader.py +6 -5
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/integration_status.py +1 -1
- package/src/agent_apprenticeship/io.py +104 -0
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/learning.py +7 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/loop.py +5 -5
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/loop_review.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/mentor_checkpoints.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/package_exporter.py +5 -3
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/progress.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/public_run.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/public_sanitizer.py +2 -2
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/release_exporter.py +7 -4
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/rubric_generation.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/schemas.py +17 -4
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/session_events.py +4 -4
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/task_intake.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/tdo.py +5 -1
- package/src/agent_apprenticeship/trace_normalizer.py +103 -0
- package/src/agent_apprenticeship/trace_prompt.py +48 -0
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/training_signals.py +1 -1
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/validation.py +3 -3
- package/src/{agent_apprenticeship_trace → agent_apprenticeship}/verifier.py +6 -5
- package/docs/bioactive_evidence_mapping_schema.md +0 -113
- package/docs/dataset_schema.md +0 -160
- package/docs/local_usage.md +0 -191
- package/docs/scaling_to_1000_tasks.md +0 -3
- package/docs/trace_schema.md +0 -192
- package/docs/v0.2_release_notes.md +0 -63
- package/src/agent_apprenticeship_trace/io.py +0 -20
- package/src/agent_apprenticeship_trace/trace_normalizer.py +0 -1026
- package/src/agent_apprenticeship_trace/trace_prompt.py +0 -78
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/actual_outputs_normalizer.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/apprentice_adapters.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/artifact_capture.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/artifact_previews.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/artifact_resolver.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/command_discovery.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/contract_diagnostics.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/contract_verifier.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/env.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/lesson_extractor.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/llm_output_normalizer.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/openai_structured.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/recipes.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/revision.py +0 -0
- /package/src/{agent_apprenticeship_trace → agent_apprenticeship}/role_runners.py +0 -0
- /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)
|