krons 0.2.1__py3-none-any.whl → 0.2.3__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.
krons/core/__init__.py CHANGED
@@ -21,6 +21,7 @@ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
21
21
  "PERSISTABLE_NODE_REGISTRY": ("krons.core.base", "PERSISTABLE_NODE_REGISTRY"),
22
22
  # Classes
23
23
  "Broadcaster": ("krons.core.base", "Broadcaster"),
24
+ "DataLoggerConfig": ("krons.core.base", "DataLoggerConfig"),
24
25
  "Edge": ("krons.core.base", "Edge"),
25
26
  "EdgeCondition": ("krons.core.base", "EdgeCondition"),
26
27
  "Element": ("krons.core.base", "Element"),
@@ -75,6 +76,7 @@ if TYPE_CHECKING:
75
76
  NODE_REGISTRY,
76
77
  PERSISTABLE_NODE_REGISTRY,
77
78
  Broadcaster,
79
+ DataLoggerConfig,
78
80
  Edge,
79
81
  EdgeCondition,
80
82
  Element,
@@ -103,6 +105,7 @@ __all__ = [
103
105
  "PERSISTABLE_NODE_REGISTRY",
104
106
  # classes
105
107
  "Broadcaster",
108
+ "DataLoggerConfig",
106
109
  "Edge",
107
110
  "EdgeCondition",
108
111
  "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/session/session.py CHANGED
@@ -9,12 +9,14 @@ Branch is a named message progression with capability/resource access control.
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import atexit
12
13
  import contextlib
13
14
  from collections.abc import AsyncGenerator, Iterable
15
+ from pathlib import Path
14
16
  from typing import Any, Literal
15
17
  from uuid import UUID
16
18
 
17
- from pydantic import Field, model_validator
19
+ from pydantic import Field, PrivateAttr, field_serializer, model_validator
18
20
 
19
21
  from krons.core import Element, Flow, Pile, Progression
20
22
  from krons.core.types import HashableModel, Unset, UnsetType, not_sentinel
@@ -60,17 +62,40 @@ class SessionConfig(HashableModel):
60
62
  default_parse_model: str | None = None
61
63
  auto_create_default_branch: bool = True
62
64
 
65
+ # Logging configuration
66
+ log_persist_dir: str | Path | None = Field(
67
+ default=None,
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.",
73
+ )
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
+
63
80
 
64
81
  class Session(Element):
65
82
  user: str | None = None
66
83
  communications: Flow[Message, Branch] = Field(
67
84
  default_factory=lambda: Flow(item_type=Message)
68
85
  )
69
- resources: ResourceRegistry = Field(default_factory=ResourceRegistry)
70
- operations: OperationRegistry = Field(default_factory=OperationRegistry)
86
+ resources: ResourceRegistry = Field(default_factory=ResourceRegistry, exclude=True)
87
+ operations: OperationRegistry = Field(default_factory=OperationRegistry, exclude=True)
71
88
  config: SessionConfig = Field(default_factory=SessionConfig)
72
89
  default_branch_id: UUID | None = None
73
90
 
91
+ _registered_atexit: bool = PrivateAttr(default=False)
92
+ _dump_count: int = PrivateAttr(default=0)
93
+
94
+ @field_serializer("communications")
95
+ def _serialize_communications(self, flow: Flow) -> dict:
96
+ """Use Flow's custom to_dict for proper nested serialization."""
97
+ return flow.to_dict(mode="json")
98
+
74
99
  @model_validator(mode="after")
75
100
  def _validate_default_branch(self) -> Session:
76
101
  """Auto-create default branch and register built-in operations."""
@@ -83,6 +108,15 @@ class Session(Element):
83
108
  )
84
109
  self.set_default_branch(default_branch_name)
85
110
 
111
+ # Register atexit handler if configured
112
+ if (
113
+ self.config.logging_enabled
114
+ and self.config.log_auto_save_on_exit
115
+ and not self._registered_atexit
116
+ ):
117
+ atexit.register(self._save_at_exit)
118
+ self._registered_atexit = True
119
+
86
120
  # Register built-in operations (lazy import avoids circular)
87
121
  from krons.agent.operations import (
88
122
  generate,
@@ -386,6 +420,90 @@ class Session(Element):
386
420
  async for result in handler(params, ctx):
387
421
  yield result
388
422
 
423
+ def dump(self, clear: bool = False) -> Path | None:
424
+ """Sync dump entire session state for replay.
425
+
426
+ Serializes session (messages, branches, config) to JSON.
427
+ Resources and operations are excluded (re-register on restore).
428
+ To restore: Session.from_dict(data), then re-register resources.
429
+
430
+ Args:
431
+ clear: Clear communications after dump (default False).
432
+
433
+ Returns:
434
+ Path to session file, or None if logging disabled or empty.
435
+ """
436
+ from krons.utils import create_path, json_dumpb
437
+ from krons.utils.concurrency import run_async
438
+
439
+ if not self.config.logging_enabled or len(self.messages) == 0:
440
+ return None
441
+
442
+ self._dump_count += 1
443
+
444
+ filepath = run_async(
445
+ create_path(
446
+ directory=self.config.log_persist_dir,
447
+ filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
448
+ extension=".json",
449
+ timestamp=True,
450
+ file_exist_ok=True,
451
+ )
452
+ )
453
+
454
+ data = json_dumpb(self.to_dict(mode="json"), safe_fallback=True)
455
+ std_path = Path(filepath)
456
+ std_path.write_bytes(data)
457
+
458
+ if clear:
459
+ self.communications.clear()
460
+
461
+ return std_path
462
+
463
+ async def adump(self, clear: bool = False) -> Path | None:
464
+ """Async dump entire session state for replay.
465
+
466
+ Serializes the full session (messages, branches, config) to JSON.
467
+ To restore: Session.from_dict(data), then re-register resources.
468
+
469
+ Args:
470
+ clear: Clear communications after dump (default False).
471
+
472
+ Returns:
473
+ Path to session file, or None if logging disabled or empty.
474
+ """
475
+ from krons.utils import create_path, json_dumpb
476
+
477
+ if not self.config.logging_enabled or len(self.messages) == 0:
478
+ return None
479
+
480
+ async with self.messages:
481
+ self._dump_count += 1
482
+
483
+ filepath = await create_path(
484
+ directory=self.config.log_persist_dir,
485
+ filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
486
+ extension=".json",
487
+ timestamp=True,
488
+ file_exist_ok=True,
489
+ )
490
+
491
+ data = json_dumpb(self.to_dict(mode="json"), safe_fallback=True)
492
+ await filepath.write_bytes(data)
493
+
494
+ if clear:
495
+ self.communications.clear()
496
+
497
+ return Path(filepath)
498
+
499
+ def _save_at_exit(self) -> None:
500
+ """atexit callback. Dumps session synchronously. Errors are suppressed."""
501
+ if len(self.messages) > 0:
502
+ try:
503
+ self.dump(clear=False)
504
+ except Exception:
505
+ pass # Silent failure during interpreter shutdown
506
+
389
507
  def _resolve_branch(self, branch: Branch | UUID | str | None) -> Branch:
390
508
  """Resolve to Branch, falling back to default. Raises if neither available."""
391
509
  if branch is not None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: krons
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
@@ -36,13 +36,13 @@ krons/agent/third_party/anthropic_models.py,sha256=rd2Skh_Xx1S2G1JXi2YFb1j6SHm62
36
36
  krons/agent/third_party/claude_code.py,sha256=NBYMsdPheb0PqGgTO2_aUAR2XoGDsPUwsB7Nd9PjipA,23758
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
- krons/core/__init__.py,sha256=HG0wBaNCbH3xGDRNX3657g-hZo4Isu2MKaXeUXmXqa0,3496
39
+ krons/core/__init__.py,sha256=TKi3toVRYybyjUwUJlng--pfWzWihUWQBCNh0UTfTSQ,3611
40
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
47
  krons/core/base/node.py,sha256=0_-R6uNe6-LF3adQRZowSIICdy6ep3cQIQRdF0oosoQ,34699
48
48
  krons/core/base/pile.py,sha256=ZKvnX3BjMsrH989TLNZII1dDWv4yPhSRzLKXx2yzyKQ,21108
@@ -86,7 +86,7 @@ krons/session/constraints.py,sha256=DvmLKyf5eYUX2nxCDcco1-uDipGjiIJwoUxWVtX2w84,
86
86
  krons/session/exchange.py,sha256=QChEqK5NFZJpHWxPrXd8LkGBCFg6LNONd1ZEy0MufpI,9165
87
87
  krons/session/message.py,sha256=mtZ-PYCCoyWLy7BSB5TL5a0hIt0aQX2zdckOCIl6NZ0,2007
88
88
  krons/session/registry.py,sha256=13pPKmDZJaj9q27oUOdtH6-jrHP_HDFF8lCdcNF--EE,999
89
- krons/session/session.py,sha256=bBK6rYPmYu5YcmU-oWoJL9pU8pdzr9Y4Z5wcIIiuirw,13221
89
+ krons/session/session.py,sha256=2dykFkdK0DUu3kdv1v8RIevtuD3P50w_0vNIE3qs3D0,17204
90
90
  krons/utils/__init__.py,sha256=V-jTKULdofjJXxcEHygefKV_v6XE4H3poCdxPLjvf_4,1718
91
91
  krons/utils/_function_arg_parser.py,sha256=H5JVLBVY8W9ZNkZ4_YVtVVY1rFYvnIRhSrAhKEdj2Qc,3479
92
92
  krons/utils/_hash.py,sha256=W2Ma9v8-INPaGkur7GTtbF8KwuXSJNSwk8DCNPRvx8Q,6859
@@ -145,7 +145,7 @@ krons/work/rules/common/mapping.py,sha256=Loq54MNEtwpnHN0aypTjFOqwoOKLEysddHh-JE
145
145
  krons/work/rules/common/model.py,sha256=xmM6coEThf_fgIiqJiyDgvdfib_FpVeY6LgWPVcWSwU,3026
146
146
  krons/work/rules/common/number.py,sha256=cCukgMSpQu5RdYK5rXAUyop9qXgDRfLCioMvE8kIzHg,3162
147
147
  krons/work/rules/common/string.py,sha256=zHp_OLh0FL4PvmSlyDTEzb2I97-DBSEyI2zcMo10voA,5090
148
- krons-0.2.1.dist-info/METADATA,sha256=O9pmDvpEfTq1HydsJRGUwTA6IPn_veB57Em2udfOO9g,2527
149
- krons-0.2.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
150
- krons-0.2.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
151
- krons-0.2.1.dist-info/RECORD,,
148
+ krons-0.2.3.dist-info/METADATA,sha256=5VYw8_rlpQ5Q9W8EGwy8XZ19F--f_URzcmVAiG9WIqY,2527
149
+ krons-0.2.3.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
150
+ krons-0.2.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
151
+ krons-0.2.3.dist-info/RECORD,,
File without changes