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
@@ -0,0 +1,295 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import warnings
7
+ from enum import Enum
8
+ from typing import Any, Literal, Union
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ warnings.filterwarnings("ignore", category=UserWarning, module="pydantic")
13
+
14
+
15
+ # ---------- Roles & content parts ----------
16
+
17
+
18
+ class ChatRole(str, Enum):
19
+ system = "system"
20
+ developer = "developer" # modern system-like role
21
+ user = "user"
22
+ assistant = "assistant"
23
+ tool = "tool" # for tool results sent back to the model
24
+
25
+
26
+ class TextPart(BaseModel):
27
+ """Text content part for multimodal messages."""
28
+
29
+ type: Literal["text"] = "text"
30
+ text: str
31
+
32
+
33
+ class ImageURLObject(BaseModel):
34
+ """Image URL object; 'detail' is optional and model-dependent."""
35
+
36
+ url: str
37
+ detail: Literal["auto", "low", "high"] | None = Field(
38
+ default=None,
39
+ description="Optional detail control for vision models (auto/low/high).",
40
+ )
41
+
42
+
43
+ class ImageURLPart(BaseModel):
44
+ """Image content part for multimodal messages."""
45
+
46
+ type: Literal["image_url"] = "image_url"
47
+ image_url: ImageURLObject
48
+
49
+
50
+ ContentPart = TextPart | ImageURLPart
51
+
52
+
53
+ # ---------- Tool-calling structures ----------
54
+
55
+
56
+ class FunctionDef(BaseModel):
57
+ """JSON Schema function definition for tool-calling."""
58
+
59
+ name: str
60
+ description: str | None = None
61
+ parameters: dict[str, Any] = Field(
62
+ default_factory=dict,
63
+ description="JSON Schema describing function parameters.",
64
+ )
65
+
66
+
67
+ class FunctionTool(BaseModel):
68
+ type: Literal["function"] = "function"
69
+ function: FunctionDef
70
+
71
+
72
+ class FunctionCall(BaseModel):
73
+ """Legacy function_call field on assistant messages."""
74
+
75
+ name: str
76
+ arguments: str
77
+
78
+
79
+ class ToolCallFunction(BaseModel):
80
+ name: str
81
+ arguments: str
82
+
83
+
84
+ class ToolCall(BaseModel):
85
+ """Assistant's tool call (modern)."""
86
+
87
+ id: str
88
+ type: Literal["function"] = "function"
89
+ function: ToolCallFunction
90
+
91
+
92
+ class ToolChoiceFunction(BaseModel):
93
+ """Explicit tool selection."""
94
+
95
+ type: Literal["function"] = "function"
96
+ function: dict[str, str] # {"name": "<function_name>"}
97
+
98
+
99
+ ToolChoice = Union[Literal["auto", "none"], ToolChoiceFunction]
100
+
101
+
102
+ # ---------- Response format (structured outputs) ----------
103
+
104
+
105
+ class ResponseFormatText(BaseModel):
106
+ type: Literal["text"] = "text"
107
+
108
+
109
+ class ResponseFormatJSONObject(BaseModel):
110
+ type: Literal["json_object"] = "json_object"
111
+
112
+
113
+ class JSONSchemaFormat(BaseModel):
114
+ name: str
115
+ schema_: dict[str, Any] = Field(
116
+ alias="schema", description="JSON Schema definition"
117
+ )
118
+ strict: bool | None = Field(
119
+ default=None,
120
+ description="If true, disallow unspecified properties (strict schema).",
121
+ )
122
+
123
+ model_config = {"populate_by_name": True}
124
+
125
+
126
+ class ResponseFormatJSONSchema(BaseModel):
127
+ type: Literal["json_schema"] = "json_schema"
128
+ json_schema: JSONSchemaFormat
129
+
130
+
131
+ ResponseFormat = Union[
132
+ ResponseFormatText,
133
+ ResponseFormatJSONObject,
134
+ ResponseFormatJSONSchema,
135
+ ]
136
+
137
+
138
+ # ---------- Messages (discriminated by role) ----------
139
+
140
+
141
+ class SystemMessage(BaseModel):
142
+ role: Literal[ChatRole.system] = ChatRole.system
143
+ content: str | list[ContentPart]
144
+ name: str | None = None # optional per API
145
+
146
+
147
+ class DeveloperMessage(BaseModel):
148
+ role: Literal[ChatRole.developer] = ChatRole.developer
149
+ content: str | list[ContentPart]
150
+ name: str | None = None
151
+
152
+
153
+ class UserMessage(BaseModel):
154
+ role: Literal[ChatRole.user] = ChatRole.user
155
+ content: str | list[ContentPart]
156
+ name: str | None = None
157
+
158
+
159
+ class AssistantMessage(BaseModel):
160
+ role: Literal[ChatRole.assistant] = ChatRole.assistant
161
+ # Either textual content, or only tool_calls (when asking you to call tools)
162
+ content: str | list[ContentPart] | None = None
163
+ name: str | None = None
164
+ tool_calls: list[ToolCall] | None = None # modern tool-calling result
165
+ function_call: FunctionCall | None = None # legacy function-calling result
166
+
167
+
168
+ class ToolMessage(BaseModel):
169
+ role: Literal[ChatRole.tool] = ChatRole.tool
170
+ content: str # tool output returned to the model
171
+ tool_call_id: str # must reference the assistant's tool_calls[i].id
172
+
173
+
174
+ ChatMessage = (
175
+ SystemMessage | DeveloperMessage | UserMessage | AssistantMessage | ToolMessage
176
+ )
177
+
178
+ # ---------- Stream options ----------
179
+
180
+
181
+ class StreamOptions(BaseModel):
182
+ include_usage: bool | None = Field(
183
+ default=None,
184
+ description="If true, a final streamed chunk includes token usage.",
185
+ )
186
+
187
+
188
+ # ---------- Main request model ----------
189
+
190
+
191
+ class OpenAIChatCompletionsRequest(BaseModel):
192
+ """
193
+ Request body for OpenAI Chat Completions.
194
+ Endpoint: POST https://api.openai.com/v1/chat/completions
195
+ """
196
+
197
+ # Required
198
+ model: str = Field(..., description="Model name, e.g., 'gpt-4o', 'gpt-4o-mini'.") # type: ignore
199
+ messages: list[ChatMessage] = Field(
200
+ ...,
201
+ description="Conversation so far, including system/developer context.",
202
+ )
203
+
204
+ # Sampling & penalties
205
+ temperature: float | None = Field(
206
+ default=None, ge=0.0, le=2.0, description="Higher is more random."
207
+ )
208
+ top_p: float | None = Field(
209
+ default=None, ge=0.0, le=1.0, description="Nucleus sampling."
210
+ )
211
+ presence_penalty: float | None = Field(
212
+ default=None,
213
+ ge=-2.0,
214
+ le=2.0,
215
+ description="Encourages new topics; -2..2.",
216
+ )
217
+ frequency_penalty: float | None = Field(
218
+ default=None,
219
+ ge=-2.0,
220
+ le=2.0,
221
+ description="Penalizes repetition; -2..2.",
222
+ )
223
+
224
+ # Token limits
225
+ max_completion_tokens: int | None = Field(
226
+ default=None,
227
+ description="Preferred cap on generated tokens (newer models).",
228
+ )
229
+ max_tokens: int | None = Field(
230
+ default=None,
231
+ description="Legacy completion cap (still accepted by many models).",
232
+ )
233
+
234
+ # Count, stop, logits
235
+ n: int | None = Field(default=None, ge=1, description="# of choices to generate.")
236
+ stop: str | list[str] | None = Field(default=None, description="Stop sequence(s).")
237
+ logit_bias: dict[str, float] | None = Field(
238
+ default=None,
239
+ description="Map of token-id -> bias (-100..100).",
240
+ )
241
+ seed: int | None = Field(
242
+ default=None,
243
+ description="Optional reproducibility seed (model-dependent).",
244
+ )
245
+ logprobs: bool | None = None
246
+ top_logprobs: int | None = Field(
247
+ default=None,
248
+ ge=0,
249
+ description="When logprobs is true, how many top tokens to include.",
250
+ )
251
+
252
+ # Tool calling (modern)
253
+ tools: list[FunctionTool] | None = None
254
+ tool_choice: ToolChoice | None = Field(
255
+ default=None,
256
+ description="'auto' (default), 'none', or a function selection.",
257
+ )
258
+ parallel_tool_calls: bool | None = Field(
259
+ default=None,
260
+ description="Allow multiple tool calls in a single assistant turn.",
261
+ )
262
+
263
+ # Legacy function-calling (still supported)
264
+ functions: list[FunctionDef] | None = None
265
+ function_call: Literal["none", "auto"] | FunctionCall | None = None
266
+
267
+ # Structured outputs
268
+ response_format: ResponseFormat | None = None
269
+
270
+ # Streaming
271
+ stream: bool | None = None
272
+ stream_options: StreamOptions | None = None
273
+
274
+ # Routing / tiering
275
+ service_tier: Literal["auto", "default", "flex", "scale", "priority"] | None = (
276
+ Field(
277
+ default=None,
278
+ description="Processing tier; requires account eligibility.",
279
+ )
280
+ )
281
+
282
+ # Misc
283
+ user: str | None = Field(
284
+ default=None,
285
+ description="End-user identifier for abuse monitoring & analytics.",
286
+ )
287
+ store: bool | None = Field(
288
+ default=None,
289
+ description="Whether to store the response server-side (model-dependent).",
290
+ )
291
+ metadata: dict[str, Any] | None = None
292
+ reasoning_effort: Literal["low", "medium", "high"] | None = Field(
293
+ default=None,
294
+ description="For reasoning models: trade-off between speed and accuracy.",
295
+ )
krons/agent/tool.py ADDED
@@ -0,0 +1,291 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Tool - Callable function backend for agents.
5
+
6
+ A Tool wraps a callable function with:
7
+ - Parameter validation via Pydantic schema
8
+ - Normalized response format
9
+ - Integration with the ResourceBackend/Calling pattern
10
+
11
+ Usage:
12
+ @tool(name="calculator", description="Perform calculations")
13
+ async def calculator(expression: str) -> float:
14
+ return eval(expression) # simplified example
15
+
16
+ # Or create manually:
17
+ tool = Tool(
18
+ config=ToolConfig(
19
+ provider="local",
20
+ name="calculator",
21
+ description="Perform calculations",
22
+ ),
23
+ handler=calculator,
24
+ )
25
+ response = await tool.call(arguments={"expression": "2 + 2"})
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ import inspect
31
+ import logging
32
+ from collections.abc import Awaitable, Callable
33
+ from typing import Any, get_type_hints
34
+
35
+ from pydantic import BaseModel, Field
36
+
37
+ from krons.resource.backend import (
38
+ Calling,
39
+ NormalizedResponseModel,
40
+ ResourceBackend,
41
+ ResourceConfig,
42
+ )
43
+
44
+ logger = logging.getLogger(__name__)
45
+
46
+ __all__ = (
47
+ "Tool",
48
+ "ToolCalling",
49
+ "ToolConfig",
50
+ "tool",
51
+ )
52
+
53
+
54
+ class ToolConfig(ResourceConfig):
55
+ """Configuration for a Tool backend.
56
+
57
+ Extends ResourceConfig with tool-specific fields.
58
+ """
59
+
60
+ description: str = Field(default="", description="Human-readable tool description")
61
+ parameters_schema: type[BaseModel] | None = Field(
62
+ default=None,
63
+ exclude=True,
64
+ description="Pydantic model for parameter validation",
65
+ )
66
+ return_type: type | None = Field(
67
+ default=None,
68
+ exclude=True,
69
+ description="Expected return type (for documentation)",
70
+ )
71
+
72
+
73
+ class ToolCalling(Calling):
74
+ """Calling event for Tool execution.
75
+
76
+ Wraps a tool invocation with the standard Calling lifecycle (hooks, etc).
77
+ """
78
+
79
+ arguments: dict[str, Any] = Field(
80
+ default_factory=dict,
81
+ description="Arguments to pass to the tool handler",
82
+ )
83
+
84
+ @property
85
+ def call_args(self) -> dict:
86
+ """Get arguments for backend.call()."""
87
+ return {"arguments": self.arguments}
88
+
89
+
90
+ class Tool(ResourceBackend):
91
+ """Tool backend - wraps a callable function.
92
+
93
+ Attributes:
94
+ config: ToolConfig with name, description, schema
95
+ handler: The callable function to execute
96
+ """
97
+
98
+ config: ToolConfig = Field(..., description="Tool configuration")
99
+ handler: Callable[..., Any | Awaitable[Any]] = Field(
100
+ ...,
101
+ exclude=True,
102
+ description="The callable function to execute",
103
+ )
104
+
105
+ @property
106
+ def event_type(self) -> type[ToolCalling]:
107
+ """Return ToolCalling as the event type for this backend."""
108
+ return ToolCalling
109
+
110
+ @property
111
+ def description(self) -> str:
112
+ """Tool description from config."""
113
+ return self.config.description
114
+
115
+ @property
116
+ def parameters_schema(self) -> type[BaseModel] | None:
117
+ """Parameters schema from config."""
118
+ return self.config.parameters_schema
119
+
120
+ def _validate_arguments(self, arguments: dict[str, Any]) -> dict[str, Any]:
121
+ """Validate arguments against schema if defined.
122
+
123
+ Args:
124
+ arguments: Arguments dict to validate
125
+
126
+ Returns:
127
+ Validated arguments dict
128
+
129
+ Raises:
130
+ ValueError: If validation fails
131
+ """
132
+ if self.parameters_schema is None:
133
+ return arguments
134
+
135
+ try:
136
+ validated = self.parameters_schema.model_validate(arguments)
137
+ return validated.model_dump()
138
+ except Exception as e:
139
+ raise ValueError(f"Tool argument validation failed: {e}") from e
140
+
141
+ async def call(
142
+ self, arguments: dict[str, Any] | None = None
143
+ ) -> NormalizedResponseModel:
144
+ """Execute the tool handler with given arguments.
145
+
146
+ Args:
147
+ arguments: Arguments to pass to handler
148
+
149
+ Returns:
150
+ NormalizedResponseModel with result or error
151
+ """
152
+ arguments = arguments or {}
153
+
154
+ try:
155
+ # Validate arguments
156
+ validated_args = self._validate_arguments(arguments)
157
+
158
+ # Execute handler (sync or async)
159
+ if inspect.iscoroutinefunction(self.handler):
160
+ result = await self.handler(**validated_args)
161
+ else:
162
+ result = self.handler(**validated_args)
163
+
164
+ return self.normalize_response(result)
165
+
166
+ except Exception as e:
167
+ logger.exception(f"Tool '{self.name}' execution failed: {e}")
168
+ return NormalizedResponseModel(
169
+ status="error",
170
+ data=None,
171
+ error=str(e),
172
+ raw_response={"error": str(e), "arguments": arguments},
173
+ )
174
+
175
+ def normalize_response(self, raw_response: Any) -> NormalizedResponseModel:
176
+ """Normalize tool result to standard format.
177
+
178
+ Args:
179
+ raw_response: Raw result from handler
180
+
181
+ Returns:
182
+ NormalizedResponseModel with status and data
183
+ """
184
+ return NormalizedResponseModel(
185
+ status="success",
186
+ data=raw_response,
187
+ raw_response={"result": raw_response},
188
+ )
189
+
190
+ def to_openai_schema(self) -> dict[str, Any]:
191
+ """Generate OpenAI-compatible function schema.
192
+
193
+ Returns:
194
+ Dict with name, description, parameters schema
195
+ """
196
+ schema: dict[str, Any] = {
197
+ "name": self.name,
198
+ "description": self.description,
199
+ }
200
+
201
+ if self.parameters_schema:
202
+ # Get JSON schema from Pydantic model
203
+ json_schema = self.parameters_schema.model_json_schema()
204
+ # OpenAI expects parameters directly, not wrapped
205
+ schema["parameters"] = {
206
+ "type": "object",
207
+ "properties": json_schema.get("properties", {}),
208
+ "required": json_schema.get("required", []),
209
+ }
210
+ else:
211
+ schema["parameters"] = {"type": "object", "properties": {}}
212
+
213
+ return schema
214
+
215
+ def to_anthropic_schema(self) -> dict[str, Any]:
216
+ """Generate Anthropic-compatible tool schema.
217
+
218
+ Returns:
219
+ Dict with name, description, input_schema
220
+ """
221
+ schema: dict[str, Any] = {
222
+ "name": self.name,
223
+ "description": self.description,
224
+ }
225
+
226
+ if self.parameters_schema:
227
+ json_schema = self.parameters_schema.model_json_schema()
228
+ schema["input_schema"] = {
229
+ "type": "object",
230
+ "properties": json_schema.get("properties", {}),
231
+ "required": json_schema.get("required", []),
232
+ }
233
+ else:
234
+ schema["input_schema"] = {"type": "object", "properties": {}}
235
+
236
+ return schema
237
+
238
+
239
+ def tool(
240
+ name: str | None = None,
241
+ description: str = "",
242
+ provider: str = "local",
243
+ parameters_schema: type[BaseModel] | None = None,
244
+ ) -> Callable[[Callable[..., Any]], Tool]:
245
+ """Decorator to create a Tool from a function.
246
+
247
+ Args:
248
+ name: Tool name (defaults to function name)
249
+ description: Human-readable description
250
+ provider: Provider name (default: "local")
251
+ parameters_schema: Optional Pydantic model for parameter validation
252
+
253
+ Returns:
254
+ Decorator that creates a Tool
255
+
256
+ Example:
257
+ @tool(name="greet", description="Greet a person")
258
+ async def greet(name: str) -> str:
259
+ return f"Hello, {name}!"
260
+
261
+ # Use directly:
262
+ response = await greet.call(arguments={"name": "World"})
263
+ """
264
+
265
+ def decorator(func: Callable[..., Any]) -> Tool:
266
+ tool_name = name or func.__name__
267
+ tool_desc = description or func.__doc__ or ""
268
+
269
+ # Try to infer parameters schema from type hints
270
+ schema = parameters_schema
271
+ if schema is None:
272
+ hints = get_type_hints(func) if hasattr(func, "__annotations__") else {}
273
+ hints.pop("return", None)
274
+ if hints:
275
+ # Dynamically create a Pydantic model from type hints
276
+ schema = type(
277
+ f"{tool_name.title()}Params",
278
+ (BaseModel,),
279
+ {"__annotations__": hints},
280
+ )
281
+
282
+ config = ToolConfig(
283
+ provider=provider,
284
+ name=tool_name,
285
+ description=tool_desc,
286
+ parameters_schema=schema,
287
+ )
288
+
289
+ return Tool(config=config, handler=func)
290
+
291
+ return decorator
krons/core/__init__.py ADDED
@@ -0,0 +1,127 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Core module - Foundation primitives and submodules.
5
+
6
+ Re-exports base classes from core/base/ and exposes submodules:
7
+ - types: Sentinels, base types, DB types
8
+ - specs: Spec definitions, Operable, adapters
9
+
10
+ Note: Session (Message, Branch, Session, Exchange) is now at krons.session
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import TYPE_CHECKING
16
+
17
+ # Lazy import mapping - delegates to core.base
18
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
19
+ # Registries
20
+ "NODE_REGISTRY": ("krons.core.base", "NODE_REGISTRY"),
21
+ "PERSISTABLE_NODE_REGISTRY": ("krons.core.base", "PERSISTABLE_NODE_REGISTRY"),
22
+ # Classes
23
+ "Broadcaster": ("krons.core.base", "Broadcaster"),
24
+ "Edge": ("krons.core.base", "Edge"),
25
+ "EdgeCondition": ("krons.core.base", "EdgeCondition"),
26
+ "Element": ("krons.core.base", "Element"),
27
+ "Event": ("krons.core.base", "Event"),
28
+ "EventBus": ("krons.core.base", "EventBus"),
29
+ "EventStatus": ("krons.core.base", "EventStatus"),
30
+ "Execution": ("krons.core.base", "Execution"),
31
+ "Executor": ("krons.core.base", "Executor"),
32
+ "Flow": ("krons.core.base", "Flow"),
33
+ "Graph": ("krons.core.base", "Graph"),
34
+ "Handler": ("krons.core.base", "Handler"),
35
+ "Node": ("krons.core.base", "Node"),
36
+ "NodeConfig": ("krons.core.base", "NodeConfig"),
37
+ "Pile": ("krons.core.base", "Pile"),
38
+ "Processor": ("krons.core.base", "Processor"),
39
+ "Progression": ("krons.core.base", "Progression"),
40
+ # Functions
41
+ "create_node": ("krons.core.base", "create_node"),
42
+ "generate_all_ddl": ("krons.core.base", "generate_all_ddl"),
43
+ "generate_ddl": ("krons.core.base", "generate_ddl"),
44
+ "get_fk_dependencies": ("krons.core.base", "get_fk_dependencies"),
45
+ }
46
+
47
+ _LOADED: dict[str, object] = {}
48
+
49
+
50
+ def __getattr__(name: str) -> object:
51
+ """Lazy import attributes on first access."""
52
+ if name in _LOADED:
53
+ return _LOADED[name]
54
+
55
+ if name in _LAZY_IMPORTS:
56
+ from importlib import import_module
57
+
58
+ module_name, attr_name = _LAZY_IMPORTS[name]
59
+ module = import_module(module_name)
60
+ value = getattr(module, attr_name)
61
+ _LOADED[name] = value
62
+ return value
63
+
64
+ raise AttributeError(f"module 'krons.core' has no attribute {name!r}")
65
+
66
+
67
+ def __dir__() -> list[str]:
68
+ """Return all available attributes for autocomplete."""
69
+ return list(__all__)
70
+
71
+
72
+ # TYPE_CHECKING block for static analysis
73
+ if TYPE_CHECKING:
74
+ from krons.core.base import (
75
+ NODE_REGISTRY,
76
+ PERSISTABLE_NODE_REGISTRY,
77
+ Broadcaster,
78
+ Edge,
79
+ EdgeCondition,
80
+ Element,
81
+ Event,
82
+ EventBus,
83
+ EventStatus,
84
+ Execution,
85
+ Executor,
86
+ Flow,
87
+ Graph,
88
+ Handler,
89
+ Node,
90
+ NodeConfig,
91
+ Pile,
92
+ Processor,
93
+ Progression,
94
+ create_node,
95
+ generate_all_ddl,
96
+ generate_ddl,
97
+ get_fk_dependencies,
98
+ )
99
+
100
+ __all__ = [
101
+ # constants/registries
102
+ "NODE_REGISTRY",
103
+ "PERSISTABLE_NODE_REGISTRY",
104
+ # classes
105
+ "Broadcaster",
106
+ "Edge",
107
+ "EdgeCondition",
108
+ "Element",
109
+ "Event",
110
+ "EventBus",
111
+ "EventStatus",
112
+ "Execution",
113
+ "Executor",
114
+ "Flow",
115
+ "Graph",
116
+ "Handler",
117
+ "Node",
118
+ "NodeConfig",
119
+ "Pile",
120
+ "Processor",
121
+ "Progression",
122
+ # functions
123
+ "create_node",
124
+ "generate_all_ddl",
125
+ "generate_ddl",
126
+ "get_fk_dependencies",
127
+ ]