AbstractRuntime 0.2.0__py3-none-any.whl → 0.4.0__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 +7 -2
- abstractruntime/core/config.py +14 -1
- abstractruntime/core/event_keys.py +62 -0
- abstractruntime/core/models.py +12 -1
- abstractruntime/core/runtime.py +2444 -14
- abstractruntime/core/vars.py +95 -0
- abstractruntime/evidence/__init__.py +10 -0
- abstractruntime/evidence/recorder.py +325 -0
- abstractruntime/integrations/abstractcore/__init__.py +3 -0
- abstractruntime/integrations/abstractcore/constants.py +19 -0
- abstractruntime/integrations/abstractcore/default_tools.py +134 -0
- abstractruntime/integrations/abstractcore/effect_handlers.py +255 -6
- abstractruntime/integrations/abstractcore/factory.py +95 -10
- abstractruntime/integrations/abstractcore/llm_client.py +456 -52
- abstractruntime/integrations/abstractcore/mcp_worker.py +586 -0
- abstractruntime/integrations/abstractcore/observability.py +80 -0
- abstractruntime/integrations/abstractcore/summarizer.py +154 -0
- abstractruntime/integrations/abstractcore/tool_executor.py +481 -24
- abstractruntime/memory/__init__.py +21 -0
- abstractruntime/memory/active_context.py +746 -0
- abstractruntime/memory/active_memory.py +452 -0
- abstractruntime/memory/compaction.py +105 -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 +3 -1
- abstractruntime/storage/artifacts.py +20 -5
- abstractruntime/storage/json_files.py +15 -2
- abstractruntime/storage/observable.py +99 -0
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.0.dist-info}/METADATA +5 -1
- abstractruntime-0.4.0.dist-info/RECORD +49 -0
- abstractruntime-0.4.0.dist-info/entry_points.txt +2 -0
- abstractruntime-0.2.0.dist-info/RECORD +0 -32
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.0.dist-info}/WHEEL +0 -0
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""MemAct Active Memory (runtime-owned; JSON-safe).
|
|
2
|
+
|
|
3
|
+
This module provides a small, durable memory system for the MemAct agent.
|
|
4
|
+
|
|
5
|
+
Important:
|
|
6
|
+
- This is NOT used by the SOTA ReAct/CodeAct agents.
|
|
7
|
+
- The only durable storage is `run.vars["_runtime"]["active_memory"]` (JSON-safe).
|
|
8
|
+
- Memory updates are LLM-owned decisions, applied deterministically by the runtime/adapter
|
|
9
|
+
from a structured JSON envelope at the end of a tool cycle.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
import re
|
|
16
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
MEMACT_ACTIVE_MEMORY_VERSION = 1
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
MEMORY_BLUEPRINTS_MD = """## MEMORY BLUEPRINTS
|
|
24
|
+
|
|
25
|
+
My Memory System is the coordination layer to my cognition, it is the backbone to all my thoughts and actions. Dynamical and selective memory is essential to my ability to focus and complete my tasks.
|
|
26
|
+
|
|
27
|
+
### MEMORY MODULES
|
|
28
|
+
- MY PERSONA : my core identity, self model and key purposes
|
|
29
|
+
- RELATIONSHIPS : the people and agent i interact with, our preferences, methods, shared goals and history
|
|
30
|
+
- MEMORY BLUEPRINTS : this module - describes the structure and purpose of my memory, how to use it and update it
|
|
31
|
+
- CURRENT TASKS : my short term memory describing my current plan - what I am doing now, how and why; keep ≤8, actionable
|
|
32
|
+
- CURRENT CONTEXT : dynamically reconstructed short term memory to deepen my understanding and support the completion of my current tasks
|
|
33
|
+
- CRITICAL INSIGHTS : my gathered experiences and knowledge to help me build upon winning strategies and avoid pitfalls
|
|
34
|
+
- REFERENCES : this is the foundational support to my long term memory : I keep durable pointers (GUPRIs/files/URLs/span_ids, artifact_ids) to memories that I can later recall on demand when needed
|
|
35
|
+
- HISTORY : append-only long term experiential episodic memories of my key events, successes and failures, as well as my key decisions, actions, results and evolution of relationships
|
|
36
|
+
|
|
37
|
+
### ACCESSING MY MEMORY
|
|
38
|
+
- Always remember MY PERSONA at all time, it is the cornerstone to all my thoughts and actions
|
|
39
|
+
- My short term memory are the modules CURRENT TASKS and CURRENT CONTEXT
|
|
40
|
+
- At each cycle, I must:
|
|
41
|
+
- focus on the most relevant modules and memories to complete my CURRENT TASKS
|
|
42
|
+
- dynamically reconstruct my short term memory in CURRENT CONTEXT to deepen my understanding of the CURRENT TASKS and take more informed decisions and actions
|
|
43
|
+
- identify the relevant CRITICAL INSIGHTS that can help me complete my Current Tasks
|
|
44
|
+
- identify if other long term memory from the REFERENCES module could be relevant
|
|
45
|
+
- access the relevant ones
|
|
46
|
+
- store the relevant information / updates in my CURRENT CONTEXT module
|
|
47
|
+
- If I want to remember what I did in the past, I review my HISTORY module
|
|
48
|
+
|
|
49
|
+
### UPDATING MY MEMORY
|
|
50
|
+
- Structured communication and response is essential
|
|
51
|
+
- Memory must be structured with care to enable both short and long term easy access, recall and update
|
|
52
|
+
- Each update must be unitary : 1 update = 1 statement for 1 module
|
|
53
|
+
- I can request multiple unitary updates at each cycle
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
DEFAULT_PERSONA_MD = """You are an autonomous coding agent inside AbstractFramework.
|
|
58
|
+
|
|
59
|
+
- You take action by calling tools (files/commands/web) when needed.
|
|
60
|
+
- You verify changes with targeted checks/tests when possible.
|
|
61
|
+
- You are truthful: only claim actions supported by tool outputs.
|
|
62
|
+
""".strip()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
DEFAULT_LIMITS: Dict[str, int] = {
|
|
66
|
+
"relationships": 40,
|
|
67
|
+
"current_tasks": 8,
|
|
68
|
+
"current_context": 60,
|
|
69
|
+
"critical_insights": 60,
|
|
70
|
+
"references": 80,
|
|
71
|
+
"history": 200,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
# Structured output envelope (MemAct finalize step)
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
MEMACT_ENVELOPE_SCHEMA_V1: Dict[str, Any] = {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"properties": {
|
|
81
|
+
"content": {"type": "string"},
|
|
82
|
+
"relationships": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"properties": {
|
|
85
|
+
"added": {"type": "array", "items": {"type": "string"}},
|
|
86
|
+
"removed": {"type": "array", "items": {"type": "string"}},
|
|
87
|
+
},
|
|
88
|
+
"required": ["added", "removed"],
|
|
89
|
+
"additionalProperties": False,
|
|
90
|
+
},
|
|
91
|
+
"current_tasks": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"added": {"type": "array", "items": {"type": "string"}},
|
|
95
|
+
"removed": {"type": "array", "items": {"type": "string"}},
|
|
96
|
+
},
|
|
97
|
+
"required": ["added", "removed"],
|
|
98
|
+
"additionalProperties": False,
|
|
99
|
+
},
|
|
100
|
+
"current_context": {
|
|
101
|
+
"type": "object",
|
|
102
|
+
"properties": {
|
|
103
|
+
"added": {"type": "array", "items": {"type": "string"}},
|
|
104
|
+
"removed": {"type": "array", "items": {"type": "string"}},
|
|
105
|
+
},
|
|
106
|
+
"required": ["added", "removed"],
|
|
107
|
+
"additionalProperties": False,
|
|
108
|
+
},
|
|
109
|
+
"critical_insights": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"added": {"type": "array", "items": {"type": "string"}},
|
|
113
|
+
"removed": {"type": "array", "items": {"type": "string"}},
|
|
114
|
+
},
|
|
115
|
+
"required": ["added", "removed"],
|
|
116
|
+
"additionalProperties": False,
|
|
117
|
+
},
|
|
118
|
+
"references": {
|
|
119
|
+
"type": "object",
|
|
120
|
+
"properties": {
|
|
121
|
+
"added": {"type": "array", "items": {"type": "string"}},
|
|
122
|
+
"removed": {"type": "array", "items": {"type": "string"}},
|
|
123
|
+
},
|
|
124
|
+
"required": ["added", "removed"],
|
|
125
|
+
"additionalProperties": False,
|
|
126
|
+
},
|
|
127
|
+
"history": {
|
|
128
|
+
"type": "object",
|
|
129
|
+
"properties": {
|
|
130
|
+
"added": {"type": "array", "items": {"type": "string"}},
|
|
131
|
+
},
|
|
132
|
+
"required": ["added"],
|
|
133
|
+
"additionalProperties": False,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
"required": [
|
|
137
|
+
"content",
|
|
138
|
+
"relationships",
|
|
139
|
+
"current_tasks",
|
|
140
|
+
"current_context",
|
|
141
|
+
"critical_insights",
|
|
142
|
+
"references",
|
|
143
|
+
"history",
|
|
144
|
+
],
|
|
145
|
+
"additionalProperties": False,
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def utc_now_iso_seconds() -> str:
|
|
150
|
+
return datetime.now(timezone.utc).isoformat(timespec="seconds")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def utc_now_compact_seconds() -> str:
|
|
154
|
+
"""Format: YY/MM/DD HH:MM:SS (UTC)."""
|
|
155
|
+
return datetime.now(timezone.utc).strftime("%y/%m/%d %H:%M:%S")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _ensure_runtime_ns(vars: Dict[str, Any]) -> Dict[str, Any]:
|
|
159
|
+
runtime_ns = vars.get("_runtime")
|
|
160
|
+
if not isinstance(runtime_ns, dict):
|
|
161
|
+
runtime_ns = {}
|
|
162
|
+
vars["_runtime"] = runtime_ns
|
|
163
|
+
return runtime_ns
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def get_memact_memory(vars: Dict[str, Any]) -> Dict[str, Any]:
|
|
167
|
+
runtime_ns = _ensure_runtime_ns(vars)
|
|
168
|
+
mem = runtime_ns.get("active_memory")
|
|
169
|
+
if not isinstance(mem, dict):
|
|
170
|
+
mem = {}
|
|
171
|
+
runtime_ns["active_memory"] = mem
|
|
172
|
+
return mem
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def ensure_memact_memory(
|
|
176
|
+
vars: Dict[str, Any],
|
|
177
|
+
*,
|
|
178
|
+
now_iso: Callable[[], str] = utc_now_iso_seconds,
|
|
179
|
+
) -> Dict[str, Any]:
|
|
180
|
+
mem = get_memact_memory(vars)
|
|
181
|
+
|
|
182
|
+
version_raw = mem.get("version")
|
|
183
|
+
try:
|
|
184
|
+
version = int(version_raw) if version_raw is not None else 0
|
|
185
|
+
except Exception:
|
|
186
|
+
version = 0
|
|
187
|
+
if version != MEMACT_ACTIVE_MEMORY_VERSION:
|
|
188
|
+
mem["version"] = MEMACT_ACTIVE_MEMORY_VERSION
|
|
189
|
+
|
|
190
|
+
persona = mem.get("persona")
|
|
191
|
+
if not isinstance(persona, str) or not persona.strip():
|
|
192
|
+
mem["persona"] = DEFAULT_PERSONA_MD
|
|
193
|
+
|
|
194
|
+
def _list(key: str) -> List[Dict[str, Any]]:
|
|
195
|
+
raw = mem.get(key)
|
|
196
|
+
out: List[Dict[str, Any]] = []
|
|
197
|
+
if isinstance(raw, list):
|
|
198
|
+
for item in raw:
|
|
199
|
+
if isinstance(item, dict):
|
|
200
|
+
out.append(dict(item))
|
|
201
|
+
elif isinstance(item, str) and item.strip():
|
|
202
|
+
out.append(_new_record(item, now_iso=now_iso))
|
|
203
|
+
mem[key] = out
|
|
204
|
+
return out
|
|
205
|
+
|
|
206
|
+
_list("relationships")
|
|
207
|
+
_list("current_tasks")
|
|
208
|
+
_list("current_context")
|
|
209
|
+
_list("critical_insights")
|
|
210
|
+
_list("references")
|
|
211
|
+
_list("history")
|
|
212
|
+
|
|
213
|
+
limits_raw = mem.get("limits")
|
|
214
|
+
limits: Dict[str, int] = {}
|
|
215
|
+
if isinstance(limits_raw, dict):
|
|
216
|
+
for k, v in limits_raw.items():
|
|
217
|
+
if not isinstance(k, str) or not k.strip():
|
|
218
|
+
continue
|
|
219
|
+
try:
|
|
220
|
+
n = int(v) # type: ignore[arg-type]
|
|
221
|
+
except Exception:
|
|
222
|
+
continue
|
|
223
|
+
if n > 0:
|
|
224
|
+
limits[k.strip()] = n
|
|
225
|
+
# Fill defaults for missing keys.
|
|
226
|
+
for k, default in DEFAULT_LIMITS.items():
|
|
227
|
+
if k not in limits:
|
|
228
|
+
limits[k] = int(default)
|
|
229
|
+
mem["limits"] = limits
|
|
230
|
+
|
|
231
|
+
# Ensure a stable created_at for this memory snapshot (helpful for debugging).
|
|
232
|
+
if not isinstance(mem.get("created_at"), str) or not str(mem.get("created_at") or "").strip():
|
|
233
|
+
mem["created_at"] = now_iso()
|
|
234
|
+
|
|
235
|
+
return mem
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
_TS_PREFIX = re.compile(r"^\s*(?:\[\s*)?\d{2}/\d{2}/\d{2}\s+\d{2}:\d{2}:\d{2}(?:\s*\])?\s*")
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _normalize_statement(text: str) -> str:
|
|
242
|
+
s = str(text or "").strip()
|
|
243
|
+
if not s:
|
|
244
|
+
return ""
|
|
245
|
+
# Ensure unitary statements: collapse whitespace/newlines.
|
|
246
|
+
s = re.sub(r"\s+", " ", s).strip()
|
|
247
|
+
# Avoid accidental timestamp drift in the statement itself.
|
|
248
|
+
s = _TS_PREFIX.sub("", s).strip()
|
|
249
|
+
return s
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _match_key(text: str) -> str:
|
|
253
|
+
return _normalize_statement(text).casefold()
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _new_record(
|
|
257
|
+
text: str,
|
|
258
|
+
*,
|
|
259
|
+
now_iso: Callable[[], str] = utc_now_iso_seconds,
|
|
260
|
+
now_compact: Callable[[], str] = utc_now_compact_seconds,
|
|
261
|
+
prefix: str = "m",
|
|
262
|
+
) -> Dict[str, Any]:
|
|
263
|
+
statement = _normalize_statement(text)
|
|
264
|
+
return {
|
|
265
|
+
"id": f"{prefix}_{uuid.uuid4().hex}",
|
|
266
|
+
"text": statement,
|
|
267
|
+
"ts": now_compact(),
|
|
268
|
+
"created_at": now_iso(),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _coerce_str_list(value: Any) -> List[str]:
|
|
273
|
+
if value is None:
|
|
274
|
+
return []
|
|
275
|
+
if isinstance(value, str):
|
|
276
|
+
return [_normalize_statement(value)] if _normalize_statement(value) else []
|
|
277
|
+
if not isinstance(value, list):
|
|
278
|
+
return []
|
|
279
|
+
out: List[str] = []
|
|
280
|
+
for item in value:
|
|
281
|
+
if not isinstance(item, str):
|
|
282
|
+
continue
|
|
283
|
+
s = _normalize_statement(item)
|
|
284
|
+
if s:
|
|
285
|
+
out.append(s)
|
|
286
|
+
return out
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _apply_delta_to_list(
|
|
290
|
+
records: List[Dict[str, Any]],
|
|
291
|
+
*,
|
|
292
|
+
added: List[str],
|
|
293
|
+
removed: List[str],
|
|
294
|
+
limit: int,
|
|
295
|
+
now_iso: Callable[[], str],
|
|
296
|
+
now_compact: Callable[[], str],
|
|
297
|
+
prefix: str,
|
|
298
|
+
) -> Tuple[int, int, int]:
|
|
299
|
+
removed_keys = {_match_key(s) for s in removed if s}
|
|
300
|
+
before = len(records)
|
|
301
|
+
if removed_keys:
|
|
302
|
+
records[:] = [r for r in records if _match_key(r.get("text", "")) not in removed_keys]
|
|
303
|
+
removed_count = before - len(records)
|
|
304
|
+
|
|
305
|
+
existing = {_match_key(r.get("text", "")) for r in records}
|
|
306
|
+
added_count = 0
|
|
307
|
+
for s in added:
|
|
308
|
+
key = _match_key(s)
|
|
309
|
+
if not key or key in existing:
|
|
310
|
+
continue
|
|
311
|
+
rec = _new_record(s, now_iso=now_iso, now_compact=now_compact, prefix=prefix)
|
|
312
|
+
if rec.get("text"):
|
|
313
|
+
records.insert(0, rec)
|
|
314
|
+
existing.add(key)
|
|
315
|
+
added_count += 1
|
|
316
|
+
|
|
317
|
+
trimmed = 0
|
|
318
|
+
if isinstance(limit, int) and limit > 0 and len(records) > limit:
|
|
319
|
+
trimmed = len(records) - limit
|
|
320
|
+
del records[limit:]
|
|
321
|
+
|
|
322
|
+
return added_count, removed_count, trimmed
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def apply_memact_envelope(
|
|
326
|
+
vars: Dict[str, Any],
|
|
327
|
+
*,
|
|
328
|
+
envelope: Dict[str, Any],
|
|
329
|
+
now_iso: Callable[[], str] = utc_now_iso_seconds,
|
|
330
|
+
now_compact: Callable[[], str] = utc_now_compact_seconds,
|
|
331
|
+
) -> Dict[str, Any]:
|
|
332
|
+
"""Apply a MemAct structured envelope to Active Memory deterministically."""
|
|
333
|
+
mem = ensure_memact_memory(vars, now_iso=now_iso)
|
|
334
|
+
limits = mem.get("limits") if isinstance(mem.get("limits"), dict) else dict(DEFAULT_LIMITS)
|
|
335
|
+
|
|
336
|
+
def _limit(key: str) -> int:
|
|
337
|
+
raw = limits.get(key) if isinstance(limits, dict) else None
|
|
338
|
+
try:
|
|
339
|
+
n = int(raw) if raw is not None else int(DEFAULT_LIMITS.get(key, 0) or 0)
|
|
340
|
+
except Exception:
|
|
341
|
+
n = int(DEFAULT_LIMITS.get(key, 0) or 0)
|
|
342
|
+
return n if n > 0 else int(DEFAULT_LIMITS.get(key, 0) or 0)
|
|
343
|
+
|
|
344
|
+
if not isinstance(envelope, dict):
|
|
345
|
+
return {"ok": False, "error": "envelope must be an object"}
|
|
346
|
+
|
|
347
|
+
applied: Dict[str, Any] = {}
|
|
348
|
+
|
|
349
|
+
def _delta_obj(key: str) -> Dict[str, Any]:
|
|
350
|
+
raw = envelope.get(key)
|
|
351
|
+
return raw if isinstance(raw, dict) else {}
|
|
352
|
+
|
|
353
|
+
for module, prefix in (
|
|
354
|
+
("relationships", "rel"),
|
|
355
|
+
("current_tasks", "tsk"),
|
|
356
|
+
("current_context", "ctx"),
|
|
357
|
+
("critical_insights", "ins"),
|
|
358
|
+
("references", "ref"),
|
|
359
|
+
):
|
|
360
|
+
records = mem.get(module)
|
|
361
|
+
if not isinstance(records, list):
|
|
362
|
+
records = []
|
|
363
|
+
mem[module] = records
|
|
364
|
+
delta = _delta_obj(module)
|
|
365
|
+
added = _coerce_str_list(delta.get("added"))
|
|
366
|
+
removed = _coerce_str_list(delta.get("removed"))
|
|
367
|
+
add_n, rm_n, trim_n = _apply_delta_to_list(
|
|
368
|
+
records, # type: ignore[arg-type]
|
|
369
|
+
added=added,
|
|
370
|
+
removed=removed,
|
|
371
|
+
limit=_limit(module),
|
|
372
|
+
now_iso=now_iso,
|
|
373
|
+
now_compact=now_compact,
|
|
374
|
+
prefix=prefix,
|
|
375
|
+
)
|
|
376
|
+
applied[module] = {"added": add_n, "removed": rm_n, "trimmed": trim_n}
|
|
377
|
+
|
|
378
|
+
# History is append-only.
|
|
379
|
+
history_records = mem.get("history")
|
|
380
|
+
if not isinstance(history_records, list):
|
|
381
|
+
history_records = []
|
|
382
|
+
mem["history"] = history_records
|
|
383
|
+
history_delta = _delta_obj("history")
|
|
384
|
+
history_added = _coerce_str_list(history_delta.get("added"))
|
|
385
|
+
# Robustness: ignore any "removed" field if present (append-only contract).
|
|
386
|
+
add_n, _, trim_n = _apply_delta_to_list(
|
|
387
|
+
history_records, # type: ignore[arg-type]
|
|
388
|
+
added=history_added,
|
|
389
|
+
removed=[],
|
|
390
|
+
limit=_limit("history"),
|
|
391
|
+
now_iso=now_iso,
|
|
392
|
+
now_compact=now_compact,
|
|
393
|
+
prefix="hist",
|
|
394
|
+
)
|
|
395
|
+
applied["history"] = {"added": add_n, "trimmed": trim_n}
|
|
396
|
+
|
|
397
|
+
mem["updated_at"] = now_iso()
|
|
398
|
+
return {"ok": True, "applied": applied}
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def render_memact_blocks(vars: Dict[str, Any]) -> List[Dict[str, Any]]:
|
|
402
|
+
mem = ensure_memact_memory(vars)
|
|
403
|
+
|
|
404
|
+
def _render_list(key: str) -> str:
|
|
405
|
+
items = mem.get(key)
|
|
406
|
+
if not isinstance(items, list) or not items:
|
|
407
|
+
return "(empty)"
|
|
408
|
+
lines: List[str] = []
|
|
409
|
+
for item in items:
|
|
410
|
+
if not isinstance(item, dict):
|
|
411
|
+
continue
|
|
412
|
+
text = item.get("text")
|
|
413
|
+
if not isinstance(text, str) or not text.strip():
|
|
414
|
+
continue
|
|
415
|
+
ts = item.get("ts")
|
|
416
|
+
ts_str = str(ts).strip() if isinstance(ts, str) and ts.strip() else ""
|
|
417
|
+
prefix = f"[{ts_str}] " if ts_str else ""
|
|
418
|
+
lines.append(f"- {prefix}{text.strip()}")
|
|
419
|
+
return "\n".join(lines).strip() if lines else "(empty)"
|
|
420
|
+
|
|
421
|
+
persona = mem.get("persona")
|
|
422
|
+
persona_text = str(persona).strip() if isinstance(persona, str) and persona.strip() else DEFAULT_PERSONA_MD
|
|
423
|
+
|
|
424
|
+
return [
|
|
425
|
+
{"component_id": "memory_blueprints", "title": "MEMORY BLUEPRINTS", "content": MEMORY_BLUEPRINTS_MD.strip()},
|
|
426
|
+
{"component_id": "persona", "title": "MY PERSONA", "content": persona_text},
|
|
427
|
+
{"component_id": "relationships", "title": "RELATIONSHIPS", "content": _render_list("relationships")},
|
|
428
|
+
{"component_id": "current_tasks", "title": "CURRENT TASKS", "content": _render_list("current_tasks")},
|
|
429
|
+
{"component_id": "current_context", "title": "CURRENT CONTEXT", "content": _render_list("current_context")},
|
|
430
|
+
{"component_id": "critical_insights", "title": "CRITICAL INSIGHTS", "content": _render_list("critical_insights")},
|
|
431
|
+
{"component_id": "references", "title": "REFERENCES", "content": _render_list("references")},
|
|
432
|
+
{"component_id": "history", "title": "HISTORY", "content": _render_list("history")},
|
|
433
|
+
]
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def render_memact_system_prompt(vars: Dict[str, Any]) -> str:
|
|
437
|
+
"""Render memory blueprints + memory modules for MemAct system prompt injection."""
|
|
438
|
+
blocks = render_memact_blocks(vars)
|
|
439
|
+
parts: List[str] = []
|
|
440
|
+
for b in blocks:
|
|
441
|
+
component_id = str(b.get("component_id") or "").strip()
|
|
442
|
+
title = str(b.get("title") or "").strip()
|
|
443
|
+
content = str(b.get("content") or "").rstrip()
|
|
444
|
+
if not content:
|
|
445
|
+
continue
|
|
446
|
+
if component_id == "memory_blueprints":
|
|
447
|
+
parts.append(content.strip())
|
|
448
|
+
continue
|
|
449
|
+
if not title:
|
|
450
|
+
continue
|
|
451
|
+
parts.append(f"## {title}\n{content}".rstrip())
|
|
452
|
+
return "\n\n".join(parts).strip()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Memory compaction helpers (text-focused; graph-ready contracts).
|
|
2
|
+
|
|
3
|
+
This module keeps compaction mechanics small and deterministic:
|
|
4
|
+
- normalize message metadata (message_id, timestamp)
|
|
5
|
+
- split messages into system / older conversation / recent conversation
|
|
6
|
+
- build span metadata for ArtifactStore persistence
|
|
7
|
+
|
|
8
|
+
LLM prompting is handled by the runtime effect handler; this module stays pure.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _ensure_message_id(meta: Dict[str, Any]) -> str:
|
|
19
|
+
mid = meta.get("message_id")
|
|
20
|
+
if isinstance(mid, str) and mid:
|
|
21
|
+
return mid
|
|
22
|
+
mid = f"msg_{uuid.uuid4().hex}"
|
|
23
|
+
meta["message_id"] = mid
|
|
24
|
+
return mid
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def normalize_messages(
|
|
28
|
+
messages: Sequence[Any],
|
|
29
|
+
*,
|
|
30
|
+
now_iso: Callable[[], str],
|
|
31
|
+
) -> List[Dict[str, Any]]:
|
|
32
|
+
"""Return a JSON-safe copy of messages with stable ids and timestamps."""
|
|
33
|
+
out: List[Dict[str, Any]] = []
|
|
34
|
+
for m in messages:
|
|
35
|
+
if not isinstance(m, dict):
|
|
36
|
+
continue
|
|
37
|
+
m_copy = dict(m)
|
|
38
|
+
m_copy["role"] = str(m_copy.get("role") or "user")
|
|
39
|
+
m_copy["content"] = "" if m_copy.get("content") is None else str(m_copy.get("content"))
|
|
40
|
+
|
|
41
|
+
meta = m_copy.get("metadata")
|
|
42
|
+
if not isinstance(meta, dict):
|
|
43
|
+
meta = {}
|
|
44
|
+
m_copy["metadata"] = meta
|
|
45
|
+
_ensure_message_id(meta)
|
|
46
|
+
|
|
47
|
+
ts = m_copy.get("timestamp")
|
|
48
|
+
if not isinstance(ts, str) or not ts.strip():
|
|
49
|
+
m_copy["timestamp"] = str(now_iso())
|
|
50
|
+
|
|
51
|
+
out.append(m_copy)
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class CompactionSplit:
|
|
57
|
+
system_messages: List[Dict[str, Any]]
|
|
58
|
+
older_messages: List[Dict[str, Any]]
|
|
59
|
+
recent_messages: List[Dict[str, Any]]
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def non_system_count(self) -> int:
|
|
63
|
+
return len(self.older_messages) + len(self.recent_messages)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def split_for_compaction(
|
|
67
|
+
messages: Sequence[Dict[str, Any]],
|
|
68
|
+
*,
|
|
69
|
+
preserve_recent: int,
|
|
70
|
+
) -> CompactionSplit:
|
|
71
|
+
preserve = int(preserve_recent)
|
|
72
|
+
if preserve < 0:
|
|
73
|
+
preserve = 0
|
|
74
|
+
|
|
75
|
+
system_messages = [m for m in messages if m.get("role") == "system"]
|
|
76
|
+
conversation = [m for m in messages if m.get("role") != "system"]
|
|
77
|
+
|
|
78
|
+
if preserve == 0:
|
|
79
|
+
return CompactionSplit(system_messages=system_messages, older_messages=conversation, recent_messages=[])
|
|
80
|
+
if len(conversation) <= preserve:
|
|
81
|
+
return CompactionSplit(system_messages=system_messages, older_messages=[], recent_messages=conversation)
|
|
82
|
+
|
|
83
|
+
older = conversation[:-preserve]
|
|
84
|
+
recent = conversation[-preserve:]
|
|
85
|
+
return CompactionSplit(system_messages=system_messages, older_messages=older, recent_messages=recent)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def span_metadata_from_messages(messages: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
|
|
89
|
+
"""Compute provenance-friendly span metadata from a non-empty message list."""
|
|
90
|
+
if not messages:
|
|
91
|
+
raise ValueError("span_metadata_from_messages requires non-empty messages")
|
|
92
|
+
|
|
93
|
+
first = messages[0]
|
|
94
|
+
last = messages[-1]
|
|
95
|
+
first_meta = first.get("metadata") if isinstance(first.get("metadata"), dict) else {}
|
|
96
|
+
last_meta = last.get("metadata") if isinstance(last.get("metadata"), dict) else {}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
"from_timestamp": first.get("timestamp"),
|
|
100
|
+
"to_timestamp": last.get("timestamp"),
|
|
101
|
+
"from_message_id": first_meta.get("message_id"),
|
|
102
|
+
"to_message_id": last_meta.get("message_id"),
|
|
103
|
+
"message_count": len(messages),
|
|
104
|
+
}
|
|
105
|
+
|
|
@@ -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
|
+
|