krons 0.2.1__py3-none-any.whl → 0.2.2__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",
@@ -26,6 +26,8 @@ _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"),
29
31
  # node
30
32
  "NODE_REGISTRY": ("krons.core.base.node", "NODE_REGISTRY"),
31
33
  "PERSISTABLE_NODE_REGISTRY": ("krons.core.base.node", "PERSISTABLE_NODE_REGISTRY"),
@@ -77,6 +79,7 @@ if TYPE_CHECKING:
77
79
  from .eventbus import EventBus, Handler
78
80
  from .flow import Flow
79
81
  from .graph import Edge, EdgeCondition, Graph
82
+ from .log import DataLoggerConfig
80
83
  from .node import (
81
84
  NODE_REGISTRY,
82
85
  PERSISTABLE_NODE_REGISTRY,
@@ -97,6 +100,7 @@ __all__ = [
97
100
  "PERSISTABLE_NODE_REGISTRY",
98
101
  # classes
99
102
  "Broadcaster",
103
+ "DataLoggerConfig",
100
104
  "Edge",
101
105
  "EdgeCondition",
102
106
  "Element",
krons/core/base/log.py ADDED
@@ -0,0 +1,32 @@
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)
krons/session/session.py CHANGED
@@ -9,12 +9,16 @@ 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
17
+
18
+ from krons.core.base.log import DataLoggerConfig
15
19
  from uuid import UUID
16
20
 
17
- from pydantic import Field, model_validator
21
+ from pydantic import Field, PrivateAttr, model_validator
18
22
 
19
23
  from krons.core import Element, Flow, Pile, Progression
20
24
  from krons.core.types import HashableModel, Unset, UnsetType, not_sentinel
@@ -59,6 +63,10 @@ class SessionConfig(HashableModel):
59
63
  default_gen_model: str | None = None
60
64
  default_parse_model: str | None = None
61
65
  auto_create_default_branch: bool = True
66
+ log_config: DataLoggerConfig | None = Field(
67
+ default=None,
68
+ description="DataLoggerConfig for auto-logging (None = disabled)",
69
+ )
62
70
 
63
71
 
64
72
  class Session(Element):
@@ -71,6 +79,9 @@ class Session(Element):
71
79
  config: SessionConfig = Field(default_factory=SessionConfig)
72
80
  default_branch_id: UUID | None = None
73
81
 
82
+ _registered_atexit: bool = PrivateAttr(default=False)
83
+ _dump_count: int = PrivateAttr(default=0)
84
+
74
85
  @model_validator(mode="after")
75
86
  def _validate_default_branch(self) -> Session:
76
87
  """Auto-create default branch and register built-in operations."""
@@ -83,6 +94,15 @@ class Session(Element):
83
94
  )
84
95
  self.set_default_branch(default_branch_name)
85
96
 
97
+ # Register atexit handler if configured
98
+ if (
99
+ self.config.log_config is not None
100
+ and self.config.log_config.auto_save_on_exit
101
+ and not self._registered_atexit
102
+ ):
103
+ atexit.register(self._save_at_exit)
104
+ self._registered_atexit = True
105
+
86
106
  # Register built-in operations (lazy import avoids circular)
87
107
  from krons.agent.operations import (
88
108
  generate,
@@ -386,6 +406,99 @@ class Session(Element):
386
406
  async for result in handler(params, ctx):
387
407
  yield result
388
408
 
409
+ def dump_messages(self, clear: bool = False) -> Path | None:
410
+ """Sync dump all session messages to file.
411
+
412
+ Args:
413
+ clear: Clear messages after dump (default False).
414
+
415
+ Returns:
416
+ Path to written file, or None if no log_config or no messages.
417
+ """
418
+ from krons.utils import create_path, json_dumpb, json_lines_iter
419
+ from krons.utils.concurrency import run_async
420
+
421
+ if self.config.log_config is None or len(self.messages) == 0:
422
+ return None
423
+
424
+ cfg = self.config.log_config
425
+ self._dump_count += 1
426
+
427
+ filepath = run_async(
428
+ create_path(
429
+ directory=cfg.persist_dir,
430
+ filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
431
+ extension=cfg.extension,
432
+ timestamp=True,
433
+ file_exist_ok=True,
434
+ )
435
+ )
436
+
437
+ items = [msg.to_dict(mode="json") for msg in self.messages]
438
+
439
+ 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)
447
+
448
+ if clear:
449
+ self.communications.items.clear()
450
+
451
+ return std_path
452
+
453
+ async def adump_messages(self, clear: bool = False) -> Path | None:
454
+ """Async dump all session messages to file with lock protection.
455
+
456
+ Args:
457
+ clear: Clear messages after dump (default False).
458
+
459
+ Returns:
460
+ Path to written file, or None if no log_config or no messages.
461
+ """
462
+ from krons.utils import create_path, json_dumpb, json_lines_iter
463
+
464
+ if self.config.log_config is None or len(self.messages) == 0:
465
+ return None
466
+
467
+ cfg = self.config.log_config
468
+
469
+ async with self.messages:
470
+ self._dump_count += 1
471
+
472
+ filepath = await create_path(
473
+ directory=cfg.persist_dir,
474
+ filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
475
+ extension=cfg.extension,
476
+ timestamp=True,
477
+ file_exist_ok=True,
478
+ )
479
+
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)
488
+
489
+ if clear:
490
+ self.communications.items.clear()
491
+
492
+ return Path(filepath)
493
+
494
+ def _save_at_exit(self) -> None:
495
+ """atexit callback. Dumps messages synchronously. Errors are suppressed."""
496
+ if len(self.messages) > 0:
497
+ try:
498
+ self.dump_messages(clear=False)
499
+ except Exception:
500
+ pass # Silent failure during interpreter shutdown
501
+
389
502
  def _resolve_branch(self, branch: Branch | UUID | str | None) -> Branch:
390
503
  """Resolve to Branch, falling back to default. Raises if neither available."""
391
504
  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.2
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,14 +36,15 @@ 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
40
- krons/core/base/__init__.py,sha256=Edl_HH2F1l0SMHMzuEPU_IFjkigRR1vtqVXcPm-mr1o,3614
39
+ krons/core/__init__.py,sha256=TKi3toVRYybyjUwUJlng--pfWzWihUWQBCNh0UTfTSQ,3611
40
+ krons/core/base/__init__.py,sha256=emyypsuiFK0UzYsDeamD84ygXnO1TLx2A5mBxGTBidE,3755
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
45
  krons/core/base/flow.py,sha256=eqsYm8QO7bD5Y9VYnlpHFWpaUCY0rCeKGrDrFUyBVA4,12364
46
46
  krons/core/base/graph.py,sha256=FiTMArxuz6At3oYPEoamES3FtBM-AM6QnA7EQ8G18mo,15972
47
+ krons/core/base/log.py,sha256=SMJlruIG4YHk6Ls45jQ9SrPxFCcnypUmTGaWd90wiwM,923
47
48
  krons/core/base/node.py,sha256=0_-R6uNe6-LF3adQRZowSIICdy6ep3cQIQRdF0oosoQ,34699
48
49
  krons/core/base/pile.py,sha256=ZKvnX3BjMsrH989TLNZII1dDWv4yPhSRzLKXx2yzyKQ,21108
49
50
  krons/core/base/processor.py,sha256=GrdNZa5YD0tXle847ttnxuWe4Ego1MhULzWHmSz5RqQ,18768
@@ -86,7 +87,7 @@ krons/session/constraints.py,sha256=DvmLKyf5eYUX2nxCDcco1-uDipGjiIJwoUxWVtX2w84,
86
87
  krons/session/exchange.py,sha256=QChEqK5NFZJpHWxPrXd8LkGBCFg6LNONd1ZEy0MufpI,9165
87
88
  krons/session/message.py,sha256=mtZ-PYCCoyWLy7BSB5TL5a0hIt0aQX2zdckOCIl6NZ0,2007
88
89
  krons/session/registry.py,sha256=13pPKmDZJaj9q27oUOdtH6-jrHP_HDFF8lCdcNF--EE,999
89
- krons/session/session.py,sha256=bBK6rYPmYu5YcmU-oWoJL9pU8pdzr9Y4Z5wcIIiuirw,13221
90
+ krons/session/session.py,sha256=_y3-NEQxgBa51mRd6LiD7zTqJFikKCP4avLqMai6O08,16966
90
91
  krons/utils/__init__.py,sha256=V-jTKULdofjJXxcEHygefKV_v6XE4H3poCdxPLjvf_4,1718
91
92
  krons/utils/_function_arg_parser.py,sha256=H5JVLBVY8W9ZNkZ4_YVtVVY1rFYvnIRhSrAhKEdj2Qc,3479
92
93
  krons/utils/_hash.py,sha256=W2Ma9v8-INPaGkur7GTtbF8KwuXSJNSwk8DCNPRvx8Q,6859
@@ -145,7 +146,7 @@ krons/work/rules/common/mapping.py,sha256=Loq54MNEtwpnHN0aypTjFOqwoOKLEysddHh-JE
145
146
  krons/work/rules/common/model.py,sha256=xmM6coEThf_fgIiqJiyDgvdfib_FpVeY6LgWPVcWSwU,3026
146
147
  krons/work/rules/common/number.py,sha256=cCukgMSpQu5RdYK5rXAUyop9qXgDRfLCioMvE8kIzHg,3162
147
148
  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,,
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,,
File without changes