activegraph 1.0.0rc2__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.
- activegraph/__init__.py +222 -0
- activegraph/__main__.py +5 -0
- activegraph/behaviors/__init__.py +1 -0
- activegraph/behaviors/base.py +153 -0
- activegraph/behaviors/decorators.py +218 -0
- activegraph/cli/__init__.py +17 -0
- activegraph/cli/main.py +793 -0
- activegraph/cli/quickstart.py +556 -0
- activegraph/core/__init__.py +1 -0
- activegraph/core/clock.py +46 -0
- activegraph/core/event.py +33 -0
- activegraph/core/graph.py +846 -0
- activegraph/core/ids.py +135 -0
- activegraph/core/patch.py +47 -0
- activegraph/core/view.py +59 -0
- activegraph/errors.py +342 -0
- activegraph/frame.py +15 -0
- activegraph/llm/__init__.py +51 -0
- activegraph/llm/anthropic.py +339 -0
- activegraph/llm/cache.py +180 -0
- activegraph/llm/errors.py +257 -0
- activegraph/llm/prompt.py +420 -0
- activegraph/llm/provider.py +66 -0
- activegraph/llm/recorded.py +270 -0
- activegraph/llm/types.py +130 -0
- activegraph/observability/__init__.py +61 -0
- activegraph/observability/logging.py +205 -0
- activegraph/observability/metrics.py +219 -0
- activegraph/observability/migration.py +404 -0
- activegraph/observability/prometheus.py +101 -0
- activegraph/observability/status.py +83 -0
- activegraph/packs/__init__.py +999 -0
- activegraph/packs/diligence/__init__.py +86 -0
- activegraph/packs/diligence/behaviors.py +447 -0
- activegraph/packs/diligence/fixtures/__init__.py +364 -0
- activegraph/packs/diligence/fixtures/companies.py +511 -0
- activegraph/packs/diligence/object_types.py +163 -0
- activegraph/packs/diligence/settings.py +60 -0
- activegraph/packs/diligence/tools.py +124 -0
- activegraph/packs/loader.py +709 -0
- activegraph/packs/scaffold.py +317 -0
- activegraph/policy.py +20 -0
- activegraph/runtime/__init__.py +1 -0
- activegraph/runtime/behavior_graph.py +151 -0
- activegraph/runtime/budget.py +120 -0
- activegraph/runtime/config_errors.py +124 -0
- activegraph/runtime/diff.py +145 -0
- activegraph/runtime/errors.py +216 -0
- activegraph/runtime/exec_errors.py +232 -0
- activegraph/runtime/patterns.py +946 -0
- activegraph/runtime/queue.py +27 -0
- activegraph/runtime/registration_errors.py +291 -0
- activegraph/runtime/registry.py +111 -0
- activegraph/runtime/runtime.py +2441 -0
- activegraph/runtime/scheduler.py +206 -0
- activegraph/runtime/view_builder.py +65 -0
- activegraph/store/__init__.py +41 -0
- activegraph/store/base.py +66 -0
- activegraph/store/conformance.py +161 -0
- activegraph/store/errors.py +84 -0
- activegraph/store/memory.py +118 -0
- activegraph/store/postgres.py +609 -0
- activegraph/store/serde.py +202 -0
- activegraph/store/sqlite.py +446 -0
- activegraph/store/url.py +230 -0
- activegraph/tools/__init__.py +64 -0
- activegraph/tools/base.py +57 -0
- activegraph/tools/cache.py +157 -0
- activegraph/tools/context.py +48 -0
- activegraph/tools/decorators.py +70 -0
- activegraph/tools/errors.py +320 -0
- activegraph/tools/graph_query.py +94 -0
- activegraph/tools/recorded.py +205 -0
- activegraph/tools/web_fetch.py +80 -0
- activegraph/trace/__init__.py +1 -0
- activegraph/trace/causal.py +123 -0
- activegraph/trace/printer.py +495 -0
- activegraph-1.0.0rc2.dist-info/METADATA +228 -0
- activegraph-1.0.0rc2.dist-info/RECORD +82 -0
- activegraph-1.0.0rc2.dist-info/WHEEL +5 -0
- activegraph-1.0.0rc2.dist-info/entry_points.txt +5 -0
- activegraph-1.0.0rc2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
"""Trace formatter. CONTRACT #18 — format is the public contract.
|
|
2
|
+
|
|
3
|
+
Layout: tag column is left-aligned, padded to 26 chars; if the tag itself is
|
|
4
|
+
longer, exactly one space follows it.
|
|
5
|
+
|
|
6
|
+
Standard event types get specific renderings (see CONTRACT for the table).
|
|
7
|
+
Anything else is rendered as `[event.emitted] {type} k=v...`.
|
|
8
|
+
|
|
9
|
+
CONTRACT v0.5 #22: replayed events are rendered with a `[replay.event]`
|
|
10
|
+
prefix. After the last replayed event, two synthetic lines appear so
|
|
11
|
+
operators can see the load boundary:
|
|
12
|
+
[replay.complete] N events replayed, graph reconstructed
|
|
13
|
+
[runtime.idle] ready to resume
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
from activegraph.core.event import Event
|
|
21
|
+
from activegraph.core.graph import Graph
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
TAG_COL = 26
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _format_tag(tag_text: str) -> str:
|
|
28
|
+
"""Bracketed tag, left-padded to TAG_COL (or one trailing space if longer)."""
|
|
29
|
+
bracketed = f"[{tag_text}]"
|
|
30
|
+
if len(bracketed) >= TAG_COL:
|
|
31
|
+
return bracketed + " "
|
|
32
|
+
return bracketed.ljust(TAG_COL)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _plural(n: int, word: str) -> str:
|
|
36
|
+
return f"{n} {word}{'s' if n != 1 else ''}"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------- per-event formatters ----------
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _fmt_goal_created(e: Event) -> str:
|
|
43
|
+
actor = e.actor or "user"
|
|
44
|
+
goal = e.payload.get("goal", "")
|
|
45
|
+
return f'{_format_tag("goal.created")}{actor}: "{goal}"'
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _fmt_object_created(e: Event) -> str:
|
|
49
|
+
o = e.payload.get("object", {})
|
|
50
|
+
data = o.get("data", {})
|
|
51
|
+
label = data.get("title") or data.get("text") or ""
|
|
52
|
+
label_s = f' "{label}"' if label else ""
|
|
53
|
+
status = data.get("status")
|
|
54
|
+
status_s = f" ({status})" if status else ""
|
|
55
|
+
return f'{_format_tag("object.created")}{o.get("id", "?")}{label_s}{status_s}'
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fmt_object_removed(e: Event) -> str:
|
|
59
|
+
return f'{_format_tag("object.removed")}{e.payload.get("id", "?")}'
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _fmt_relation_created(e: Event) -> str:
|
|
63
|
+
r = e.payload.get("relation", {})
|
|
64
|
+
return (
|
|
65
|
+
f'{_format_tag("relation.created")}'
|
|
66
|
+
f'{r.get("source", "?")} --{r.get("type", "?")}--> {r.get("target", "?")}'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _fmt_relation_removed(e: Event) -> str:
|
|
71
|
+
return f'{_format_tag("relation.removed")}{e.payload.get("id", "?")}'
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _fmt_patch_applied(e: Event) -> str:
|
|
75
|
+
target = e.payload.get("target", "?")
|
|
76
|
+
diff = e.payload.get("diff", {})
|
|
77
|
+
if not diff:
|
|
78
|
+
return f'{_format_tag("patch.applied")}{target} (no change)'
|
|
79
|
+
lines = []
|
|
80
|
+
for field, change in diff.items():
|
|
81
|
+
old = change.get("old")
|
|
82
|
+
new = change.get("new")
|
|
83
|
+
body = f"{target} {field}: {old} -> {new}"
|
|
84
|
+
lines.append(f'{_format_tag("patch.applied")}{body}')
|
|
85
|
+
return "\n".join(lines)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fmt_patch_proposed(e: Event) -> str:
|
|
89
|
+
p = e.payload.get("patch", {})
|
|
90
|
+
return (
|
|
91
|
+
f'{_format_tag("patch.proposed")}'
|
|
92
|
+
f'{p.get("target", "?")} {p.get("op", "?")} by {p.get("proposed_by", "?")}'
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _fmt_patch_rejected(e: Event) -> str:
|
|
97
|
+
return (
|
|
98
|
+
f'{_format_tag("patch.rejected")}'
|
|
99
|
+
f'{e.payload.get("patch_id", "?")}: {e.payload.get("reason", "?")}'
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _fmt_behavior_started(e: Event) -> str:
|
|
104
|
+
name = e.payload.get("behavior", "?")
|
|
105
|
+
triggering_type = e.payload.get("triggering_event_type")
|
|
106
|
+
triggering_id = e.payload.get("triggering_object_id")
|
|
107
|
+
if triggering_type and triggering_id:
|
|
108
|
+
return f'{_format_tag("behavior.started")}{name} (matched {triggering_type}: {triggering_id})'
|
|
109
|
+
return f'{_format_tag("behavior.started")}{name}'
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _fmt_relation_behavior_started(e: Event) -> str:
|
|
113
|
+
name = e.payload.get("behavior", "?")
|
|
114
|
+
triggering_type = e.payload.get("triggering_event_type", "?")
|
|
115
|
+
relation_type = e.payload.get("relation_type", "?")
|
|
116
|
+
return (
|
|
117
|
+
f'{_format_tag("relation_behavior.started")}'
|
|
118
|
+
f'{name} (matched {triggering_type} on {relation_type} edge)'
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _fmt_behavior_completed(e: Event) -> str:
|
|
123
|
+
name = e.payload.get("behavior", "?")
|
|
124
|
+
n_obj = int(e.payload.get("objects_created", 0))
|
|
125
|
+
n_rel = int(e.payload.get("relations_created", 0))
|
|
126
|
+
# Count summary only when behavior produced structure (>= 2 mutations
|
|
127
|
+
# combined). Single-action behaviors render as just `name`. See CONTRACT.
|
|
128
|
+
if n_obj + n_rel >= 2:
|
|
129
|
+
return (
|
|
130
|
+
f'{_format_tag("behavior.completed")}'
|
|
131
|
+
f'{name} ({_plural(n_obj, "object")}, {_plural(n_rel, "relation")})'
|
|
132
|
+
)
|
|
133
|
+
return f'{_format_tag("behavior.completed")}{name}'
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _fmt_behavior_failed(e: Event) -> str:
|
|
137
|
+
name = e.payload.get("behavior", "?")
|
|
138
|
+
et = e.payload.get("exception_type", "?")
|
|
139
|
+
msg = e.payload.get("message", "")
|
|
140
|
+
return f'{_format_tag("behavior.failed")}{name}: {et}: {msg}'
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _fmt_llm_requested(e: Event, *, hide_prompt_normalized: bool = False) -> str:
|
|
144
|
+
"""CONTRACT v0.6 #14 (+ v0.7 turn_index + v0.9.1 prompt_normalized rollup):
|
|
145
|
+
`[llm.requested] evt_NNN behavior model=... tokens_in~NNNN budget_remaining=$X.XX`
|
|
146
|
+
|
|
147
|
+
The `~` prefix on `tokens_in` marks an estimate; absent if no
|
|
148
|
+
pre-call count was made (no cost budget OR cache hit). The
|
|
149
|
+
`budget_remaining=$...` segment is dropped if no cost budget.
|
|
150
|
+
v0.7 adds `turn=N` for tool-loop turns past the first.
|
|
151
|
+
|
|
152
|
+
`prompt_normalized=true` was on every line in v0.7-v0.9. v0.9.1
|
|
153
|
+
rolls it up to a single `[trace.flags]` header when uniform across
|
|
154
|
+
all non-replayed `llm.requested` events, dropping the per-line
|
|
155
|
+
flag. Mixed-state traces (rare) keep the per-line flag for
|
|
156
|
+
precision. `hide_prompt_normalized=True` is set by the trace
|
|
157
|
+
facade when the rollup is active.
|
|
158
|
+
"""
|
|
159
|
+
p = e.payload or {}
|
|
160
|
+
name = p.get("behavior", "?")
|
|
161
|
+
parts: list[str] = [f"{e.id} {name}", f"model={p.get('model', '?')}"]
|
|
162
|
+
if p.get("cache_hit"):
|
|
163
|
+
parts.append("cache_hit=true")
|
|
164
|
+
turn_idx = p.get("turn_index")
|
|
165
|
+
if turn_idx is not None and turn_idx > 0:
|
|
166
|
+
parts.append(f"turn={turn_idx}")
|
|
167
|
+
if "estimated_input_tokens" in p:
|
|
168
|
+
parts.append(f"tokens_in~{p['estimated_input_tokens']}")
|
|
169
|
+
if p.get("budget_remaining_usd") is not None:
|
|
170
|
+
parts.append(f"budget_remaining=${_money(p['budget_remaining_usd'])}")
|
|
171
|
+
if p.get("prompt_normalized") and not hide_prompt_normalized:
|
|
172
|
+
parts.append("prompt_normalized=true")
|
|
173
|
+
return f'{_format_tag("llm.requested")}{" ".join([parts[0], " ".join(parts[1:])])}'
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _fmt_llm_responded(e: Event) -> str:
|
|
177
|
+
"""CONTRACT v0.6 #14:
|
|
178
|
+
`[llm.responded] evt_NNN behavior tokens_in=NNNN tokens_out=NNN cost=$X.XXX latency=X.Xs`
|
|
179
|
+
|
|
180
|
+
Cache hits render with `cache_hit=true` and no cost/latency segments
|
|
181
|
+
(latency is the cached response's recorded latency, not now).
|
|
182
|
+
"""
|
|
183
|
+
p = e.payload or {}
|
|
184
|
+
name = p.get("behavior", "?")
|
|
185
|
+
parts: list[str] = [f"{e.id} {name}"]
|
|
186
|
+
if p.get("cache_hit"):
|
|
187
|
+
parts.append("cache_hit=true")
|
|
188
|
+
in_tok = p.get("input_tokens")
|
|
189
|
+
out_tok = p.get("output_tokens")
|
|
190
|
+
if in_tok is not None:
|
|
191
|
+
parts.append(f"tokens_in={in_tok}")
|
|
192
|
+
if out_tok is not None:
|
|
193
|
+
parts.append(f"tokens_out={out_tok}")
|
|
194
|
+
cost = p.get("cost_usd")
|
|
195
|
+
if cost is not None and not p.get("cache_hit"):
|
|
196
|
+
parts.append(f"cost=${_money(cost)}")
|
|
197
|
+
lat = p.get("latency_seconds")
|
|
198
|
+
if lat is not None and not p.get("cache_hit"):
|
|
199
|
+
parts.append(f"latency={float(lat):.1f}s")
|
|
200
|
+
return f'{_format_tag("llm.responded")}{" ".join([parts[0], " ".join(parts[1:])])}'
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _money(v: Any) -> str:
|
|
204
|
+
try:
|
|
205
|
+
return f"{float(v):.3f}"
|
|
206
|
+
except (TypeError, ValueError):
|
|
207
|
+
return str(v)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------- v0.7 trace lines: tool.* / pattern.* / behavior.scheduled ------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _fmt_tool_requested(e: Event) -> str:
|
|
214
|
+
"""CONTRACT v0.7 #18:
|
|
215
|
+
`[tool.requested] evt_NNN behavior tool=name args_hash=AAA cache_hit=false`
|
|
216
|
+
"""
|
|
217
|
+
p = e.payload or {}
|
|
218
|
+
name = p.get("behavior", "?")
|
|
219
|
+
tool = p.get("tool", "?")
|
|
220
|
+
parts: list[str] = [
|
|
221
|
+
f"{e.id} {name}",
|
|
222
|
+
f"tool={tool}",
|
|
223
|
+
f"args_hash={_short_hash(p.get('args_hash', '?'))}",
|
|
224
|
+
]
|
|
225
|
+
if p.get("cache_hit"):
|
|
226
|
+
parts.append("cache_hit=true")
|
|
227
|
+
if p.get("deterministic"):
|
|
228
|
+
parts.append("deterministic=true")
|
|
229
|
+
return f'{_format_tag("tool.requested")}{" ".join([parts[0], " ".join(parts[1:])])}'
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _fmt_tool_responded(e: Event) -> str:
|
|
233
|
+
"""CONTRACT v0.7 #18:
|
|
234
|
+
`[tool.responded] evt_NNN behavior tool=name latency=X.Xs cost=$X.XXX cache_hit=false`
|
|
235
|
+
"""
|
|
236
|
+
p = e.payload or {}
|
|
237
|
+
name = p.get("behavior", "?")
|
|
238
|
+
tool = p.get("tool", "?")
|
|
239
|
+
parts: list[str] = [f"{e.id} {name}", f"tool={tool}"]
|
|
240
|
+
if p.get("cache_hit"):
|
|
241
|
+
parts.append("cache_hit=true")
|
|
242
|
+
err = p.get("error")
|
|
243
|
+
if err:
|
|
244
|
+
reason = err.get("reason", "tool.error") if isinstance(err, dict) else "tool.error"
|
|
245
|
+
parts.append(f"error={reason}")
|
|
246
|
+
else:
|
|
247
|
+
lat = p.get("latency_seconds")
|
|
248
|
+
cost = p.get("cost_usd")
|
|
249
|
+
if lat is not None and not p.get("cache_hit"):
|
|
250
|
+
parts.append(f"latency={float(lat):.1f}s")
|
|
251
|
+
if cost is not None and not p.get("cache_hit"):
|
|
252
|
+
parts.append(f"cost=${_money(cost)}")
|
|
253
|
+
return f'{_format_tag("tool.responded")}{" ".join([parts[0], " ".join(parts[1:])])}'
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _fmt_pattern_matched(e: Event) -> str:
|
|
257
|
+
"""CONTRACT v0.7 #18:
|
|
258
|
+
`[pattern.matched] evt_NNN behavior matches=N`
|
|
259
|
+
"""
|
|
260
|
+
p = e.payload or {}
|
|
261
|
+
name = p.get("behavior", "?")
|
|
262
|
+
n = int(p.get("matches_count", 0))
|
|
263
|
+
return (
|
|
264
|
+
f'{_format_tag("pattern.matched")}'
|
|
265
|
+
f'{e.id} {name} matches={n}'
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _fmt_behavior_scheduled(e: Event) -> str:
|
|
270
|
+
"""CONTRACT v0.7 #18:
|
|
271
|
+
`[behavior.scheduled] evt_NNN behavior activate_after=N_events`
|
|
272
|
+
"""
|
|
273
|
+
p = e.payload or {}
|
|
274
|
+
name = p.get("behavior", "?")
|
|
275
|
+
n = int(p.get("activate_after", 0))
|
|
276
|
+
return (
|
|
277
|
+
f'{_format_tag("behavior.scheduled")}'
|
|
278
|
+
f'{e.id} {name} activate_after={n}_event{"s" if n != 1 else ""}'
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _short_hash(h: str) -> str:
|
|
283
|
+
"""Trim a long hex hash to 8 chars for trace readability."""
|
|
284
|
+
if not isinstance(h, str):
|
|
285
|
+
return str(h)
|
|
286
|
+
return h[:8] if len(h) > 8 else h
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _fmt_runtime_idle(_: Event) -> str:
|
|
290
|
+
return f'{_format_tag("runtime.idle")}queue empty, budget remaining'
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _fmt_pack_loaded(e: Event) -> str:
|
|
294
|
+
"""CONTRACT v0.9 #25:
|
|
295
|
+
`[pack.loaded] diligence v0.1.0 (8 object_types, 6 relation_types,
|
|
296
|
+
7 behaviors, 3 tools, 2 policies, 5 prompts)`
|
|
297
|
+
|
|
298
|
+
The pack.loaded event payload carries the pack name, version, and
|
|
299
|
+
the full structural inventory. The trace line summarizes counts;
|
|
300
|
+
the full payload is in the JSONL export and on `activegraph
|
|
301
|
+
inspect --pack-version` for operators who need the prompt hashes.
|
|
302
|
+
"""
|
|
303
|
+
p = e.payload or {}
|
|
304
|
+
name = p.get("name", "?")
|
|
305
|
+
version = p.get("version", "?")
|
|
306
|
+
# (count, singular, plural) — "policy" needs the irregular plural;
|
|
307
|
+
# the rest are regular but the table keeps the form explicit so
|
|
308
|
+
# future additions don't drift through _plural()'s simple +s rule.
|
|
309
|
+
counts = [
|
|
310
|
+
(len(p.get("object_types") or []), "object_type", "object_types"),
|
|
311
|
+
(len(p.get("relation_types") or []), "relation_type", "relation_types"),
|
|
312
|
+
(len(p.get("behaviors") or []), "behavior", "behaviors"),
|
|
313
|
+
(len(p.get("tools") or []), "tool", "tools"),
|
|
314
|
+
(len(p.get("policies") or []), "policy", "policies"),
|
|
315
|
+
(len(p.get("prompts") or {}), "prompt", "prompts"),
|
|
316
|
+
]
|
|
317
|
+
summary = ", ".join(
|
|
318
|
+
f"{n} {singular if n == 1 else plural}"
|
|
319
|
+
for n, singular, plural in counts
|
|
320
|
+
if n > 0
|
|
321
|
+
)
|
|
322
|
+
return f'{_format_tag("pack.loaded")}{name} v{version} ({summary})'
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _fmt_runtime_budget_exhausted(e: Event) -> str:
|
|
326
|
+
by = e.payload.get("exhausted_by", "?")
|
|
327
|
+
return f'{_format_tag("runtime.budget_exhausted")}stopped: {by}'
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _fmt_event_emitted(e: Event) -> str:
|
|
331
|
+
"""Fallback for user-emitted (custom) events."""
|
|
332
|
+
payload_kvs = []
|
|
333
|
+
for k, v in (e.payload or {}).items():
|
|
334
|
+
payload_kvs.append(f"{k}={_short(v)}")
|
|
335
|
+
body = " ".join([e.type] + payload_kvs)
|
|
336
|
+
return f'{_format_tag("event.emitted")}{body}'
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _short(v: Any) -> str:
|
|
340
|
+
if isinstance(v, str):
|
|
341
|
+
return v
|
|
342
|
+
return repr(v)
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
_FORMATTERS = {
|
|
346
|
+
"goal.created": _fmt_goal_created,
|
|
347
|
+
"object.created": _fmt_object_created,
|
|
348
|
+
"object.removed": _fmt_object_removed,
|
|
349
|
+
"relation.created": _fmt_relation_created,
|
|
350
|
+
"relation.removed": _fmt_relation_removed,
|
|
351
|
+
"patch.applied": _fmt_patch_applied,
|
|
352
|
+
"patch.proposed": _fmt_patch_proposed,
|
|
353
|
+
"patch.rejected": _fmt_patch_rejected,
|
|
354
|
+
"behavior.started": _fmt_behavior_started,
|
|
355
|
+
"behavior.completed": _fmt_behavior_completed,
|
|
356
|
+
"behavior.failed": _fmt_behavior_failed,
|
|
357
|
+
"behavior.scheduled": _fmt_behavior_scheduled,
|
|
358
|
+
"relation_behavior.started": _fmt_relation_behavior_started,
|
|
359
|
+
"llm.requested": _fmt_llm_requested,
|
|
360
|
+
"llm.responded": _fmt_llm_responded,
|
|
361
|
+
"tool.requested": _fmt_tool_requested,
|
|
362
|
+
"tool.responded": _fmt_tool_responded,
|
|
363
|
+
"pattern.matched": _fmt_pattern_matched,
|
|
364
|
+
"runtime.idle": _fmt_runtime_idle,
|
|
365
|
+
"pack.loaded": _fmt_pack_loaded,
|
|
366
|
+
"runtime.budget_exhausted": _fmt_runtime_budget_exhausted,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def format_event(event: Event, *, hide_prompt_normalized: bool = False) -> str:
|
|
371
|
+
if event.type == "llm.requested":
|
|
372
|
+
return _fmt_llm_requested(event, hide_prompt_normalized=hide_prompt_normalized)
|
|
373
|
+
fn = _FORMATTERS.get(event.type, _fmt_event_emitted)
|
|
374
|
+
return fn(event)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ---------- v0.9.1: prompt_normalized rollup ----------
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _compute_prompt_normalized_rollup(
|
|
381
|
+
events: list[Event],
|
|
382
|
+
replayed: set[str],
|
|
383
|
+
) -> dict | None:
|
|
384
|
+
"""Return rollup info if every non-replayed `llm.requested` event carries
|
|
385
|
+
`prompt_normalized=true`. Returns None if there are no such events or if
|
|
386
|
+
the flag is mixed across them — in the mixed case the per-line flag is
|
|
387
|
+
kept (rare; signals a real divergence worth seeing).
|
|
388
|
+
"""
|
|
389
|
+
llm_reqs = [
|
|
390
|
+
e for e in events
|
|
391
|
+
if e.type == "llm.requested" and e.id not in replayed
|
|
392
|
+
]
|
|
393
|
+
if not llm_reqs:
|
|
394
|
+
return None
|
|
395
|
+
if not all((e.payload or {}).get("prompt_normalized") for e in llm_reqs):
|
|
396
|
+
return None
|
|
397
|
+
return {"prompt_normalized": True, "count": len(llm_reqs)}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _fmt_trace_flags(rollup: dict) -> str:
|
|
401
|
+
n = int(rollup["count"])
|
|
402
|
+
return (
|
|
403
|
+
f'{_format_tag("trace.flags")}'
|
|
404
|
+
f'prompt_normalized=true ({_plural(n, "llm request")})'
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------- replay rendering (CONTRACT v0.5 #22) ----------
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def _fmt_replay(event: Event) -> str:
|
|
412
|
+
"""Render a replayed event with the `[replay.event]` prefix.
|
|
413
|
+
|
|
414
|
+
Format: `[replay.event] <evt_id> <event.type> <one-line summary>`
|
|
415
|
+
"""
|
|
416
|
+
t = event.type
|
|
417
|
+
p = event.payload or {}
|
|
418
|
+
if t == "object.created":
|
|
419
|
+
o = p.get("object", {})
|
|
420
|
+
oid = o.get("id", "?")
|
|
421
|
+
label = (o.get("data") or {}).get("title") or (o.get("data") or {}).get("text") or ""
|
|
422
|
+
label_s = f' "{label}"' if label else ""
|
|
423
|
+
body = f"{event.id} {t} {oid}{label_s}"
|
|
424
|
+
elif t == "relation.created":
|
|
425
|
+
r = p.get("relation", {})
|
|
426
|
+
body = f'{event.id} {t} {r.get("source")} --{r.get("type")}--> {r.get("target")}'
|
|
427
|
+
elif t == "patch.applied":
|
|
428
|
+
body = f'{event.id} {t} {p.get("target", "?")}'
|
|
429
|
+
elif t == "goal.created":
|
|
430
|
+
body = f'{event.id} {t} "{p.get("goal", "")}"'
|
|
431
|
+
elif t in ("behavior.started", "behavior.completed", "behavior.failed", "relation_behavior.started"):
|
|
432
|
+
body = f'{event.id} {t} {p.get("behavior", "?")}'
|
|
433
|
+
else:
|
|
434
|
+
body = f"{event.id} {t}"
|
|
435
|
+
return f'{_format_tag("replay.event")}{body}'
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def _fmt_replay_complete(n: int) -> str:
|
|
439
|
+
return f'{_format_tag("replay.complete")}{n} events replayed, graph reconstructed'
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _fmt_replay_ready() -> str:
|
|
443
|
+
return f'{_format_tag("runtime.idle")}ready to resume'
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
# ---------- Trace facade exposed via runtime.trace ----------
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
class Trace:
|
|
450
|
+
def __init__(self, graph: Graph) -> None:
|
|
451
|
+
self._graph = graph
|
|
452
|
+
|
|
453
|
+
def lines(self) -> list[str]:
|
|
454
|
+
replayed = self._graph.replayed_ids
|
|
455
|
+
replayed_count = len(replayed)
|
|
456
|
+
rollup = _compute_prompt_normalized_rollup(
|
|
457
|
+
list(self._graph.events), replayed
|
|
458
|
+
)
|
|
459
|
+
hide_per_line = rollup is not None
|
|
460
|
+
out: list[str] = []
|
|
461
|
+
emitted_boundary = False
|
|
462
|
+
emitted_flags = False
|
|
463
|
+
seen_replayed = 0
|
|
464
|
+
for e in self._graph.events:
|
|
465
|
+
if e.id in replayed:
|
|
466
|
+
out.append(_fmt_replay(e))
|
|
467
|
+
seen_replayed += 1
|
|
468
|
+
continue
|
|
469
|
+
if replayed_count > 0 and not emitted_boundary:
|
|
470
|
+
out.append(_fmt_replay_complete(replayed_count))
|
|
471
|
+
out.append(_fmt_replay_ready())
|
|
472
|
+
emitted_boundary = True
|
|
473
|
+
if rollup is not None and not emitted_flags:
|
|
474
|
+
out.append(_fmt_trace_flags(rollup))
|
|
475
|
+
emitted_flags = True
|
|
476
|
+
line = format_event(e, hide_prompt_normalized=hide_per_line)
|
|
477
|
+
out.append(line)
|
|
478
|
+
if replayed_count > 0 and not emitted_boundary:
|
|
479
|
+
out.append(_fmt_replay_complete(replayed_count))
|
|
480
|
+
out.append(_fmt_replay_ready())
|
|
481
|
+
return out
|
|
482
|
+
|
|
483
|
+
def print(self) -> None:
|
|
484
|
+
for line in self.lines():
|
|
485
|
+
print(line)
|
|
486
|
+
|
|
487
|
+
def export(self, path: str) -> None:
|
|
488
|
+
with open(path, "w") as f:
|
|
489
|
+
for line in self.lines():
|
|
490
|
+
f.write(line + "\n")
|
|
491
|
+
|
|
492
|
+
def causal_chain(self, object_id: str) -> str:
|
|
493
|
+
from activegraph.trace.causal import causal_chain
|
|
494
|
+
|
|
495
|
+
return causal_chain(self._graph, object_id)
|