krons 0.2.2__py3-none-any.whl → 0.2.4__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.
@@ -2,7 +2,7 @@ from typing import Any, Protocol, runtime_checkable
2
2
 
3
3
  from pydantic import BaseModel
4
4
 
5
- from krons.core.types import Enum, MaybeUnset, Unset
5
+ from krons.core.types import Enum
6
6
 
7
7
 
8
8
  @runtime_checkable
@@ -1,8 +1,7 @@
1
1
  # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
2
  # SPDX-License-Identifier: Apache-2.0
3
3
 
4
- from collections.abc import Callable
5
- from typing import TYPE_CHECKING, Any, Literal, cast
4
+ from typing import Any, cast
6
5
 
7
6
  from pydantic import JsonValue
8
7
 
@@ -26,8 +26,6 @@ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
26
26
  "Edge": ("krons.core.base.graph", "Edge"),
27
27
  "EdgeCondition": ("krons.core.base.graph", "EdgeCondition"),
28
28
  "Graph": ("krons.core.base.graph", "Graph"),
29
- # log
30
- "DataLoggerConfig": ("krons.core.base.log", "DataLoggerConfig"),
31
29
  # node
32
30
  "NODE_REGISTRY": ("krons.core.base.node", "NODE_REGISTRY"),
33
31
  "PERSISTABLE_NODE_REGISTRY": ("krons.core.base.node", "PERSISTABLE_NODE_REGISTRY"),
@@ -79,7 +77,6 @@ if TYPE_CHECKING:
79
77
  from .eventbus import EventBus, Handler
80
78
  from .flow import Flow
81
79
  from .graph import Edge, EdgeCondition, Graph
82
- from .log import DataLoggerConfig
83
80
  from .node import (
84
81
  NODE_REGISTRY,
85
82
  PERSISTABLE_NODE_REGISTRY,
@@ -100,7 +97,6 @@ __all__ = [
100
97
  "PERSISTABLE_NODE_REGISTRY",
101
98
  # classes
102
99
  "Broadcaster",
103
- "DataLoggerConfig",
104
100
  "Edge",
105
101
  "EdgeCondition",
106
102
  "Element",
krons/core/base/flow.py CHANGED
@@ -321,6 +321,13 @@ class Flow(Element, Generic[E, P]):
321
321
 
322
322
  return self.items.remove(uid)
323
323
 
324
+ @synchronized
325
+ def clear(self) -> None:
326
+ """Clear all items and progressions."""
327
+ self.items.clear()
328
+ self.progressions.clear()
329
+ self._progression_names.clear()
330
+
324
331
  def __repr__(self) -> str:
325
332
  name_str = f", name='{self.name}'" if self.name else ""
326
333
  return f"Flow(items={len(self.items)}, progressions={len(self.progressions)}{name_str})"
krons/core/base/node.py CHANGED
@@ -745,9 +745,12 @@ def create_node(
745
745
  has_embedding = True
746
746
 
747
747
  # 1. Build all possible specs
748
+ # Extract meta_key from config_kwargs if provided, else use default
749
+ meta_key = config_kwargs.get("meta_key", "node_metadata")
748
750
  all_specs = ContentSpecs.get_specs(
749
751
  content_type=content if content else Unset,
750
752
  dim=resolved_embedding_dim,
753
+ meta_key=meta_key,
751
754
  ) + AuditSpecs.get_specs(use_uuid=True)
752
755
 
753
756
  # 2. Track which fields to include
@@ -837,19 +840,17 @@ def _extract_base_type(annotation: Any) -> Any:
837
840
  return annotation
838
841
 
839
842
 
840
- def generate_ddl(
841
- node_cls: type[Node],
842
- *,
843
- include_audit_columns: bool = True,
844
- ) -> str:
843
+ def generate_ddl(node_cls: type[Node]) -> str:
845
844
  """Generate CREATE TABLE DDL from Node subclass.
846
845
 
847
846
  Flattens content fields (if configured), adds audit columns, and
848
847
  generates PostgreSQL DDL with pgvector support for embeddings.
849
848
 
849
+ Audit column inclusion is driven by NodeConfig settings (track_updated_at,
850
+ soft_delete, versioning, etc.).
851
+
850
852
  Args:
851
853
  node_cls: Persistable Node subclass (must have table_name)
852
- include_audit_columns: Include audit columns from NodeConfig
853
854
 
854
855
  Returns:
855
856
  CREATE TABLE IF NOT EXISTS statement
@@ -875,7 +876,8 @@ def generate_ddl(
875
876
  )
876
877
 
877
878
  all_specs = ContentSpecs.get_specs(
878
- dim=config.embedding_dim if config.embedding_enabled else Unset
879
+ dim=config.embedding_dim if config.embedding_enabled else Unset,
880
+ meta_key=config.meta_key,
879
881
  ) + AuditSpecs.get_specs(use_uuid=True)
880
882
 
881
883
  # Flatten content: extract fields from BaseModel instead of generic JSONB
@@ -900,25 +902,24 @@ def generate_ddl(
900
902
  ):
901
903
  include.add("content")
902
904
 
903
- include.add("metadata")
904
-
905
- if include_audit_columns:
906
- if config.track_updated_at:
907
- include.add("updated_at")
908
- if config.track_updated_by:
909
- include.add("updated_by")
910
- if config.track_is_active:
911
- include.add("is_active")
912
- if config.soft_delete:
913
- include.update({"is_deleted", "deleted_at"})
914
- if config.track_deleted_by:
915
- include.add("deleted_by")
916
- if config.versioning:
917
- include.add("version")
918
- if config.content_hashing:
919
- include.add("content_hash")
920
- if config.integrity_hashing:
921
- include.add("integrity_hash")
905
+ if not config.is_sentinel_field("meta_key") and config.meta_key != "metadata":
906
+ include.add(config.meta_key)
907
+
908
+ audit_cols = {
909
+ "updated_at": config.track_updated_at,
910
+ "updated_by": config.track_updated_by,
911
+ "is_active": config.track_is_active,
912
+ "is_deleted": config.soft_delete,
913
+ "deleted_at": config.soft_delete,
914
+ "deleted_by": config.soft_delete and config.track_deleted_by,
915
+ "version": config.versioning,
916
+ "content_hash": config.content_hashing,
917
+ "integrity_hash": config.integrity_hashing,
918
+ }
919
+
920
+ for col, enabled in audit_cols.items():
921
+ if enabled:
922
+ include.add(col)
922
923
 
923
924
  # If flattened, include the extracted content field names
924
925
  if config.flatten_content and content_type is not None:
@@ -2,10 +2,52 @@ from __future__ import annotations
2
2
 
3
3
  import types
4
4
  from functools import reduce
5
- from typing import Any, Union, get_args, get_origin
5
+ from typing import Annotated, Any, ForwardRef, Union, get_args, get_origin
6
+ from uuid import UUID
7
+
8
+
9
+ def _resolve_forward_ref(fwd: ForwardRef) -> dict[str, Any]:
10
+ """Handle ForwardRef annotations (from 'from __future__ import annotations').
11
+
12
+ Parses the string representation to extract type info for DDL generation.
13
+ FK[Model] -> Annotated[UUID, FKMeta(model_name)], Vector[dim] -> list[float], etc.
14
+
15
+ Uses parse_forward_ref from db_types as canonical parser.
16
+ """
17
+ from krons.core.types.db_types import parse_forward_ref
18
+
19
+ fk, vec, nullable = parse_forward_ref(fwd)
20
+
21
+ # FK[Model] -> Annotated[UUID, FKMeta]
22
+ if fk is not None:
23
+ base_type = Annotated[UUID, fk]
24
+ return {"base_type": base_type, "nullable": nullable, "listable": False}
25
+
26
+ # Vector[dim] -> Annotated[list[float], VectorMeta]
27
+ if vec is not None:
28
+ base_type = Annotated[list[float], vec]
29
+ return {"base_type": base_type, "nullable": nullable, "listable": False}
30
+
31
+ # Default: treat as generic type (will map to TEXT in SQL)
32
+ return {"base_type": str, "nullable": nullable, "listable": False}
6
33
 
7
34
 
8
35
  def resolve_annotation_to_base_types(annotation: Any) -> dict[str, Any]:
36
+ """Resolve an annotation to its base types, detecting nullable and listable.
37
+
38
+ Args:
39
+ annotation: Type annotation to resolve (may include Optional, list, etc.)
40
+
41
+ Returns:
42
+ Dict with keys:
43
+ - base_type: The innermost type
44
+ - nullable: Whether None is allowed
45
+ - listable: Whether it's a list type
46
+ """
47
+ # Handle ForwardRef (from 'from __future__ import annotations')
48
+ if isinstance(annotation, ForwardRef):
49
+ return _resolve_forward_ref(annotation)
50
+
9
51
  def resolve_nullable_inner_type(_anno: Any) -> tuple[bool, Any]:
10
52
  origin = get_origin(_anno)
11
53
 
@@ -33,12 +33,14 @@ class ContentSpecs(BaseModel):
33
33
  *,
34
34
  content_type: type | UnsetType = Unset,
35
35
  dim: int | UnsetType = Unset,
36
+ meta_key: str | UnsetType = Unset,
36
37
  ) -> list[Spec]:
37
38
  """Get list of content Specs.
38
39
 
39
40
  Args:
40
41
  content_type: Type for content/metadata fields (default: dict).
41
42
  dim: Embedding dimension. Unset = list[float], int = Vector[dim].
43
+ meta_key: DB alias for metadata field (e.g., "node_metadata").
42
44
  """
43
45
  operable = Operable.from_structure(cls)
44
46
  specs = {spec.name: spec for spec in operable.get_specs()}
@@ -48,6 +50,10 @@ class ContentSpecs(BaseModel):
48
50
  specs["content"] = Spec(content_type, name="content").as_nullable()
49
51
  specs["metadata"] = Spec(content_type, name="metadata").as_nullable()
50
52
 
53
+ # Add meta_key alias if specified (DB mode uses this to avoid SQL reserved word)
54
+ if meta_key is not Unset and isinstance(meta_key, str):
55
+ specs[meta_key] = Spec(dict[str, Any], name=meta_key).as_nullable()
56
+
51
57
  # Override embedding with vector dimension if specified
52
58
  if dim is not Unset and isinstance(dim, int):
53
59
  specs["embedding"] = Spec(
@@ -14,8 +14,9 @@ Extraction:
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
+ import re
17
18
  import types
18
- from typing import Annotated, Any, Literal, Union, get_args, get_origin
19
+ from typing import Annotated, Any, ForwardRef, Literal, Union, get_args, get_origin
19
20
  from uuid import UUID
20
21
 
21
22
  from krons.core.types._sentinel import Unset, UnsetType, not_sentinel
@@ -32,6 +33,7 @@ __all__ = [
32
33
  "Vector",
33
34
  "VectorMeta",
34
35
  "extract_kron_db_meta",
36
+ "parse_forward_ref",
35
37
  ]
36
38
 
37
39
 
@@ -200,6 +202,88 @@ def _find_in_field_info(field_info: Any, meta_type: type) -> Any | None:
200
202
  return None
201
203
 
202
204
 
205
+ def _is_spec_like(obj: Any) -> bool:
206
+ """Check if object looks like a Spec (duck typing to avoid hard import)."""
207
+ return (
208
+ hasattr(obj, "get")
209
+ and hasattr(obj, "__class__")
210
+ and "Spec" in type(obj).__name__
211
+ )
212
+
213
+
214
+ def _detect_nullable(arg: str) -> bool:
215
+ """Detect nullability from annotation string.
216
+
217
+ Handles:
218
+ - X | None, None | X (PEP 604 union)
219
+ - Optional[X] (typing.Optional)
220
+ - Union[X, None], Union[None, X] (typing.Union with nested types)
221
+ """
222
+ # PEP 604 style: X | None or None | X
223
+ if re.search(r"\|\s*None\b", arg) or re.search(r"\bNone\s*\|", arg):
224
+ return True
225
+ # Optional[X]
226
+ if re.search(r"\bOptional\s*\[", arg):
227
+ return True
228
+ # Union[..., None, ...] - if Union is present and None is anywhere in string
229
+ # This handles nested types like Union[FK[User], None]
230
+ if re.search(r"\bUnion\s*\[", arg) and re.search(r"\bNone\b", arg):
231
+ return True
232
+ return False
233
+
234
+
235
+ def parse_forward_ref(
236
+ fwd: ForwardRef,
237
+ ) -> tuple[FKMeta | None, VectorMeta | None, bool]:
238
+ """Parse FK/Vector metadata and nullability from a ForwardRef string.
239
+
240
+ Canonical parser for ForwardRef annotations from 'from __future__ import annotations'.
241
+
242
+ Handles:
243
+ - FK[Model], FK["Model"], FK['Model'] (bare and string refs)
244
+ - Vector[1536] (dimension as int literal)
245
+ - Nullability: X | None, Optional[X], Union[X, None]
246
+
247
+ Args:
248
+ fwd: ForwardRef to parse
249
+
250
+ Returns:
251
+ Tuple of (fk_meta, vector_meta, is_nullable)
252
+ - fk_meta: FKMeta if FK[...] found, else None
253
+ - vector_meta: VectorMeta if Vector[dim] found, else None
254
+ - is_nullable: True if nullable pattern detected
255
+ """
256
+ arg = fwd.__forward_arg__
257
+ fk: FKMeta | None = None
258
+ vec: VectorMeta | None = None
259
+ nullable = _detect_nullable(arg)
260
+
261
+ # Match FK[ModelName] or FK["ModelName"] or FK['ModelName']
262
+ fk_match = re.search(r"FK\[(['\"]?)(\w+)\1\]", arg)
263
+ if fk_match:
264
+ model_name = fk_match.group(2)
265
+ fk = FKMeta(model_name)
266
+
267
+ # Match Vector[dim]
268
+ vec_match = re.search(r"Vector\[(\d+)\]", arg)
269
+ if vec_match:
270
+ dim = int(vec_match.group(1))
271
+ vec = VectorMeta(dim)
272
+
273
+ return fk, vec, nullable
274
+
275
+
276
+ def _extract_from_forward_ref(
277
+ fwd: ForwardRef,
278
+ ) -> tuple[FKMeta | UnsetType, VectorMeta | UnsetType]:
279
+ """Extract FK/Vector metadata from a ForwardRef (returns Unset for missing).
280
+
281
+ Wrapper around parse_forward_ref for extract_kron_db_meta compatibility.
282
+ """
283
+ fk, vec, _ = parse_forward_ref(fwd)
284
+ return (fk if fk is not None else Unset, vec if vec is not None else Unset)
285
+
286
+
203
287
  def extract_kron_db_meta(
204
288
  from_: Any,
205
289
  metas: Literal["FK", "Vector", "BOTH"] = "BOTH",
@@ -209,7 +293,7 @@ def extract_kron_db_meta(
209
293
  Unified extraction dispatching on source type:
210
294
  - FieldInfo: searches Pydantic metadata and annotation
211
295
  - type/annotation: searches Annotated/Union structure
212
- - Spec: reads spec metadata directly
296
+ - Spec: reads spec metadata directly (if available)
213
297
 
214
298
  Args:
215
299
  from_: FieldInfo, type annotation, or Spec instance
@@ -228,6 +312,10 @@ def extract_kron_db_meta(
228
312
  if metas in ("Vector", "BOTH"):
229
313
  vec = _find_in_field_info(from_, VectorMeta) or Unset
230
314
 
315
+ elif isinstance(from_, ForwardRef):
316
+ # Handle ForwardRef from 'from __future__ import annotations'
317
+ fk, vec = _extract_from_forward_ref(from_)
318
+
231
319
  elif get_origin(from_) is not None or isinstance(from_, type):
232
320
  # Raw type annotation
233
321
  if metas in ("FK", "BOTH"):
@@ -235,23 +323,20 @@ def extract_kron_db_meta(
235
323
  if metas in ("Vector", "BOTH"):
236
324
  vec = _find_in_annotation(from_, VectorMeta) or Unset
237
325
 
326
+ elif _is_spec_like(from_):
327
+ # Spec-like object (duck typed to avoid circular imports)
328
+ if metas in ("FK", "BOTH"):
329
+ fk_val = from_.get("as_fk", Unset)
330
+ if not_sentinel(fk_val, {"none"}) and isinstance(fk_val, FKMeta):
331
+ fk = fk_val
332
+ if metas in ("Vector", "BOTH"):
333
+ vec_val = from_.get("embedding", Unset)
334
+ if not_sentinel(vec_val, {"none"}) and isinstance(vec_val, VectorMeta):
335
+ vec = vec_val
238
336
  else:
239
- # Try Spec (lazy import to avoid circular)
240
- from krons.core.specs.spec import Spec
241
-
242
- if isinstance(from_, Spec):
243
- if metas in ("FK", "BOTH"):
244
- fk_val = from_.get("as_fk", Unset)
245
- if not_sentinel(fk_val, {"none"}) and isinstance(fk_val, FKMeta):
246
- fk = fk_val
247
- if metas in ("Vector", "BOTH"):
248
- vec_val = from_.get("embedding", Unset)
249
- if not_sentinel(vec_val, {"none"}) and isinstance(vec_val, VectorMeta):
250
- vec = vec_val
251
- else:
252
- raise TypeError(
253
- f"from_ must be FieldInfo, type annotation, or Spec, got {type(from_).__name__}"
254
- )
337
+ raise TypeError(
338
+ f"from_ must be FieldInfo, type annotation, or Spec, got {type(from_).__name__}"
339
+ )
255
340
 
256
341
  if metas == "FK":
257
342
  return fk
krons/session/session.py CHANGED
@@ -14,11 +14,9 @@ import contextlib
14
14
  from collections.abc import AsyncGenerator, Iterable
15
15
  from pathlib import Path
16
16
  from typing import Any, Literal
17
-
18
- from krons.core.base.log import DataLoggerConfig
19
17
  from uuid import UUID
20
18
 
21
- from pydantic import Field, PrivateAttr, model_validator
19
+ from pydantic import Field, PrivateAttr, field_serializer, model_validator
22
20
 
23
21
  from krons.core import Element, Flow, Pile, Progression
24
22
  from krons.core.types import HashableModel, Unset, UnsetType, not_sentinel
@@ -63,25 +61,43 @@ class SessionConfig(HashableModel):
63
61
  default_gen_model: str | None = None
64
62
  default_parse_model: str | None = None
65
63
  auto_create_default_branch: bool = True
66
- log_config: DataLoggerConfig | None = Field(
64
+
65
+ # Logging configuration
66
+ log_persist_dir: str | Path | None = Field(
67
67
  default=None,
68
- description="DataLoggerConfig for auto-logging (None = disabled)",
68
+ description="Directory for session dumps. None disables logging.",
69
+ )
70
+ log_auto_save_on_exit: bool = Field(
71
+ default=True,
72
+ description="Register atexit handler on Session creation.",
69
73
  )
70
74
 
75
+ @property
76
+ def logging_enabled(self) -> bool:
77
+ """True if logging is configured (log_persist_dir is set)."""
78
+ return self.log_persist_dir is not None
79
+
71
80
 
72
81
  class Session(Element):
73
82
  user: str | None = None
74
83
  communications: Flow[Message, Branch] = Field(
75
84
  default_factory=lambda: Flow(item_type=Message)
76
85
  )
77
- resources: ResourceRegistry = Field(default_factory=ResourceRegistry)
78
- operations: OperationRegistry = Field(default_factory=OperationRegistry)
86
+ resources: ResourceRegistry = Field(default_factory=ResourceRegistry, exclude=True)
87
+ operations: OperationRegistry = Field(
88
+ default_factory=OperationRegistry, exclude=True
89
+ )
79
90
  config: SessionConfig = Field(default_factory=SessionConfig)
80
91
  default_branch_id: UUID | None = None
81
92
 
82
93
  _registered_atexit: bool = PrivateAttr(default=False)
83
94
  _dump_count: int = PrivateAttr(default=0)
84
95
 
96
+ @field_serializer("communications")
97
+ def _serialize_communications(self, flow: Flow) -> dict:
98
+ """Use Flow's custom to_dict for proper nested serialization."""
99
+ return flow.to_dict(mode="json")
100
+
85
101
  @model_validator(mode="after")
86
102
  def _validate_default_branch(self) -> Session:
87
103
  """Auto-create default branch and register built-in operations."""
@@ -96,8 +112,8 @@ class Session(Element):
96
112
 
97
113
  # Register atexit handler if configured
98
114
  if (
99
- self.config.log_config is not None
100
- and self.config.log_config.auto_save_on_exit
115
+ self.config.logging_enabled
116
+ and self.config.log_auto_save_on_exit
101
117
  and not self._registered_atexit
102
118
  ):
103
119
  atexit.register(self._save_at_exit)
@@ -406,96 +422,87 @@ class Session(Element):
406
422
  async for result in handler(params, ctx):
407
423
  yield result
408
424
 
409
- def dump_messages(self, clear: bool = False) -> Path | None:
410
- """Sync dump all session messages to file.
425
+ def dump(self, clear: bool = False) -> Path | None:
426
+ """Sync dump entire session state for replay.
427
+
428
+ Serializes session (messages, branches, config) to JSON.
429
+ Resources and operations are excluded (re-register on restore).
430
+ To restore: Session.from_dict(data), then re-register resources.
411
431
 
412
432
  Args:
413
- clear: Clear messages after dump (default False).
433
+ clear: Clear communications after dump (default False).
414
434
 
415
435
  Returns:
416
- Path to written file, or None if no log_config or no messages.
436
+ Path to session file, or None if logging disabled or empty.
417
437
  """
418
- from krons.utils import create_path, json_dumpb, json_lines_iter
438
+ from krons.utils import create_path, json_dumpb
419
439
  from krons.utils.concurrency import run_async
420
440
 
421
- if self.config.log_config is None or len(self.messages) == 0:
441
+ if not self.config.logging_enabled or len(self.messages) == 0:
422
442
  return None
423
443
 
424
- cfg = self.config.log_config
425
444
  self._dump_count += 1
426
445
 
427
446
  filepath = run_async(
428
447
  create_path(
429
- directory=cfg.persist_dir,
448
+ directory=self.config.log_persist_dir,
430
449
  filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
431
- extension=cfg.extension,
450
+ extension=".json",
432
451
  timestamp=True,
433
452
  file_exist_ok=True,
434
453
  )
435
454
  )
436
455
 
437
- items = [msg.to_dict(mode="json") for msg in self.messages]
438
-
456
+ data = json_dumpb(self.to_dict(mode="json"), safe_fallback=True)
439
457
  std_path = Path(filepath)
440
- if cfg.extension == ".jsonl":
441
- with std_path.open("wb") as f:
442
- for chunk in json_lines_iter(items, safe_fallback=True):
443
- f.write(chunk)
444
- else:
445
- data = json_dumpb(items, safe_fallback=True)
446
- std_path.write_bytes(data)
458
+ std_path.write_bytes(data)
447
459
 
448
460
  if clear:
449
- self.communications.items.clear()
461
+ self.communications.clear()
450
462
 
451
463
  return std_path
452
464
 
453
- async def adump_messages(self, clear: bool = False) -> Path | None:
454
- """Async dump all session messages to file with lock protection.
465
+ async def adump(self, clear: bool = False) -> Path | None:
466
+ """Async dump entire session state for replay.
467
+
468
+ Serializes the full session (messages, branches, config) to JSON.
469
+ To restore: Session.from_dict(data), then re-register resources.
455
470
 
456
471
  Args:
457
- clear: Clear messages after dump (default False).
472
+ clear: Clear communications after dump (default False).
458
473
 
459
474
  Returns:
460
- Path to written file, or None if no log_config or no messages.
475
+ Path to session file, or None if logging disabled or empty.
461
476
  """
462
- from krons.utils import create_path, json_dumpb, json_lines_iter
477
+ from krons.utils import create_path, json_dumpb
463
478
 
464
- if self.config.log_config is None or len(self.messages) == 0:
479
+ if not self.config.logging_enabled or len(self.messages) == 0:
465
480
  return None
466
481
 
467
- cfg = self.config.log_config
468
-
469
482
  async with self.messages:
470
483
  self._dump_count += 1
471
484
 
472
485
  filepath = await create_path(
473
- directory=cfg.persist_dir,
486
+ directory=self.config.log_persist_dir,
474
487
  filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
475
- extension=cfg.extension,
488
+ extension=".json",
476
489
  timestamp=True,
477
490
  file_exist_ok=True,
478
491
  )
479
492
 
480
- items = [msg.to_dict(mode="json") for msg in self.messages]
481
-
482
- if cfg.extension == ".jsonl":
483
- content = b"".join(json_lines_iter(items, safe_fallback=True))
484
- await filepath.write_bytes(content)
485
- else:
486
- data = json_dumpb(items, safe_fallback=True)
487
- await filepath.write_bytes(data)
493
+ data = json_dumpb(self.to_dict(mode="json"), safe_fallback=True)
494
+ await filepath.write_bytes(data)
488
495
 
489
496
  if clear:
490
- self.communications.items.clear()
497
+ self.communications.clear()
491
498
 
492
499
  return Path(filepath)
493
500
 
494
501
  def _save_at_exit(self) -> None:
495
- """atexit callback. Dumps messages synchronously. Errors are suppressed."""
502
+ """atexit callback. Dumps session synchronously. Errors are suppressed."""
496
503
  if len(self.messages) > 0:
497
504
  try:
498
- self.dump_messages(clear=False)
505
+ self.dump(clear=False)
499
506
  except Exception:
500
507
  pass # Silent failure during interpreter shutdown
501
508
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: krons
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Spec-based composable framework for building type-safe systems
5
5
  Project-URL: Homepage, https://github.com/khive-ai/krons
6
6
  Project-URL: Repository, https://github.com/khive-ai/krons
@@ -10,9 +10,9 @@ krons/agent/mcps/wrapper.py,sha256=tW4aXzMMBqh4BtcuEaLxYqLxttGRJ06UQSjWmqdsGfI,2
10
10
  krons/agent/message/__init__.py,sha256=vumj0lHyraEoshw0tx8yWt5fyUUuAC3qVBiSLQuq-9A,499
11
11
  krons/agent/message/action.py,sha256=ynONthCd78gWzl3dbQughJOGd-Iys6lbOzzYwuLXGWU,2047
12
12
  krons/agent/message/assistant.py,sha256=V75490V9mQLKbBkH9cYAmnva2-Q3hwCAj_4flLUCvOE,1459
13
- krons/agent/message/common.py,sha256=j9rpVoZhH6FV9JLVv6K-IgRJqPGnT5Vcw80UsEWv5ME,1282
13
+ krons/agent/message/common.py,sha256=9z2QRoOLdZ_CrChn0bqC7j1Ard660h7osMIfzGYd5i4,1263
14
14
  krons/agent/message/instruction.py,sha256=jouxli_yDwRcLJhXs8f9cR0MdreW3pNRDIw2ha7K4uE,4793
15
- krons/agent/message/prepare_msg.py,sha256=85XB-S2qxzXAUxn0cu46LNbWq0JL6w1IO1soJ3ci0Y4,7017
15
+ krons/agent/message/prepare_msg.py,sha256=YkA4S9J_UPJ5zNRjJtnBRovTAM2QGkyyIx6xLmWkN-w,6956
16
16
  krons/agent/message/role.py,sha256=wKY5gIov4yHuvHiXEGFJ8YIaDR2YcmyH83JnDLgnFtI,1397
17
17
  krons/agent/message/system.py,sha256=vCwFrYtM8K-5bVvl-mo0xW-Q_1AWqIAOgwDnNQ41DeA,1884
18
18
  krons/agent/operations/__init__.py,sha256=UqKGE99xyYBGHnEHPVLq8Bf7dvs195gAnpMH3zpjNNA,2095
@@ -37,15 +37,14 @@ krons/agent/third_party/claude_code.py,sha256=NBYMsdPheb0PqGgTO2_aUAR2XoGDsPUwsB
37
37
  krons/agent/third_party/gemini_models.py,sha256=2dag2WbSTKLBd8t0F7t_TXCcl7aqsTFgDI6XLkhsZwM,17416
38
38
  krons/agent/third_party/openai_models.py,sha256=L-GkJ-bBW5zwPbgCWEYHMNqRiZfFTAUJE96AVZoDAkQ,8040
39
39
  krons/core/__init__.py,sha256=TKi3toVRYybyjUwUJlng--pfWzWihUWQBCNh0UTfTSQ,3611
40
- krons/core/base/__init__.py,sha256=emyypsuiFK0UzYsDeamD84ygXnO1TLx2A5mBxGTBidE,3755
40
+ krons/core/base/__init__.py,sha256=Edl_HH2F1l0SMHMzuEPU_IFjkigRR1vtqVXcPm-mr1o,3614
41
41
  krons/core/base/broadcaster.py,sha256=Es__WfL-j2h5NS6aU18fgaFkZsJIL-zDe86Qvlea-io,4013
42
42
  krons/core/base/element.py,sha256=LqBbmPttXoDAFy56JzWhSJ4sBw_LW7hMGxZUog85N8g,7642
43
43
  krons/core/base/event.py,sha256=iZQ65Xg3a8eaCEl4Hca-pxPNtckhrHkydEqYyppbuo0,11524
44
44
  krons/core/base/eventbus.py,sha256=z45ORtGey5A1N9hdyEPOhlX1whaTTdqdk7g3XEDL-_8,3761
45
- krons/core/base/flow.py,sha256=eqsYm8QO7bD5Y9VYnlpHFWpaUCY0rCeKGrDrFUyBVA4,12364
45
+ krons/core/base/flow.py,sha256=bFG5x7PEbWApsD24GTgpD5VsedLO6364scaAxvYw6LM,12561
46
46
  krons/core/base/graph.py,sha256=FiTMArxuz6At3oYPEoamES3FtBM-AM6QnA7EQ8G18mo,15972
47
- krons/core/base/log.py,sha256=SMJlruIG4YHk6Ls45jQ9SrPxFCcnypUmTGaWd90wiwM,923
48
- krons/core/base/node.py,sha256=0_-R6uNe6-LF3adQRZowSIICdy6ep3cQIQRdF0oosoQ,34699
47
+ krons/core/base/node.py,sha256=Kg9rSKHjbeqxA9ypnJCJ2CKPIxfm5Do3fpdGxzgVu2w,34891
49
48
  krons/core/base/pile.py,sha256=ZKvnX3BjMsrH989TLNZII1dDWv4yPhSRzLKXx2yzyKQ,21108
50
49
  krons/core/base/processor.py,sha256=GrdNZa5YD0tXle847ttnxuWe4Ego1MhULzWHmSz5RqQ,18768
51
50
  krons/core/base/progression.py,sha256=srcrmp_zALgg7II5TJsuMM9DdNJszgTxLxvES0WBOTE,9844
@@ -55,7 +54,7 @@ krons/core/specs/operable.py,sha256=zO15i56_UzTTuM3yg_-EMtQY1Equ_WBi3rUCoCKrYck,
55
54
  krons/core/specs/protocol.py,sha256=Se-Cow7de4hF4vkvw5qYj14c37JJ-4aJSDIvJHFyst8,4392
56
55
  krons/core/specs/spec.py,sha256=QaVWN3ZrX8l1x15xXtfpNGzSIV01X0H3jy9Mjv9vT8Y,18706
57
56
  krons/core/specs/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
58
- krons/core/specs/adapters/_utils.py,sha256=ChWH-C2LRXaXiWM3n7P0DZ8Oci5dSDq59DIJtHx2h-g,1365
57
+ krons/core/specs/adapters/_utils.py,sha256=0H5aGHqx8sGrnZTrH3ng_tN4hnSkOEvX3tfbCuBE_0g,2954
59
58
  krons/core/specs/adapters/dataclass_field.py,sha256=thhgC3iQsrkfMkKzB4XmDbwh4fpLYocdL0KvY_zxbzo,8802
60
59
  krons/core/specs/adapters/factory.py,sha256=_LoSMCtHNkK5kNL5LUDjHvJJ2BtDHAaW1gly5LpNPXQ,1572
61
60
  krons/core/specs/adapters/pydantic_adapter.py,sha256=UxnOtJdeIyJEPBBjSACWE-JVc0yWxHSepz5i2SZhS6A,10410
@@ -63,12 +62,12 @@ krons/core/specs/adapters/sql_ddl.py,sha256=NTttvx16X926qeQNWxqdoLlToeiMwufpKEPW
63
62
  krons/core/specs/catalog/__init__.py,sha256=TwS7hvGxa4-2Usu5rKGlhmUS0hhBlRzVNFNKDE7q8eU,1231
64
63
  krons/core/specs/catalog/_audit.py,sha256=g-L4eSJw3H0hfejex5LwsnepaGyTUPrL8JbPU7lPycY,1260
65
64
  krons/core/specs/catalog/_common.py,sha256=ItOPWV1fEeaz6ss5XNZYUetp204saIs7s5Bl0Jt8AOQ,1207
66
- krons/core/specs/catalog/_content.py,sha256=pQAbgjoObZcAQLe8Wg-_sqQ8VYZUGsRQJnjuiCjH-p0,1972
65
+ krons/core/specs/catalog/_content.py,sha256=XeWYlTX2Jf_Hl5H0c3OLOSGPn1TJOIZ65RCd5WJFi8Y,2324
67
66
  krons/core/specs/catalog/_enforcement.py,sha256=JlKF_qGc-Xk7E_ebvq6lQs19iRBAWk2TOe1Qm7XMf1k,2126
68
67
  krons/core/types/__init__.py,sha256=pJ6kuop7vSLZp5jMBs-CQ7CHRjvUuO8kHhpZ34KM85I,1049
69
68
  krons/core/types/_sentinel.py,sha256=QJGyRAYNkJ2ylw6dhxPHJNgmxqBdorBd-xx7pNrUykw,8855
70
69
  krons/core/types/base.py,sha256=Un3MrPGuyns9SgPczJ5vbQExemTO-9B2Eks87NDd--A,12330
71
- krons/core/types/db_types.py,sha256=3ZaAXJe4dartmF-IyKr59NVlL2EbGpx7AtVqqbrIXIk,8136
70
+ krons/core/types/db_types.py,sha256=zY-DQH-TIDo3XbZBgbIAxdgLI2X1A500F4_NeoH2jVI,10929
72
71
  krons/core/types/identity.py,sha256=Ccb5oJArC1PEzdzNPlLi_oK87uKx7BFevX9rvY9y-14,1949
73
72
  krons/resource/__init__.py,sha256=h83vOSa6zXXMRdfxch7yg7kGy6Pe_6c0wWANByATbZ8,2783
74
73
  krons/resource/backend.py,sha256=mW1kkAZrUwnBZeMOY_0I_egpTKpuNyIs5Mps4r2cOxk,10979
@@ -87,7 +86,7 @@ krons/session/constraints.py,sha256=DvmLKyf5eYUX2nxCDcco1-uDipGjiIJwoUxWVtX2w84,
87
86
  krons/session/exchange.py,sha256=QChEqK5NFZJpHWxPrXd8LkGBCFg6LNONd1ZEy0MufpI,9165
88
87
  krons/session/message.py,sha256=mtZ-PYCCoyWLy7BSB5TL5a0hIt0aQX2zdckOCIl6NZ0,2007
89
88
  krons/session/registry.py,sha256=13pPKmDZJaj9q27oUOdtH6-jrHP_HDFF8lCdcNF--EE,999
90
- krons/session/session.py,sha256=_y3-NEQxgBa51mRd6LiD7zTqJFikKCP4avLqMai6O08,16966
89
+ krons/session/session.py,sha256=D-ENpVZYCQZ80iiPiuuJXj8jt6qtoHkDSbSWYYM4bFs,17218
91
90
  krons/utils/__init__.py,sha256=V-jTKULdofjJXxcEHygefKV_v6XE4H3poCdxPLjvf_4,1718
92
91
  krons/utils/_function_arg_parser.py,sha256=H5JVLBVY8W9ZNkZ4_YVtVVY1rFYvnIRhSrAhKEdj2Qc,3479
93
92
  krons/utils/_hash.py,sha256=W2Ma9v8-INPaGkur7GTtbF8KwuXSJNSwk8DCNPRvx8Q,6859
@@ -146,7 +145,7 @@ krons/work/rules/common/mapping.py,sha256=Loq54MNEtwpnHN0aypTjFOqwoOKLEysddHh-JE
146
145
  krons/work/rules/common/model.py,sha256=xmM6coEThf_fgIiqJiyDgvdfib_FpVeY6LgWPVcWSwU,3026
147
146
  krons/work/rules/common/number.py,sha256=cCukgMSpQu5RdYK5rXAUyop9qXgDRfLCioMvE8kIzHg,3162
148
147
  krons/work/rules/common/string.py,sha256=zHp_OLh0FL4PvmSlyDTEzb2I97-DBSEyI2zcMo10voA,5090
149
- krons-0.2.2.dist-info/METADATA,sha256=X1gYH9GGL-V2GhH3_iy96GzRZOCwO-HKt7NpNrzmgcg,2527
150
- krons-0.2.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
151
- krons-0.2.2.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
152
- krons-0.2.2.dist-info/RECORD,,
148
+ krons-0.2.4.dist-info/METADATA,sha256=Gx3-BD7Bbtp9hwlcgThGcWABG9DlklrBV9z8N3kUQ7Q,2527
149
+ krons-0.2.4.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
150
+ krons-0.2.4.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
151
+ krons-0.2.4.dist-info/RECORD,,
krons/core/base/log.py DELETED
@@ -1,32 +0,0 @@
1
- # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
- # SPDX-License-Identifier: Apache-2.0
3
-
4
- """Logging configuration for Session message persistence.
5
-
6
- Provides DataLoggerConfig for configuring automatic message dumps.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- from pathlib import Path
12
- from typing import Literal
13
-
14
- from pydantic import Field
15
-
16
- from krons.core.types import HashableModel
17
-
18
- __all__ = ("DataLoggerConfig",)
19
-
20
-
21
- class DataLoggerConfig(HashableModel):
22
- """Configuration for Session message persistence.
23
-
24
- Attributes:
25
- persist_dir: Directory for dump files.
26
- extension: Output format (.json array or .jsonl newline-delimited).
27
- auto_save_on_exit: Register atexit handler on Session creation.
28
- """
29
-
30
- persist_dir: str | Path = Field(default="./logs")
31
- extension: Literal[".json", ".jsonl"] = Field(default=".jsonl")
32
- auto_save_on_exit: bool = Field(default=True)
File without changes