krons 0.1.1__py3-none-any.whl → 0.2.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 (142) hide show
  1. krons/__init__.py +49 -0
  2. krons/agent/__init__.py +144 -0
  3. krons/agent/mcps/__init__.py +14 -0
  4. krons/agent/mcps/loader.py +287 -0
  5. krons/agent/mcps/wrapper.py +799 -0
  6. krons/agent/message/__init__.py +20 -0
  7. krons/agent/message/action.py +69 -0
  8. krons/agent/message/assistant.py +52 -0
  9. krons/agent/message/common.py +49 -0
  10. krons/agent/message/instruction.py +130 -0
  11. krons/agent/message/prepare_msg.py +187 -0
  12. krons/agent/message/role.py +53 -0
  13. krons/agent/message/system.py +53 -0
  14. krons/agent/operations/__init__.py +82 -0
  15. krons/agent/operations/act.py +100 -0
  16. krons/agent/operations/generate.py +145 -0
  17. krons/agent/operations/llm_reparse.py +89 -0
  18. krons/agent/operations/operate.py +247 -0
  19. krons/agent/operations/parse.py +243 -0
  20. krons/agent/operations/react.py +286 -0
  21. krons/agent/operations/specs.py +235 -0
  22. krons/agent/operations/structure.py +151 -0
  23. krons/agent/operations/utils.py +79 -0
  24. krons/agent/providers/__init__.py +17 -0
  25. krons/agent/providers/anthropic_messages.py +146 -0
  26. krons/agent/providers/claude_code.py +276 -0
  27. krons/agent/providers/gemini.py +268 -0
  28. krons/agent/providers/match.py +75 -0
  29. krons/agent/providers/oai_chat.py +174 -0
  30. krons/agent/third_party/__init__.py +2 -0
  31. krons/agent/third_party/anthropic_models.py +154 -0
  32. krons/agent/third_party/claude_code.py +682 -0
  33. krons/agent/third_party/gemini_models.py +508 -0
  34. krons/agent/third_party/openai_models.py +295 -0
  35. krons/agent/tool.py +291 -0
  36. krons/core/__init__.py +56 -74
  37. krons/core/base/__init__.py +121 -0
  38. krons/core/{broadcaster.py → base/broadcaster.py} +7 -3
  39. krons/core/{element.py → base/element.py} +13 -5
  40. krons/core/{event.py → base/event.py} +39 -6
  41. krons/core/{eventbus.py → base/eventbus.py} +3 -1
  42. krons/core/{flow.py → base/flow.py} +11 -4
  43. krons/core/{graph.py → base/graph.py} +24 -8
  44. krons/core/{node.py → base/node.py} +44 -19
  45. krons/core/{pile.py → base/pile.py} +22 -8
  46. krons/core/{processor.py → base/processor.py} +21 -7
  47. krons/core/{progression.py → base/progression.py} +3 -1
  48. krons/{specs → core/specs}/__init__.py +0 -5
  49. krons/{specs → core/specs}/adapters/dataclass_field.py +16 -8
  50. krons/{specs → core/specs}/adapters/pydantic_adapter.py +11 -5
  51. krons/{specs → core/specs}/adapters/sql_ddl.py +14 -8
  52. krons/{specs → core/specs}/catalog/__init__.py +2 -2
  53. krons/{specs → core/specs}/catalog/_audit.py +2 -2
  54. krons/{specs → core/specs}/catalog/_common.py +2 -2
  55. krons/{specs → core/specs}/catalog/_content.py +4 -4
  56. krons/{specs → core/specs}/catalog/_enforcement.py +3 -3
  57. krons/{specs → core/specs}/factory.py +5 -5
  58. krons/{specs → core/specs}/operable.py +8 -2
  59. krons/{specs → core/specs}/protocol.py +4 -2
  60. krons/{specs → core/specs}/spec.py +23 -11
  61. krons/{types → core/types}/base.py +4 -2
  62. krons/{types → core/types}/db_types.py +2 -2
  63. krons/errors.py +13 -13
  64. krons/protocols.py +9 -4
  65. krons/resource/__init__.py +89 -0
  66. krons/{services → resource}/backend.py +48 -22
  67. krons/{services → resource}/endpoint.py +28 -14
  68. krons/{services → resource}/hook.py +20 -7
  69. krons/{services → resource}/imodel.py +46 -28
  70. krons/{services → resource}/registry.py +26 -24
  71. krons/{services → resource}/utilities/rate_limited_executor.py +7 -3
  72. krons/{services → resource}/utilities/rate_limiter.py +3 -1
  73. krons/{services → resource}/utilities/resilience.py +15 -5
  74. krons/resource/utilities/token_calculator.py +185 -0
  75. krons/session/__init__.py +12 -17
  76. krons/session/constraints.py +70 -0
  77. krons/session/exchange.py +11 -3
  78. krons/session/message.py +3 -1
  79. krons/session/registry.py +35 -0
  80. krons/session/session.py +165 -174
  81. krons/utils/__init__.py +45 -0
  82. krons/utils/_function_arg_parser.py +99 -0
  83. krons/utils/_pythonic_function_call.py +249 -0
  84. krons/utils/_to_list.py +9 -3
  85. krons/utils/_utils.py +6 -2
  86. krons/utils/concurrency/_async_call.py +4 -2
  87. krons/utils/concurrency/_errors.py +3 -1
  88. krons/utils/concurrency/_patterns.py +3 -1
  89. krons/utils/concurrency/_resource_tracker.py +6 -2
  90. krons/utils/display.py +257 -0
  91. krons/utils/fuzzy/__init__.py +6 -1
  92. krons/utils/fuzzy/_fuzzy_match.py +14 -8
  93. krons/utils/fuzzy/_string_similarity.py +3 -1
  94. krons/utils/fuzzy/_to_dict.py +3 -1
  95. krons/utils/schemas/__init__.py +26 -0
  96. krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
  97. krons/utils/schemas/_formatter.py +72 -0
  98. krons/utils/schemas/_minimal_yaml.py +151 -0
  99. krons/utils/schemas/_typescript.py +153 -0
  100. krons/utils/validators/__init__.py +3 -0
  101. krons/utils/validators/_validate_image_url.py +56 -0
  102. krons/work/__init__.py +126 -0
  103. krons/work/engine.py +333 -0
  104. krons/work/form.py +305 -0
  105. krons/{operations → work/operations}/__init__.py +7 -4
  106. krons/{operations → work/operations}/builder.py +1 -1
  107. krons/{enforcement → work/operations}/context.py +36 -5
  108. krons/{operations → work/operations}/flow.py +13 -5
  109. krons/{operations → work/operations}/node.py +45 -43
  110. krons/work/operations/registry.py +103 -0
  111. krons/{specs → work}/phrase.py +130 -13
  112. krons/{enforcement → work}/policy.py +3 -3
  113. krons/work/report.py +268 -0
  114. krons/work/rules/__init__.py +47 -0
  115. krons/{enforcement → work/rules}/common/boolean.py +3 -1
  116. krons/{enforcement → work/rules}/common/choice.py +9 -3
  117. krons/{enforcement → work/rules}/common/number.py +3 -1
  118. krons/{enforcement → work/rules}/common/string.py +9 -3
  119. krons/{enforcement → work/rules}/rule.py +1 -1
  120. krons/{enforcement → work/rules}/validator.py +20 -5
  121. krons/{enforcement → work}/service.py +16 -7
  122. krons/work/worker.py +266 -0
  123. {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/METADATA +15 -1
  124. krons-0.2.0.dist-info/RECORD +154 -0
  125. krons/enforcement/__init__.py +0 -57
  126. krons/operations/registry.py +0 -92
  127. krons/services/__init__.py +0 -81
  128. krons-0.1.1.dist-info/RECORD +0 -101
  129. /krons/{specs → core/specs}/adapters/__init__.py +0 -0
  130. /krons/{specs → core/specs}/adapters/_utils.py +0 -0
  131. /krons/{specs → core/specs}/adapters/factory.py +0 -0
  132. /krons/{types → core/types}/__init__.py +0 -0
  133. /krons/{types → core/types}/_sentinel.py +0 -0
  134. /krons/{types → core/types}/identity.py +0 -0
  135. /krons/{services → resource}/utilities/__init__.py +0 -0
  136. /krons/{services → resource}/utilities/header_factory.py +0 -0
  137. /krons/{enforcement → work/rules}/common/__init__.py +0 -0
  138. /krons/{enforcement → work/rules}/common/mapping.py +0 -0
  139. /krons/{enforcement → work/rules}/common/model.py +0 -0
  140. /krons/{enforcement → work/rules}/registry.py +0 -0
  141. {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
  142. {krons-0.1.1.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,185 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ import logging
5
+ from collections.abc import Callable
6
+ from typing import cast
7
+
8
+ import tiktoken
9
+
10
+ from krons.errors import KronsError
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class TokenCalculationError(KronsError):
16
+ """Raised when token calculation fails due to encoding/model errors."""
17
+
18
+ default_message = "Token calculation failed"
19
+ default_retryable = False
20
+
21
+
22
+ def get_encoding_name(value: str | None) -> str:
23
+ """Get encoding name for model, with fallback chain.
24
+
25
+ Returns:
26
+ Encoding name (defaults to o200k_base if model/encoding not found or None)
27
+ """
28
+ if value is None:
29
+ return "o200k_base"
30
+
31
+ try:
32
+ enc = tiktoken.encoding_for_model(value)
33
+ return enc.name
34
+ except KeyError:
35
+ # Not a known model name, try as encoding name
36
+ try:
37
+ tiktoken.get_encoding(value)
38
+ return value
39
+ except Exception as e:
40
+ logger.warning(
41
+ f"Unknown model/encoding '{value}', falling back to o200k_base: {e}"
42
+ )
43
+ return "o200k_base"
44
+
45
+
46
+ class TokenCalculator:
47
+ @staticmethod
48
+ def calculate_message_tokens(messages: list[dict], /, **kwargs) -> int:
49
+ model = kwargs.get("model", "gpt-4o")
50
+ image_token_cost = kwargs.get("image_token_cost", 500)
51
+ tokenizer = tiktoken.get_encoding(get_encoding_name(model)).encode
52
+
53
+ num_tokens = 0
54
+ for msg in messages:
55
+ num_tokens += 4
56
+ _c = msg.get("content")
57
+ num_tokens += TokenCalculator._calculate_chatitem(
58
+ _c,
59
+ tokenizer=tokenizer,
60
+ model_name=model,
61
+ image_token_cost=image_token_cost,
62
+ )
63
+ return num_tokens # buffer for chat
64
+
65
+ @staticmethod
66
+ def calculate_embed_token(inputs: list[str], /, **kwargs) -> int:
67
+ if not inputs:
68
+ raise ValueError("inputs must be a non-empty list of strings")
69
+
70
+ try:
71
+ tokenizer = tiktoken.get_encoding(
72
+ get_encoding_name(kwargs.get("model", "text-embedding-3-small"))
73
+ ).encode
74
+
75
+ return sum(
76
+ TokenCalculator._calculate_embed_item(i, tokenizer=tokenizer)
77
+ for i in inputs
78
+ )
79
+ except TokenCalculationError:
80
+ # Re-raise from nested calls
81
+ raise
82
+ except Exception as e:
83
+ logger.error(f"Failed to calculate embed tokens: {e}", exc_info=True)
84
+ raise TokenCalculationError(f"Embed token calculation failed: {e}") from e
85
+
86
+ @staticmethod
87
+ def tokenize(
88
+ s_: str | None = None,
89
+ /,
90
+ encoding_name: str | None = None,
91
+ tokenizer: Callable | None = None,
92
+ decoder: Callable | None = None,
93
+ return_tokens: bool = False,
94
+ return_decoded: bool = False,
95
+ ) -> int | list[int] | tuple[int, str]:
96
+ if not s_:
97
+ return 0
98
+
99
+ if not callable(tokenizer):
100
+ encoding_name = get_encoding_name(encoding_name)
101
+ tokenizer = tiktoken.get_encoding(encoding_name).encode
102
+ if not callable(decoder):
103
+ # Use encoding_name if available, otherwise fallback to default
104
+ decoder_encoding = encoding_name if encoding_name else "o200k_base"
105
+ decoder = tiktoken.get_encoding(decoder_encoding).decode
106
+
107
+ try:
108
+ if return_tokens:
109
+ if return_decoded:
110
+ a = tokenizer(s_)
111
+ return len(a), decoder(a)
112
+ return tokenizer(s_)
113
+ return len(tokenizer(s_))
114
+ except Exception as e:
115
+ # Actual encoding failure during tokenization - this is an error
116
+ logger.error(
117
+ f"Tokenization failed for input (len={len(s_) if s_ else 0}): {e}",
118
+ exc_info=True,
119
+ )
120
+ raise TokenCalculationError(f"Tokenization failed: {e}") from e
121
+
122
+ @staticmethod
123
+ def _calculate_chatitem(
124
+ i_, tokenizer: Callable, model_name: str, image_token_cost: int = 500
125
+ ) -> int:
126
+ try:
127
+ if isinstance(i_, str):
128
+ # tokenize returns int when return_tokens=False (default)
129
+ return cast(int, TokenCalculator.tokenize(i_, tokenizer=tokenizer))
130
+
131
+ if isinstance(i_, dict):
132
+ if "text" in i_:
133
+ return TokenCalculator._calculate_chatitem(
134
+ str(i_["text"]), tokenizer, model_name, image_token_cost
135
+ )
136
+ elif "image_url" in i_:
137
+ return image_token_cost
138
+
139
+ if isinstance(i_, list):
140
+ return sum(
141
+ TokenCalculator._calculate_chatitem(
142
+ x, tokenizer, model_name, image_token_cost
143
+ )
144
+ for x in i_
145
+ )
146
+
147
+ # Unknown type - return 0 is valid (no text content)
148
+ return 0
149
+ except TokenCalculationError:
150
+ # Re-raise tokenization errors from nested calls
151
+ raise
152
+ except Exception as e:
153
+ logger.error(
154
+ f"Failed to calculate chat item tokens (type={type(i_).__name__}): {e}",
155
+ exc_info=True,
156
+ )
157
+ raise TokenCalculationError(
158
+ f"Chat item token calculation failed: {e}"
159
+ ) from e
160
+
161
+ @staticmethod
162
+ def _calculate_embed_item(s_, tokenizer: Callable) -> int:
163
+ try:
164
+ if isinstance(s_, str):
165
+ # tokenize returns int when return_tokens=False (default)
166
+ return cast(int, TokenCalculator.tokenize(s_, tokenizer=tokenizer))
167
+
168
+ if isinstance(s_, list):
169
+ return sum(
170
+ TokenCalculator._calculate_embed_item(x, tokenizer) for x in s_
171
+ )
172
+
173
+ # Unknown type - return 0 is valid (no text content)
174
+ return 0
175
+ except TokenCalculationError:
176
+ # Re-raise tokenization errors from nested calls
177
+ raise
178
+ except Exception as e:
179
+ logger.error(
180
+ f"Failed to calculate embed item tokens (type={type(s_).__name__}): {e}",
181
+ exc_info=True,
182
+ )
183
+ raise TokenCalculationError(
184
+ f"Embed item token calculation failed: {e}"
185
+ ) from e
krons/session/__init__.py CHANGED
@@ -10,23 +10,19 @@ Core types:
10
10
  Exchange: Async message router between entity mailboxes.
11
11
 
12
12
  Validators (raise on failure):
13
- resource_must_exist_in_session
14
- resource_must_be_accessible_by_branch
15
- capabilities_must_be_subset_of_branch
16
- resolve_branch_exists_in_session
13
+ resource_must_exist
14
+ resource_must_be_accessible
15
+ capabilities_must_be_granted
17
16
  """
18
17
 
18
+ from .constraints import (
19
+ capabilities_must_be_granted,
20
+ resource_must_be_accessible,
21
+ resource_must_exist,
22
+ )
19
23
  from .exchange import Exchange
20
24
  from .message import Message
21
- from .session import (
22
- Branch,
23
- Session,
24
- SessionConfig,
25
- capabilities_must_be_subset_of_branch,
26
- resolve_branch_exists_in_session,
27
- resource_must_be_accessible_by_branch,
28
- resource_must_exist_in_session,
29
- )
25
+ from .session import Branch, Session, SessionConfig
30
26
 
31
27
  __all__ = (
32
28
  "Branch",
@@ -34,8 +30,7 @@ __all__ = (
34
30
  "Message",
35
31
  "Session",
36
32
  "SessionConfig",
37
- "capabilities_must_be_subset_of_branch",
38
- "resolve_branch_exists_in_session",
39
- "resource_must_be_accessible_by_branch",
40
- "resource_must_exist_in_session",
33
+ "capabilities_must_be_granted",
34
+ "resource_must_be_accessible",
35
+ "resource_must_exist",
41
36
  )
@@ -0,0 +1,70 @@
1
+ from krons.errors import AccessError, ConfigurationError, ExistsError, NotFoundError
2
+
3
+ __all__ = (
4
+ "resource_must_exist",
5
+ "resource_must_be_accessible",
6
+ "capabilities_must_be_granted",
7
+ "branch_name_must_be_unique",
8
+ "genai_model_must_be_configured",
9
+ )
10
+
11
+
12
+ def resource_must_exist(session, name: str):
13
+ """Validate resource exists in session. Raise NotFoundError if not."""
14
+ if not session.resources.has(name):
15
+ raise NotFoundError(
16
+ f"Service '{name}' not found in session services",
17
+ details={"available": session.resources.list_names()},
18
+ )
19
+
20
+
21
+ def resource_must_be_accessible(branch, name: str) -> None:
22
+ """Validate branch has resource access. Raise AccessError if not."""
23
+ if name not in branch.resources:
24
+ raise AccessError(
25
+ f"Branch '{branch.name}' has no access to resource '{name}'",
26
+ details={
27
+ "branch": branch.name,
28
+ "resource": name,
29
+ "available": list(branch.resources),
30
+ },
31
+ )
32
+
33
+
34
+ def capabilities_must_be_granted(branch, capabilities: set[str]) -> None:
35
+ """Validate branch has all capabilities. Raise AccessError listing missing."""
36
+ if not capabilities.issubset(branch.capabilities):
37
+ missing = capabilities - branch.capabilities
38
+ raise AccessError(
39
+ f"Branch '{branch.name}' missing capabilities: {missing}",
40
+ details={
41
+ "requested": sorted(capabilities),
42
+ "available": sorted(branch.capabilities),
43
+ },
44
+ )
45
+
46
+
47
+ def branch_name_must_be_unique(session, name: str) -> None:
48
+ try:
49
+ session.communications.get_progression(name)
50
+ # If we get here, the name exists - not unique
51
+ raise ExistsError(f"Branch with name '{name}' already exists")
52
+ except KeyError:
53
+ # KeyError means name not found - it's unique, which is good
54
+ pass
55
+
56
+
57
+ def genai_model_must_be_configured(session) -> None:
58
+ """Validate session has a default GenAI model configured.
59
+
60
+ Args:
61
+ session: Session to check
62
+
63
+ Raises:
64
+ ConfigurationError: If no default model configured
65
+ """
66
+ if session.default_gen_model is None:
67
+ raise ConfigurationError(
68
+ "Session has no default_gen_model configured",
69
+ details={"session_id": str(session.id)},
70
+ )
krons/session/exchange.py CHANGED
@@ -134,11 +134,17 @@ class Exchange(Element):
134
134
  except Exception:
135
135
  message_copy = message.model_copy()
136
136
  deliveries.append((other_id, message_copy))
137
- elif message.recipient is not None and message.recipient in self._owner_index:
137
+ elif (
138
+ message.recipient is not None
139
+ and message.recipient in self._owner_index
140
+ ):
138
141
  deliveries.append((message.recipient, message))
139
142
  if deliveries:
140
143
  await concurrency.gather(
141
- *[self._deliver_to(recipient_id, message) for recipient_id, message in deliveries],
144
+ *[
145
+ self._deliver_to(recipient_id, message)
146
+ for recipient_id, message in deliveries
147
+ ],
142
148
  return_exceptions=True,
143
149
  )
144
150
 
@@ -196,7 +202,9 @@ class Exchange(Element):
196
202
  if flow is None:
197
203
  raise ValueError(f"Sender {sender} not registered")
198
204
 
199
- message = Message(sender=sender, recipient=recipient, content=content, channel=channel)
205
+ message = Message(
206
+ sender=sender, recipient=recipient, content=content, channel=channel
207
+ )
200
208
  flow.add_item(message, progressions=OUTBOX)
201
209
  return message
202
210
 
krons/session/message.py CHANGED
@@ -27,7 +27,9 @@ class Message(Node):
27
27
  content: Any
28
28
  sender: UUID | None = None
29
29
  recipient: UUID | None = None
30
- channel: str | None = Field(None, description="Optional namespace for message grouping")
30
+ channel: str | None = Field(
31
+ None, description="Optional namespace for message grouping"
32
+ )
31
33
 
32
34
  @property
33
35
  def is_broadcast(self) -> bool:
@@ -0,0 +1,35 @@
1
+ from krons.core import Pile
2
+ from krons.core.types import ID
3
+
4
+ from .session import Session
5
+
6
+ SESSION_REGISTRY: Pile[Session] = Pile(item_type=Session, strict_type=True)
7
+
8
+
9
+ async def get_session(session_id: ID[Session]) -> Session:
10
+ if session_id not in SESSION_REGISTRY:
11
+ raise ValueError(f"Session with id {session_id} not found in registry.")
12
+ async with SESSION_REGISTRY:
13
+ return SESSION_REGISTRY[session_id]
14
+
15
+
16
+ async def create_session():
17
+ session = Session()
18
+ async with SESSION_REGISTRY:
19
+ SESSION_REGISTRY.add(session)
20
+
21
+
22
+ async def delete_session(session_id: ID[Session]):
23
+ if session_id not in SESSION_REGISTRY:
24
+ raise ValueError(f"Session with id {session_id} not found in registry.")
25
+ async with SESSION_REGISTRY:
26
+ SESSION_REGISTRY.remove(session_id)
27
+
28
+
29
+ async def list_sessions_ids() -> list[ID[Session]]:
30
+ return list(SESSION_REGISTRY.keys())
31
+
32
+
33
+ async def clear_sessions():
34
+ async with SESSION_REGISTRY:
35
+ SESSION_REGISTRY.clear()