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.
- abstractruntime/__init__.py +83 -3
- abstractruntime/core/config.py +82 -2
- abstractruntime/core/event_keys.py +62 -0
- abstractruntime/core/models.py +17 -1
- abstractruntime/core/policy.py +74 -3
- abstractruntime/core/runtime.py +3334 -28
- abstractruntime/core/vars.py +103 -2
- abstractruntime/evidence/__init__.py +10 -0
- abstractruntime/evidence/recorder.py +325 -0
- abstractruntime/history_bundle.py +772 -0
- abstractruntime/integrations/abstractcore/__init__.py +6 -0
- abstractruntime/integrations/abstractcore/constants.py +19 -0
- abstractruntime/integrations/abstractcore/default_tools.py +258 -0
- abstractruntime/integrations/abstractcore/effect_handlers.py +2622 -32
- abstractruntime/integrations/abstractcore/embeddings_client.py +69 -0
- abstractruntime/integrations/abstractcore/factory.py +149 -16
- abstractruntime/integrations/abstractcore/llm_client.py +891 -55
- abstractruntime/integrations/abstractcore/mcp_worker.py +587 -0
- abstractruntime/integrations/abstractcore/observability.py +80 -0
- abstractruntime/integrations/abstractcore/session_attachments.py +946 -0
- abstractruntime/integrations/abstractcore/summarizer.py +154 -0
- abstractruntime/integrations/abstractcore/tool_executor.py +509 -31
- abstractruntime/integrations/abstractcore/workspace_scoped_tools.py +561 -0
- abstractruntime/integrations/abstractmemory/__init__.py +3 -0
- abstractruntime/integrations/abstractmemory/effect_handlers.py +946 -0
- abstractruntime/memory/__init__.py +21 -0
- abstractruntime/memory/active_context.py +751 -0
- abstractruntime/memory/active_memory.py +452 -0
- abstractruntime/memory/compaction.py +105 -0
- abstractruntime/memory/kg_packets.py +164 -0
- abstractruntime/memory/memact_composer.py +175 -0
- abstractruntime/memory/recall_levels.py +163 -0
- abstractruntime/memory/token_budget.py +86 -0
- abstractruntime/rendering/__init__.py +17 -0
- abstractruntime/rendering/agent_trace_report.py +256 -0
- abstractruntime/rendering/json_stringify.py +136 -0
- abstractruntime/scheduler/scheduler.py +93 -2
- abstractruntime/storage/__init__.py +7 -2
- abstractruntime/storage/artifacts.py +175 -32
- abstractruntime/storage/base.py +17 -1
- abstractruntime/storage/commands.py +339 -0
- abstractruntime/storage/in_memory.py +41 -1
- abstractruntime/storage/json_files.py +210 -14
- abstractruntime/storage/observable.py +136 -0
- abstractruntime/storage/offloading.py +433 -0
- abstractruntime/storage/sqlite.py +836 -0
- abstractruntime/visualflow_compiler/__init__.py +29 -0
- abstractruntime/visualflow_compiler/adapters/__init__.py +11 -0
- abstractruntime/visualflow_compiler/adapters/agent_adapter.py +126 -0
- abstractruntime/visualflow_compiler/adapters/context_adapter.py +109 -0
- abstractruntime/visualflow_compiler/adapters/control_adapter.py +615 -0
- abstractruntime/visualflow_compiler/adapters/effect_adapter.py +1051 -0
- abstractruntime/visualflow_compiler/adapters/event_adapter.py +307 -0
- abstractruntime/visualflow_compiler/adapters/function_adapter.py +97 -0
- abstractruntime/visualflow_compiler/adapters/memact_adapter.py +114 -0
- abstractruntime/visualflow_compiler/adapters/subflow_adapter.py +74 -0
- abstractruntime/visualflow_compiler/adapters/variable_adapter.py +316 -0
- abstractruntime/visualflow_compiler/compiler.py +3832 -0
- abstractruntime/visualflow_compiler/flow.py +247 -0
- abstractruntime/visualflow_compiler/visual/__init__.py +13 -0
- abstractruntime/visualflow_compiler/visual/agent_ids.py +29 -0
- abstractruntime/visualflow_compiler/visual/builtins.py +1376 -0
- abstractruntime/visualflow_compiler/visual/code_executor.py +214 -0
- abstractruntime/visualflow_compiler/visual/executor.py +2804 -0
- abstractruntime/visualflow_compiler/visual/models.py +211 -0
- abstractruntime/workflow_bundle/__init__.py +52 -0
- abstractruntime/workflow_bundle/models.py +236 -0
- abstractruntime/workflow_bundle/packer.py +317 -0
- abstractruntime/workflow_bundle/reader.py +87 -0
- abstractruntime/workflow_bundle/registry.py +587 -0
- abstractruntime-0.4.1.dist-info/METADATA +177 -0
- abstractruntime-0.4.1.dist-info/RECORD +86 -0
- abstractruntime-0.4.1.dist-info/entry_points.txt +2 -0
- abstractruntime-0.2.0.dist-info/METADATA +0 -163
- abstractruntime-0.2.0.dist-info/RECORD +0 -32
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.1.dist-info}/WHEEL +0 -0
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Rendering utilities (JSON-safe) for host UX and workflow nodes.
|
|
2
|
+
|
|
3
|
+
This module intentionally lives in AbstractRuntime so multiple hosts (AbstractFlow,
|
|
4
|
+
AbstractCode, future runners) can reuse the same rendering logic without duplicating
|
|
5
|
+
semantics in higher layers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .agent_trace_report import render_agent_trace_markdown
|
|
9
|
+
from .json_stringify import JsonStringifyMode, stringify_json
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"JsonStringifyMode",
|
|
13
|
+
"render_agent_trace_markdown",
|
|
14
|
+
"stringify_json",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|