AbstractRuntime 0.2.0__py3-none-any.whl → 0.4.1__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.
Files changed (77) hide show
  1. abstractruntime/__init__.py +83 -3
  2. abstractruntime/core/config.py +82 -2
  3. abstractruntime/core/event_keys.py +62 -0
  4. abstractruntime/core/models.py +17 -1
  5. abstractruntime/core/policy.py +74 -3
  6. abstractruntime/core/runtime.py +3334 -28
  7. abstractruntime/core/vars.py +103 -2
  8. abstractruntime/evidence/__init__.py +10 -0
  9. abstractruntime/evidence/recorder.py +325 -0
  10. abstractruntime/history_bundle.py +772 -0
  11. abstractruntime/integrations/abstractcore/__init__.py +6 -0
  12. abstractruntime/integrations/abstractcore/constants.py +19 -0
  13. abstractruntime/integrations/abstractcore/default_tools.py +258 -0
  14. abstractruntime/integrations/abstractcore/effect_handlers.py +2622 -32
  15. abstractruntime/integrations/abstractcore/embeddings_client.py +69 -0
  16. abstractruntime/integrations/abstractcore/factory.py +149 -16
  17. abstractruntime/integrations/abstractcore/llm_client.py +891 -55
  18. abstractruntime/integrations/abstractcore/mcp_worker.py +587 -0
  19. abstractruntime/integrations/abstractcore/observability.py +80 -0
  20. abstractruntime/integrations/abstractcore/session_attachments.py +946 -0
  21. abstractruntime/integrations/abstractcore/summarizer.py +154 -0
  22. abstractruntime/integrations/abstractcore/tool_executor.py +509 -31
  23. abstractruntime/integrations/abstractcore/workspace_scoped_tools.py +561 -0
  24. abstractruntime/integrations/abstractmemory/__init__.py +3 -0
  25. abstractruntime/integrations/abstractmemory/effect_handlers.py +946 -0
  26. abstractruntime/memory/__init__.py +21 -0
  27. abstractruntime/memory/active_context.py +751 -0
  28. abstractruntime/memory/active_memory.py +452 -0
  29. abstractruntime/memory/compaction.py +105 -0
  30. abstractruntime/memory/kg_packets.py +164 -0
  31. abstractruntime/memory/memact_composer.py +175 -0
  32. abstractruntime/memory/recall_levels.py +163 -0
  33. abstractruntime/memory/token_budget.py +86 -0
  34. abstractruntime/rendering/__init__.py +17 -0
  35. abstractruntime/rendering/agent_trace_report.py +256 -0
  36. abstractruntime/rendering/json_stringify.py +136 -0
  37. abstractruntime/scheduler/scheduler.py +93 -2
  38. abstractruntime/storage/__init__.py +7 -2
  39. abstractruntime/storage/artifacts.py +175 -32
  40. abstractruntime/storage/base.py +17 -1
  41. abstractruntime/storage/commands.py +339 -0
  42. abstractruntime/storage/in_memory.py +41 -1
  43. abstractruntime/storage/json_files.py +210 -14
  44. abstractruntime/storage/observable.py +136 -0
  45. abstractruntime/storage/offloading.py +433 -0
  46. abstractruntime/storage/sqlite.py +836 -0
  47. abstractruntime/visualflow_compiler/__init__.py +29 -0
  48. abstractruntime/visualflow_compiler/adapters/__init__.py +11 -0
  49. abstractruntime/visualflow_compiler/adapters/agent_adapter.py +126 -0
  50. abstractruntime/visualflow_compiler/adapters/context_adapter.py +109 -0
  51. abstractruntime/visualflow_compiler/adapters/control_adapter.py +615 -0
  52. abstractruntime/visualflow_compiler/adapters/effect_adapter.py +1051 -0
  53. abstractruntime/visualflow_compiler/adapters/event_adapter.py +307 -0
  54. abstractruntime/visualflow_compiler/adapters/function_adapter.py +97 -0
  55. abstractruntime/visualflow_compiler/adapters/memact_adapter.py +114 -0
  56. abstractruntime/visualflow_compiler/adapters/subflow_adapter.py +74 -0
  57. abstractruntime/visualflow_compiler/adapters/variable_adapter.py +316 -0
  58. abstractruntime/visualflow_compiler/compiler.py +3832 -0
  59. abstractruntime/visualflow_compiler/flow.py +247 -0
  60. abstractruntime/visualflow_compiler/visual/__init__.py +13 -0
  61. abstractruntime/visualflow_compiler/visual/agent_ids.py +29 -0
  62. abstractruntime/visualflow_compiler/visual/builtins.py +1376 -0
  63. abstractruntime/visualflow_compiler/visual/code_executor.py +214 -0
  64. abstractruntime/visualflow_compiler/visual/executor.py +2804 -0
  65. abstractruntime/visualflow_compiler/visual/models.py +211 -0
  66. abstractruntime/workflow_bundle/__init__.py +52 -0
  67. abstractruntime/workflow_bundle/models.py +236 -0
  68. abstractruntime/workflow_bundle/packer.py +317 -0
  69. abstractruntime/workflow_bundle/reader.py +87 -0
  70. abstractruntime/workflow_bundle/registry.py +587 -0
  71. abstractruntime-0.4.1.dist-info/METADATA +177 -0
  72. abstractruntime-0.4.1.dist-info/RECORD +86 -0
  73. abstractruntime-0.4.1.dist-info/entry_points.txt +2 -0
  74. abstractruntime-0.2.0.dist-info/METADATA +0 -163
  75. abstractruntime-0.2.0.dist-info/RECORD +0 -32
  76. {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.1.dist-info}/WHEEL +0 -0
  77. {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -32,8 +32,27 @@ from .core.policy import (
32
32
  from .storage.base import QueryableRunStore
33
33
  from .storage.in_memory import InMemoryLedgerStore, InMemoryRunStore
34
34
  from .storage.json_files import JsonFileRunStore, JsonlLedgerStore
35
+ from .storage.sqlite import (
36
+ SqliteCommandCursorStore,
37
+ SqliteCommandStore,
38
+ SqliteDatabase,
39
+ SqliteLedgerStore,
40
+ SqliteRunStore,
41
+ )
42
+ from .storage.commands import (
43
+ CommandAppendResult,
44
+ CommandCursorStore,
45
+ CommandRecord,
46
+ CommandStore,
47
+ InMemoryCommandCursorStore,
48
+ InMemoryCommandStore,
49
+ JsonFileCommandCursorStore,
50
+ JsonlCommandStore,
51
+ )
35
52
  from .storage.ledger_chain import HashChainedLedgerStore, verify_ledger_chain
53
+ from .storage.observable import ObservableLedgerStore, ObservableLedgerStoreProtocol
36
54
  from .storage.snapshots import Snapshot, SnapshotStore, InMemorySnapshotStore, JsonSnapshotStore
55
+ from .storage.offloading import OffloadingLedgerStore, OffloadingRunStore, offload_large_values
37
56
  from .storage.artifacts import (
38
57
  Artifact,
39
58
  ArtifactMetadata,
@@ -54,6 +73,29 @@ from .scheduler import (
54
73
  ScheduledRuntime,
55
74
  create_scheduled_runtime,
56
75
  )
76
+ from .memory import ActiveContextPolicy, TimeRange
77
+ from .workflow_bundle import (
78
+ WORKFLOW_BUNDLE_FORMAT_VERSION_V1,
79
+ InstalledWorkflowBundle,
80
+ WorkflowBundle,
81
+ WorkflowBundleEntrypoint,
82
+ WorkflowBundleError,
83
+ WorkflowBundleManifest,
84
+ WorkflowBundleRegistry,
85
+ WorkflowBundleRegistryError,
86
+ WorkflowEntrypointRef,
87
+ default_workflow_bundles_dir,
88
+ open_workflow_bundle,
89
+ sanitize_bundle_id,
90
+ sanitize_bundle_version,
91
+ workflow_bundle_manifest_from_dict,
92
+ workflow_bundle_manifest_to_dict,
93
+ )
94
+ from .history_bundle import (
95
+ RUN_HISTORY_BUNDLE_VERSION_V1,
96
+ export_run_history_bundle,
97
+ persist_workflow_snapshot,
98
+ )
57
99
 
58
100
  __all__ = [
59
101
  # Core models
@@ -79,8 +121,26 @@ __all__ = [
79
121
  "InMemoryLedgerStore",
80
122
  "JsonFileRunStore",
81
123
  "JsonlLedgerStore",
124
+ "SqliteDatabase",
125
+ "SqliteRunStore",
126
+ "SqliteLedgerStore",
127
+ "CommandRecord",
128
+ "CommandAppendResult",
129
+ "CommandStore",
130
+ "CommandCursorStore",
131
+ "InMemoryCommandStore",
132
+ "JsonlCommandStore",
133
+ "InMemoryCommandCursorStore",
134
+ "JsonFileCommandCursorStore",
135
+ "SqliteCommandStore",
136
+ "SqliteCommandCursorStore",
82
137
  "HashChainedLedgerStore",
83
138
  "verify_ledger_chain",
139
+ "ObservableLedgerStore",
140
+ "ObservableLedgerStoreProtocol",
141
+ "OffloadingRunStore",
142
+ "OffloadingLedgerStore",
143
+ "offload_large_values",
84
144
  "Snapshot",
85
145
  "SnapshotStore",
86
146
  "InMemorySnapshotStore",
@@ -104,7 +164,27 @@ __all__ = [
104
164
  "RetryPolicy",
105
165
  "NoRetryPolicy",
106
166
  "compute_idempotency_key",
167
+ # Memory
168
+ "ActiveContextPolicy",
169
+ "TimeRange",
170
+ # WorkflowBundles (portable distribution unit)
171
+ "WORKFLOW_BUNDLE_FORMAT_VERSION_V1",
172
+ "WorkflowBundleError",
173
+ "WorkflowBundleEntrypoint",
174
+ "WorkflowBundleManifest",
175
+ "WorkflowBundle",
176
+ "InstalledWorkflowBundle",
177
+ "WorkflowBundleRegistry",
178
+ "WorkflowBundleRegistryError",
179
+ "WorkflowEntrypointRef",
180
+ "default_workflow_bundles_dir",
181
+ "sanitize_bundle_id",
182
+ "sanitize_bundle_version",
183
+ "workflow_bundle_manifest_from_dict",
184
+ "workflow_bundle_manifest_to_dict",
185
+ "open_workflow_bundle",
186
+ # Run history bundle (portable replay)
187
+ "RUN_HISTORY_BUNDLE_VERSION_V1",
188
+ "export_run_history_bundle",
189
+ "persist_workflow_snapshot",
107
190
  ]
108
-
109
-
110
-
@@ -12,6 +12,13 @@ from __future__ import annotations
12
12
  from dataclasses import dataclass, field
13
13
  from typing import Any, Dict, Optional
14
14
 
15
+ from .vars import DEFAULT_MAX_TOKENS
16
+
17
+ # Truncation policy: keep mechanisms, but default to disabled.
18
+ # A positive value enables a conservative auto-cap for `max_input_tokens` when callers do not
19
+ # explicitly set an input budget. `-1` disables this cap (no automatic truncation).
20
+ DEFAULT_RECOMMENDED_MAX_INPUT_TOKENS = -1
21
+
15
22
 
16
23
  @dataclass(frozen=True)
17
24
  class RuntimeConfig:
@@ -29,6 +36,8 @@ class RuntimeConfig:
29
36
  max_output_tokens: Maximum tokens for LLM response (None = provider default)
30
37
  warn_tokens_pct: Percentage threshold for token warnings (default: 80)
31
38
  max_history_messages: Maximum conversation history messages (-1 = unlimited)
39
+ provider: Default provider id for this Runtime (best-effort; used for run metadata)
40
+ model: Default model id for this Runtime (best-effort; used for run metadata)
32
41
  model_capabilities: Dict of model capabilities from LLM provider
33
42
 
34
43
  Example:
@@ -45,11 +54,16 @@ class RuntimeConfig:
45
54
  # Token/context window management
46
55
  max_tokens: Optional[int] = None # None = query from model capabilities
47
56
  max_output_tokens: Optional[int] = None # None = use provider default
57
+ max_input_tokens: Optional[int] = None # None = auto-calculate from max_tokens/max_output_tokens
48
58
  warn_tokens_pct: int = 80
49
59
 
50
60
  # History management
51
61
  max_history_messages: int = -1 # -1 = unlimited (send all messages)
52
62
 
63
+ # Default routing metadata (optional; depends on how the Runtime was constructed)
64
+ provider: Optional[str] = None
65
+ model: Optional[str] = None
66
+
53
67
  # Model capabilities (populated from LLM client)
54
68
  model_capabilities: Dict[str, Any] = field(default_factory=dict)
55
69
 
@@ -60,14 +74,77 @@ class RuntimeConfig:
60
74
  Dict with canonical limit values for storage in RunState.vars["_limits"].
61
75
  Uses model_capabilities as fallback for max_tokens if not explicitly set.
62
76
  """
77
+ max_tokens = self.max_tokens
78
+ if max_tokens is None:
79
+ max_tokens = self.model_capabilities.get("max_tokens")
80
+ if max_tokens is None:
81
+ max_tokens = DEFAULT_MAX_TOKENS
82
+
83
+ max_output_tokens = self.max_output_tokens
84
+ if max_output_tokens is None:
85
+ # Best-effort: persist the provider/model default so agent logic can reason about
86
+ # output-size constraints (e.g., chunk large tool arguments like file contents).
87
+ max_output_tokens = self.model_capabilities.get("max_output_tokens")
88
+ # If capabilities are unavailable and max_output_tokens is unset, keep it as None
89
+ # (meaning: provider default). Do not force a conservative output cap here.
90
+
91
+ # ADR-0008 alignment:
92
+ # - max_tokens: total context window size
93
+ # - max_output_tokens: output budget
94
+ # - max_input_tokens: explicit or derived input budget (may be smaller than max_tokens-max_output_tokens)
95
+ #
96
+ # Constraint: max_input_tokens + max_output_tokens + delta <= max_tokens
97
+ delta = 256
98
+ effective_max_input_tokens = self.max_input_tokens
99
+
100
+ try:
101
+ max_tokens_int = int(max_tokens) if max_tokens is not None else None
102
+ except Exception:
103
+ max_tokens_int = None
104
+ try:
105
+ max_output_int = int(max_output_tokens) if max_output_tokens is not None else None
106
+ except Exception:
107
+ max_output_int = None
108
+
109
+ if (
110
+ max_tokens_int is not None
111
+ and max_tokens_int > 0
112
+ and max_output_int is not None
113
+ and max_output_int >= 0
114
+ and effective_max_input_tokens is not None
115
+ ):
116
+ # If callers explicitly set max_input_tokens, clamp it to the context-window constraint.
117
+ max_allowed_in = max(0, int(max_tokens_int) - int(max_output_int) - int(delta))
118
+ try:
119
+ effective_max_input_tokens = int(effective_max_input_tokens)
120
+ except Exception:
121
+ effective_max_input_tokens = max_allowed_in
122
+ if effective_max_input_tokens < 0:
123
+ effective_max_input_tokens = 0
124
+ if effective_max_input_tokens > max_allowed_in:
125
+ effective_max_input_tokens = max_allowed_in
126
+
127
+ # Optional conservative auto-cap (disabled by default with -1).
128
+ if (
129
+ self.max_input_tokens is None
130
+ and effective_max_input_tokens is not None
131
+ and isinstance(DEFAULT_RECOMMENDED_MAX_INPUT_TOKENS, int)
132
+ and DEFAULT_RECOMMENDED_MAX_INPUT_TOKENS > 0
133
+ ):
134
+ try:
135
+ effective_max_input_tokens = min(int(effective_max_input_tokens), int(DEFAULT_RECOMMENDED_MAX_INPUT_TOKENS))
136
+ except Exception:
137
+ pass
138
+
63
139
  return {
64
140
  # Iteration control
65
141
  "max_iterations": self.max_iterations,
66
142
  "current_iteration": 0,
67
143
 
68
144
  # Token management
69
- "max_tokens": self.max_tokens or self.model_capabilities.get("max_tokens", 32768),
70
- "max_output_tokens": self.max_output_tokens,
145
+ "max_tokens": max_tokens,
146
+ "max_output_tokens": max_output_tokens,
147
+ "max_input_tokens": effective_max_input_tokens,
71
148
  "estimated_tokens_used": 0,
72
149
 
73
150
  # History management
@@ -95,7 +172,10 @@ class RuntimeConfig:
95
172
  warn_iterations_pct=self.warn_iterations_pct,
96
173
  max_tokens=self.max_tokens,
97
174
  max_output_tokens=self.max_output_tokens,
175
+ max_input_tokens=self.max_input_tokens,
98
176
  warn_tokens_pct=self.warn_tokens_pct,
99
177
  max_history_messages=self.max_history_messages,
178
+ provider=self.provider,
179
+ model=self.model,
100
180
  model_capabilities=capabilities,
101
181
  )
@@ -0,0 +1,62 @@
1
+ """abstractruntime.core.event_keys
2
+
3
+ Durable event key conventions.
4
+
5
+ Why this exists:
6
+ - `WAIT_EVENT` needs a stable `wait_key` that external hosts can compute.
7
+ - Visual editors and other hosts (AbstractCode, servers) must agree on the same
8
+ key format without importing UI-specific code.
9
+
10
+ We keep this module dependency-light (stdlib only).
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Optional
16
+
17
+
18
+ def build_event_wait_key(
19
+ *,
20
+ scope: str,
21
+ name: str,
22
+ session_id: Optional[str] = None,
23
+ workflow_id: Optional[str] = None,
24
+ run_id: Optional[str] = None,
25
+ ) -> str:
26
+ """Build a durable wait_key for event-driven workflows.
27
+
28
+ Format:
29
+ evt:{scope}:{scope_id}:{name}
30
+
31
+ Scopes:
32
+ - session: `scope_id` is the workflow instance/session identifier (recommended default)
33
+ - workflow: `scope_id` is the workflow_id
34
+ - run: `scope_id` is the run_id
35
+ - global: `scope_id` is the literal string "global"
36
+ """
37
+ scope_norm = str(scope or "session").strip().lower()
38
+ name_norm = str(name or "").strip()
39
+ if not name_norm:
40
+ raise ValueError("event name is required")
41
+
42
+ scope_id: Optional[str]
43
+ if scope_norm == "session":
44
+ scope_id = str(session_id or "").strip() if session_id is not None else ""
45
+ elif scope_norm == "workflow":
46
+ scope_id = str(workflow_id or "").strip() if workflow_id is not None else ""
47
+ elif scope_norm == "run":
48
+ scope_id = str(run_id or "").strip() if run_id is not None else ""
49
+ elif scope_norm == "global":
50
+ scope_id = "global"
51
+ else:
52
+ raise ValueError(f"unknown event scope: {scope!r}")
53
+
54
+ if not scope_id:
55
+ raise ValueError(f"missing scope id for scope={scope_norm!r}")
56
+
57
+ return f"evt:{scope_norm}:{scope_id}:{name_norm}"
58
+
59
+
60
+
61
+
62
+
@@ -46,10 +46,27 @@ class EffectType(str, Enum):
46
46
  WAIT_EVENT = "wait_event"
47
47
  WAIT_UNTIL = "wait_until"
48
48
  ASK_USER = "ask_user"
49
+ ANSWER_USER = "answer_user"
50
+
51
+ # Eventing
52
+ EMIT_EVENT = "emit_event"
49
53
 
50
54
  # Integrations (implemented via pluggable handlers)
51
55
  LLM_CALL = "llm_call"
52
56
  TOOL_CALLS = "tool_calls"
57
+ MEMORY_QUERY = "memory_query"
58
+ MEMORY_TAG = "memory_tag"
59
+ MEMORY_COMPACT = "memory_compact"
60
+ MEMORY_NOTE = "memory_note"
61
+ MEMORY_REHYDRATE = "memory_rehydrate"
62
+
63
+ # Semantic / KG memory (host-provided handlers)
64
+ MEMORY_KG_ASSERT = "memory_kg_assert"
65
+ MEMORY_KG_QUERY = "memory_kg_query"
66
+ MEMORY_KG_RESOLVE = "memory_kg_resolve"
67
+
68
+ # Debug / inspection (schema-only tools -> runtime effects)
69
+ VARS_QUERY = "vars_query"
53
70
 
54
71
  # Composition
55
72
  START_SUBWORKFLOW = "start_subworkflow"
@@ -279,4 +296,3 @@ class LimitWarning:
279
296
  def __post_init__(self) -> None:
280
297
  if self.maximum > 0:
281
298
  self.pct = round(self.current / self.maximum * 100, 1)
282
-
@@ -15,7 +15,76 @@ import json
15
15
  from dataclasses import dataclass
16
16
  from typing import Any, Dict, Optional, Protocol
17
17
 
18
- from .models import Effect, RunState
18
+ from .models import Effect, EffectType, RunState
19
+
20
+
21
+ def _loads_dict_like(value: Any) -> Optional[Dict[str, Any]]:
22
+ if value is None:
23
+ return None
24
+ if isinstance(value, dict):
25
+ return dict(value)
26
+ if not isinstance(value, str):
27
+ return None
28
+ text = value.strip()
29
+ if not text:
30
+ return None
31
+ try:
32
+ parsed = json.loads(text)
33
+ except Exception:
34
+ return None
35
+ return parsed if isinstance(parsed, dict) else None
36
+
37
+
38
+ def _normalize_tool_call_for_idempotency(value: Any) -> Any:
39
+ if not isinstance(value, dict):
40
+ return value
41
+
42
+ out = dict(value)
43
+ # Provider/model-emitted IDs are not semantic; remove them from the idempotency hash.
44
+ for k in ("call_id", "id", "runtime_call_id", "model_call_id", "idempotency_key"):
45
+ out.pop(k, None)
46
+
47
+ name = out.get("name")
48
+ if isinstance(name, str):
49
+ out["name"] = name.strip()
50
+
51
+ args = out.get("arguments")
52
+ if isinstance(args, str):
53
+ parsed = _loads_dict_like(args)
54
+ out["arguments"] = parsed if isinstance(parsed, dict) else {}
55
+ elif not isinstance(args, dict):
56
+ out["arguments"] = {}
57
+
58
+ func = out.get("function")
59
+ if isinstance(func, dict):
60
+ # Some callers pass OpenAI-style shapes; preserve semantics, but strip IDs.
61
+ out["function"] = _normalize_tool_call_for_idempotency(func)
62
+
63
+ return out
64
+
65
+
66
+ def _normalize_effect_payload_for_idempotency(effect: Effect) -> Dict[str, Any]:
67
+ if not isinstance(effect.payload, dict):
68
+ return {}
69
+ payload = dict(effect.payload)
70
+
71
+ if effect.type != EffectType.TOOL_CALLS:
72
+ return payload
73
+
74
+ tool_calls = payload.get("tool_calls")
75
+ if isinstance(tool_calls, list):
76
+ payload["tool_calls"] = [_normalize_tool_call_for_idempotency(tc) for tc in tool_calls]
77
+
78
+ allowed_tools = payload.get("allowed_tools")
79
+ if isinstance(allowed_tools, list):
80
+ uniq = {
81
+ str(t).strip()
82
+ for t in allowed_tools
83
+ if isinstance(t, str) and t.strip()
84
+ }
85
+ payload["allowed_tools"] = sorted(uniq)
86
+
87
+ return payload
19
88
 
20
89
 
21
90
  class EffectPolicy(Protocol):
@@ -110,11 +179,12 @@ class DefaultEffectPolicy:
110
179
  This ensures the same effect at the same point in the same run
111
180
  gets the same key, enabling deduplication on restart.
112
181
  """
182
+ normalized_payload = _normalize_effect_payload_for_idempotency(effect)
113
183
  key_data = {
114
184
  "run_id": run.run_id,
115
185
  "node_id": node_id,
116
186
  "effect_type": effect.type.value,
117
- "effect_payload": effect.payload,
187
+ "effect_payload": normalized_payload,
118
188
  }
119
189
  key_json = json.dumps(key_data, sort_keys=True, separators=(",", ":"))
120
190
  return hashlib.sha256(key_json.encode()).hexdigest()[:32]
@@ -156,11 +226,12 @@ def compute_idempotency_key(
156
226
 
157
227
  Useful when you need to compute a key without a full policy.
158
228
  """
229
+ normalized_payload = _normalize_effect_payload_for_idempotency(effect)
159
230
  key_data = {
160
231
  "run_id": run_id,
161
232
  "node_id": node_id,
162
233
  "effect_type": effect.type.value,
163
- "effect_payload": effect.payload,
234
+ "effect_payload": normalized_payload,
164
235
  }
165
236
  key_json = json.dumps(key_data, sort_keys=True, separators=(",", ":"))
166
237
  return hashlib.sha256(key_json.encode()).hexdigest()[:32]