krons 0.1.0__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 (162) 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 +127 -0
  37. krons/core/base/__init__.py +121 -0
  38. {kronos/core → krons/core/base}/broadcaster.py +7 -3
  39. {kronos/core → krons/core/base}/element.py +15 -7
  40. {kronos/core → krons/core/base}/event.py +41 -8
  41. {kronos/core → krons/core/base}/eventbus.py +4 -2
  42. {kronos/core → krons/core/base}/flow.py +14 -7
  43. {kronos/core → krons/core/base}/graph.py +27 -11
  44. {kronos/core → krons/core/base}/node.py +47 -22
  45. {kronos/core → krons/core/base}/pile.py +26 -12
  46. {kronos/core → krons/core/base}/processor.py +23 -9
  47. {kronos/core → krons/core/base}/progression.py +5 -3
  48. {kronos → krons/core}/specs/__init__.py +0 -5
  49. {kronos → krons/core}/specs/adapters/dataclass_field.py +16 -8
  50. {kronos → krons/core}/specs/adapters/pydantic_adapter.py +11 -5
  51. {kronos → krons/core}/specs/adapters/sql_ddl.py +16 -10
  52. {kronos → krons/core}/specs/catalog/__init__.py +2 -2
  53. {kronos → krons/core}/specs/catalog/_audit.py +3 -3
  54. {kronos → krons/core}/specs/catalog/_common.py +2 -2
  55. {kronos → krons/core}/specs/catalog/_content.py +5 -5
  56. {kronos → krons/core}/specs/catalog/_enforcement.py +4 -4
  57. {kronos → krons/core}/specs/factory.py +7 -7
  58. {kronos → krons/core}/specs/operable.py +9 -3
  59. {kronos → krons/core}/specs/protocol.py +4 -2
  60. {kronos → krons/core}/specs/spec.py +25 -13
  61. {kronos → krons/core}/types/base.py +7 -5
  62. {kronos → krons/core}/types/db_types.py +2 -2
  63. {kronos → krons/core}/types/identity.py +1 -1
  64. {kronos → krons}/errors.py +13 -13
  65. {kronos → krons}/protocols.py +9 -4
  66. krons/resource/__init__.py +89 -0
  67. {kronos/services → krons/resource}/backend.py +50 -24
  68. {kronos/services → krons/resource}/endpoint.py +28 -14
  69. {kronos/services → krons/resource}/hook.py +22 -9
  70. {kronos/services → krons/resource}/imodel.py +50 -32
  71. {kronos/services → krons/resource}/registry.py +27 -25
  72. {kronos/services → krons/resource}/utilities/rate_limited_executor.py +10 -6
  73. {kronos/services → krons/resource}/utilities/rate_limiter.py +4 -2
  74. {kronos/services → krons/resource}/utilities/resilience.py +17 -7
  75. krons/resource/utilities/token_calculator.py +185 -0
  76. {kronos → krons}/session/__init__.py +12 -17
  77. krons/session/constraints.py +70 -0
  78. {kronos → krons}/session/exchange.py +14 -6
  79. {kronos → krons}/session/message.py +4 -2
  80. krons/session/registry.py +35 -0
  81. {kronos → krons}/session/session.py +165 -174
  82. krons/utils/__init__.py +85 -0
  83. krons/utils/_function_arg_parser.py +99 -0
  84. krons/utils/_pythonic_function_call.py +249 -0
  85. {kronos → krons}/utils/_to_list.py +9 -3
  86. {kronos → krons}/utils/_utils.py +9 -5
  87. {kronos → krons}/utils/concurrency/__init__.py +38 -38
  88. {kronos → krons}/utils/concurrency/_async_call.py +6 -4
  89. {kronos → krons}/utils/concurrency/_errors.py +3 -1
  90. {kronos → krons}/utils/concurrency/_patterns.py +3 -1
  91. {kronos → krons}/utils/concurrency/_resource_tracker.py +6 -2
  92. krons/utils/display.py +257 -0
  93. {kronos → krons}/utils/fuzzy/__init__.py +6 -1
  94. {kronos → krons}/utils/fuzzy/_fuzzy_match.py +14 -8
  95. {kronos → krons}/utils/fuzzy/_string_similarity.py +3 -1
  96. {kronos → krons}/utils/fuzzy/_to_dict.py +3 -1
  97. krons/utils/schemas/__init__.py +26 -0
  98. krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
  99. krons/utils/schemas/_formatter.py +72 -0
  100. krons/utils/schemas/_minimal_yaml.py +151 -0
  101. krons/utils/schemas/_typescript.py +153 -0
  102. {kronos → krons}/utils/sql/_sql_validation.py +1 -1
  103. krons/utils/validators/__init__.py +3 -0
  104. krons/utils/validators/_validate_image_url.py +56 -0
  105. krons/work/__init__.py +126 -0
  106. krons/work/engine.py +333 -0
  107. krons/work/form.py +305 -0
  108. {kronos → krons/work}/operations/__init__.py +7 -4
  109. {kronos → krons/work}/operations/builder.py +4 -4
  110. {kronos/enforcement → krons/work/operations}/context.py +37 -6
  111. {kronos → krons/work}/operations/flow.py +17 -9
  112. krons/work/operations/node.py +103 -0
  113. krons/work/operations/registry.py +103 -0
  114. {kronos/specs → krons/work}/phrase.py +131 -14
  115. {kronos/enforcement → krons/work}/policy.py +3 -3
  116. krons/work/report.py +268 -0
  117. krons/work/rules/__init__.py +47 -0
  118. {kronos/enforcement → krons/work/rules}/common/boolean.py +3 -1
  119. {kronos/enforcement → krons/work/rules}/common/choice.py +9 -3
  120. {kronos/enforcement → krons/work/rules}/common/number.py +3 -1
  121. {kronos/enforcement → krons/work/rules}/common/string.py +9 -3
  122. {kronos/enforcement → krons/work/rules}/rule.py +2 -2
  123. {kronos/enforcement → krons/work/rules}/validator.py +21 -6
  124. {kronos/enforcement → krons/work}/service.py +16 -7
  125. krons/work/worker.py +266 -0
  126. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/METADATA +19 -5
  127. krons-0.2.0.dist-info/RECORD +154 -0
  128. kronos/core/__init__.py +0 -145
  129. kronos/enforcement/__init__.py +0 -57
  130. kronos/operations/node.py +0 -101
  131. kronos/operations/registry.py +0 -92
  132. kronos/services/__init__.py +0 -81
  133. kronos/specs/adapters/__init__.py +0 -0
  134. kronos/utils/__init__.py +0 -40
  135. krons-0.1.0.dist-info/RECORD +0 -101
  136. {kronos → krons/core/specs/adapters}/__init__.py +0 -0
  137. {kronos → krons/core}/specs/adapters/_utils.py +0 -0
  138. {kronos → krons/core}/specs/adapters/factory.py +0 -0
  139. {kronos → krons/core}/types/__init__.py +0 -0
  140. {kronos → krons/core}/types/_sentinel.py +0 -0
  141. {kronos → krons}/py.typed +0 -0
  142. {kronos/services → krons/resource}/utilities/__init__.py +0 -0
  143. {kronos/services → krons/resource}/utilities/header_factory.py +0 -0
  144. {kronos → krons}/utils/_hash.py +0 -0
  145. {kronos → krons}/utils/_json_dump.py +0 -0
  146. {kronos → krons}/utils/_lazy_init.py +0 -0
  147. {kronos → krons}/utils/_to_num.py +0 -0
  148. {kronos → krons}/utils/concurrency/_cancel.py +0 -0
  149. {kronos → krons}/utils/concurrency/_primitives.py +0 -0
  150. {kronos → krons}/utils/concurrency/_priority_queue.py +0 -0
  151. {kronos → krons}/utils/concurrency/_run_async.py +0 -0
  152. {kronos → krons}/utils/concurrency/_task.py +0 -0
  153. {kronos → krons}/utils/concurrency/_utils.py +0 -0
  154. {kronos → krons}/utils/fuzzy/_extract_json.py +0 -0
  155. {kronos → krons}/utils/fuzzy/_fuzzy_json.py +0 -0
  156. {kronos → krons}/utils/sql/__init__.py +0 -0
  157. {kronos/enforcement → krons/work/rules}/common/__init__.py +0 -0
  158. {kronos/enforcement → krons/work/rules}/common/mapping.py +0 -0
  159. {kronos/enforcement → krons/work/rules}/common/model.py +0 -0
  160. {kronos/enforcement → krons/work/rules}/registry.py +0 -0
  161. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
  162. {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,11 +7,11 @@ from typing import TYPE_CHECKING, Any
7
7
 
8
8
  from pydantic import Field, field_serializer, field_validator
9
9
 
10
- from kronos.core import Element, Executor
11
- from kronos.protocols import Invocable, implements
12
- from kronos.utils.concurrency import sleep
10
+ from krons.core import Element, Executor
11
+ from krons.protocols import Invocable, implements
12
+ from krons.utils.concurrency import sleep
13
13
 
14
- from .backend import ServiceBackend
14
+ from .backend import ResourceBackend
15
15
  from .endpoint import Endpoint
16
16
  from .hook import HookRegistry
17
17
  from .utilities.rate_limited_executor import RateLimitedExecutor
@@ -26,14 +26,14 @@ __all__ = ("iModel",)
26
26
 
27
27
  @implements(Invocable)
28
28
  class iModel(Element): # noqa: N801
29
- """Unified service interface wrapping ServiceBackend with rate limiting and hooks.
29
+ """Unified resource interface wrapping ResourceBackend with rate limiting and hooks.
30
30
 
31
- Combines ServiceBackend (API abstraction) with optional:
31
+ Combines ResourceBackend (API abstraction) with optional:
32
32
  - Rate limiting: TokenBucket (simple) or Executor (event-driven)
33
33
  - Hook registry: Lifecycle callbacks at PreEventCreate/PreInvocation/PostInvocation
34
34
 
35
35
  Attributes:
36
- backend: ServiceBackend instance (e.g., Endpoint for HTTP APIs).
36
+ backend: ResourceBackend instance (e.g., Endpoint for HTTP APIs).
37
37
  rate_limiter: Optional TokenBucket for simple blocking rate limits.
38
38
  executor: Optional Executor for event-driven processing with rate limiting.
39
39
  hook_registry: Optional HookRegistry for invocation lifecycle callbacks.
@@ -43,9 +43,9 @@ class iModel(Element): # noqa: N801
43
43
  _EXECUTOR_POLL_TIMEOUT_ITERATIONS = 100
44
44
  _EXECUTOR_POLL_SLEEP_INTERVAL = 0.1
45
45
 
46
- backend: ServiceBackend | None = Field(
46
+ backend: ResourceBackend | None = Field(
47
47
  None,
48
- description="ServiceBackend instance (e.g., Endpoint)",
48
+ description="ResourceBackend instance (e.g., Endpoint)",
49
49
  )
50
50
 
51
51
  rate_limiter: TokenBucket | None = Field(
@@ -70,7 +70,7 @@ class iModel(Element): # noqa: N801
70
70
 
71
71
  def __init__(
72
72
  self,
73
- backend: ServiceBackend,
73
+ backend: ResourceBackend,
74
74
  rate_limiter: TokenBucket | None = None,
75
75
  executor: Executor | None = None,
76
76
  hook_registry: HookRegistry | None = None,
@@ -78,10 +78,10 @@ class iModel(Element): # noqa: N801
78
78
  capacity_refresh_time: float = 60,
79
79
  limit_requests: int | None = None,
80
80
  ):
81
- """Initialize iModel with ServiceBackend.
81
+ """Initialize iModel with ResourceBackend.
82
82
 
83
83
  Args:
84
- backend: ServiceBackend instance (required).
84
+ backend: ResourceBackend instance (required).
85
85
  rate_limiter: TokenBucket for simple blocking rate limits.
86
86
  executor: Executor for event-driven processing.
87
87
  hook_registry: HookRegistry for lifecycle callbacks.
@@ -114,21 +114,21 @@ class iModel(Element): # noqa: N801
114
114
 
115
115
  @property
116
116
  def name(self) -> str:
117
- """Service name from backend."""
117
+ """Resource name from backend."""
118
118
  if self.backend is None:
119
119
  raise RuntimeError("Backend not configured")
120
120
  return self.backend.name
121
121
 
122
122
  @property
123
123
  def version(self) -> str:
124
- """Service version from backend."""
124
+ """Resource version from backend."""
125
125
  if self.backend is None:
126
126
  raise RuntimeError("Backend not configured")
127
127
  return self.backend.version or ""
128
128
 
129
129
  @property
130
130
  def tags(self) -> set[str]:
131
- """Service tags from backend."""
131
+ """Resource tags from backend."""
132
132
  if self.backend is None:
133
133
  raise RuntimeError("Backend not configured")
134
134
  return self.backend.tags
@@ -181,7 +181,11 @@ class iModel(Element): # noqa: N801
181
181
  hook_phase=HookPhase.PreEventCreate,
182
182
  event_like=calling_type,
183
183
  registry=self.hook_registry,
184
- exit=(create_event_exit_hook if create_event_exit_hook is not None else False),
184
+ exit=(
185
+ create_event_exit_hook
186
+ if create_event_exit_hook is not None
187
+ else False
188
+ ),
185
189
  timeout=create_event_hook_timeout,
186
190
  streaming=False,
187
191
  params=create_event_hook_params or {},
@@ -194,11 +198,12 @@ class iModel(Element): # noqa: N801
194
198
  )
195
199
 
196
200
  payload = self.backend.create_payload(request=arguments)
201
+ # Handle backends that return (payload, headers) tuple
202
+ if isinstance(payload, tuple):
203
+ payload, _ = payload
197
204
  calling: Calling = calling_type(
198
205
  backend=self.backend,
199
206
  payload=payload,
200
- timeout=timeout,
201
- streaming=streaming,
202
207
  )
203
208
 
204
209
  if self.hook_registry is not None and self.hook_registry._can_handle(
@@ -206,7 +211,9 @@ class iModel(Element): # noqa: N801
206
211
  ):
207
212
  calling.create_pre_invoke_hook(
208
213
  hook_registry=self.hook_registry,
209
- exit_hook=(pre_invoke_exit_hook if pre_invoke_exit_hook is not None else False),
214
+ exit_hook=(
215
+ pre_invoke_exit_hook if pre_invoke_exit_hook is not None else False
216
+ ),
210
217
  hook_timeout=pre_invoke_hook_timeout,
211
218
  hook_params=pre_invoke_hook_params or {},
212
219
  )
@@ -216,7 +223,11 @@ class iModel(Element): # noqa: N801
216
223
  ):
217
224
  calling.create_post_invoke_hook(
218
225
  hook_registry=self.hook_registry,
219
- exit_hook=(post_invoke_exit_hook if post_invoke_exit_hook is not None else False),
226
+ exit_hook=(
227
+ post_invoke_exit_hook
228
+ if post_invoke_exit_hook is not None
229
+ else False
230
+ ),
220
231
  hook_timeout=post_invoke_hook_timeout,
221
232
  hook_params=post_invoke_hook_params or {},
222
233
  )
@@ -281,7 +292,8 @@ class iModel(Element): # noqa: N801
281
292
  # Poll for completion (fast backends see ~100-200% overhead, slow backends <10%)
282
293
  interval = poll_interval or self._EXECUTOR_POLL_SLEEP_INTERVAL
283
294
  timeout_seconds = poll_timeout or (
284
- self._EXECUTOR_POLL_TIMEOUT_ITERATIONS * self._EXECUTOR_POLL_SLEEP_INTERVAL
295
+ self._EXECUTOR_POLL_TIMEOUT_ITERATIONS
296
+ * self._EXECUTOR_POLL_SLEEP_INTERVAL
285
297
  )
286
298
  max_iterations = int(timeout_seconds / interval)
287
299
  ctr = 0
@@ -300,7 +312,9 @@ class iModel(Element): # noqa: N801
300
312
  f"Event aborted after 3 permission denials (rate limited): {calling.id}"
301
313
  )
302
314
  elif calling.execution.status.value == "failed":
303
- raise calling.execution.error or RuntimeError(f"Event failed: {calling.id}")
315
+ raise calling.execution.error or RuntimeError(
316
+ f"Event failed: {calling.id}"
317
+ )
304
318
 
305
319
  self._store_claude_code_session_id(calling)
306
320
  return calling
@@ -317,7 +331,7 @@ class iModel(Element): # noqa: N801
317
331
 
318
332
  def _store_claude_code_session_id(self, calling: Calling) -> None:
319
333
  """Extract and store Claude Code session_id for context continuation."""
320
- from kronos.types import is_sentinel
334
+ from krons.core.types import is_sentinel
321
335
 
322
336
  from .backend import NormalizedResponse
323
337
 
@@ -334,7 +348,7 @@ class iModel(Element): # noqa: N801
334
348
  self.provider_metadata["session_id"] = session_id
335
349
 
336
350
  @field_serializer("backend")
337
- def _serialize_backend(self, backend: ServiceBackend) -> dict[str, Any] | None:
351
+ def _serialize_backend(self, backend: ResourceBackend) -> dict[str, Any] | None:
338
352
  """Serialize backend to dict with kron_class for polymorphic restoration."""
339
353
  if backend is None:
340
354
  return None
@@ -384,24 +398,24 @@ class iModel(Element): # noqa: N801
384
398
 
385
399
  @field_validator("backend", mode="before")
386
400
  @classmethod
387
- def _deserialize_backend(cls, v: Any) -> ServiceBackend:
401
+ def _deserialize_backend(cls, v: Any) -> ResourceBackend:
388
402
  """Reconstruct backend via Element polymorphic deserialization."""
389
403
  if v is None:
390
404
  raise ValueError("backend is required")
391
405
 
392
- if isinstance(v, ServiceBackend):
406
+ if isinstance(v, ResourceBackend):
393
407
  return v
394
408
 
395
409
  if not isinstance(v, dict):
396
- raise ValueError("backend must be a dict or ServiceBackend instance")
410
+ raise ValueError("backend must be a dict or ResourceBackend instance")
397
411
 
398
- from kronos.core import Element
412
+ from krons.core import Element
399
413
 
400
414
  backend = Element.from_dict(v)
401
415
 
402
- if not isinstance(backend, ServiceBackend):
416
+ if not isinstance(backend, ResourceBackend):
403
417
  raise ValueError(
404
- f"Deserialized backend must be ServiceBackend subclass, got: {type(backend).__name__}"
418
+ f"Deserialized backend must be ResourceBackend subclass, got: {type(backend).__name__}"
405
419
  )
406
420
  return backend
407
421
 
@@ -420,9 +434,13 @@ class iModel(Element): # noqa: N801
420
434
 
421
435
  config = {**v}
422
436
  if "request_bucket" in config and isinstance(config["request_bucket"], dict):
423
- config["request_bucket"] = TokenBucket(RateLimitConfig(**config["request_bucket"]))
437
+ config["request_bucket"] = TokenBucket(
438
+ RateLimitConfig(**config["request_bucket"])
439
+ )
424
440
  if "token_bucket" in config and isinstance(config["token_bucket"], dict):
425
- config["token_bucket"] = TokenBucket(RateLimitConfig(**config["token_bucket"]))
441
+ config["token_bucket"] = TokenBucket(
442
+ RateLimitConfig(**config["token_bucket"])
443
+ )
426
444
 
427
445
  return RateLimitedExecutor(processor_config=config)
428
446
 
@@ -6,24 +6,24 @@ from __future__ import annotations
6
6
  from typing import Any
7
7
  from uuid import UUID
8
8
 
9
- from kronos.core import Pile
10
- from kronos.types import Undefined, UndefinedType, is_sentinel
9
+ from krons.core import Pile
10
+ from krons.core.types import Undefined, UndefinedType, is_sentinel
11
11
 
12
12
  from .imodel import iModel
13
13
 
14
- __all__ = ("ServiceRegistry",)
14
+ __all__ = ("ResourceRegistry",)
15
15
 
16
16
 
17
- class ServiceRegistry:
18
- """Service registry managing iModel instances with O(1) name-based lookup.
17
+ class ResourceRegistry:
18
+ """Resource registry managing iModel instances with O(1) name-based lookup.
19
19
 
20
20
  Provides type-safe storage via Pile[iModel] with name-based indexing.
21
- Services must have unique names; duplicates raise ValueError unless update=True.
21
+ Resources must have unique names; duplicates raise ValueError unless update=True.
22
22
 
23
23
  Example:
24
- >>> registry = ServiceRegistry()
24
+ >>> registry = ResourceRegistry()
25
25
  >>> registry.register(iModel(backend=my_endpoint))
26
- >>> model = registry.get("my_service")
26
+ >>> model = registry.get("my_resource")
27
27
  >>> tagged = registry.list_by_tag("api")
28
28
  """
29
29
 
@@ -39,17 +39,17 @@ class ServiceRegistry:
39
39
 
40
40
  Args:
41
41
  model: iModel instance to register.
42
- update: If True, replaces existing service with same name.
42
+ update: If True, replaces existing resource with same name.
43
43
 
44
44
  Returns:
45
- UUID of registered service.
45
+ UUID of registered resource.
46
46
 
47
47
  Raises:
48
- ValueError: If service name exists and update=False.
48
+ ValueError: If resource name exists and update=False.
49
49
  """
50
50
  if model.name in self._name_index:
51
51
  if not update:
52
- raise ValueError(f"Service '{model.name}' already registered")
52
+ raise ValueError(f"Resource '{model.name}' already registered")
53
53
  # Update: remove old, add new
54
54
  old_uid = self._name_index[model.name]
55
55
  self._pile.remove(old_uid)
@@ -60,15 +60,17 @@ class ServiceRegistry:
60
60
  return model.id
61
61
 
62
62
  def unregister(self, name: str) -> iModel:
63
- """Remove and return service by name. Raises KeyError if not found."""
63
+ """Remove and return resource by name. Raises KeyError if not found."""
64
64
  if name not in self._name_index:
65
- raise KeyError(f"Service '{name}' not found")
65
+ raise KeyError(f"Resource '{name}' not found")
66
66
 
67
67
  uid = self._name_index.pop(name)
68
68
  return self._pile.remove(uid)
69
69
 
70
- def get(self, name: str | UUID | iModel, default: Any | UndefinedType = Undefined) -> iModel:
71
- """Get service by name, UUID, or return iModel passthrough. Raises KeyError if not found."""
70
+ def get(
71
+ self, name: str | UUID | iModel, default: Any | UndefinedType = Undefined
72
+ ) -> iModel:
73
+ """Get resource by name, UUID, or return iModel passthrough. Raises KeyError if not found."""
72
74
  if isinstance(name, UUID):
73
75
  return self._pile[name]
74
76
  if isinstance(name, iModel):
@@ -76,40 +78,40 @@ class ServiceRegistry:
76
78
  if name not in self._name_index:
77
79
  if not is_sentinel(default):
78
80
  return default
79
- raise KeyError(f"Service '{name}' not found")
81
+ raise KeyError(f"Resource '{name}' not found")
80
82
 
81
83
  uid = self._name_index[name]
82
84
  return self._pile[uid]
83
85
 
84
86
  def has(self, name: str) -> bool:
85
- """Check if service exists."""
87
+ """Check if resource exists."""
86
88
  return name in self._name_index
87
89
 
88
90
  def list_names(self) -> list[str]:
89
- """List all registered service names."""
91
+ """List all registered resource names."""
90
92
  return list(self._name_index.keys())
91
93
 
92
94
  def list_by_tag(self, tag: str) -> Pile[iModel]:
93
- """Filter services by tag, returns Pile of matching iModels."""
95
+ """Filter resources by tag, returns Pile of matching iModels."""
94
96
  return self._pile[lambda m: tag in m.tags]
95
97
 
96
98
  def count(self) -> int:
97
- """Count registered services."""
99
+ """Count registered resources."""
98
100
  return len(self._pile)
99
101
 
100
102
  def clear(self) -> None:
101
- """Remove all registered services."""
103
+ """Remove all registered resources."""
102
104
  self._pile.clear()
103
105
  self._name_index.clear()
104
106
 
105
107
  def __len__(self) -> int:
106
- """Return number of registered services."""
108
+ """Return number of registered resources."""
107
109
  return len(self._pile)
108
110
 
109
111
  def __contains__(self, name: str) -> bool:
110
- """Check if service exists (supports `name in registry`)."""
112
+ """Check if resource exists (supports `name in registry`)."""
111
113
  return name in self._name_index
112
114
 
113
115
  def __repr__(self) -> str:
114
116
  """String representation."""
115
- return f"ServiceRegistry(count={len(self)})"
117
+ return f"ResourceRegistry(count={len(self)})"
@@ -14,16 +14,16 @@ from typing import TYPE_CHECKING, Any, Self
14
14
 
15
15
  from typing_extensions import override
16
16
 
17
- from kronos.core import Event, Executor, Processor
18
- from kronos.services.endpoint import APICalling
19
- from kronos.utils.concurrency import get_cancelled_exc_class, sleep
17
+ from krons.core import Event, Executor, Processor
18
+ from krons.resource.endpoint import APICalling
19
+ from krons.utils.concurrency import get_cancelled_exc_class, sleep
20
20
 
21
21
  from .rate_limiter import TokenBucket
22
22
 
23
23
  if TYPE_CHECKING:
24
24
  import asyncio
25
25
 
26
- from kronos.core import Pile
26
+ from krons.core import Pile
27
27
 
28
28
  __all__ = ("RateLimitedExecutor", "RateLimitedProcessor")
29
29
 
@@ -213,8 +213,12 @@ class RateLimitedProcessor(Processor):
213
213
  "concurrency_limit": self.concurrency_limit,
214
214
  "max_queue_size": self.max_queue_size,
215
215
  "max_denial_tracking": self.max_denial_tracking,
216
- "request_bucket": (self.request_bucket.to_dict() if self.request_bucket else None),
217
- "token_bucket": (self.token_bucket.to_dict() if self.token_bucket else None),
216
+ "request_bucket": (
217
+ self.request_bucket.to_dict() if self.request_bucket else None
218
+ ),
219
+ "token_bucket": (
220
+ self.token_bucket.to_dict() if self.token_bucket else None
221
+ ),
218
222
  }
219
223
 
220
224
 
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
  import logging
9
9
  from dataclasses import dataclass
10
10
 
11
- from kronos.utils.concurrency import Lock, current_time, sleep
11
+ from krons.utils.concurrency import Lock, current_time, sleep
12
12
 
13
13
  __all__ = ("RateLimitConfig", "TokenBucket")
14
14
 
@@ -105,7 +105,9 @@ class TokenBucket:
105
105
 
106
106
  if self.tokens >= tokens:
107
107
  self.tokens -= tokens
108
- logger.debug(f"Acquired {tokens} tokens, {self.tokens:.2f} remaining")
108
+ logger.debug(
109
+ f"Acquired {tokens} tokens, {self.tokens:.2f} remaining"
110
+ )
109
111
  return True
110
112
 
111
113
  deficit = tokens - self.tokens
@@ -16,8 +16,8 @@ from dataclasses import dataclass, field
16
16
  from enum import Enum
17
17
  from typing import Any, TypedDict, TypeVar
18
18
 
19
- from kronos.errors import KronConnectionError
20
- from kronos.utils.concurrency import Lock, current_time, sleep
19
+ from krons.errors import KronConnectionError
20
+ from krons.utils.concurrency import Lock, current_time, sleep
21
21
 
22
22
 
23
23
  class _MetricsDict(TypedDict):
@@ -199,7 +199,9 @@ class CircuitBreaker:
199
199
  if now - self.last_failure_time >= self.recovery_time:
200
200
  await self._change_state(CircuitState.HALF_OPEN)
201
201
  else:
202
- recovery_remaining = self.recovery_time - (now - self.last_failure_time)
202
+ recovery_remaining = self.recovery_time - (
203
+ now - self.last_failure_time
204
+ )
203
205
  self._metrics["rejected_count"] += 1
204
206
 
205
207
  logger.warning(
@@ -223,7 +225,9 @@ class CircuitBreaker:
223
225
 
224
226
  return True, 0.0
225
227
 
226
- async def execute(self, func: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any) -> T:
228
+ async def execute(
229
+ self, func: Callable[..., Awaitable[T]], *args: Any, **kwargs: Any
230
+ ) -> T:
227
231
  """Execute async function with circuit breaker protection.
228
232
 
229
233
  Args:
@@ -259,7 +263,9 @@ class CircuitBreaker:
259
263
  return result
260
264
 
261
265
  except Exception as e:
262
- is_excluded = any(isinstance(e, exc_type) for exc_type in self.excluded_exceptions)
266
+ is_excluded = any(
267
+ isinstance(e, exc_type) for exc_type in self.excluded_exceptions
268
+ )
263
269
 
264
270
  if not is_excluded:
265
271
  async with self._lock:
@@ -322,7 +328,9 @@ class RetryConfig:
322
328
  Returns:
323
329
  Delay in seconds before next retry
324
330
  """
325
- delay = min(self.initial_delay * (self.exponential_base**attempt), self.max_delay)
331
+ delay = min(
332
+ self.initial_delay * (self.exponential_base**attempt), self.max_delay
333
+ )
326
334
  if self.jitter:
327
335
  delay = delay * (0.5 + random.random() * 0.5)
328
336
  return delay
@@ -394,7 +402,9 @@ async def retry_with_backoff(
394
402
  last_exception = e
395
403
 
396
404
  if attempt >= max_retries:
397
- logger.error(f"All {max_retries} retry attempts exhausted for {func.__name__}: {e}")
405
+ logger.error(
406
+ f"All {max_retries} retry attempts exhausted for {func.__name__}: {e}"
407
+ )
398
408
  raise
399
409
 
400
410
  delay = min(initial_delay * (exponential_base**attempt), max_delay)
@@ -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