execweave 0.6.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.
- execweave/__init__.py +3 -0
- execweave/__main__.py +5 -0
- execweave/analysis.py +406 -0
- execweave/backends.py +63 -0
- execweave/benchmark.py +80 -0
- execweave/claude_adapter.py +448 -0
- execweave/claude_hook_cli.py +101 -0
- execweave/claude_record.py +106 -0
- execweave/cli.py +588 -0
- execweave/codex_adapter.py +314 -0
- execweave/codex_hook_cli.py +98 -0
- execweave/codex_record.py +111 -0
- execweave/collector.py +301 -0
- execweave/correlation.py +604 -0
- execweave/cursor_adapter.py +347 -0
- execweave/cursor_hook_cli.py +82 -0
- execweave/cursor_record.py +96 -0
- execweave/filesystem.py +103 -0
- execweave/focus.py +118 -0
- execweave/gemini_adapter.py +265 -0
- execweave/gemini_hook_cli.py +77 -0
- execweave/gemini_record.py +94 -0
- execweave/graph.py +300 -0
- execweave/graph_ops.py +446 -0
- execweave/inference_gateway.py +422 -0
- execweave/inference_gateway_cli.py +106 -0
- execweave/inference_identity.py +76 -0
- execweave/inference_identity_cli.py +60 -0
- execweave/live.py +275 -0
- execweave/model_runtime.py +535 -0
- execweave/model_runtime_cli.py +154 -0
- execweave/opencode_adapter.py +316 -0
- execweave/opencode_hook_cli.py +57 -0
- execweave/opencode_plugin_cli.py +110 -0
- execweave/opencode_record.py +96 -0
- execweave/overhead_benchmark.py +440 -0
- execweave/provider_record.py +215 -0
- execweave/schema.py +62 -0
- execweave/semantic.py +346 -0
- execweave/sink.py +33 -0
- execweave/strace_backend.py +682 -0
- execweave/validate.py +193 -0
- execweave/viewer.py +283 -0
- execweave/workflow.py +114 -0
- execweave-0.6.0.dist-info/METADATA +356 -0
- execweave-0.6.0.dist-info/RECORD +49 -0
- execweave-0.6.0.dist-info/WHEEL +4 -0
- execweave-0.6.0.dist-info/entry_points.txt +17 -0
- execweave-0.6.0.dist-info/licenses/LICENSE +21 -0
execweave/graph.py
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import asdict, dataclass, field
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .validate import validate_event_stream
|
|
10
|
+
|
|
11
|
+
GRAPH_SCHEMA_VERSION = "0.1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class GraphNode:
|
|
16
|
+
id: str
|
|
17
|
+
type: str
|
|
18
|
+
name: str | None = None
|
|
19
|
+
attributes: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
first_seen: str | None = None
|
|
21
|
+
last_seen: str | None = None
|
|
22
|
+
event_count: int = 0
|
|
23
|
+
event_types: set[str] = field(default_factory=set)
|
|
24
|
+
|
|
25
|
+
def observe(self, entity: dict[str, Any], event: dict[str, Any]) -> None:
|
|
26
|
+
self.name = self.name or entity.get("name")
|
|
27
|
+
incoming = entity.get("attributes") or {}
|
|
28
|
+
if isinstance(incoming, dict):
|
|
29
|
+
for key, value in incoming.items():
|
|
30
|
+
self.attributes.setdefault(key, value)
|
|
31
|
+
timestamp = event.get("timestamp")
|
|
32
|
+
if isinstance(timestamp, str):
|
|
33
|
+
self.first_seen = min(self.first_seen, timestamp) if self.first_seen else timestamp
|
|
34
|
+
self.last_seen = max(self.last_seen, timestamp) if self.last_seen else timestamp
|
|
35
|
+
self.event_count += 1
|
|
36
|
+
event_type = event.get("event_type")
|
|
37
|
+
if isinstance(event_type, str):
|
|
38
|
+
self.event_types.add(event_type)
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict[str, Any]:
|
|
41
|
+
payload = asdict(self)
|
|
42
|
+
payload["event_types"] = sorted(self.event_types)
|
|
43
|
+
return payload
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class GraphEdge:
|
|
48
|
+
id: str
|
|
49
|
+
source: str
|
|
50
|
+
target: str
|
|
51
|
+
relation: str
|
|
52
|
+
count: int = 0
|
|
53
|
+
first_seen: str | None = None
|
|
54
|
+
last_seen: str | None = None
|
|
55
|
+
first_sequence: int | None = None
|
|
56
|
+
last_sequence: int | None = None
|
|
57
|
+
event_ids: list[str] = field(default_factory=list)
|
|
58
|
+
event_types: set[str] = field(default_factory=set)
|
|
59
|
+
backends: set[str] = field(default_factory=set)
|
|
60
|
+
attributions: set[str] = field(default_factory=set)
|
|
61
|
+
causal_values: set[bool] = field(default_factory=set)
|
|
62
|
+
inferred_values: set[bool] = field(default_factory=set)
|
|
63
|
+
inference_methods: set[str] = field(default_factory=set)
|
|
64
|
+
identity_exact_values: set[bool] = field(default_factory=set)
|
|
65
|
+
identity_methods: set[str] = field(default_factory=set)
|
|
66
|
+
identity_hashes: set[str] = field(default_factory=set)
|
|
67
|
+
confidence_values: list[float] = field(default_factory=list)
|
|
68
|
+
confidence_semantics: set[str] = field(default_factory=set)
|
|
69
|
+
supporting_event_ids: set[str] = field(default_factory=set)
|
|
70
|
+
|
|
71
|
+
def observe(self, event: dict[str, Any]) -> None:
|
|
72
|
+
self.count += 1
|
|
73
|
+
timestamp = event.get("timestamp")
|
|
74
|
+
if isinstance(timestamp, str):
|
|
75
|
+
self.first_seen = min(self.first_seen, timestamp) if self.first_seen else timestamp
|
|
76
|
+
self.last_seen = max(self.last_seen, timestamp) if self.last_seen else timestamp
|
|
77
|
+
sequence = event.get("sequence")
|
|
78
|
+
if isinstance(sequence, int) and not isinstance(sequence, bool):
|
|
79
|
+
self.first_sequence = (
|
|
80
|
+
min(self.first_sequence, sequence) if self.first_sequence is not None else sequence
|
|
81
|
+
)
|
|
82
|
+
self.last_sequence = (
|
|
83
|
+
max(self.last_sequence, sequence) if self.last_sequence is not None else sequence
|
|
84
|
+
)
|
|
85
|
+
event_id = event.get("event_id")
|
|
86
|
+
if isinstance(event_id, str):
|
|
87
|
+
self.event_ids.append(event_id)
|
|
88
|
+
event_type = event.get("event_type")
|
|
89
|
+
if isinstance(event_type, str):
|
|
90
|
+
self.event_types.add(event_type)
|
|
91
|
+
attributes = event.get("attributes") or {}
|
|
92
|
+
if isinstance(attributes, dict):
|
|
93
|
+
backend = attributes.get("backend")
|
|
94
|
+
if isinstance(backend, str):
|
|
95
|
+
self.backends.add(backend)
|
|
96
|
+
attribution = attributes.get("attribution")
|
|
97
|
+
if isinstance(attribution, str):
|
|
98
|
+
self.attributions.add(attribution)
|
|
99
|
+
causal = attributes.get("causal")
|
|
100
|
+
if isinstance(causal, bool):
|
|
101
|
+
self.causal_values.add(causal)
|
|
102
|
+
inferred = attributes.get("inferred")
|
|
103
|
+
if isinstance(inferred, bool):
|
|
104
|
+
self.inferred_values.add(inferred)
|
|
105
|
+
method = attributes.get("inference_method")
|
|
106
|
+
if isinstance(method, str) and method:
|
|
107
|
+
self.inference_methods.add(method)
|
|
108
|
+
identity_exact = attributes.get("identity_exact")
|
|
109
|
+
if isinstance(identity_exact, bool):
|
|
110
|
+
self.identity_exact_values.add(identity_exact)
|
|
111
|
+
identity_method = attributes.get("identity_method")
|
|
112
|
+
if isinstance(identity_method, str) and identity_method:
|
|
113
|
+
self.identity_methods.add(identity_method)
|
|
114
|
+
identity_hash = attributes.get("shared_request_id_hash")
|
|
115
|
+
if isinstance(identity_hash, str) and identity_hash:
|
|
116
|
+
self.identity_hashes.add(identity_hash)
|
|
117
|
+
confidence = attributes.get("confidence")
|
|
118
|
+
if isinstance(confidence, (int, float)) and not isinstance(confidence, bool):
|
|
119
|
+
self.confidence_values.append(float(confidence))
|
|
120
|
+
semantics = attributes.get("confidence_semantics")
|
|
121
|
+
if isinstance(semantics, str) and semantics:
|
|
122
|
+
self.confidence_semantics.add(semantics)
|
|
123
|
+
supporting = attributes.get("supporting_event_ids")
|
|
124
|
+
if isinstance(supporting, list):
|
|
125
|
+
self.supporting_event_ids.update(
|
|
126
|
+
value for value in supporting if isinstance(value, str) and value
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def to_dict(self) -> dict[str, Any]:
|
|
130
|
+
if self.causal_values == {True}:
|
|
131
|
+
causal: bool | None = True
|
|
132
|
+
elif self.causal_values == {False}:
|
|
133
|
+
causal = False
|
|
134
|
+
else:
|
|
135
|
+
causal = None
|
|
136
|
+
if self.inferred_values == {True}:
|
|
137
|
+
inferred: bool | None = True
|
|
138
|
+
elif self.inferred_values == {False}:
|
|
139
|
+
inferred = False
|
|
140
|
+
else:
|
|
141
|
+
inferred = None
|
|
142
|
+
if self.identity_exact_values == {True}:
|
|
143
|
+
identity_exact: bool | None = True
|
|
144
|
+
elif self.identity_exact_values == {False}:
|
|
145
|
+
identity_exact = False
|
|
146
|
+
else:
|
|
147
|
+
identity_exact = None
|
|
148
|
+
return {
|
|
149
|
+
"id": self.id,
|
|
150
|
+
"source": self.source,
|
|
151
|
+
"target": self.target,
|
|
152
|
+
"relation": self.relation,
|
|
153
|
+
"count": self.count,
|
|
154
|
+
"first_seen": self.first_seen,
|
|
155
|
+
"last_seen": self.last_seen,
|
|
156
|
+
"first_sequence": self.first_sequence,
|
|
157
|
+
"last_sequence": self.last_sequence,
|
|
158
|
+
"event_ids": self.event_ids,
|
|
159
|
+
"event_types": sorted(self.event_types),
|
|
160
|
+
"backends": sorted(self.backends),
|
|
161
|
+
"attributions": sorted(self.attributions),
|
|
162
|
+
"causal": causal,
|
|
163
|
+
"inferred": inferred,
|
|
164
|
+
"inference_methods": sorted(self.inference_methods),
|
|
165
|
+
"identity_exact": identity_exact,
|
|
166
|
+
"identity_methods": sorted(self.identity_methods),
|
|
167
|
+
"identity_hashes": sorted(self.identity_hashes),
|
|
168
|
+
"confidence_min": min(self.confidence_values) if self.confidence_values else None,
|
|
169
|
+
"confidence_max": max(self.confidence_values) if self.confidence_values else None,
|
|
170
|
+
"confidence_semantics": sorted(self.confidence_semantics),
|
|
171
|
+
"supporting_event_ids": sorted(self.supporting_event_ids),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
@dataclass
|
|
176
|
+
class ExecutionGraph:
|
|
177
|
+
session_id: str
|
|
178
|
+
source_path: str
|
|
179
|
+
source_schema_versions: list[str]
|
|
180
|
+
event_count: int
|
|
181
|
+
nodes: list[GraphNode]
|
|
182
|
+
edges: list[GraphEdge]
|
|
183
|
+
built_at: str = field(
|
|
184
|
+
default_factory=lambda: datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def to_dict(self) -> dict[str, Any]:
|
|
188
|
+
return {
|
|
189
|
+
"graph_schema_version": GRAPH_SCHEMA_VERSION,
|
|
190
|
+
"session_id": self.session_id,
|
|
191
|
+
"source_path": self.source_path,
|
|
192
|
+
"source_schema_versions": self.source_schema_versions,
|
|
193
|
+
"event_count": self.event_count,
|
|
194
|
+
"node_count": len(self.nodes),
|
|
195
|
+
"edge_count": len(self.edges),
|
|
196
|
+
"built_at": self.built_at,
|
|
197
|
+
"nodes": [node.to_dict() for node in self.nodes],
|
|
198
|
+
"edges": [edge.to_dict() for edge in self.edges],
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _edge_id(source: str, relation: str, target: str) -> str:
|
|
203
|
+
return f"{source}--{relation}-->{target}"
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _load_events(path: Path) -> list[dict[str, Any]]:
|
|
207
|
+
return [
|
|
208
|
+
json.loads(line)
|
|
209
|
+
for line in path.read_text(encoding="utf-8").splitlines()
|
|
210
|
+
if line.strip()
|
|
211
|
+
]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def build_execution_graph(
|
|
215
|
+
path: str | Path,
|
|
216
|
+
*,
|
|
217
|
+
allow_incomplete: bool = False,
|
|
218
|
+
) -> ExecutionGraph:
|
|
219
|
+
source_path = Path(path).expanduser().resolve()
|
|
220
|
+
validation = validate_event_stream(
|
|
221
|
+
source_path,
|
|
222
|
+
require_complete_session=not allow_incomplete,
|
|
223
|
+
)
|
|
224
|
+
if not validation.valid:
|
|
225
|
+
details = "; ".join(validation.errors)
|
|
226
|
+
raise ValueError(f"invalid ExecWeave event stream: {details}")
|
|
227
|
+
|
|
228
|
+
events = _load_events(source_path)
|
|
229
|
+
nodes: dict[str, GraphNode] = {}
|
|
230
|
+
edges: dict[tuple[str, str, str], GraphEdge] = {}
|
|
231
|
+
|
|
232
|
+
for event in events:
|
|
233
|
+
source = event.get("source")
|
|
234
|
+
target = event.get("target")
|
|
235
|
+
|
|
236
|
+
for entity in (source, target):
|
|
237
|
+
if not isinstance(entity, dict):
|
|
238
|
+
continue
|
|
239
|
+
entity_id = entity.get("id")
|
|
240
|
+
entity_type = entity.get("type")
|
|
241
|
+
if not isinstance(entity_id, str) or not isinstance(entity_type, str):
|
|
242
|
+
continue
|
|
243
|
+
node = nodes.get(entity_id)
|
|
244
|
+
if node is None:
|
|
245
|
+
node = GraphNode(
|
|
246
|
+
id=entity_id,
|
|
247
|
+
type=entity_type,
|
|
248
|
+
name=entity.get("name") if isinstance(entity.get("name"), str) else None,
|
|
249
|
+
)
|
|
250
|
+
nodes[entity_id] = node
|
|
251
|
+
node.observe(entity, event)
|
|
252
|
+
|
|
253
|
+
if not isinstance(source, dict) or not isinstance(target, dict):
|
|
254
|
+
continue
|
|
255
|
+
source_id = source.get("id")
|
|
256
|
+
target_id = target.get("id")
|
|
257
|
+
relation = event.get("relation")
|
|
258
|
+
if not all(isinstance(value, str) and value for value in (source_id, target_id, relation)):
|
|
259
|
+
continue
|
|
260
|
+
key = (source_id, relation, target_id)
|
|
261
|
+
edge = edges.get(key)
|
|
262
|
+
if edge is None:
|
|
263
|
+
edge = GraphEdge(
|
|
264
|
+
id=_edge_id(source_id, relation, target_id),
|
|
265
|
+
source=source_id,
|
|
266
|
+
target=target_id,
|
|
267
|
+
relation=relation,
|
|
268
|
+
)
|
|
269
|
+
edges[key] = edge
|
|
270
|
+
edge.observe(event)
|
|
271
|
+
|
|
272
|
+
session_id = validation.session_ids[0] if validation.session_ids else "unknown"
|
|
273
|
+
return ExecutionGraph(
|
|
274
|
+
session_id=session_id,
|
|
275
|
+
source_path=str(source_path),
|
|
276
|
+
source_schema_versions=validation.schema_versions,
|
|
277
|
+
event_count=len(events),
|
|
278
|
+
nodes=sorted(nodes.values(), key=lambda node: (node.type, node.id)),
|
|
279
|
+
edges=sorted(edges.values(), key=lambda edge: (edge.source, edge.relation, edge.target)),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def write_execution_graph(
|
|
284
|
+
graph: ExecutionGraph,
|
|
285
|
+
path: str | Path,
|
|
286
|
+
*,
|
|
287
|
+
metadata: dict[str, Any] | None = None,
|
|
288
|
+
) -> Path:
|
|
289
|
+
output = Path(path).expanduser().resolve()
|
|
290
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
291
|
+
if output.exists() and output.stat().st_size > 0:
|
|
292
|
+
raise FileExistsError(f"ExecWeave graph output already exists: {output}")
|
|
293
|
+
payload = graph.to_dict()
|
|
294
|
+
if metadata is not None:
|
|
295
|
+
payload["metadata"] = metadata
|
|
296
|
+
output.write_text(
|
|
297
|
+
json.dumps(payload, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
|
298
|
+
encoding="utf-8",
|
|
299
|
+
)
|
|
300
|
+
return output
|