traceforge-toolkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- traceforge/__init__.py +72 -0
- traceforge/__main__.py +5 -0
- traceforge/_generated.py +254 -0
- traceforge/adapters/__init__.py +12 -0
- traceforge/adapters/base.py +82 -0
- traceforge/adapters/genai_otel.py +164 -0
- traceforge/adapters/mapped_json.py +297 -0
- traceforge/adapters/otel.py +220 -0
- traceforge/boundary/__init__.py +46 -0
- traceforge/boundary/data/boundary-model.joblib +0 -0
- traceforge/boundary/decode.py +87 -0
- traceforge/boundary/features.py +137 -0
- traceforge/boundary/inference.py +267 -0
- traceforge/boundary/inferencer.py +146 -0
- traceforge/classify/__init__.py +95 -0
- traceforge/classify/cmd.py +109 -0
- traceforge/classify/coding.py +213 -0
- traceforge/classify/config.py +554 -0
- traceforge/classify/core.py +266 -0
- traceforge/classify/data/binary_info.yaml +358 -0
- traceforge/classify/data/canonical_tools.yaml +251 -0
- traceforge/classify/data/effect_overrides.yaml +480 -0
- traceforge/classify/data/mcp_profiles.yaml +711 -0
- traceforge/classify/data/recommendation_rules.yaml +111 -0
- traceforge/classify/data/risk.yaml +534 -0
- traceforge/classify/data/shell_defaults.yaml +95 -0
- traceforge/classify/data/shell_rules.yaml +1192 -0
- traceforge/classify/data/tool_classifications.yaml +215 -0
- traceforge/classify/data/verb_inference.yaml +218 -0
- traceforge/classify/mcp.py +181 -0
- traceforge/classify/phases.py +75 -0
- traceforge/classify/powershell.py +93 -0
- traceforge/classify/registry.py +138 -0
- traceforge/classify/risk.py +553 -0
- traceforge/classify/rules.py +215 -0
- traceforge/classify/schema.yaml +282 -0
- traceforge/classify/shell.py +503 -0
- traceforge/classify/tools.py +91 -0
- traceforge/classify/workflow.py +32 -0
- traceforge/cli/__init__.py +42 -0
- traceforge/cli/config_cmd.py +130 -0
- traceforge/cli/detect.py +46 -0
- traceforge/cli/download_cmd.py +74 -0
- traceforge/cli/factory.py +60 -0
- traceforge/cli/gate_cmd.py +30 -0
- traceforge/cli/init_cmd.py +86 -0
- traceforge/cli/replay.py +130 -0
- traceforge/cli/runner.py +181 -0
- traceforge/cli/score.py +217 -0
- traceforge/cli/status.py +65 -0
- traceforge/cli/watch.py +297 -0
- traceforge/config/__init__.py +80 -0
- traceforge/config/defaults.py +149 -0
- traceforge/config/loader.py +239 -0
- traceforge/config/mappings.py +104 -0
- traceforge/config/models.py +579 -0
- traceforge/enricher.py +576 -0
- traceforge/formatting/__init__.py +12 -0
- traceforge/formatting/budget.py +92 -0
- traceforge/formatting/density.py +154 -0
- traceforge/gate/__init__.py +17 -0
- traceforge/gate/client.py +145 -0
- traceforge/gate/external.py +305 -0
- traceforge/gate/registry.py +108 -0
- traceforge/gate/server.py +174 -0
- traceforge/gates/__init__.py +8 -0
- traceforge/gates/pii.py +352 -0
- traceforge/gates/pii_patterns.yaml +228 -0
- traceforge/governance/__init__.py +121 -0
- traceforge/governance/assessor.py +441 -0
- traceforge/governance/budget.py +59 -0
- traceforge/governance/canonical.py +56 -0
- traceforge/governance/codec.py +414 -0
- traceforge/governance/context.py +249 -0
- traceforge/governance/drift.py +196 -0
- traceforge/governance/emitter.py +234 -0
- traceforge/governance/envelope.py +138 -0
- traceforge/governance/ifc.py +364 -0
- traceforge/governance/integrity.py +162 -0
- traceforge/governance/labeler.py +250 -0
- traceforge/governance/mcp_drift.py +328 -0
- traceforge/governance/monitor.py +539 -0
- traceforge/governance/observer.py +276 -0
- traceforge/governance/persistence.py +401 -0
- traceforge/governance/phase1.py +77 -0
- traceforge/governance/pii.py +276 -0
- traceforge/governance/pipeline.py +840 -0
- traceforge/governance/registry.py +110 -0
- traceforge/governance/results.py +183 -0
- traceforge/governance/risk_wrapper.py +113 -0
- traceforge/governance/rules.py +368 -0
- traceforge/governance/scorer.py +262 -0
- traceforge/governance/shield.py +318 -0
- traceforge/governance/state.py +466 -0
- traceforge/governance/types.py +176 -0
- traceforge/mappings/__init__.py +1 -0
- traceforge/mappings/aider.yaml +216 -0
- traceforge/mappings/aider_markdown.yaml +113 -0
- traceforge/mappings/amazonq.yaml +56 -0
- traceforge/mappings/antigravity.yaml +88 -0
- traceforge/mappings/claude.yaml +93 -0
- traceforge/mappings/cline.yaml +286 -0
- traceforge/mappings/codex.yaml +158 -0
- traceforge/mappings/continue_dev.yaml +57 -0
- traceforge/mappings/copilot.yaml +266 -0
- traceforge/mappings/copilot_markdown.yaml +72 -0
- traceforge/mappings/copilot_vscode.yaml +181 -0
- traceforge/mappings/crewai.yaml +592 -0
- traceforge/mappings/goose.yaml +128 -0
- traceforge/mappings/langgraph.yaml +165 -0
- traceforge/mappings/maf.yaml +90 -0
- traceforge/mappings/maf_transcript.yaml +99 -0
- traceforge/mappings/openai_agents.yaml +144 -0
- traceforge/mappings/opencode.yaml +473 -0
- traceforge/mappings/openhands.yaml +320 -0
- traceforge/mappings/pydantic_ai.yaml +183 -0
- traceforge/mappings/smolagents.yaml +89 -0
- traceforge/mappings/sweagent.yaml +82 -0
- traceforge/migrations/__init__.py +1 -0
- traceforge/migrations/env.py +60 -0
- traceforge/migrations/models.py +145 -0
- traceforge/migrations/runner.py +44 -0
- traceforge/migrations/script.py.mako +25 -0
- traceforge/migrations/versions/0001_initial.py +176 -0
- traceforge/migrations/versions/__init__.py +4 -0
- traceforge/models.py +29 -0
- traceforge/parsers/__init__.py +21 -0
- traceforge/parsers/aider.py +416 -0
- traceforge/parsers/base.py +226 -0
- traceforge/parsers/copilot.py +314 -0
- traceforge/phase/__init__.py +50 -0
- traceforge/phase/data/phase-model.joblib +0 -0
- traceforge/phase/data/potion-base-8M/README.md +99 -0
- traceforge/phase/data/potion-base-8M/config.json +13 -0
- traceforge/phase/data/potion-base-8M/model.safetensors +0 -0
- traceforge/phase/data/potion-base-8M/modules.json +14 -0
- traceforge/phase/data/potion-base-8M/tokenizer.json +1 -0
- traceforge/phase/event_rows.py +107 -0
- traceforge/phase/features.py +468 -0
- traceforge/phase/inference.py +279 -0
- traceforge/phase/inferencer.py +171 -0
- traceforge/phase/segmentation.py +258 -0
- traceforge/pipeline.py +891 -0
- traceforge/preprocessors/__init__.py +34 -0
- traceforge/preprocessors/amazonq.py +224 -0
- traceforge/preprocessors/antigravity.py +116 -0
- traceforge/preprocessors/claude.py +95 -0
- traceforge/preprocessors/cline.py +36 -0
- traceforge/preprocessors/codex.py +311 -0
- traceforge/preprocessors/continue_dev.py +119 -0
- traceforge/preprocessors/copilot_vscode.py +171 -0
- traceforge/preprocessors/goose.py +156 -0
- traceforge/preprocessors/maf_transcript.py +84 -0
- traceforge/preprocessors/openai_agents.py +86 -0
- traceforge/preprocessors/opencode.py +85 -0
- traceforge/preprocessors/openhands.py +36 -0
- traceforge/preprocessors/pydantic_ai.py +62 -0
- traceforge/preprocessors/registry.py +24 -0
- traceforge/preprocessors/smolagents.py +90 -0
- traceforge/py.typed +0 -0
- traceforge/sdk/__init__.py +59 -0
- traceforge/sdk/gate_policy.py +63 -0
- traceforge/sdk/gate_types.py +140 -0
- traceforge/sdk/pipeline.py +265 -0
- traceforge/sdk/verdict.py +81 -0
- traceforge/sinks/__init__.py +23 -0
- traceforge/sinks/base.py +95 -0
- traceforge/sinks/callback.py +132 -0
- traceforge/sinks/console.py +112 -0
- traceforge/sinks/factory.py +94 -0
- traceforge/sinks/jsonl.py +125 -0
- traceforge/sinks/otel_exporter.py +212 -0
- traceforge/sinks/parquet.py +260 -0
- traceforge/sinks/s3.py +206 -0
- traceforge/sinks/sqlite_output.py +234 -0
- traceforge/sinks/webhook.py +136 -0
- traceforge/sources/__init__.py +18 -0
- traceforge/sources/auto_detect.py +173 -0
- traceforge/sources/base.py +45 -0
- traceforge/sources/file_poll.py +136 -0
- traceforge/sources/file_watch.py +221 -0
- traceforge/sources/http_poll.py +141 -0
- traceforge/sources/replay.py +63 -0
- traceforge/sources/sqlite.py +187 -0
- traceforge/sources/sse.py +198 -0
- traceforge/telemetry/__init__.py +192 -0
- traceforge/title/__init__.py +23 -0
- traceforge/title/_resolve.py +79 -0
- traceforge/title/context.py +295 -0
- traceforge/title/data/boilerplate_files.json +14 -0
- traceforge/title/heuristics.py +429 -0
- traceforge/title/hygiene.py +90 -0
- traceforge/title/inference.py +314 -0
- traceforge/title/inferencer.py +477 -0
- traceforge/title/naming.py +398 -0
- traceforge/trace.py +291 -0
- traceforge/tracking/__init__.py +30 -0
- traceforge/tracking/models.py +115 -0
- traceforge/tracking/phase_tracker.py +288 -0
- traceforge/types.py +315 -0
- traceforge_toolkit-0.1.0.dist-info/METADATA +188 -0
- traceforge_toolkit-0.1.0.dist-info/RECORD +205 -0
- traceforge_toolkit-0.1.0.dist-info/WHEEL +4 -0
- traceforge_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- traceforge_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Preprocessors for normalizing framework-specific event structures.
|
|
2
|
+
|
|
3
|
+
Each preprocessor transforms raw dicts into flat dicts suitable for
|
|
4
|
+
type_field-based YAML mapping. They handle compound discriminators,
|
|
5
|
+
nested structures, and field-presence-based typing.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from traceforge.preprocessors.registry import (
|
|
9
|
+
PreprocessorFn,
|
|
10
|
+
get_preprocessor,
|
|
11
|
+
register_preprocessor,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
# Import all preprocessor modules to trigger registration
|
|
15
|
+
import traceforge.preprocessors.cline # noqa: F401
|
|
16
|
+
import traceforge.preprocessors.continue_dev # noqa: F401
|
|
17
|
+
import traceforge.preprocessors.goose # noqa: F401
|
|
18
|
+
import traceforge.preprocessors.openhands # noqa: F401
|
|
19
|
+
import traceforge.preprocessors.pydantic_ai # noqa: F401
|
|
20
|
+
import traceforge.preprocessors.openai_agents # noqa: F401
|
|
21
|
+
import traceforge.preprocessors.claude # noqa: F401
|
|
22
|
+
import traceforge.preprocessors.smolagents # noqa: F401
|
|
23
|
+
import traceforge.preprocessors.codex # noqa: F401
|
|
24
|
+
import traceforge.preprocessors.amazonq # noqa: F401
|
|
25
|
+
import traceforge.preprocessors.maf_transcript # noqa: F401
|
|
26
|
+
import traceforge.preprocessors.opencode # noqa: F401
|
|
27
|
+
import traceforge.preprocessors.copilot_vscode # noqa: F401
|
|
28
|
+
import traceforge.preprocessors.antigravity # noqa: F401
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"PreprocessorFn",
|
|
32
|
+
"get_preprocessor",
|
|
33
|
+
"register_preprocessor",
|
|
34
|
+
]
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""Amazon Q Developer preprocessor — extract tool calls from SQLite conversations table."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _amazonq_text(content: Any) -> str:
|
|
12
|
+
"""Flatten an Amazon Q ToolUseResultBlock list into text."""
|
|
13
|
+
if isinstance(content, str):
|
|
14
|
+
return content
|
|
15
|
+
if isinstance(content, list):
|
|
16
|
+
parts = []
|
|
17
|
+
for b in content:
|
|
18
|
+
if isinstance(b, dict):
|
|
19
|
+
# externally-tagged: {"Text": "..."} | {"Json": {...}}
|
|
20
|
+
if "Text" in b:
|
|
21
|
+
parts.append(str(b["Text"]))
|
|
22
|
+
elif "Json" in b:
|
|
23
|
+
parts.append(json.dumps(b["Json"]))
|
|
24
|
+
elif b.get("type") == "text":
|
|
25
|
+
parts.append(b.get("text", ""))
|
|
26
|
+
return " ".join(p for p in parts if p)
|
|
27
|
+
return ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _expand_amazonq_pair(entry: dict[str, Any], cid: str) -> list[dict[str, Any]]:
|
|
31
|
+
"""Expand one HistoryEntry {user, assistant} pair into block events.
|
|
32
|
+
|
|
33
|
+
Current Amazon Q schema uses externally-tagged Rust enums:
|
|
34
|
+
user.content = {Prompt:{prompt}} | {ToolUseResults:{tool_use_results:[...]}}
|
|
35
|
+
| {CancelledToolUses:{prompt, tool_use_results}}
|
|
36
|
+
assistant = {Response:{content}} | {ToolUse:{content, tool_uses:[...]}}
|
|
37
|
+
tool_use = {id, name, args, ...}; result = {tool_use_id, content, status}
|
|
38
|
+
"""
|
|
39
|
+
out: list[dict[str, Any]] = []
|
|
40
|
+
user = entry.get("user")
|
|
41
|
+
if isinstance(user, dict):
|
|
42
|
+
content = user.get("content", {})
|
|
43
|
+
prompt = None
|
|
44
|
+
results = None
|
|
45
|
+
if isinstance(content, dict):
|
|
46
|
+
if "Prompt" in content:
|
|
47
|
+
prompt = (content["Prompt"] or {}).get("prompt")
|
|
48
|
+
elif "CancelledToolUses" in content:
|
|
49
|
+
cancelled = content["CancelledToolUses"] or {}
|
|
50
|
+
prompt = cancelled.get("prompt")
|
|
51
|
+
results = cancelled.get("tool_use_results")
|
|
52
|
+
elif "ToolUseResults" in content:
|
|
53
|
+
results = (content["ToolUseResults"] or {}).get("tool_use_results")
|
|
54
|
+
if prompt:
|
|
55
|
+
out.append({"block_type": "message.user", "conversation_id": cid, "content": prompt})
|
|
56
|
+
for r in results or []:
|
|
57
|
+
if not isinstance(r, dict):
|
|
58
|
+
continue
|
|
59
|
+
out.append(
|
|
60
|
+
{
|
|
61
|
+
"block_type": "tool.result",
|
|
62
|
+
"conversation_id": cid,
|
|
63
|
+
"tool_call_id": r.get("tool_use_id", ""),
|
|
64
|
+
"is_error": r.get("status") == "Error",
|
|
65
|
+
"output": _amazonq_text(r.get("content", "")),
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
assistant = entry.get("assistant")
|
|
70
|
+
if isinstance(assistant, dict):
|
|
71
|
+
body = assistant.get("Response") or assistant.get("ToolUse") or {}
|
|
72
|
+
if isinstance(body, dict):
|
|
73
|
+
if body.get("content"):
|
|
74
|
+
out.append(
|
|
75
|
+
{
|
|
76
|
+
"block_type": "message.assistant",
|
|
77
|
+
"conversation_id": cid,
|
|
78
|
+
"content": body["content"],
|
|
79
|
+
}
|
|
80
|
+
)
|
|
81
|
+
for tu in body.get("tool_uses", []) or []:
|
|
82
|
+
if not isinstance(tu, dict):
|
|
83
|
+
continue
|
|
84
|
+
out.append(
|
|
85
|
+
{
|
|
86
|
+
"block_type": "tool.call",
|
|
87
|
+
"conversation_id": cid,
|
|
88
|
+
"tool_call_id": tu.get("id", ""),
|
|
89
|
+
"tool_name": tu.get("name", ""),
|
|
90
|
+
"arguments": tu.get("args", {}),
|
|
91
|
+
}
|
|
92
|
+
)
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@register_preprocessor("amazonq")
|
|
97
|
+
def preprocess_amazonq(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
98
|
+
"""Normalize an Amazon Q conversation row into tool call events.
|
|
99
|
+
|
|
100
|
+
Amazon Q Developer stores conversations in:
|
|
101
|
+
~/.local/share/amazon-q/data.sqlite3 (Linux)
|
|
102
|
+
~/Library/Application Support/amazon-q/data.sqlite3 (macOS)
|
|
103
|
+
|
|
104
|
+
The conversations table has a JSON `value` column containing the full
|
|
105
|
+
conversation structure. This preprocessor is invoked per-row after the
|
|
106
|
+
source reads rows from SQLite.
|
|
107
|
+
|
|
108
|
+
Expected input shape (one row from conversations table):
|
|
109
|
+
{
|
|
110
|
+
"conversation_id": "...",
|
|
111
|
+
"value": "{...}", # JSON string with messages array
|
|
112
|
+
"created_at": "...",
|
|
113
|
+
"updated_at": "..."
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
The JSON value structure contains messages with role/content pairs.
|
|
117
|
+
Tool calls are embedded in assistant messages as tool_use content blocks.
|
|
118
|
+
"""
|
|
119
|
+
# If already preprocessed (has block_type but no value/data), pass through
|
|
120
|
+
if "block_type" in obj and "value" not in obj and "data" not in obj:
|
|
121
|
+
return [obj]
|
|
122
|
+
|
|
123
|
+
conversation_id = obj.get("conversation_id", obj.get("id", ""))
|
|
124
|
+
value_raw = obj.get("value", obj.get("data", ""))
|
|
125
|
+
|
|
126
|
+
# Parse the JSON value column
|
|
127
|
+
if isinstance(value_raw, str):
|
|
128
|
+
try:
|
|
129
|
+
value = json.loads(value_raw)
|
|
130
|
+
except (json.JSONDecodeError, TypeError):
|
|
131
|
+
return [
|
|
132
|
+
{
|
|
133
|
+
"block_type": "raw.parse_error",
|
|
134
|
+
"conversation_id": conversation_id,
|
|
135
|
+
"_raw": value_raw,
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
elif isinstance(value_raw, dict):
|
|
139
|
+
value = value_raw
|
|
140
|
+
else:
|
|
141
|
+
return [obj]
|
|
142
|
+
|
|
143
|
+
messages = value.get("messages", value.get("history", []))
|
|
144
|
+
if not isinstance(messages, list):
|
|
145
|
+
return [{"block_type": "raw.no_messages", "conversation_id": conversation_id, **value}]
|
|
146
|
+
|
|
147
|
+
# Amazon Q persists conversation_id INSIDE the value blob; the SQLite `key`
|
|
148
|
+
# column is the working directory, not the id.
|
|
149
|
+
conversation_id = value.get("conversation_id", conversation_id)
|
|
150
|
+
results: list[dict[str, Any]] = []
|
|
151
|
+
|
|
152
|
+
# Current Amazon Q format: history is a list of {user, assistant, request_metadata}
|
|
153
|
+
# pairs, with externally-tagged Rust enums for content. Detect and expand.
|
|
154
|
+
if value.get("history") and any(
|
|
155
|
+
isinstance(e, dict) and ("user" in e or "assistant" in e) for e in messages
|
|
156
|
+
):
|
|
157
|
+
for entry in messages:
|
|
158
|
+
if not isinstance(entry, dict):
|
|
159
|
+
continue
|
|
160
|
+
results.extend(_expand_amazonq_pair(entry, conversation_id))
|
|
161
|
+
return (
|
|
162
|
+
results
|
|
163
|
+
if results
|
|
164
|
+
else [{"block_type": "raw.empty", "conversation_id": conversation_id}]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
for msg in messages:
|
|
168
|
+
if not isinstance(msg, dict):
|
|
169
|
+
continue
|
|
170
|
+
role = msg.get("role", "")
|
|
171
|
+
content = msg.get("content", "")
|
|
172
|
+
|
|
173
|
+
# Content can be a string or array of content blocks
|
|
174
|
+
if isinstance(content, str):
|
|
175
|
+
results.append(
|
|
176
|
+
{
|
|
177
|
+
"block_type": f"message.{role}" if role else "message.unknown",
|
|
178
|
+
"conversation_id": conversation_id,
|
|
179
|
+
"content": content,
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
elif isinstance(content, list):
|
|
183
|
+
for block in content:
|
|
184
|
+
if not isinstance(block, dict):
|
|
185
|
+
continue
|
|
186
|
+
block_type = block.get("type", "")
|
|
187
|
+
|
|
188
|
+
if block_type == "text":
|
|
189
|
+
results.append(
|
|
190
|
+
{
|
|
191
|
+
"block_type": f"message.{role}" if role else "message.unknown",
|
|
192
|
+
"conversation_id": conversation_id,
|
|
193
|
+
"content": block.get("text", ""),
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
elif block_type == "tool_use":
|
|
198
|
+
results.append(
|
|
199
|
+
{
|
|
200
|
+
"block_type": "tool.call",
|
|
201
|
+
"conversation_id": conversation_id,
|
|
202
|
+
"tool_call_id": block.get("id", ""),
|
|
203
|
+
"tool_name": block.get("name", ""),
|
|
204
|
+
"arguments": block.get("input", {}),
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
elif block_type == "tool_result":
|
|
209
|
+
result_content = block.get("content", "")
|
|
210
|
+
if isinstance(result_content, list):
|
|
211
|
+
result_content = " ".join(
|
|
212
|
+
b.get("text", "") for b in result_content if isinstance(b, dict)
|
|
213
|
+
)
|
|
214
|
+
results.append(
|
|
215
|
+
{
|
|
216
|
+
"block_type": "tool.result",
|
|
217
|
+
"conversation_id": conversation_id,
|
|
218
|
+
"tool_call_id": block.get("tool_use_id", ""),
|
|
219
|
+
"is_error": block.get("is_error", False),
|
|
220
|
+
"output": result_content,
|
|
221
|
+
}
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return results if results else [{"block_type": "raw.empty", "conversation_id": conversation_id}]
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""Google Antigravity preprocessor — replay the SDK ``conversation.history``.
|
|
2
|
+
|
|
3
|
+
The Antigravity Python SDK exposes a finished agent run as
|
|
4
|
+
``agent.conversation.history``: a ``list[google.antigravity.types.Step]`` that the
|
|
5
|
+
capture scripts dump verbatim with ``model_dump(mode="json")``. Each JSONL line is
|
|
6
|
+
therefore one serialized ``Step`` with these fields (see the SDK's
|
|
7
|
+
``local_connection.LocalConnectionStep.from_dict`` which constructs them):
|
|
8
|
+
|
|
9
|
+
id, step_index, type, source, target, status, content, content_delta,
|
|
10
|
+
thinking, thinking_delta, tool_calls[], error, is_complete_response,
|
|
11
|
+
structured_output, usage_metadata
|
|
12
|
+
|
|
13
|
+
There is no single native ``type_field`` because the event identity is a compound
|
|
14
|
+
of ``type`` (StepType) and ``source`` (StepSource) — e.g. a ``TEXT_RESPONSE`` is a
|
|
15
|
+
user message or an assistant message depending on its source. This preprocessor
|
|
16
|
+
synthesizes a flat ``event_type`` discriminator from that pair and fans a
|
|
17
|
+
``TOOL_CALL`` step out into one event per entry in ``tool_calls`` (an
|
|
18
|
+
Antigravity step may batch several calls).
|
|
19
|
+
|
|
20
|
+
Authoritative shape note: builtin tool *output* is consumed inside the Go
|
|
21
|
+
localharness and fed back to the model — it is never surfaced as a result field on
|
|
22
|
+
a history Step. So history carries tool CALLS (with args) but not tool RESULTS;
|
|
23
|
+
this preprocessor intentionally emits no tool-result event.
|
|
24
|
+
|
|
25
|
+
Stateless: each Step is self-describing, so no cross-line reconstruction is needed
|
|
26
|
+
(unlike the copilot_vscode journal). Enum fields serialize to their string values
|
|
27
|
+
(``"TEXT_RESPONSE"``, ``"MODEL"``, ``"TARGET_ENVIRONMENT"``, ``"DONE"`` ...).
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
from typing import Any
|
|
33
|
+
|
|
34
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
35
|
+
|
|
36
|
+
# StepType (string) -> handler. TEXT_RESPONSE is resolved by source; TOOL_CALL
|
|
37
|
+
# fans out; the rest are 1:1.
|
|
38
|
+
_TEXT = "TEXT_RESPONSE"
|
|
39
|
+
_THINKING = "THINKING"
|
|
40
|
+
_TOOL_CALL = "TOOL_CALL"
|
|
41
|
+
_SYSTEM = "SYSTEM_MESSAGE"
|
|
42
|
+
_FINISH = "FINISH"
|
|
43
|
+
_COMPACTION = "COMPACTION"
|
|
44
|
+
|
|
45
|
+
# StepSource (string) for a TEXT_RESPONSE -> event_type.
|
|
46
|
+
_TEXT_BY_SOURCE = {
|
|
47
|
+
"USER": "user_message",
|
|
48
|
+
"MODEL": "assistant_message",
|
|
49
|
+
"SYSTEM": "system_message",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _common(obj: dict[str, Any]) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"step_id": obj.get("id"),
|
|
56
|
+
"step_index": obj.get("step_index"),
|
|
57
|
+
"source": obj.get("source"),
|
|
58
|
+
"target": obj.get("target"),
|
|
59
|
+
"status": obj.get("status"),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@register_preprocessor("antigravity")
|
|
64
|
+
def preprocess_antigravity(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
65
|
+
"""Expand one serialized Antigravity ``types.Step`` into typed flat events."""
|
|
66
|
+
# Passthrough for already-typed rows (generic conformance probes feed a line
|
|
67
|
+
# keyed by the post-preprocessor type_field, with no native Step ``type``).
|
|
68
|
+
if "type" not in obj and obj.get("event_type"):
|
|
69
|
+
return [obj]
|
|
70
|
+
|
|
71
|
+
stype = obj.get("type")
|
|
72
|
+
base = _common(obj)
|
|
73
|
+
|
|
74
|
+
if stype == _TEXT:
|
|
75
|
+
event_type = _TEXT_BY_SOURCE.get(obj.get("source"), "assistant_message")
|
|
76
|
+
return [{**base, "event_type": event_type, "content": obj.get("content")}]
|
|
77
|
+
|
|
78
|
+
if stype == _THINKING:
|
|
79
|
+
return [{**base, "event_type": "thinking", "content": obj.get("thinking")}]
|
|
80
|
+
|
|
81
|
+
if stype == _SYSTEM:
|
|
82
|
+
return [{**base, "event_type": "system_message", "content": obj.get("content")}]
|
|
83
|
+
|
|
84
|
+
if stype == _TOOL_CALL:
|
|
85
|
+
out: list[dict[str, Any]] = []
|
|
86
|
+
for call in obj.get("tool_calls") or []:
|
|
87
|
+
if not isinstance(call, dict):
|
|
88
|
+
continue
|
|
89
|
+
out.append(
|
|
90
|
+
{
|
|
91
|
+
**base,
|
|
92
|
+
"event_type": "tool_call",
|
|
93
|
+
"tool_name": call.get("name"),
|
|
94
|
+
"args": call.get("args"),
|
|
95
|
+
"tool_call_id": call.get("id"),
|
|
96
|
+
"path": call.get("canonical_path"),
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
return out
|
|
100
|
+
|
|
101
|
+
if stype == _FINISH:
|
|
102
|
+
return [
|
|
103
|
+
{
|
|
104
|
+
**base,
|
|
105
|
+
"event_type": "finish",
|
|
106
|
+
"content": obj.get("content"),
|
|
107
|
+
"structured_output": obj.get("structured_output"),
|
|
108
|
+
}
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
if stype == _COMPACTION:
|
|
112
|
+
return [{**base, "event_type": "compaction"}]
|
|
113
|
+
|
|
114
|
+
# Unknown / UNKNOWN step types deliberately fall through (no event_type) so
|
|
115
|
+
# the golden 0-raw test surfaces genuine upstream drift rather than masking it.
|
|
116
|
+
return []
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Claude preprocessor — flatten content blocks into individual events."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@register_preprocessor("claude")
|
|
11
|
+
def preprocess_claude(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
12
|
+
"""Flatten Claude wire-format messages into per-block dicts.
|
|
13
|
+
|
|
14
|
+
Claude wire format:
|
|
15
|
+
- user: {type: "user", message: {content: "..."}}
|
|
16
|
+
- assistant: {type: "assistant", message: {content: [{type: "text", ...}, ...]}}
|
|
17
|
+
- result: {type: "result", subtype: ..., usage: {...}, ...}
|
|
18
|
+
- system: {type: "system", ...}
|
|
19
|
+
|
|
20
|
+
Assistant messages contain a list of content blocks, each of which
|
|
21
|
+
becomes a separate normalized dict with a synthesized ``block_type``
|
|
22
|
+
discriminator (e.g. "assistant.text", "assistant.tool_use").
|
|
23
|
+
|
|
24
|
+
User messages with string content get block_type "user.text".
|
|
25
|
+
Result messages pass through with block_type "result".
|
|
26
|
+
"""
|
|
27
|
+
msg_type = obj.get("type")
|
|
28
|
+
if not msg_type:
|
|
29
|
+
return [obj]
|
|
30
|
+
|
|
31
|
+
if msg_type == "result":
|
|
32
|
+
normalized = dict(obj)
|
|
33
|
+
normalized["block_type"] = "result"
|
|
34
|
+
# Flatten usage dict to top level for dot-path access
|
|
35
|
+
usage = normalized.get("usage")
|
|
36
|
+
if isinstance(usage, dict):
|
|
37
|
+
for k, v in usage.items():
|
|
38
|
+
normalized[f"usage_{k}"] = v
|
|
39
|
+
return [normalized]
|
|
40
|
+
|
|
41
|
+
if msg_type == "system":
|
|
42
|
+
normalized = dict(obj)
|
|
43
|
+
normalized["block_type"] = "system"
|
|
44
|
+
return [normalized]
|
|
45
|
+
|
|
46
|
+
message = obj.get("message")
|
|
47
|
+
if not isinstance(message, dict):
|
|
48
|
+
return [obj]
|
|
49
|
+
|
|
50
|
+
content = message.get("content")
|
|
51
|
+
|
|
52
|
+
if msg_type == "user":
|
|
53
|
+
if isinstance(content, str):
|
|
54
|
+
return [{"block_type": "user.text", "content": content}]
|
|
55
|
+
if isinstance(content, list):
|
|
56
|
+
return _flatten_blocks(content, "user")
|
|
57
|
+
return [{"block_type": "user.text", "content": str(content) if content else ""}]
|
|
58
|
+
|
|
59
|
+
if msg_type == "assistant":
|
|
60
|
+
if isinstance(content, list):
|
|
61
|
+
return _flatten_blocks(content, "assistant")
|
|
62
|
+
return [obj]
|
|
63
|
+
|
|
64
|
+
return [obj]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _flatten_blocks(blocks: list[Any], context: str) -> list[dict[str, Any]]:
|
|
68
|
+
"""Flatten a list of content blocks into normalized dicts."""
|
|
69
|
+
results: list[dict[str, Any]] = []
|
|
70
|
+
for block in blocks:
|
|
71
|
+
if not isinstance(block, dict):
|
|
72
|
+
continue
|
|
73
|
+
block_kind = block.get("type", "unknown")
|
|
74
|
+
normalized = dict(block)
|
|
75
|
+
normalized["block_type"] = f"{context}.{block_kind}"
|
|
76
|
+
|
|
77
|
+
# For tool_result blocks, handle list-of-dicts content → joined text
|
|
78
|
+
if block_kind == "tool_result":
|
|
79
|
+
normalized["success"] = not block.get("is_error", False)
|
|
80
|
+
raw_content = block.get("content")
|
|
81
|
+
if isinstance(raw_content, list):
|
|
82
|
+
parts = []
|
|
83
|
+
for item in raw_content:
|
|
84
|
+
if isinstance(item, dict) and item.get("type") == "text":
|
|
85
|
+
parts.append(item.get("text", ""))
|
|
86
|
+
elif isinstance(item, str):
|
|
87
|
+
parts.append(item)
|
|
88
|
+
normalized["content_text"] = "\n".join(parts) if parts else None
|
|
89
|
+
elif isinstance(raw_content, str):
|
|
90
|
+
normalized["content_text"] = raw_content
|
|
91
|
+
else:
|
|
92
|
+
normalized["content_text"] = None
|
|
93
|
+
|
|
94
|
+
results.append(normalized)
|
|
95
|
+
return results
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Cline preprocessor — compound type + say/ask subtype discriminator."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@register_preprocessor("cline")
|
|
12
|
+
def preprocess_cline(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
13
|
+
"""Synthesize compound type from Cline's type + say/ask subtype.
|
|
14
|
+
|
|
15
|
+
Cline events have type="ask"|"say" with the subtype in the
|
|
16
|
+
corresponding field. Synthesizes "say.api_req_started" etc.
|
|
17
|
+
Parses JSON text field for structured subtypes into top-level fields.
|
|
18
|
+
"""
|
|
19
|
+
msg_type = obj.get("type") # "ask" or "say"
|
|
20
|
+
subtype = obj.get(msg_type) if msg_type in ("ask", "say") else None
|
|
21
|
+
|
|
22
|
+
if subtype:
|
|
23
|
+
normalized = dict(obj)
|
|
24
|
+
normalized["type"] = f"{msg_type}.{subtype}"
|
|
25
|
+
|
|
26
|
+
# Parse JSON text field for known subtypes that embed structured data
|
|
27
|
+
text = normalized.get("text")
|
|
28
|
+
if text and subtype in ("api_req_started", "api_req_finished", "tool"):
|
|
29
|
+
try:
|
|
30
|
+
parsed = json.loads(text)
|
|
31
|
+
if isinstance(parsed, dict):
|
|
32
|
+
normalized["parsed"] = parsed
|
|
33
|
+
except (json.JSONDecodeError, ValueError):
|
|
34
|
+
pass
|
|
35
|
+
return [normalized]
|
|
36
|
+
return [obj]
|