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.
- krons/agent/message/common.py +1 -1
- krons/agent/message/prepare_msg.py +1 -2
- krons/core/base/__init__.py +0 -4
- krons/core/base/flow.py +7 -0
- krons/core/base/node.py +27 -26
- krons/core/specs/adapters/_utils.py +43 -1
- krons/core/specs/catalog/_content.py +6 -0
- krons/core/types/db_types.py +103 -18
- krons/session/session.py +56 -49
- {krons-0.2.2.dist-info → krons-0.2.4.dist-info}/METADATA +1 -1
- {krons-0.2.2.dist-info → krons-0.2.4.dist-info}/RECORD +13 -14
- krons/core/base/log.py +0 -32
- {krons-0.2.2.dist-info → krons-0.2.4.dist-info}/WHEEL +0 -0
- {krons-0.2.2.dist-info → krons-0.2.4.dist-info}/licenses/LICENSE +0 -0
krons/agent/message/common.py
CHANGED
|
@@ -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
|
|
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
|
|
krons/core/base/__init__.py
CHANGED
|
@@ -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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
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(
|
krons/core/types/db_types.py
CHANGED
|
@@ -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
|
-
|
|
240
|
-
|
|
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
|
-
|
|
64
|
+
|
|
65
|
+
# Logging configuration
|
|
66
|
+
log_persist_dir: str | Path | None = Field(
|
|
67
67
|
default=None,
|
|
68
|
-
description="
|
|
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(
|
|
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.
|
|
100
|
-
and self.config.
|
|
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
|
|
410
|
-
"""Sync dump
|
|
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
|
|
433
|
+
clear: Clear communications after dump (default False).
|
|
414
434
|
|
|
415
435
|
Returns:
|
|
416
|
-
Path to
|
|
436
|
+
Path to session file, or None if logging disabled or empty.
|
|
417
437
|
"""
|
|
418
|
-
from krons.utils import create_path, json_dumpb
|
|
438
|
+
from krons.utils import create_path, json_dumpb
|
|
419
439
|
from krons.utils.concurrency import run_async
|
|
420
440
|
|
|
421
|
-
if self.config.
|
|
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=
|
|
448
|
+
directory=self.config.log_persist_dir,
|
|
430
449
|
filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
|
|
431
|
-
extension=
|
|
450
|
+
extension=".json",
|
|
432
451
|
timestamp=True,
|
|
433
452
|
file_exist_ok=True,
|
|
434
453
|
)
|
|
435
454
|
)
|
|
436
455
|
|
|
437
|
-
|
|
438
|
-
|
|
456
|
+
data = json_dumpb(self.to_dict(mode="json"), safe_fallback=True)
|
|
439
457
|
std_path = Path(filepath)
|
|
440
|
-
|
|
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.
|
|
461
|
+
self.communications.clear()
|
|
450
462
|
|
|
451
463
|
return std_path
|
|
452
464
|
|
|
453
|
-
async def
|
|
454
|
-
"""Async dump
|
|
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
|
|
472
|
+
clear: Clear communications after dump (default False).
|
|
458
473
|
|
|
459
474
|
Returns:
|
|
460
|
-
Path to
|
|
475
|
+
Path to session file, or None if logging disabled or empty.
|
|
461
476
|
"""
|
|
462
|
-
from krons.utils import create_path, json_dumpb
|
|
477
|
+
from krons.utils import create_path, json_dumpb
|
|
463
478
|
|
|
464
|
-
if self.config.
|
|
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=
|
|
486
|
+
directory=self.config.log_persist_dir,
|
|
474
487
|
filename=f"session_{str(self.id)[:8]}_{self._dump_count}",
|
|
475
|
-
extension=
|
|
488
|
+
extension=".json",
|
|
476
489
|
timestamp=True,
|
|
477
490
|
file_exist_ok=True,
|
|
478
491
|
)
|
|
479
492
|
|
|
480
|
-
|
|
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.
|
|
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
|
|
502
|
+
"""atexit callback. Dumps session synchronously. Errors are suppressed."""
|
|
496
503
|
if len(self.messages) > 0:
|
|
497
504
|
try:
|
|
498
|
-
self.
|
|
505
|
+
self.dump(clear=False)
|
|
499
506
|
except Exception:
|
|
500
507
|
pass # Silent failure during interpreter shutdown
|
|
501
508
|
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
45
|
+
krons/core/base/flow.py,sha256=bFG5x7PEbWApsD24GTgpD5VsedLO6364scaAxvYw6LM,12561
|
|
46
46
|
krons/core/base/graph.py,sha256=FiTMArxuz6At3oYPEoamES3FtBM-AM6QnA7EQ8G18mo,15972
|
|
47
|
-
krons/core/base/
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
150
|
-
krons-0.2.
|
|
151
|
-
krons-0.2.
|
|
152
|
-
krons-0.2.
|
|
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
|
|
File without changes
|