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.
Files changed (36) hide show
  1. abstractruntime/__init__.py +7 -2
  2. abstractruntime/core/config.py +14 -1
  3. abstractruntime/core/event_keys.py +62 -0
  4. abstractruntime/core/models.py +12 -1
  5. abstractruntime/core/runtime.py +2444 -14
  6. abstractruntime/core/vars.py +95 -0
  7. abstractruntime/evidence/__init__.py +10 -0
  8. abstractruntime/evidence/recorder.py +325 -0
  9. abstractruntime/integrations/abstractcore/__init__.py +3 -0
  10. abstractruntime/integrations/abstractcore/constants.py +19 -0
  11. abstractruntime/integrations/abstractcore/default_tools.py +134 -0
  12. abstractruntime/integrations/abstractcore/effect_handlers.py +255 -6
  13. abstractruntime/integrations/abstractcore/factory.py +95 -10
  14. abstractruntime/integrations/abstractcore/llm_client.py +456 -52
  15. abstractruntime/integrations/abstractcore/mcp_worker.py +586 -0
  16. abstractruntime/integrations/abstractcore/observability.py +80 -0
  17. abstractruntime/integrations/abstractcore/summarizer.py +154 -0
  18. abstractruntime/integrations/abstractcore/tool_executor.py +481 -24
  19. abstractruntime/memory/__init__.py +21 -0
  20. abstractruntime/memory/active_context.py +746 -0
  21. abstractruntime/memory/active_memory.py +452 -0
  22. abstractruntime/memory/compaction.py +105 -0
  23. abstractruntime/rendering/__init__.py +17 -0
  24. abstractruntime/rendering/agent_trace_report.py +256 -0
  25. abstractruntime/rendering/json_stringify.py +136 -0
  26. abstractruntime/scheduler/scheduler.py +93 -2
  27. abstractruntime/storage/__init__.py +3 -1
  28. abstractruntime/storage/artifacts.py +20 -5
  29. abstractruntime/storage/json_files.py +15 -2
  30. abstractruntime/storage/observable.py +99 -0
  31. {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.0.dist-info}/METADATA +5 -1
  32. abstractruntime-0.4.0.dist-info/RECORD +49 -0
  33. abstractruntime-0.4.0.dist-info/entry_points.txt +2 -0
  34. abstractruntime-0.2.0.dist-info/RECORD +0 -32
  35. {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.0.dist-info}/WHEEL +0 -0
  36. {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
+