AbstractRuntime 0.4.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 (65) hide show
  1. abstractruntime/__init__.py +76 -1
  2. abstractruntime/core/config.py +68 -1
  3. abstractruntime/core/models.py +5 -0
  4. abstractruntime/core/policy.py +74 -3
  5. abstractruntime/core/runtime.py +1002 -126
  6. abstractruntime/core/vars.py +8 -2
  7. abstractruntime/evidence/recorder.py +1 -1
  8. abstractruntime/history_bundle.py +772 -0
  9. abstractruntime/integrations/abstractcore/__init__.py +3 -0
  10. abstractruntime/integrations/abstractcore/default_tools.py +127 -3
  11. abstractruntime/integrations/abstractcore/effect_handlers.py +2440 -99
  12. abstractruntime/integrations/abstractcore/embeddings_client.py +69 -0
  13. abstractruntime/integrations/abstractcore/factory.py +68 -20
  14. abstractruntime/integrations/abstractcore/llm_client.py +447 -15
  15. abstractruntime/integrations/abstractcore/mcp_worker.py +1 -0
  16. abstractruntime/integrations/abstractcore/session_attachments.py +946 -0
  17. abstractruntime/integrations/abstractcore/tool_executor.py +31 -10
  18. abstractruntime/integrations/abstractcore/workspace_scoped_tools.py +561 -0
  19. abstractruntime/integrations/abstractmemory/__init__.py +3 -0
  20. abstractruntime/integrations/abstractmemory/effect_handlers.py +946 -0
  21. abstractruntime/memory/active_context.py +6 -1
  22. abstractruntime/memory/kg_packets.py +164 -0
  23. abstractruntime/memory/memact_composer.py +175 -0
  24. abstractruntime/memory/recall_levels.py +163 -0
  25. abstractruntime/memory/token_budget.py +86 -0
  26. abstractruntime/storage/__init__.py +4 -1
  27. abstractruntime/storage/artifacts.py +158 -30
  28. abstractruntime/storage/base.py +17 -1
  29. abstractruntime/storage/commands.py +339 -0
  30. abstractruntime/storage/in_memory.py +41 -1
  31. abstractruntime/storage/json_files.py +195 -12
  32. abstractruntime/storage/observable.py +38 -1
  33. abstractruntime/storage/offloading.py +433 -0
  34. abstractruntime/storage/sqlite.py +836 -0
  35. abstractruntime/visualflow_compiler/__init__.py +29 -0
  36. abstractruntime/visualflow_compiler/adapters/__init__.py +11 -0
  37. abstractruntime/visualflow_compiler/adapters/agent_adapter.py +126 -0
  38. abstractruntime/visualflow_compiler/adapters/context_adapter.py +109 -0
  39. abstractruntime/visualflow_compiler/adapters/control_adapter.py +615 -0
  40. abstractruntime/visualflow_compiler/adapters/effect_adapter.py +1051 -0
  41. abstractruntime/visualflow_compiler/adapters/event_adapter.py +307 -0
  42. abstractruntime/visualflow_compiler/adapters/function_adapter.py +97 -0
  43. abstractruntime/visualflow_compiler/adapters/memact_adapter.py +114 -0
  44. abstractruntime/visualflow_compiler/adapters/subflow_adapter.py +74 -0
  45. abstractruntime/visualflow_compiler/adapters/variable_adapter.py +316 -0
  46. abstractruntime/visualflow_compiler/compiler.py +3832 -0
  47. abstractruntime/visualflow_compiler/flow.py +247 -0
  48. abstractruntime/visualflow_compiler/visual/__init__.py +13 -0
  49. abstractruntime/visualflow_compiler/visual/agent_ids.py +29 -0
  50. abstractruntime/visualflow_compiler/visual/builtins.py +1376 -0
  51. abstractruntime/visualflow_compiler/visual/code_executor.py +214 -0
  52. abstractruntime/visualflow_compiler/visual/executor.py +2804 -0
  53. abstractruntime/visualflow_compiler/visual/models.py +211 -0
  54. abstractruntime/workflow_bundle/__init__.py +52 -0
  55. abstractruntime/workflow_bundle/models.py +236 -0
  56. abstractruntime/workflow_bundle/packer.py +317 -0
  57. abstractruntime/workflow_bundle/reader.py +87 -0
  58. abstractruntime/workflow_bundle/registry.py +587 -0
  59. abstractruntime-0.4.1.dist-info/METADATA +177 -0
  60. abstractruntime-0.4.1.dist-info/RECORD +86 -0
  61. abstractruntime-0.4.0.dist-info/METADATA +0 -167
  62. abstractruntime-0.4.0.dist-info/RECORD +0 -49
  63. {abstractruntime-0.4.0.dist-info → abstractruntime-0.4.1.dist-info}/WHEEL +0 -0
  64. {abstractruntime-0.4.0.dist-info → abstractruntime-0.4.1.dist-info}/entry_points.txt +0 -0
  65. {abstractruntime-0.4.0.dist-info → abstractruntime-0.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -357,7 +357,12 @@ class ActiveContextPolicy:
357
357
  lines.append(f"{role}: {content}" if role else content)
358
358
  text = "\n".join([l for l in lines if l]).strip()
359
359
  if len(text) > 360:
360
- return text[:357] + "…"
360
+ #[WARNING:TRUNCATION] bounded preview for UI/observability
361
+ marker = "… (truncated)"
362
+ keep = max(0, 360 - len(marker))
363
+ if keep <= 0:
364
+ return marker[:360].rstrip()
365
+ return text[:keep].rstrip() + marker
361
366
  return text
362
367
 
363
368
  for artifact_id in resolved_artifacts:
@@ -0,0 +1,164 @@
1
+ """KG → Active Memory packetization (v0).
2
+
3
+ This module bridges symbolic KG assertions (subject/predicate/object + metadata)
4
+ into a bounded, LLM-friendly Active Memory block.
5
+
6
+ Goals:
7
+ - deterministic ordering + formatting
8
+ - token-budgeted packing (uses AbstractCore TokenUtils when available)
9
+ - provenance-preserving packets (span_id, writer ids, retrieval scores)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import re
15
+ from typing import Any, Dict, Iterable, List, Optional, Tuple
16
+
17
+ from .token_budget import estimate_tokens
18
+
19
+
20
+ KG_MEMORY_PACKETS_VERSION = 0
21
+
22
+ _WS = re.compile(r"\s+")
23
+
24
+
25
+ def _clean_text(value: Any) -> str:
26
+ if value is None:
27
+ return ""
28
+ s = value if isinstance(value, str) else str(value)
29
+ return _WS.sub(" ", s).strip()
30
+
31
+
32
+ def packetize_assertions(items: Iterable[Any]) -> List[Dict[str, Any]]:
33
+ """Convert assertion dicts into compact, JSON-safe memory packets (v0)."""
34
+ packets: list[Dict[str, Any]] = []
35
+ seen: set[tuple[str, str, str, str]] = set()
36
+
37
+ for item in items:
38
+ if not isinstance(item, dict):
39
+ continue
40
+ subject = _clean_text(item.get("subject"))
41
+ predicate = _clean_text(item.get("predicate"))
42
+ obj = _clean_text(item.get("object"))
43
+ if not (subject and predicate and obj):
44
+ continue
45
+
46
+ observed_at = _clean_text(item.get("observed_at"))
47
+
48
+ # Retrieval metadata (semantic queries) is stored in attributes._retrieval.
49
+ retrieval_score: Optional[float] = None
50
+ attrs = item.get("attributes") if isinstance(item.get("attributes"), dict) else {}
51
+ ret = attrs.get("_retrieval") if isinstance(attrs, dict) else None
52
+ if isinstance(ret, dict):
53
+ s = ret.get("score")
54
+ if isinstance(s, (int, float)):
55
+ retrieval_score = float(s)
56
+
57
+ prov = item.get("provenance") if isinstance(item.get("provenance"), dict) else {}
58
+ span_id = _clean_text(prov.get("span_id"))
59
+ writer_run_id = _clean_text(prov.get("writer_run_id"))
60
+ writer_workflow_id = _clean_text(prov.get("writer_workflow_id"))
61
+
62
+ scope = _clean_text(item.get("scope"))
63
+ owner_id = _clean_text(item.get("owner_id"))
64
+
65
+ # Stable statement surface for LLM consumption.
66
+ statement = f"{subject} —{predicate}→ {obj}"
67
+
68
+ key = (subject.casefold(), predicate.casefold(), obj.casefold(), observed_at)
69
+ if key in seen:
70
+ continue
71
+ seen.add(key)
72
+
73
+ pkt: Dict[str, Any] = {
74
+ "version": KG_MEMORY_PACKETS_VERSION,
75
+ "statement": statement,
76
+ "subject": subject,
77
+ "predicate": predicate,
78
+ "object": obj,
79
+ }
80
+ if observed_at:
81
+ pkt["observed_at"] = observed_at
82
+ if scope:
83
+ pkt["scope"] = scope
84
+ if owner_id:
85
+ pkt["owner_id"] = owner_id
86
+ if span_id:
87
+ pkt["span_id"] = span_id
88
+ if writer_run_id:
89
+ pkt["writer_run_id"] = writer_run_id
90
+ if writer_workflow_id:
91
+ pkt["writer_workflow_id"] = writer_workflow_id
92
+ if retrieval_score is not None:
93
+ pkt["retrieval_score"] = retrieval_score
94
+
95
+ packets.append(pkt)
96
+
97
+ return packets
98
+
99
+
100
+ def pack_active_memory_text(
101
+ packets: Iterable[Dict[str, Any]],
102
+ *,
103
+ scope: str,
104
+ max_input_tokens: int,
105
+ model: Optional[str] = None,
106
+ title: str = "KG ACTIVE MEMORY",
107
+ include_scores: bool = True,
108
+ ) -> Tuple[str, List[Dict[str, Any]], int, int]:
109
+ """Render a token-budgeted Active Memory block.
110
+
111
+ Returns:
112
+ (text, kept_packets, estimated_tokens, dropped_packets)
113
+ """
114
+ budget = int(max_input_tokens) if isinstance(max_input_tokens, int) else int(max_input_tokens or 0)
115
+ if budget <= 0:
116
+ return "", [], 0, 0
117
+
118
+ scope2 = _clean_text(scope) or "session"
119
+
120
+ packets_list = [p for p in packets if isinstance(p, dict)]
121
+
122
+ order_hint = "similarity-desc" if include_scores else "observed_at-desc"
123
+ header_lines = [
124
+ f"## {title}",
125
+ f"(scope={scope2}; order={order_hint}; budget={budget} max_input_tokens)",
126
+ "",
127
+ ]
128
+ lines = list(header_lines)
129
+ tokens = estimate_tokens("\n".join(lines), model=model)
130
+
131
+ kept: list[Dict[str, Any]] = []
132
+
133
+ for pkt in packets_list:
134
+ stmt = _clean_text(pkt.get("statement"))
135
+ if not stmt:
136
+ continue
137
+
138
+ ts = _clean_text(pkt.get("observed_at"))
139
+ prefix = f"[{ts}] " if ts else ""
140
+
141
+ parts: list[str] = []
142
+ span_id = _clean_text(pkt.get("span_id"))
143
+ if span_id:
144
+ parts.append(f"span:{span_id}")
145
+
146
+ if include_scores:
147
+ score = pkt.get("retrieval_score")
148
+ if isinstance(score, (int, float)):
149
+ parts.append(f"score:{float(score):.3f}")
150
+
151
+ suffix = f" ({'; '.join(parts)})" if parts else ""
152
+ line = f"- {prefix}{stmt}{suffix}"
153
+
154
+ add = estimate_tokens("\n" + line, model=model)
155
+ if tokens + add > budget:
156
+ break
157
+
158
+ tokens += add
159
+ lines.append(line)
160
+ kept.append(pkt)
161
+
162
+ text = "\n".join(lines).strip() if kept else ""
163
+ dropped_total = max(0, len(packets_list) - len(kept))
164
+ return text, kept, int(tokens if kept else 0), int(dropped_total)
@@ -0,0 +1,175 @@
1
+ """KG/spans → MemAct Active Memory composition helpers (v0).
2
+
3
+ This module provides deterministic, bounded mapping from durable memory sources
4
+ (starting with the temporal KG) into MemAct's structured Active Memory blocks.
5
+
6
+ Design goals:
7
+ - deterministic: stable ordering + stable statements
8
+ - bounded: honors packet budgets and optional max_items caps
9
+ - auditable: returns a JSON-safe trace describing inputs + selections
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from typing import Any, Dict, List, Optional, Tuple
15
+
16
+
17
+ def _as_dict_list(value: Any) -> List[Dict[str, Any]]:
18
+ if not isinstance(value, list):
19
+ return []
20
+ out: List[Dict[str, Any]] = []
21
+ for item in value:
22
+ if isinstance(item, dict):
23
+ out.append(dict(item))
24
+ return out
25
+
26
+
27
+ def _normalize_marker(marker: Any) -> str:
28
+ s = marker if isinstance(marker, str) else str(marker or "")
29
+ s2 = s.strip()
30
+ if not s2:
31
+ return "KG:"
32
+ return s2
33
+
34
+
35
+ def _has_marker(text: Any, marker: str) -> bool:
36
+ if not isinstance(text, str):
37
+ return False
38
+ s = text.strip()
39
+ if not s:
40
+ return False
41
+ # Case-insensitive prefix match.
42
+ return s.casefold().startswith(marker.casefold())
43
+
44
+
45
+ def _packet_statement(pkt: Dict[str, Any]) -> str:
46
+ stmt = pkt.get("statement")
47
+ if isinstance(stmt, str) and stmt.strip():
48
+ return stmt.strip()
49
+ subj = pkt.get("subject")
50
+ pred = pkt.get("predicate")
51
+ obj = pkt.get("object")
52
+ parts = [subj, pred, obj]
53
+ if all(isinstance(p, str) and p.strip() for p in parts):
54
+ return f"{subj.strip()} —{pred.strip()}→ {obj.strip()}"
55
+ return ""
56
+
57
+
58
+ def _render_memact_context_line(pkt: Dict[str, Any], *, marker: str) -> str:
59
+ stmt = _packet_statement(pkt)
60
+ if not stmt:
61
+ return ""
62
+ marker2 = _normalize_marker(marker)
63
+
64
+ # Keep the injected statement stable for dedupe (avoid including retrieval_score).
65
+ suffix_parts: List[str] = []
66
+ span_id = pkt.get("span_id")
67
+ if isinstance(span_id, str) and span_id.strip():
68
+ suffix_parts.append(f"span:{span_id.strip()}")
69
+ writer_workflow_id = pkt.get("writer_workflow_id")
70
+ if isinstance(writer_workflow_id, str) and writer_workflow_id.strip():
71
+ suffix_parts.append(f"wf:{writer_workflow_id.strip()}")
72
+
73
+ suffix = f" ({'; '.join(suffix_parts)})" if suffix_parts else ""
74
+ return f"{marker2} {stmt}{suffix}".strip()
75
+
76
+
77
+ def _packets_from_kg_result(kg_result: Dict[str, Any]) -> Tuple[List[Dict[str, Any]], str]:
78
+ packets = _as_dict_list(kg_result.get("packets"))
79
+ if packets:
80
+ return packets, "kg_result.packets"
81
+ # Fallback: derive packets from raw items (no packing / max_input_tokens=0).
82
+ items = _as_dict_list(kg_result.get("items"))
83
+ if not items:
84
+ return [], "none"
85
+ try:
86
+ from .kg_packets import packetize_assertions
87
+
88
+ return packetize_assertions(items), "packetize_assertions(items)"
89
+ except Exception:
90
+ return [], "packetize_failed"
91
+
92
+
93
+ def compose_memact_current_context_from_kg_result(
94
+ vars: Dict[str, Any],
95
+ *,
96
+ kg_result: Dict[str, Any],
97
+ stimulus: str,
98
+ marker: str = "KG:",
99
+ max_items: Optional[int] = None,
100
+ ) -> Dict[str, Any]:
101
+ """Apply a KG-derived reconstruction of MemAct CURRENT CONTEXT.
102
+
103
+ Behavior (v0):
104
+ - Remove any existing CURRENT CONTEXT entries prefixed with `marker`
105
+ (treat them as "previous KG reconstruction").
106
+ - Add new entries derived from the KG packets (or packetized items fallback).
107
+ - Returns `{ok, delta, trace}` where `delta` matches MemAct envelope fields.
108
+ """
109
+ if not isinstance(vars, dict):
110
+ return {"ok": False, "error": "vars must be a dict"}
111
+ if not isinstance(kg_result, dict):
112
+ return {"ok": False, "error": "kg_result must be a dict"}
113
+
114
+ try:
115
+ from .active_memory import ensure_memact_memory, apply_memact_envelope
116
+ except Exception as e: # pragma: no cover
117
+ return {"ok": False, "error": f"memact active_memory unavailable: {e}"}
118
+
119
+ mem = ensure_memact_memory(vars)
120
+ marker2 = _normalize_marker(marker)
121
+
122
+ existing_ctx = mem.get("current_context")
123
+ existing_ctx_list = existing_ctx if isinstance(existing_ctx, list) else []
124
+ removed: List[str] = []
125
+ for rec in existing_ctx_list:
126
+ if not isinstance(rec, dict):
127
+ continue
128
+ text = rec.get("text")
129
+ if _has_marker(text, marker2):
130
+ removed.append(str(text))
131
+
132
+ packets, packet_source = _packets_from_kg_result(kg_result)
133
+ if isinstance(max_items, int) and max_items > 0 and len(packets) > max_items:
134
+ packets = packets[: int(max_items)]
135
+
136
+ added: List[str] = []
137
+ selected: List[Dict[str, Any]] = []
138
+ for pkt in packets:
139
+ line = _render_memact_context_line(pkt, marker=marker2)
140
+ if not line:
141
+ continue
142
+ added.append(line)
143
+ if len(selected) < 50:
144
+ selected.append(
145
+ {
146
+ "statement": _packet_statement(pkt),
147
+ "span_id": pkt.get("span_id"),
148
+ "observed_at": pkt.get("observed_at"),
149
+ "retrieval_score": pkt.get("retrieval_score"),
150
+ }
151
+ )
152
+
153
+ delta: Dict[str, Any] = {"current_context": {"added": added, "removed": removed}}
154
+ applied = apply_memact_envelope(vars, envelope=delta)
155
+
156
+ trace: Dict[str, Any] = {
157
+ "stimulus": str(stimulus or "").strip(),
158
+ "marker": marker2,
159
+ "packet_source": packet_source,
160
+ "kg": {
161
+ "ok": bool(kg_result.get("ok")) if "ok" in kg_result else None,
162
+ "count": kg_result.get("count"),
163
+ "packed_count": kg_result.get("packed_count"),
164
+ "estimated_tokens": kg_result.get("estimated_tokens"),
165
+ "dropped": kg_result.get("dropped"),
166
+ "effort": kg_result.get("effort"),
167
+ "warnings": kg_result.get("warnings"),
168
+ },
169
+ "delta_preview": {"removed": len(removed), "added": len(added)},
170
+ "selected": selected,
171
+ "applied": applied,
172
+ }
173
+ ok = bool(applied.get("ok")) if isinstance(applied, dict) else False
174
+ return {"ok": ok, "delta": delta, "trace": trace, "active_memory": mem}
175
+
@@ -0,0 +1,163 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+ from typing import Any, Optional
6
+
7
+
8
+ class RecallLevel(str, Enum):
9
+ """Framework-wide recall effort policy (binding).
10
+
11
+ See `docs/adr/memory-recall-levels.md`.
12
+ """
13
+
14
+ URGENT = "urgent"
15
+ STANDARD = "standard"
16
+ DEEP = "deep"
17
+
18
+
19
+ def parse_recall_level(raw: Any) -> Optional[RecallLevel]:
20
+ """Parse a recall_level value.
21
+
22
+ Returns:
23
+ - None when the field is absent/blank (caller did not opt into policy).
24
+ - RecallLevel when valid.
25
+
26
+ Raises:
27
+ - ValueError when a non-blank value is present but invalid (no silent fallback).
28
+ """
29
+
30
+ if raw is None:
31
+ return None
32
+ if isinstance(raw, bool):
33
+ raise ValueError("recall_level must be a string")
34
+ s = str(raw).strip().lower()
35
+ if not s:
36
+ return None
37
+ if s in ("urgent", "standard", "deep"):
38
+ return RecallLevel(s)
39
+ raise ValueError(f"Unknown recall_level: {s}")
40
+
41
+
42
+ @dataclass(frozen=True)
43
+ class SpanRecallPolicy:
44
+ """Budgets for span-index recall (memory_query)."""
45
+
46
+ limit_spans_default: int
47
+ limit_spans_max: int
48
+
49
+ deep_allowed: bool
50
+ deep_limit_spans_max: int
51
+ deep_limit_messages_per_span_max: int
52
+
53
+ connected_allowed: bool
54
+ neighbor_hops_max: int
55
+
56
+ max_messages_default: int
57
+ max_messages_max: int
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class RehydratePolicy:
62
+ """Budgets for rehydration into context (memory_rehydrate)."""
63
+
64
+ max_messages_default: int
65
+ max_messages_max: int
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class KgQueryPolicy:
70
+ """Budgets for KG recall (memory_kg_query)."""
71
+
72
+ min_score_default: float
73
+ min_score_floor: float
74
+ limit_default: int
75
+ limit_max: int
76
+ max_input_tokens_default: int
77
+ max_input_tokens_max: int
78
+
79
+
80
+ @dataclass(frozen=True)
81
+ class RecallPolicy:
82
+ level: RecallLevel
83
+ span: SpanRecallPolicy
84
+ rehydrate: RehydratePolicy
85
+ kg: KgQueryPolicy
86
+
87
+
88
+ _POLICIES: dict[RecallLevel, RecallPolicy] = {
89
+ RecallLevel.URGENT: RecallPolicy(
90
+ level=RecallLevel.URGENT,
91
+ span=SpanRecallPolicy(
92
+ limit_spans_default=2,
93
+ limit_spans_max=3,
94
+ deep_allowed=False,
95
+ deep_limit_spans_max=0,
96
+ deep_limit_messages_per_span_max=0,
97
+ connected_allowed=False,
98
+ neighbor_hops_max=0,
99
+ max_messages_default=30,
100
+ max_messages_max=60,
101
+ ),
102
+ rehydrate=RehydratePolicy(max_messages_default=30, max_messages_max=60),
103
+ kg=KgQueryPolicy(
104
+ min_score_default=0.55,
105
+ min_score_floor=0.5,
106
+ limit_default=20,
107
+ limit_max=40,
108
+ max_input_tokens_default=600,
109
+ max_input_tokens_max=1000,
110
+ ),
111
+ ),
112
+ RecallLevel.STANDARD: RecallPolicy(
113
+ level=RecallLevel.STANDARD,
114
+ span=SpanRecallPolicy(
115
+ limit_spans_default=5,
116
+ limit_spans_max=8,
117
+ deep_allowed=True,
118
+ deep_limit_spans_max=25,
119
+ deep_limit_messages_per_span_max=200,
120
+ connected_allowed=True,
121
+ neighbor_hops_max=1,
122
+ max_messages_default=80,
123
+ max_messages_max=150,
124
+ ),
125
+ rehydrate=RehydratePolicy(max_messages_default=80, max_messages_max=200),
126
+ kg=KgQueryPolicy(
127
+ min_score_default=0.4,
128
+ min_score_floor=0.25,
129
+ limit_default=80,
130
+ limit_max=200,
131
+ max_input_tokens_default=1200,
132
+ max_input_tokens_max=3000,
133
+ ),
134
+ ),
135
+ RecallLevel.DEEP: RecallPolicy(
136
+ level=RecallLevel.DEEP,
137
+ span=SpanRecallPolicy(
138
+ limit_spans_default=8,
139
+ limit_spans_max=20,
140
+ deep_allowed=True,
141
+ deep_limit_spans_max=100,
142
+ deep_limit_messages_per_span_max=400,
143
+ connected_allowed=True,
144
+ neighbor_hops_max=2,
145
+ max_messages_default=200,
146
+ max_messages_max=600,
147
+ ),
148
+ rehydrate=RehydratePolicy(max_messages_default=200, max_messages_max=800),
149
+ kg=KgQueryPolicy(
150
+ min_score_default=0.25,
151
+ min_score_floor=0.0,
152
+ limit_default=200,
153
+ limit_max=1000,
154
+ max_input_tokens_default=2400,
155
+ max_input_tokens_max=6000,
156
+ ),
157
+ ),
158
+ }
159
+
160
+
161
+ def policy_for(level: RecallLevel) -> RecallPolicy:
162
+ return _POLICIES[level]
163
+
@@ -0,0 +1,86 @@
1
+ """Token-budget helpers for active context selection.
2
+
3
+ These utilities are intentionally best-effort and dependency-light:
4
+ - Prefer AbstractCore's TokenUtils when available (better model-aware heuristics).
5
+ - Fall back to simple character-based estimation otherwise.
6
+
7
+ They exist to keep VisualFlow workflows bounded by an explicit `max_input_tokens`
8
+ budget (ADR-0008) even when the underlying model supports much larger contexts.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from typing import Any, Dict, Iterable, List, Optional
14
+
15
+
16
+ def estimate_tokens(text: str, *, model: Optional[str] = None) -> int:
17
+ """Best-effort token estimation for a string."""
18
+ s = str(text or "")
19
+ if not s:
20
+ return 0
21
+ try:
22
+ from abstractcore.utils.token_utils import TokenUtils
23
+
24
+ return int(TokenUtils.estimate_tokens(s, model))
25
+ except Exception:
26
+ # Conservative fallback: ~4 chars per token.
27
+ return max(1, int(len(s) / 4))
28
+
29
+
30
+ def estimate_message_tokens(message: Dict[str, Any], *, model: Optional[str] = None) -> int:
31
+ """Estimate tokens for a chat message dict (role+content)."""
32
+ if not isinstance(message, dict):
33
+ return 0
34
+ role = str(message.get("role") or "").strip()
35
+ content = "" if message.get("content") is None else str(message.get("content"))
36
+ # Include a small role prefix so token estimation reflects chat formatting overhead.
37
+ text = f"{role}: {content}" if role else content
38
+ return estimate_tokens(text, model=model)
39
+
40
+
41
+ def trim_messages_to_max_input_tokens(
42
+ messages: Iterable[Any],
43
+ *,
44
+ max_input_tokens: int,
45
+ model: Optional[str] = None,
46
+ ) -> List[Dict[str, Any]]:
47
+ """Trim oldest non-system messages until the estimated budget fits.
48
+
49
+ Rules:
50
+ - Preserve all system messages.
51
+ - Preserve the most recent non-system message (typically the user prompt).
52
+ - Drop oldest non-system messages first.
53
+ """
54
+ try:
55
+ budget = int(max_input_tokens)
56
+ except Exception:
57
+ return [dict(m) for m in messages if isinstance(m, dict)]
58
+
59
+ if budget <= 0:
60
+ return [dict(m) for m in messages if isinstance(m, dict)]
61
+
62
+ typed: List[Dict[str, Any]] = [dict(m) for m in messages if isinstance(m, dict)]
63
+ system_messages = [m for m in typed if m.get("role") == "system"]
64
+ non_system = [m for m in typed if m.get("role") != "system"]
65
+
66
+ if not non_system:
67
+ return system_messages
68
+
69
+ # Pre-compute per-message token estimates (cheaper than re-tokenizing whole text repeatedly).
70
+ sys_tokens = sum(estimate_message_tokens(m, model=model) for m in system_messages)
71
+ non_tokens = [estimate_message_tokens(m, model=model) for m in non_system]
72
+
73
+ # Always keep the final non-system message.
74
+ kept: List[Dict[str, Any]] = [non_system[-1]]
75
+ total = sys_tokens + non_tokens[-1]
76
+
77
+ # If we're already over budget, we still return system + last message.
78
+ for msg, tok in zip(reversed(non_system[:-1]), reversed(non_tokens[:-1])):
79
+ if total + tok > budget:
80
+ break
81
+ kept.append(msg)
82
+ total += tok
83
+
84
+ kept.reverse()
85
+ return system_messages + kept
86
+
@@ -5,6 +5,7 @@ from .in_memory import InMemoryRunStore, InMemoryLedgerStore
5
5
  from .json_files import JsonFileRunStore, JsonlLedgerStore
6
6
  from .ledger_chain import HashChainedLedgerStore, verify_ledger_chain
7
7
  from .observable import ObservableLedgerStore, ObservableLedgerStoreProtocol
8
+ from .offloading import OffloadingLedgerStore, OffloadingRunStore, offload_large_values
8
9
  from .snapshots import Snapshot, SnapshotStore, InMemorySnapshotStore, JsonSnapshotStore
9
10
 
10
11
  __all__ = [
@@ -19,9 +20,11 @@ __all__ = [
19
20
  "verify_ledger_chain",
20
21
  "ObservableLedgerStore",
21
22
  "ObservableLedgerStoreProtocol",
23
+ "OffloadingRunStore",
24
+ "OffloadingLedgerStore",
25
+ "offload_large_values",
22
26
  "Snapshot",
23
27
  "SnapshotStore",
24
28
  "InMemorySnapshotStore",
25
29
  "JsonSnapshotStore",
26
30
  ]
27
-