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,124 @@
|
|
|
1
|
+
"""Runtime-side configuration error leaves. v1.0 PR-F.
|
|
2
|
+
|
|
3
|
+
Configuration errors fire when the caller constructs or calls the
|
|
4
|
+
runtime with arguments that violate a static contract — wrong types,
|
|
5
|
+
conflicting kwargs, out-of-range values, operations that require a
|
|
6
|
+
specific backend that isn't attached, state that's already set and
|
|
7
|
+
can't be re-set. The errors are caller-actionable: the developer
|
|
8
|
+
either fixes the call or restructures their setup.
|
|
9
|
+
|
|
10
|
+
Three classes here cover the audit:
|
|
11
|
+
|
|
12
|
+
- :class:`InvalidRuntimeConfiguration` — most ValueError shapes
|
|
13
|
+
(conflicting args, missing required args, out-of-range values).
|
|
14
|
+
Multi-inherits :class:`ValueError` for back-compat.
|
|
15
|
+
- :class:`InvalidArgumentType` — wrong-type values passed to
|
|
16
|
+
constructors. Multi-inherits :class:`TypeError`.
|
|
17
|
+
- :class:`IncompatibleRuntimeState` — operations that require a
|
|
18
|
+
specific runtime state (e.g., fork requires a SQLite-backed
|
|
19
|
+
runtime; graph already has a store attached). Multi-inherits
|
|
20
|
+
:class:`RuntimeError`.
|
|
21
|
+
|
|
22
|
+
See CONTRACT v1.0 PR-F for the audit table mapping each migrated
|
|
23
|
+
raise site to its class.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any, Optional
|
|
29
|
+
|
|
30
|
+
from activegraph.errors import ConfigurationError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class InvalidRuntimeConfiguration(ConfigurationError, ValueError):
|
|
34
|
+
"""Caller-provided configuration is invalid (conflicting arguments,
|
|
35
|
+
missing required argument, out-of-range value).
|
|
36
|
+
|
|
37
|
+
Multi-inherits :class:`ValueError` so user code that catches the
|
|
38
|
+
builtin around runtime construction or method calls keeps working.
|
|
39
|
+
|
|
40
|
+
Construct with a one-line ``summary`` plus the three structured
|
|
41
|
+
fields. The recovery prose is per-call-site, not table-driven —
|
|
42
|
+
each configuration mistake has a different fix.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
_doc_slug = "invalid-runtime-configuration"
|
|
46
|
+
|
|
47
|
+
def __init__(
|
|
48
|
+
self,
|
|
49
|
+
summary: str,
|
|
50
|
+
*,
|
|
51
|
+
what_failed: str,
|
|
52
|
+
why: str,
|
|
53
|
+
how_to_fix: str,
|
|
54
|
+
context: Optional[dict[str, Any]] = None,
|
|
55
|
+
) -> None:
|
|
56
|
+
ConfigurationError.__init__(
|
|
57
|
+
self,
|
|
58
|
+
summary,
|
|
59
|
+
what_failed=what_failed,
|
|
60
|
+
why=why,
|
|
61
|
+
how_to_fix=how_to_fix,
|
|
62
|
+
context=context,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class InvalidArgumentType(ConfigurationError, TypeError):
|
|
67
|
+
"""A value passed to a constructor or method has the wrong type.
|
|
68
|
+
|
|
69
|
+
Multi-inherits :class:`TypeError`. Used when the framework's
|
|
70
|
+
contract is type-based (e.g., :class:`PostgresEventStore` accepts
|
|
71
|
+
a URL string, a psycopg.Connection, or a psycopg_pool.ConnectionPool —
|
|
72
|
+
anything else is refused at construction).
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
_doc_slug = "invalid-argument-type"
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
summary: str,
|
|
80
|
+
*,
|
|
81
|
+
what_failed: str,
|
|
82
|
+
why: str,
|
|
83
|
+
how_to_fix: str,
|
|
84
|
+
context: Optional[dict[str, Any]] = None,
|
|
85
|
+
) -> None:
|
|
86
|
+
ConfigurationError.__init__(
|
|
87
|
+
self,
|
|
88
|
+
summary,
|
|
89
|
+
what_failed=what_failed,
|
|
90
|
+
why=why,
|
|
91
|
+
how_to_fix=how_to_fix,
|
|
92
|
+
context=context,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class IncompatibleRuntimeState(ConfigurationError, RuntimeError):
|
|
97
|
+
"""An operation requires a runtime state that isn't satisfied —
|
|
98
|
+
either a state that must be set but isn't, or a state that mustn't
|
|
99
|
+
be set but is.
|
|
100
|
+
|
|
101
|
+
Examples: ``runtime.fork()`` requires a SQLite-backed runtime;
|
|
102
|
+
``graph.attach_store()`` requires no existing store. Multi-inherits
|
|
103
|
+
:class:`RuntimeError` for back-compat.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
_doc_slug = "incompatible-runtime-state"
|
|
107
|
+
|
|
108
|
+
def __init__(
|
|
109
|
+
self,
|
|
110
|
+
summary: str,
|
|
111
|
+
*,
|
|
112
|
+
what_failed: str,
|
|
113
|
+
why: str,
|
|
114
|
+
how_to_fix: str,
|
|
115
|
+
context: Optional[dict[str, Any]] = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
ConfigurationError.__init__(
|
|
118
|
+
self,
|
|
119
|
+
summary,
|
|
120
|
+
what_failed=what_failed,
|
|
121
|
+
why=why,
|
|
122
|
+
how_to_fix=how_to_fix,
|
|
123
|
+
context=context,
|
|
124
|
+
)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Structural diff between two runs (typically parent vs fork).
|
|
2
|
+
|
|
3
|
+
CONTRACT v0.5 #10: diff is structural only — divergent objects, divergent
|
|
4
|
+
relations, and which event ranges belong to each side. Semantic comparison
|
|
5
|
+
(e.g., "do these two claims express the same idea?") is a behavior's job,
|
|
6
|
+
not the runtime's.
|
|
7
|
+
|
|
8
|
+
Diff is computed by walking both graphs' final state and event logs. We
|
|
9
|
+
ignore lifecycle events (`behavior.*`, `runtime.*`) when computing the
|
|
10
|
+
event partition — they're scaffolding, not history.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import copy
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Optional
|
|
18
|
+
|
|
19
|
+
from activegraph.core.event import Event
|
|
20
|
+
from activegraph.core.graph import Graph, Object, Relation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_LIFECYCLE_PREFIXES = ("behavior.", "relation_behavior.", "runtime.")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _is_lifecycle(e: Event) -> bool:
|
|
27
|
+
return any(e.type.startswith(p) for p in _LIFECYCLE_PREFIXES)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class DivergentObject:
|
|
32
|
+
id: str
|
|
33
|
+
in_parent: Optional[dict[str, Any]] # to_dict snapshot or None
|
|
34
|
+
in_fork: Optional[dict[str, Any]]
|
|
35
|
+
|
|
36
|
+
def summary(self) -> str:
|
|
37
|
+
if self.in_parent is None:
|
|
38
|
+
return f"{self.id} only in fork"
|
|
39
|
+
if self.in_fork is None:
|
|
40
|
+
return f"{self.id} only in parent"
|
|
41
|
+
return f"{self.id} differs (parent v{self.in_parent.get('version')} ↔ fork v{self.in_fork.get('version')})"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class DivergentRelation:
|
|
46
|
+
id: str
|
|
47
|
+
in_parent: Optional[dict[str, Any]]
|
|
48
|
+
in_fork: Optional[dict[str, Any]]
|
|
49
|
+
|
|
50
|
+
def summary(self) -> str:
|
|
51
|
+
if self.in_parent is None:
|
|
52
|
+
r = self.in_fork
|
|
53
|
+
return f"{self.id} only in fork ({r['source']} --{r['type']}--> {r['target']})"
|
|
54
|
+
if self.in_fork is None:
|
|
55
|
+
r = self.in_parent
|
|
56
|
+
return f"{self.id} only in parent ({r['source']} --{r['type']}--> {r['target']})"
|
|
57
|
+
return f"{self.id} differs"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass
|
|
61
|
+
class Diff:
|
|
62
|
+
parent_run_id: str
|
|
63
|
+
fork_run_id: str
|
|
64
|
+
shared_events: list[Event] = field(default_factory=list)
|
|
65
|
+
parent_only_events: list[Event] = field(default_factory=list)
|
|
66
|
+
fork_only_events: list[Event] = field(default_factory=list)
|
|
67
|
+
divergent_objects: list[DivergentObject] = field(default_factory=list)
|
|
68
|
+
divergent_relations: list[DivergentRelation] = field(default_factory=list)
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
def is_identical(self) -> bool:
|
|
72
|
+
return (
|
|
73
|
+
not self.parent_only_events
|
|
74
|
+
and not self.fork_only_events
|
|
75
|
+
and not self.divergent_objects
|
|
76
|
+
and not self.divergent_relations
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def compute_diff(parent: Graph, fork: Graph, parent_run_id: str, fork_run_id: str) -> Diff:
|
|
81
|
+
parent_events = [e for e in parent.events if not _is_lifecycle(e)]
|
|
82
|
+
fork_events = [e for e in fork.events if not _is_lifecycle(e)]
|
|
83
|
+
|
|
84
|
+
# Shared prefix: events that match by id, type AND payload. Same id with
|
|
85
|
+
# different content means the fork already diverged (logical ids are
|
|
86
|
+
# scoped to run_id — CONTRACT #12 — so collisions after the fork point
|
|
87
|
+
# are expected and must not be flattened into "shared".)
|
|
88
|
+
shared: list[Event] = []
|
|
89
|
+
i = 0
|
|
90
|
+
while i < len(parent_events) and i < len(fork_events):
|
|
91
|
+
a, b = parent_events[i], fork_events[i]
|
|
92
|
+
if a.id == b.id and a.type == b.type and a.payload == b.payload:
|
|
93
|
+
shared.append(a)
|
|
94
|
+
i += 1
|
|
95
|
+
else:
|
|
96
|
+
break
|
|
97
|
+
parent_only = parent_events[i:]
|
|
98
|
+
fork_only = fork_events[i:]
|
|
99
|
+
|
|
100
|
+
# Object divergence.
|
|
101
|
+
obj_diffs: list[DivergentObject] = []
|
|
102
|
+
parent_objs = {o.id: o for o in parent.all_objects()}
|
|
103
|
+
fork_objs = {o.id: o for o in fork.all_objects()}
|
|
104
|
+
for oid in sorted(set(parent_objs) | set(fork_objs)):
|
|
105
|
+
po = parent_objs.get(oid)
|
|
106
|
+
fo = fork_objs.get(oid)
|
|
107
|
+
pd = _normalize_object(po) if po else None
|
|
108
|
+
fd = _normalize_object(fo) if fo else None
|
|
109
|
+
if pd != fd:
|
|
110
|
+
obj_diffs.append(DivergentObject(id=oid, in_parent=pd, in_fork=fd))
|
|
111
|
+
|
|
112
|
+
# Relation divergence.
|
|
113
|
+
rel_diffs: list[DivergentRelation] = []
|
|
114
|
+
parent_rels = {r.id: r for r in parent.all_relations()}
|
|
115
|
+
fork_rels = {r.id: r for r in fork.all_relations()}
|
|
116
|
+
for rid in sorted(set(parent_rels) | set(fork_rels)):
|
|
117
|
+
pr = parent_rels.get(rid)
|
|
118
|
+
fr = fork_rels.get(rid)
|
|
119
|
+
pd = _normalize_relation(pr) if pr else None
|
|
120
|
+
fd = _normalize_relation(fr) if fr else None
|
|
121
|
+
if pd != fd:
|
|
122
|
+
rel_diffs.append(DivergentRelation(id=rid, in_parent=pd, in_fork=fd))
|
|
123
|
+
|
|
124
|
+
return Diff(
|
|
125
|
+
parent_run_id=parent_run_id,
|
|
126
|
+
fork_run_id=fork_run_id,
|
|
127
|
+
shared_events=shared,
|
|
128
|
+
parent_only_events=parent_only,
|
|
129
|
+
fork_only_events=fork_only,
|
|
130
|
+
divergent_objects=obj_diffs,
|
|
131
|
+
divergent_relations=rel_diffs,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _normalize_object(o: Object) -> dict[str, Any]:
|
|
136
|
+
"""Strip provenance (timestamps, run_id) so equality is structural."""
|
|
137
|
+
d = o.to_dict()
|
|
138
|
+
d.pop("provenance", None)
|
|
139
|
+
return d
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _normalize_relation(r: Relation) -> dict[str, Any]:
|
|
143
|
+
d = r.to_dict()
|
|
144
|
+
d.pop("provenance", None)
|
|
145
|
+
return d
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Replay errors. CONTRACT v0.5 #7 (replay strictness), CONTRACT v1.0 #C1
|
|
2
|
+
(error format) — migrated to the ActiveGraphError hierarchy in v1.0 PR-A.
|
|
3
|
+
|
|
4
|
+
`ReplayDivergenceError` is the canonical replay error and the reference
|
|
5
|
+
error class for the v1.0 message rewrite series. Its message obeys the
|
|
6
|
+
locked format (CONTRACT v1.0 #3); three distinct call sites in the
|
|
7
|
+
runtime produce three distinct messages, discriminated by the inputs
|
|
8
|
+
to ``__init__`` and snapshot-tested individually:
|
|
9
|
+
|
|
10
|
+
1. **prompt_hash_mismatch** — the LLM-cache prompt hash for a recorded
|
|
11
|
+
``llm.requested`` event no longer matches the live re-run hash.
|
|
12
|
+
Something changed in the behavior code, prompt template, or a
|
|
13
|
+
tool's input arguments.
|
|
14
|
+
2. **type_mismatch** — an event at the same stream position has a
|
|
15
|
+
different ``type`` than recorded. The behavior graph produced a
|
|
16
|
+
different shape of work.
|
|
17
|
+
3. **length_mismatch** — the recorded stream and the live re-run
|
|
18
|
+
produced different numbers of events. A behavior was added, removed,
|
|
19
|
+
or short-circuits differently.
|
|
20
|
+
|
|
21
|
+
The signature ``ReplayDivergenceError(event_id=..., expected=..., actual=...)``
|
|
22
|
+
is preserved from v0.5 so existing call sites and tests stay valid; the
|
|
23
|
+
discriminator is inferred from the inputs.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
from activegraph.errors import ReplayError
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
_NO_RECORDED_EVENT_SENTINEL = "<no recorded event>"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ReplayDivergenceError(ReplayError):
|
|
37
|
+
"""Raised when a replay (``replay_strict=True``) or a fork produces an
|
|
38
|
+
event stream that does not match the recorded log.
|
|
39
|
+
|
|
40
|
+
``event_id`` pins the first divergence point so an operator can jump
|
|
41
|
+
directly to it. ``expected`` and ``actual`` describe what was
|
|
42
|
+
recorded vs. what the live re-run produced; one is ``None`` when the
|
|
43
|
+
re-run finished early or produced an extra event with no recorded
|
|
44
|
+
counterpart.
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
_doc_slug = "replay-divergence-error"
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
*,
|
|
52
|
+
event_id: str,
|
|
53
|
+
expected: str,
|
|
54
|
+
actual: str | None,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.event_id = event_id
|
|
57
|
+
self.expected = expected
|
|
58
|
+
self.actual = actual
|
|
59
|
+
kind, summary, what_failed, why, how_to_fix = _build_message(
|
|
60
|
+
event_id=event_id, expected=expected, actual=actual
|
|
61
|
+
)
|
|
62
|
+
self.kind = kind
|
|
63
|
+
context: dict[str, Any] = {
|
|
64
|
+
"event_id": event_id,
|
|
65
|
+
"kind": kind,
|
|
66
|
+
"expected": expected,
|
|
67
|
+
"actual": actual,
|
|
68
|
+
}
|
|
69
|
+
super().__init__(
|
|
70
|
+
summary,
|
|
71
|
+
what_failed=what_failed,
|
|
72
|
+
why=why,
|
|
73
|
+
how_to_fix=how_to_fix,
|
|
74
|
+
context=context,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_message(
|
|
79
|
+
*, event_id: str, expected: str, actual: str | None
|
|
80
|
+
) -> tuple[str, str, str, str, str]:
|
|
81
|
+
"""Return ``(kind, summary, what_failed, why, how_to_fix)`` for the
|
|
82
|
+
three replay-divergence shapes. The discriminator is the input shape:
|
|
83
|
+
|
|
84
|
+
- ``expected`` starts with ``prompt_hash=`` -> prompt_hash_mismatch
|
|
85
|
+
- ``expected`` is ``"<no recorded event>"`` or ``actual is None`` ->
|
|
86
|
+
length_mismatch
|
|
87
|
+
- otherwise -> type_mismatch
|
|
88
|
+
"""
|
|
89
|
+
if isinstance(expected, str) and expected.startswith("prompt_hash="):
|
|
90
|
+
return _prompt_hash_message(event_id, expected, actual)
|
|
91
|
+
if expected == _NO_RECORDED_EVENT_SENTINEL or actual is None:
|
|
92
|
+
return _length_message(event_id, expected, actual)
|
|
93
|
+
return _type_message(event_id, expected, actual)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _prompt_hash_message(
|
|
97
|
+
event_id: str, expected: str, actual: str | None,
|
|
98
|
+
) -> tuple[str, str, str, str, str]:
|
|
99
|
+
actual_str = actual if actual is not None else "<no live response>"
|
|
100
|
+
return (
|
|
101
|
+
"prompt_hash_mismatch",
|
|
102
|
+
f"replay diverged at {event_id}: LLM prompt hash mismatch",
|
|
103
|
+
(
|
|
104
|
+
f"Event {event_id} (an `llm.requested` event in the recorded log) had a "
|
|
105
|
+
f"different prompt hash during this replay than the parent run recorded:\n"
|
|
106
|
+
f" recorded: {expected}\n"
|
|
107
|
+
f" live: {actual_str}"
|
|
108
|
+
),
|
|
109
|
+
(
|
|
110
|
+
"The replay cache keys on the full prompt hash, so any change to an LLM "
|
|
111
|
+
"behavior's code, a prompt template, a system message, or a tool's input "
|
|
112
|
+
"arguments produces a mismatch. The framework refuses to silently substitute "
|
|
113
|
+
"a stale cached response under a new prompt — that would break the audit "
|
|
114
|
+
"trail the cache is designed to preserve."
|
|
115
|
+
),
|
|
116
|
+
(
|
|
117
|
+
f"If the change was intentional (you edited a behavior or a prompt template),\n"
|
|
118
|
+
f"re-record the cache from the divergence point:\n"
|
|
119
|
+
f" activegraph fork <parent-run> --at-event {event_id} --record\n"
|
|
120
|
+
f"\n"
|
|
121
|
+
f"If the change was unintentional, diff your code against the recorded run's\n"
|
|
122
|
+
f"pack version and revert the change:\n"
|
|
123
|
+
f" activegraph inspect <parent-run> --pack-version\n"
|
|
124
|
+
f"\n"
|
|
125
|
+
f"To see the full recorded prompt for this event:\n"
|
|
126
|
+
f" activegraph inspect <parent-run> --event {event_id}"
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _type_message(
|
|
132
|
+
event_id: str, expected: str, actual: str | None,
|
|
133
|
+
) -> tuple[str, str, str, str, str]:
|
|
134
|
+
actual_str = actual if actual is not None else "<no live event>"
|
|
135
|
+
return (
|
|
136
|
+
"type_mismatch",
|
|
137
|
+
f"replay diverged at {event_id}: event type mismatch",
|
|
138
|
+
(
|
|
139
|
+
f"At the stream position pinned to event {event_id}, the live re-run "
|
|
140
|
+
f"produced a different event type than recorded:\n"
|
|
141
|
+
f" recorded: {expected!r}\n"
|
|
142
|
+
f" live: {actual_str!r}"
|
|
143
|
+
),
|
|
144
|
+
(
|
|
145
|
+
"Strict replay compares the type stream of non-lifecycle events between the "
|
|
146
|
+
"recorded log and the live re-run. A type mismatch means the behavior graph "
|
|
147
|
+
"took a different branch — usually because a behavior's `where` filter, a "
|
|
148
|
+
"pattern subscription, or a conditional `graph.emit` changed since the "
|
|
149
|
+
"recorded run."
|
|
150
|
+
),
|
|
151
|
+
(
|
|
152
|
+
f"Identify the behavior that produced event {event_id} in the recorded log:\n"
|
|
153
|
+
f" activegraph inspect <parent-run> --event {event_id}\n"
|
|
154
|
+
f"\n"
|
|
155
|
+
f"Diff that behavior against your current source. If the change was\n"
|
|
156
|
+
f"intentional, re-run without `replay_strict=True` (or fork with --record\n"
|
|
157
|
+
f"from the divergence point). If unintentional, revert the behavior."
|
|
158
|
+
),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _length_message(
|
|
163
|
+
event_id: str, expected: str, actual: str | None,
|
|
164
|
+
) -> tuple[str, str, str, str, str]:
|
|
165
|
+
if actual is None:
|
|
166
|
+
return (
|
|
167
|
+
"length_mismatch",
|
|
168
|
+
f"replay diverged at {event_id}: live re-run finished early",
|
|
169
|
+
(
|
|
170
|
+
f"The recorded log contained event {event_id} (type {expected!r}) at this "
|
|
171
|
+
f"position, but the live re-run terminated before producing it.\n"
|
|
172
|
+
f" recorded: {expected!r}\n"
|
|
173
|
+
f" live: <no event produced>"
|
|
174
|
+
),
|
|
175
|
+
(
|
|
176
|
+
"Strict replay requires the live re-run to produce the same number and "
|
|
177
|
+
"shape of non-lifecycle events as the recording. A short live re-run means "
|
|
178
|
+
"a behavior that fired in the recorded run no longer fires, or short-"
|
|
179
|
+
"circuits earlier — usually because a pattern subscription, a `where` "
|
|
180
|
+
"filter, or a guard condition was tightened since the recording."
|
|
181
|
+
),
|
|
182
|
+
(
|
|
183
|
+
f"Identify the behavior that produced {event_id} in the recorded log:\n"
|
|
184
|
+
f" activegraph inspect <parent-run> --event {event_id}\n"
|
|
185
|
+
f"\n"
|
|
186
|
+
f"Compare that behavior's current trigger conditions against the recorded\n"
|
|
187
|
+
f"run's. If the change was intentional, fork with --record from the\n"
|
|
188
|
+
f"divergence point to refresh the recording. If unintentional, revert."
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
return (
|
|
192
|
+
"length_mismatch",
|
|
193
|
+
f"replay diverged at {event_id}: live re-run produced an unrecorded event",
|
|
194
|
+
(
|
|
195
|
+
f"At the position pinned to event {event_id}, the live re-run produced an "
|
|
196
|
+
f"event of type {actual!r}, but the recorded log had no event here.\n"
|
|
197
|
+
f" recorded: <no event recorded>\n"
|
|
198
|
+
f" live: {actual!r}"
|
|
199
|
+
),
|
|
200
|
+
(
|
|
201
|
+
"Strict replay requires the live re-run's event stream to match the "
|
|
202
|
+
"recording position-for-position. An extra live event means a behavior "
|
|
203
|
+
"fires now that did not fire in the recorded run — usually because a new "
|
|
204
|
+
"behavior was added, or a pattern subscription was loosened."
|
|
205
|
+
),
|
|
206
|
+
(
|
|
207
|
+
f"List the behaviors currently registered and compare against the recorded\n"
|
|
208
|
+
f"pack version:\n"
|
|
209
|
+
f" activegraph inspect <parent-run> --behaviors\n"
|
|
210
|
+
f"\n"
|
|
211
|
+
f"If the new behavior is intentional, re-record from this position:\n"
|
|
212
|
+
f" activegraph fork <parent-run> --at-event {event_id} --record\n"
|
|
213
|
+
f"\n"
|
|
214
|
+
f"If the behavior shouldn't fire here, tighten its trigger conditions."
|
|
215
|
+
),
|
|
216
|
+
)
|