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,311 @@
|
|
|
1
|
+
"""Codex CLI preprocessor — normalize JSONL rollout lines into tool call events."""
|
|
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("codex")
|
|
12
|
+
def preprocess_codex(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
13
|
+
"""Normalize a Codex CLI rollout line into traceforge-friendly dicts.
|
|
14
|
+
|
|
15
|
+
Codex CLI JSONL format (~/.codex/sessions/*.jsonl):
|
|
16
|
+
Each line: {"timestamp": "...", "type": "...", "payload": {...}}
|
|
17
|
+
|
|
18
|
+
Type discriminator values:
|
|
19
|
+
- "session_meta" — session metadata (first line)
|
|
20
|
+
- "response_item" — model outputs (function_call, function_call_output, message)
|
|
21
|
+
- "event_msg" — lifecycle events (exec_command_begin/end, mcp_tool_call_begin/end)
|
|
22
|
+
|
|
23
|
+
We flatten the double-type nesting into a single `block_type` field:
|
|
24
|
+
- response_item.function_call → "tool.shell_call"
|
|
25
|
+
- response_item.function_call_output → "tool.shell_result"
|
|
26
|
+
- event_msg.exec_command_begin → "tool.exec_begin"
|
|
27
|
+
- event_msg.exec_command_end → "tool.exec_end"
|
|
28
|
+
- event_msg.mcp_tool_call_begin → "tool.mcp_call"
|
|
29
|
+
- event_msg.mcp_tool_call_end → "tool.mcp_result"
|
|
30
|
+
- session_meta → "session.meta"
|
|
31
|
+
- response_item.message (role=assistant) → "message.assistant"
|
|
32
|
+
- event_msg.user_message → "message.user"
|
|
33
|
+
"""
|
|
34
|
+
top_type = obj.get("type", "")
|
|
35
|
+
payload = obj.get("payload") or {}
|
|
36
|
+
timestamp = obj.get("timestamp", "")
|
|
37
|
+
|
|
38
|
+
# If already preprocessed (has block_type but no rollout structure), pass through
|
|
39
|
+
if "block_type" in obj and not top_type:
|
|
40
|
+
return [obj]
|
|
41
|
+
|
|
42
|
+
if not isinstance(payload, dict):
|
|
43
|
+
return [obj]
|
|
44
|
+
|
|
45
|
+
base = {"_timestamp": timestamp}
|
|
46
|
+
|
|
47
|
+
if top_type == "session_meta":
|
|
48
|
+
return [
|
|
49
|
+
{
|
|
50
|
+
**base,
|
|
51
|
+
"block_type": "session.meta",
|
|
52
|
+
"session_id": payload.get("id", ""),
|
|
53
|
+
"model_provider": payload.get("model_provider", ""),
|
|
54
|
+
"cwd": payload.get("cwd", ""),
|
|
55
|
+
"cli_version": payload.get("cli_version", ""),
|
|
56
|
+
}
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
if top_type == "response_item":
|
|
60
|
+
return _handle_response_item(payload, base)
|
|
61
|
+
|
|
62
|
+
if top_type == "event_msg":
|
|
63
|
+
return _handle_event_msg(payload, base)
|
|
64
|
+
|
|
65
|
+
if top_type == "turn_context":
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
# Pass through unknown types
|
|
69
|
+
return [{**base, "block_type": f"raw.{top_type}", **payload}]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _handle_response_item(payload: dict[str, Any], base: dict[str, Any]) -> list[dict[str, Any]]:
|
|
73
|
+
"""Handle response_item lines (model output)."""
|
|
74
|
+
item_type = payload.get("type", "")
|
|
75
|
+
|
|
76
|
+
if item_type == "function_call":
|
|
77
|
+
# Shell/built-in tool call from model
|
|
78
|
+
arguments_raw = payload.get("arguments", "{}")
|
|
79
|
+
# arguments is a JSON-encoded string for function_call
|
|
80
|
+
try:
|
|
81
|
+
arguments = (
|
|
82
|
+
json.loads(arguments_raw) if isinstance(arguments_raw, str) else arguments_raw
|
|
83
|
+
)
|
|
84
|
+
except (json.JSONDecodeError, TypeError):
|
|
85
|
+
arguments = {"_raw": arguments_raw}
|
|
86
|
+
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
**base,
|
|
90
|
+
"block_type": "tool.shell_call",
|
|
91
|
+
"call_id": payload.get("call_id", ""),
|
|
92
|
+
"tool_name": payload.get("name", "shell"),
|
|
93
|
+
"arguments": arguments,
|
|
94
|
+
"namespace": payload.get("namespace"),
|
|
95
|
+
}
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
if item_type == "function_call_output":
|
|
99
|
+
output = payload.get("output", "")
|
|
100
|
+
return [
|
|
101
|
+
{
|
|
102
|
+
**base,
|
|
103
|
+
"block_type": "tool.shell_result",
|
|
104
|
+
"call_id": payload.get("call_id", ""),
|
|
105
|
+
"output": output,
|
|
106
|
+
}
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
if item_type == "message":
|
|
110
|
+
role = payload.get("role", "")
|
|
111
|
+
content_blocks = payload.get("content", [])
|
|
112
|
+
if role == "developer":
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
text = ""
|
|
116
|
+
if isinstance(content_blocks, list):
|
|
117
|
+
text = " ".join(
|
|
118
|
+
b.get("text", "")
|
|
119
|
+
for b in content_blocks
|
|
120
|
+
if isinstance(b, dict) and b.get("type") in {"output_text", "input_text"}
|
|
121
|
+
)
|
|
122
|
+
elif isinstance(content_blocks, str):
|
|
123
|
+
text = content_blocks
|
|
124
|
+
return [
|
|
125
|
+
{
|
|
126
|
+
**base,
|
|
127
|
+
"block_type": f"message.{role}" if role else "message.unknown",
|
|
128
|
+
"content": text,
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
if item_type == "reasoning":
|
|
133
|
+
return []
|
|
134
|
+
|
|
135
|
+
# Other response items (e.g. local_shell_call, custom_tool_call)
|
|
136
|
+
if item_type == "local_shell_call":
|
|
137
|
+
action = payload.get("action", {})
|
|
138
|
+
return [
|
|
139
|
+
{
|
|
140
|
+
**base,
|
|
141
|
+
"block_type": "tool.shell_call",
|
|
142
|
+
"call_id": payload.get("call_id", ""),
|
|
143
|
+
"tool_name": "shell",
|
|
144
|
+
"arguments": action if isinstance(action, dict) else {"_raw": action},
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
|
|
148
|
+
if item_type == "custom_tool_call":
|
|
149
|
+
input_raw = payload.get("input", "{}")
|
|
150
|
+
try:
|
|
151
|
+
arguments = json.loads(input_raw) if isinstance(input_raw, str) else input_raw
|
|
152
|
+
except (json.JSONDecodeError, TypeError):
|
|
153
|
+
arguments = {"_raw": input_raw}
|
|
154
|
+
return [
|
|
155
|
+
{
|
|
156
|
+
**base,
|
|
157
|
+
"block_type": "tool.custom_call",
|
|
158
|
+
"call_id": payload.get("call_id", ""),
|
|
159
|
+
"tool_name": payload.get("name", ""),
|
|
160
|
+
"arguments": arguments,
|
|
161
|
+
}
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
if item_type == "custom_tool_call_output":
|
|
165
|
+
return [
|
|
166
|
+
{
|
|
167
|
+
**base,
|
|
168
|
+
"block_type": "tool.custom_result",
|
|
169
|
+
"call_id": payload.get("call_id", ""),
|
|
170
|
+
"tool_name": payload.get("name", ""),
|
|
171
|
+
"output": payload.get("output", ""),
|
|
172
|
+
}
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
**base,
|
|
178
|
+
"block_type": f"response.{item_type}",
|
|
179
|
+
**{k: v for k, v in payload.items() if k != "type"},
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _handle_event_msg(payload: dict[str, Any], base: dict[str, Any]) -> list[dict[str, Any]]:
|
|
185
|
+
"""Handle event_msg lines (lifecycle events)."""
|
|
186
|
+
event_type = payload.get("type", "")
|
|
187
|
+
|
|
188
|
+
if event_type == "exec_command_begin":
|
|
189
|
+
return [
|
|
190
|
+
{
|
|
191
|
+
**base,
|
|
192
|
+
"block_type": "tool.exec_begin",
|
|
193
|
+
"call_id": payload.get("call_id", ""),
|
|
194
|
+
"command": payload.get("command", []),
|
|
195
|
+
"cwd": payload.get("cwd", ""),
|
|
196
|
+
}
|
|
197
|
+
]
|
|
198
|
+
|
|
199
|
+
if event_type == "exec_command_end":
|
|
200
|
+
return [
|
|
201
|
+
{
|
|
202
|
+
**base,
|
|
203
|
+
"block_type": "tool.exec_end",
|
|
204
|
+
"call_id": payload.get("call_id", ""),
|
|
205
|
+
"command": payload.get("command", []),
|
|
206
|
+
"cwd": payload.get("cwd", ""),
|
|
207
|
+
"exit_code": payload.get("exit_code"),
|
|
208
|
+
"stdout": payload.get("stdout", ""),
|
|
209
|
+
"stderr": payload.get("stderr", ""),
|
|
210
|
+
"status": payload.get("status", ""),
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
if event_type == "patch_apply_begin":
|
|
215
|
+
return [
|
|
216
|
+
{
|
|
217
|
+
**base,
|
|
218
|
+
"block_type": "tool.exec_begin",
|
|
219
|
+
"call_id": payload.get("call_id", ""),
|
|
220
|
+
"command": "apply_patch",
|
|
221
|
+
"cwd": payload.get("cwd", ""),
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
if event_type == "patch_apply_end":
|
|
226
|
+
return [
|
|
227
|
+
{
|
|
228
|
+
**base,
|
|
229
|
+
"block_type": "tool.exec_end",
|
|
230
|
+
"call_id": payload.get("call_id", ""),
|
|
231
|
+
"command": "apply_patch",
|
|
232
|
+
"cwd": payload.get("cwd", ""),
|
|
233
|
+
"exit_code": 0 if payload.get("success") else 1,
|
|
234
|
+
"stdout": payload.get("stdout", ""),
|
|
235
|
+
"stderr": payload.get("stderr", ""),
|
|
236
|
+
"status": payload.get("status", ""),
|
|
237
|
+
"changes": payload.get("changes", {}),
|
|
238
|
+
}
|
|
239
|
+
]
|
|
240
|
+
|
|
241
|
+
if event_type == "mcp_tool_call_begin":
|
|
242
|
+
invocation = payload.get("invocation") or {}
|
|
243
|
+
return [
|
|
244
|
+
{
|
|
245
|
+
**base,
|
|
246
|
+
"block_type": "tool.mcp_call",
|
|
247
|
+
"call_id": payload.get("call_id", ""),
|
|
248
|
+
"tool_name": invocation.get("tool", ""),
|
|
249
|
+
"server": invocation.get("server", ""),
|
|
250
|
+
"arguments": invocation.get("arguments"),
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
if event_type == "mcp_tool_call_end":
|
|
255
|
+
invocation = payload.get("invocation") or {}
|
|
256
|
+
result = payload.get("result", {})
|
|
257
|
+
# Result is {"Ok": {...}} or {"Err": "..."}
|
|
258
|
+
ok_val = result.get("Ok", {}) if isinstance(result, dict) else {}
|
|
259
|
+
err_val = result.get("Err") if isinstance(result, dict) else None
|
|
260
|
+
return [
|
|
261
|
+
{
|
|
262
|
+
**base,
|
|
263
|
+
"block_type": "tool.mcp_result",
|
|
264
|
+
"call_id": payload.get("call_id", ""),
|
|
265
|
+
"tool_name": invocation.get("tool", ""),
|
|
266
|
+
"server": invocation.get("server", ""),
|
|
267
|
+
"is_error": err_val is not None
|
|
268
|
+
or (isinstance(ok_val, dict) and ok_val.get("is_error", False)),
|
|
269
|
+
"output": err_val if err_val else ok_val,
|
|
270
|
+
}
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
if event_type == "exec_approval_request":
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
**base,
|
|
277
|
+
"block_type": "tool.approval_request",
|
|
278
|
+
"call_id": payload.get("call_id", ""),
|
|
279
|
+
"command": payload.get("command", ""),
|
|
280
|
+
}
|
|
281
|
+
]
|
|
282
|
+
|
|
283
|
+
if event_type == "user_message":
|
|
284
|
+
return [
|
|
285
|
+
{
|
|
286
|
+
**base,
|
|
287
|
+
"block_type": "message.user",
|
|
288
|
+
"content": payload.get("message", ""),
|
|
289
|
+
}
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
if event_type == "agent_message":
|
|
293
|
+
return [
|
|
294
|
+
{
|
|
295
|
+
**base,
|
|
296
|
+
"block_type": "message.assistant",
|
|
297
|
+
"content": payload.get("message", ""),
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
|
|
301
|
+
if event_type in {"task_started", "task_complete", "token_count"}:
|
|
302
|
+
return []
|
|
303
|
+
|
|
304
|
+
# Pass through other event types
|
|
305
|
+
return [
|
|
306
|
+
{
|
|
307
|
+
**base,
|
|
308
|
+
"block_type": f"event.{event_type}",
|
|
309
|
+
**{k: v for k, v in payload.items() if k != "type"},
|
|
310
|
+
}
|
|
311
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Continue.dev preprocessor — flatten session JSON history into per-message events."""
|
|
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("continue")
|
|
12
|
+
def preprocess_continue(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
13
|
+
"""Flatten Continue.dev session JSON into per-message dicts.
|
|
14
|
+
|
|
15
|
+
Continue.dev session format (~/.continue/sessions/{id}.json):
|
|
16
|
+
{
|
|
17
|
+
"sessionId": "...",
|
|
18
|
+
"title": "...",
|
|
19
|
+
"history": [
|
|
20
|
+
{"message": {"role": "user", "content": "..."}},
|
|
21
|
+
{"message": {"role": "assistant", "content": "...", "toolCalls": [...]}},
|
|
22
|
+
{"message": {"role": "tool", "toolCallId": "...", "content": "..."}}
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Note: Continue.dev persists camelCase keys (toolCalls, toolCallId) — its
|
|
27
|
+
internal TypeScript ChatMessage types are written to disk via JSON.stringify
|
|
28
|
+
with no rename. snake_case (tool_calls/tool_call_id) only appears on the
|
|
29
|
+
OpenAI provider wire format, not in the session file on disk.
|
|
30
|
+
|
|
31
|
+
Each history entry becomes a normalized dict with:
|
|
32
|
+
- block_type: "user.message", "assistant.message", "assistant.tool_use", "tool.result"
|
|
33
|
+
- Flattened message fields
|
|
34
|
+
"""
|
|
35
|
+
history = obj.get("history")
|
|
36
|
+
if not isinstance(history, list):
|
|
37
|
+
return [obj]
|
|
38
|
+
|
|
39
|
+
session_id = obj.get("sessionId", "")
|
|
40
|
+
results: list[dict[str, Any]] = []
|
|
41
|
+
|
|
42
|
+
for entry in history:
|
|
43
|
+
message = entry.get("message") if isinstance(entry, dict) else None
|
|
44
|
+
if not isinstance(message, dict):
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
role = message.get("role", "")
|
|
48
|
+
content = message.get("content")
|
|
49
|
+
# Continue.dev persists session JSON with camelCase keys (TypeScript
|
|
50
|
+
# JSON.stringify of its internal ChatMessage types). snake_case keys
|
|
51
|
+
# only appear on the OpenAI provider wire format, never on disk.
|
|
52
|
+
tool_calls = message.get("toolCalls")
|
|
53
|
+
|
|
54
|
+
if role == "user":
|
|
55
|
+
results.append(
|
|
56
|
+
{
|
|
57
|
+
"block_type": "user.message",
|
|
58
|
+
"session_id": session_id,
|
|
59
|
+
"content": content,
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
elif role == "assistant":
|
|
64
|
+
# Assistant messages may contain both text and tool_calls
|
|
65
|
+
if content:
|
|
66
|
+
results.append(
|
|
67
|
+
{
|
|
68
|
+
"block_type": "assistant.message",
|
|
69
|
+
"session_id": session_id,
|
|
70
|
+
"content": content,
|
|
71
|
+
}
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if isinstance(tool_calls, list):
|
|
75
|
+
for tc in tool_calls:
|
|
76
|
+
if not isinstance(tc, dict):
|
|
77
|
+
continue
|
|
78
|
+
func = tc.get("function", {})
|
|
79
|
+
args_raw = func.get("arguments", "{}") if isinstance(func, dict) else "{}"
|
|
80
|
+
# Parse arguments string to dict for consistency with other preprocessors
|
|
81
|
+
if isinstance(args_raw, str):
|
|
82
|
+
try:
|
|
83
|
+
arguments = json.loads(args_raw)
|
|
84
|
+
except (json.JSONDecodeError, ValueError):
|
|
85
|
+
arguments = {"_raw": args_raw}
|
|
86
|
+
else:
|
|
87
|
+
arguments = args_raw if isinstance(args_raw, dict) else {}
|
|
88
|
+
results.append(
|
|
89
|
+
{
|
|
90
|
+
"block_type": "assistant.tool_use",
|
|
91
|
+
"session_id": session_id,
|
|
92
|
+
"tool_call_id": tc.get("id", ""),
|
|
93
|
+
"tool_name": func.get("name", "") if isinstance(func, dict) else "",
|
|
94
|
+
"arguments": arguments,
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
elif role == "thinking":
|
|
99
|
+
# Claude extended thinking / OpenAI reasoning tokens persisted on disk
|
|
100
|
+
# as role "thinking" (ThinkingChatMessage). Without this they are dropped.
|
|
101
|
+
results.append(
|
|
102
|
+
{
|
|
103
|
+
"block_type": "assistant.thinking",
|
|
104
|
+
"session_id": session_id,
|
|
105
|
+
"content": content,
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
elif role == "tool":
|
|
110
|
+
results.append(
|
|
111
|
+
{
|
|
112
|
+
"block_type": "tool.result",
|
|
113
|
+
"session_id": session_id,
|
|
114
|
+
"tool_call_id": message.get("toolCallId", ""),
|
|
115
|
+
"content": content,
|
|
116
|
+
}
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
return results if results else [obj]
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""VS Code Copilot Chat preprocessor — replay the ChatModel journal.
|
|
2
|
+
|
|
3
|
+
VS Code persists each Copilot Chat session as a line-delimited journal at
|
|
4
|
+
``workspaceStorage/<hash>/chatSessions/<sessionId>.jsonl`` (ChatModel version 3).
|
|
5
|
+
Each physical line is one journal record:
|
|
6
|
+
|
|
7
|
+
* ``{"kind": 0, "v": {...}}`` — full snapshot (session start)
|
|
8
|
+
* ``{"kind": 1, "k": [path...], "v": x}`` — *set* value ``x`` at ``path``
|
|
9
|
+
* ``{"kind": 2, "k": [path...], "v": [...]}`` — *append* (list-extend) at ``path``
|
|
10
|
+
|
|
11
|
+
Paths mix dict keys and integer array indices, e.g.
|
|
12
|
+
``["requests", 2, "response"]``. The interesting state lives under ``requests[]``:
|
|
13
|
+
each request carries the user ``message``, a streamed ``response`` part list
|
|
14
|
+
(``thinking`` / ``toolInvocationSerialized`` / markdown text / file refs), and a
|
|
15
|
+
terminal ``result`` with timings.
|
|
16
|
+
|
|
17
|
+
Because the adapter feeds records one physical line at a time, this preprocessor
|
|
18
|
+
keeps a small amount of module-level state (a mirror of the ``requests`` index ->
|
|
19
|
+
metadata map) so streamed response parts can be attributed to their originating
|
|
20
|
+
request. State is reset whenever a snapshot (``kind == 0``) is seen, which is the
|
|
21
|
+
first record of every session file, so state never bleeds across sessions.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from traceforge.preprocessors.registry import register_preprocessor
|
|
29
|
+
|
|
30
|
+
# Per-session reconstruction state (reset on every snapshot record).
|
|
31
|
+
_REQ_IDS: dict[int, str | None] = {}
|
|
32
|
+
_REQ_MODELS: dict[int, Any] = {}
|
|
33
|
+
_REQ_TS: dict[int, Any] = {}
|
|
34
|
+
_REQ_COUNT = [0] # boxed int so helpers can mutate it
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _reset() -> None:
|
|
38
|
+
_REQ_IDS.clear()
|
|
39
|
+
_REQ_MODELS.clear()
|
|
40
|
+
_REQ_TS.clear()
|
|
41
|
+
_REQ_COUNT[0] = 0
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _agent_id(req: dict[str, Any]) -> Any:
|
|
45
|
+
agent = req.get("agent")
|
|
46
|
+
if isinstance(agent, dict):
|
|
47
|
+
return agent.get("id") or agent.get("name")
|
|
48
|
+
return agent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _emit_part(part: Any, idx: int) -> dict[str, Any] | None:
|
|
52
|
+
"""Turn one streamed ``response`` part into a flat, typed dict."""
|
|
53
|
+
if not isinstance(part, dict):
|
|
54
|
+
return None
|
|
55
|
+
flat = dict(part)
|
|
56
|
+
# Markdown content parts have no ``kind`` discriminator; everything else
|
|
57
|
+
# (thinking / toolInvocationSerialized / inlineReference / ...) does.
|
|
58
|
+
flat["event_type"] = part.get("kind") or "assistant_text"
|
|
59
|
+
flat["request_id"] = _REQ_IDS.get(idx)
|
|
60
|
+
flat["model"] = _REQ_MODELS.get(idx)
|
|
61
|
+
flat["timestamp"] = _REQ_TS.get(idx)
|
|
62
|
+
return flat
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _emit_request(req: dict[str, Any], idx: int) -> list[dict[str, Any]]:
|
|
66
|
+
"""Register a request at ``idx`` and emit its user message + any inline parts."""
|
|
67
|
+
_REQ_IDS[idx] = req.get("requestId")
|
|
68
|
+
_REQ_MODELS[idx] = req.get("modelId")
|
|
69
|
+
_REQ_TS[idx] = req.get("timestamp")
|
|
70
|
+
|
|
71
|
+
message = req.get("message")
|
|
72
|
+
text = message.get("text") if isinstance(message, dict) else message
|
|
73
|
+
|
|
74
|
+
out: list[dict[str, Any]] = [
|
|
75
|
+
{
|
|
76
|
+
"event_type": "user_message",
|
|
77
|
+
"request_id": req.get("requestId"),
|
|
78
|
+
"model": req.get("modelId"),
|
|
79
|
+
"agent": _agent_id(req),
|
|
80
|
+
"text": text,
|
|
81
|
+
"timestamp": req.get("timestamp"),
|
|
82
|
+
}
|
|
83
|
+
]
|
|
84
|
+
# A compacted journal may carry the full response/result inline on the
|
|
85
|
+
# request itself; a streamed one appends them later. Emit whatever is here.
|
|
86
|
+
for part in req.get("response") or []:
|
|
87
|
+
emitted = _emit_part(part, idx)
|
|
88
|
+
if emitted is not None:
|
|
89
|
+
out.append(emitted)
|
|
90
|
+
result = req.get("result")
|
|
91
|
+
if isinstance(result, dict):
|
|
92
|
+
out.append(_emit_result(result, idx))
|
|
93
|
+
return out
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _emit_result(result: dict[str, Any], idx: int) -> dict[str, Any]:
|
|
97
|
+
timings = result.get("timings") if isinstance(result.get("timings"), dict) else {}
|
|
98
|
+
return {
|
|
99
|
+
"event_type": "request_result",
|
|
100
|
+
"request_id": _REQ_IDS.get(idx),
|
|
101
|
+
"model": _REQ_MODELS.get(idx),
|
|
102
|
+
"first_progress_ms": timings.get("firstProgress"),
|
|
103
|
+
"total_elapsed_ms": timings.get("totalElapsed"),
|
|
104
|
+
"timestamp": _REQ_TS.get(idx),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@register_preprocessor("copilot_vscode")
|
|
109
|
+
def preprocess_copilot_vscode(obj: dict[str, Any]) -> list[dict[str, Any]]:
|
|
110
|
+
"""Expand one VS Code ChatModel journal record into typed flat events."""
|
|
111
|
+
# Passthrough for already-typed rows (generic conformance probes feed a line
|
|
112
|
+
# keyed by the post-preprocessor type_field, with no journal ``kind``).
|
|
113
|
+
if "kind" not in obj and obj.get("event_type"):
|
|
114
|
+
return [obj]
|
|
115
|
+
|
|
116
|
+
kind = obj.get("kind")
|
|
117
|
+
|
|
118
|
+
# ── Snapshot: session start (+ any requests already present) ──────────────
|
|
119
|
+
if kind == 0:
|
|
120
|
+
_reset()
|
|
121
|
+
snap = obj.get("v") if isinstance(obj.get("v"), dict) else {}
|
|
122
|
+
out: list[dict[str, Any]] = [
|
|
123
|
+
{
|
|
124
|
+
"event_type": "session_started",
|
|
125
|
+
"session_id": snap.get("sessionId"),
|
|
126
|
+
"version": snap.get("version"),
|
|
127
|
+
"responder": snap.get("responderUsername"),
|
|
128
|
+
"initial_location": snap.get("initialLocation"),
|
|
129
|
+
"timestamp": snap.get("creationDate"),
|
|
130
|
+
}
|
|
131
|
+
]
|
|
132
|
+
for req in snap.get("requests") or []:
|
|
133
|
+
if isinstance(req, dict):
|
|
134
|
+
out.extend(_emit_request(req, _REQ_COUNT[0]))
|
|
135
|
+
_REQ_COUNT[0] += 1
|
|
136
|
+
return out
|
|
137
|
+
|
|
138
|
+
path = obj.get("k")
|
|
139
|
+
value = obj.get("v")
|
|
140
|
+
if not isinstance(path, list) or not path:
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
# ── Append (list-extend) records ──────────────────────────────────────────
|
|
144
|
+
if kind == 2:
|
|
145
|
+
# New requests appended to the top-level requests array.
|
|
146
|
+
if path == ["requests"]:
|
|
147
|
+
out = []
|
|
148
|
+
for req in value or []:
|
|
149
|
+
if isinstance(req, dict):
|
|
150
|
+
out.extend(_emit_request(req, _REQ_COUNT[0]))
|
|
151
|
+
_REQ_COUNT[0] += 1
|
|
152
|
+
return out
|
|
153
|
+
# Streamed response parts appended to requests[i].response.
|
|
154
|
+
if len(path) == 3 and path[0] == "requests" and path[2] == "response":
|
|
155
|
+
idx = path[1]
|
|
156
|
+
out = []
|
|
157
|
+
for part in value or []:
|
|
158
|
+
emitted = _emit_part(part, idx)
|
|
159
|
+
if emitted is not None:
|
|
160
|
+
out.append(emitted)
|
|
161
|
+
return out
|
|
162
|
+
return []
|
|
163
|
+
|
|
164
|
+
# ── Set records: only the terminal per-request result is interesting ──────
|
|
165
|
+
if kind == 1:
|
|
166
|
+
if len(path) == 3 and path[0] == "requests" and path[2] == "result":
|
|
167
|
+
if isinstance(value, dict):
|
|
168
|
+
return [_emit_result(value, path[1])]
|
|
169
|
+
return []
|
|
170
|
+
|
|
171
|
+
return []
|