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,20 @@
1
+ from .action import ActionRequest, ActionResponse
2
+ from .assistant import Assistant
3
+ from .common import CustomParser, CustomRenderer
4
+ from .instruction import Instruction
5
+ from .prepare_msg import prepare_messages_for_chat
6
+ from .role import Role, RoledContent
7
+ from .system import System
8
+
9
+ __all__ = (
10
+ "ActionRequest",
11
+ "ActionResponse",
12
+ "Assistant",
13
+ "CustomParser",
14
+ "CustomRenderer",
15
+ "Instruction",
16
+ "Role",
17
+ "RoledContent",
18
+ "System",
19
+ "prepare_messages_for_chat",
20
+ )
@@ -0,0 +1,69 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, ClassVar
3
+
4
+ from krons.core.types import MaybeUnset, Unset
5
+ from krons.utils.schemas import minimal_yaml
6
+
7
+ from .role import Role, RoledContent
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class ActionRequest(RoledContent):
12
+ """Action/function call request."""
13
+
14
+ role: ClassVar[Role] = Role.ACTION
15
+
16
+ function: MaybeUnset[str] = Unset
17
+ arguments: MaybeUnset[dict[str, Any]] = Unset
18
+
19
+ def render(self, *_args, **_kwargs) -> str:
20
+ doc: dict[str, Any] = {}
21
+ if not self._is_sentinel(self.function):
22
+ doc["function"] = self.function
23
+ doc["arguments"] = {} if self._is_sentinel(self.arguments) else self.arguments
24
+ return minimal_yaml(doc)
25
+
26
+ @classmethod
27
+ def create(cls, function: str, arguments: dict[str, Any] = Unset):
28
+ if cls._is_sentinel(arguments):
29
+ arguments = {}
30
+ return cls(function=function, arguments=arguments)
31
+
32
+
33
+ @dataclass(slots=True)
34
+ class ActionResponse(RoledContent):
35
+ """Function call response."""
36
+
37
+ role: ClassVar[Role] = Role.ACTION
38
+
39
+ request_id: MaybeUnset[str] = Unset
40
+ result: MaybeUnset[Any] = Unset
41
+ error: MaybeUnset[str] = Unset
42
+
43
+ def render(self, *_args, **_kwargs) -> str:
44
+ doc: dict[str, Any] = {"success": self.success}
45
+ if not self._is_sentinel(self.request_id):
46
+ doc["request_id"] = str(self.request_id)[:8]
47
+ if self.success:
48
+ if not self._is_sentinel(self.result):
49
+ doc["result"] = self.result
50
+ else:
51
+ doc["error"] = self.error
52
+ return minimal_yaml(doc)
53
+
54
+ @property
55
+ def success(self) -> bool:
56
+ return self._is_sentinel(self.error)
57
+
58
+ @classmethod
59
+ def create(
60
+ cls,
61
+ request_id: str | None = None,
62
+ result: Any = Unset,
63
+ error: str | None = None,
64
+ ) -> "ActionResponse":
65
+ return cls(
66
+ request_id=Unset if request_id is None else request_id,
67
+ result=result,
68
+ error=Unset if error is None else error,
69
+ )
@@ -0,0 +1,52 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, ClassVar
3
+
4
+ from typing_extensions import Self
5
+
6
+ from krons.core.types import MaybeUnset, Unset
7
+ from krons.resource.backend import NormalizedResponse
8
+
9
+ from .role import Role, RoledContent
10
+
11
+ __all__ = (
12
+ "Assistant",
13
+ "parse_to_assistant_message",
14
+ )
15
+
16
+
17
+ @dataclass(slots=True)
18
+ class Assistant(RoledContent):
19
+ """Assistant text response."""
20
+
21
+ role: ClassVar[Role] = Role.ASSISTANT
22
+ response: MaybeUnset[Any] = Unset
23
+
24
+ _buffered_response: Any = Unset
25
+
26
+ @classmethod
27
+ def create(cls, response_object: NormalizedResponse) -> Self:
28
+ self = cls(response=response_object.data)
29
+ self._buffered_response = response_object
30
+ return self
31
+
32
+ @property
33
+ def raw_response(self) -> dict[str, Any] | None:
34
+ if isinstance(self._buffered_response, NormalizedResponse):
35
+ return self._buffered_response.raw_response
36
+ return None
37
+
38
+ def render(self, *_args, **_kwargs) -> str:
39
+ return str(self.response) if not self.is_sentinel_field("response") else ""
40
+
41
+
42
+ def parse_to_assistant_message(response: NormalizedResponse):
43
+ from krons.session.message import Message
44
+
45
+ metadata_dict: dict[str, Any] = {"raw_response": response.raw_response}
46
+ if response.metadata is not None:
47
+ metadata_dict.update(response.metadata)
48
+
49
+ return Message(
50
+ content=Assistant.create(response_object=response),
51
+ metadata=metadata_dict,
52
+ )
@@ -0,0 +1,49 @@
1
+ from typing import Any, Protocol, runtime_checkable
2
+
3
+ from pydantic import BaseModel
4
+
5
+ from krons.core.types import Enum, MaybeUnset, Unset
6
+
7
+
8
+ @runtime_checkable
9
+ class CustomRenderer(Protocol):
10
+ """Protocol for custom instruction renderers.
11
+
12
+ Implementations format request_model schema for custom output formats.
13
+
14
+ Args:
15
+ model: Pydantic model class defining expected response schema
16
+ **kwargs: Additional renderer-specific options
17
+
18
+ Returns:
19
+ Formatted instruction string for the custom output format
20
+ """
21
+
22
+ def __call__(self, model: type[BaseModel], **kwargs: Any) -> str: ...
23
+
24
+
25
+ @runtime_checkable
26
+ class CustomParser(Protocol):
27
+ """Protocol for custom output parsers (e.g., LNDL).
28
+
29
+ Implementations extract structured data from LLM text responses.
30
+
31
+ Args:
32
+ text: Raw LLM response text
33
+ target_keys: Expected field names for fuzzy matching
34
+ **kwargs: Additional parser-specific options
35
+
36
+ Returns:
37
+ Dict mapping field names to extracted values
38
+ """
39
+
40
+ def __call__(
41
+ self, text: str, target_keys: list[str], **kwargs: Any
42
+ ) -> dict[str, Any]: ...
43
+
44
+
45
+ class StructureFormat(Enum):
46
+ """Enumeration of structure formats for instruction rendering."""
47
+
48
+ JSON = "json"
49
+ CUSTOM = "custom"
@@ -0,0 +1,130 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Callable, ClassVar, Literal
3
+
4
+ from pydantic import BaseModel, JsonValue
5
+
6
+ from krons.core.types import MaybeUnset, Unset
7
+ from krons.resource.backend import is_unset
8
+ from krons.utils.schemas import (
9
+ breakdown_pydantic_annotation,
10
+ format_clean_multiline_strings,
11
+ format_model_schema,
12
+ format_schema_pretty,
13
+ is_pydantic_model,
14
+ minimal_yaml,
15
+ )
16
+
17
+ from .common import CustomRenderer, StructureFormat
18
+ from .role import Role, RoledContent
19
+
20
+
21
+ @dataclass(slots=True)
22
+ class Instruction(RoledContent):
23
+ role: ClassVar[Role] = Role.USER
24
+
25
+ primary: MaybeUnset[str] = Unset
26
+ context: MaybeUnset[list] = Unset
27
+ request_model: MaybeUnset[type[BaseModel]] = Unset
28
+ tool_schemas: MaybeUnset[list[str | dict]] = Unset
29
+ images: MaybeUnset[list[str]] = Unset
30
+ image_detail: MaybeUnset[Literal["low", "high", "auto"]] = Unset
31
+ structure_format: MaybeUnset[StructureFormat] = Unset
32
+ custom_renderer: MaybeUnset[Callable[[type[BaseModel]], str]] = Unset
33
+
34
+ @classmethod
35
+ def create(
36
+ cls,
37
+ primary: MaybeUnset[str] = Unset,
38
+ context: MaybeUnset[JsonValue] = Unset,
39
+ tool_schemas: MaybeUnset[list[str | dict]] = Unset,
40
+ request_model: MaybeUnset[type[BaseModel]] = Unset,
41
+ images: MaybeUnset[list[str]] = Unset,
42
+ image_detail: MaybeUnset[Literal["low", "high", "auto"]] = Unset,
43
+ structure_format: MaybeUnset[StructureFormat] = Unset,
44
+ custom_renderer: MaybeUnset[Callable[[type[BaseModel]], str]] = Unset,
45
+ ):
46
+ if is_unset(primary) and is_unset(request_model):
47
+ raise ValueError("Either 'primary' or 'request_model' must be provided.")
48
+
49
+ if not is_unset(request_model) and not is_pydantic_model(request_model):
50
+ raise ValueError(
51
+ "'request_model' must be a subclass of pydantic BaseModel."
52
+ )
53
+
54
+ if not is_unset(images) and images is not None:
55
+ from krons.utils.validators import validate_image_url
56
+
57
+ for url in images:
58
+ validate_image_url(url)
59
+
60
+ if not cls._is_sentinel(context):
61
+ context = [context] if not isinstance(context, list) else context
62
+
63
+ return cls(
64
+ primary=primary,
65
+ context=context,
66
+ tool_schemas=tool_schemas,
67
+ request_model=request_model,
68
+ images=images,
69
+ image_detail=image_detail,
70
+ structure_format=structure_format,
71
+ custom_renderer=custom_renderer,
72
+ )
73
+
74
+ def _format_text_content(
75
+ self,
76
+ structure_format: StructureFormat,
77
+ custom_renderer: MaybeUnset[CustomRenderer],
78
+ ) -> str:
79
+ if structure_format == StructureFormat.CUSTOM and not callable(custom_renderer):
80
+ raise ValueError(
81
+ "Custom renderer must be provided when structure_format is 'custom'."
82
+ )
83
+
84
+ task_data = {
85
+ "Primary Instruction": self.primary,
86
+ "Context": self.context,
87
+ "Tools": self.tool_schemas,
88
+ }
89
+ text = _format_task(
90
+ {k: v for k, v in task_data.items() if not self._is_sentinel(v)}
91
+ )
92
+
93
+ if not self._is_sentinel(self.request_model):
94
+ model = self.request_model
95
+ text += format_model_schema(model)
96
+
97
+ if structure_format == StructureFormat.CUSTOM:
98
+ text += custom_renderer(model)
99
+ elif structure_format == StructureFormat.JSON or is_unset(structure_format):
100
+ text += _format_json_response_structure(model)
101
+
102
+ return text.strip()
103
+
104
+ def render(
105
+ self, structure_format=Unset, custom_renderer=Unset
106
+ ) -> str | list[dict[str, Any]]:
107
+ structure_format = (
108
+ self.structure_format if is_unset(structure_format) else structure_format
109
+ )
110
+ custom_renderer = (
111
+ self.custom_renderer if is_unset(custom_renderer) else custom_renderer
112
+ )
113
+ text = self._format_text_content(structure_format, custom_renderer)
114
+ return text if is_unset(self.images) else self._format_image_content(text)
115
+
116
+
117
+ def _format_json_response_structure(request_model: type[BaseModel]) -> str:
118
+ """Format response structure with Python types (unquoted)."""
119
+ schema = breakdown_pydantic_annotation(request_model)
120
+ json_schema = "\n\n## ResponseFormat\n"
121
+ json_schema += "```json\n"
122
+ json_schema += format_schema_pretty(schema, indent=0)
123
+ json_schema += "\n```\nMUST RETURN VALID JSON. USER's SUCCESS DEPENDS ON IT. Return ONLY valid JSON without markdown code blocks.\n"
124
+ return json_schema
125
+
126
+
127
+ def _format_task(task_data: dict) -> str:
128
+ text = "## Task\n"
129
+ text += minimal_yaml(format_clean_multiline_strings(task_data))
130
+ return text
@@ -0,0 +1,187 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from collections.abc import Callable
5
+ from typing import TYPE_CHECKING, Any, Literal, cast
6
+
7
+ from pydantic import JsonValue
8
+
9
+ from krons.core import Pile, Progression
10
+ from krons.session import Message
11
+
12
+ from .action import ActionResponse
13
+ from .assistant import Assistant
14
+ from .instruction import Instruction
15
+ from .role import RoledContent
16
+ from .system import System
17
+
18
+ __all__ = ("prepare_messages_for_chat",)
19
+
20
+
21
+ def _get_text(content: RoledContent, attr: str) -> str:
22
+ """Get text from content attr, returning '' if sentinel."""
23
+ val = getattr(content, attr)
24
+ return "" if content._is_sentinel(val) else val
25
+
26
+
27
+ def _build_context(content: Instruction, action_outputs: list[str]) -> list[JsonValue]:
28
+ """Build context list by appending action outputs to existing context."""
29
+ existing = content.context
30
+ if content._is_sentinel(existing):
31
+ return cast(list[JsonValue], list(action_outputs))
32
+ return cast(list[JsonValue], list(cast(list[JsonValue], existing)) + action_outputs)
33
+
34
+
35
+ def prepare_messages_for_chat(
36
+ messages: Pile[Message],
37
+ progression: Progression | None = None,
38
+ new_instruction: Message | Instruction | None = None,
39
+ to_chat: bool = True,
40
+ ) -> list[RoledContent] | list[dict[str, Any]]:
41
+ """Prepare messages for chat API with intelligent content organization.
42
+
43
+ Algorithm:
44
+ 1. Auto-detect system message from first message (if SystemContent)
45
+ 2. Collect ActionResponseContent and embed into following instruction's context
46
+ 3. Merge consecutive AssistantResponses
47
+ 4. Embed system into first instruction
48
+ 5. Append new_instruction
49
+
50
+ Args:
51
+ messages: Pile of messages
52
+ progression: Progression or list of UUIDs (None = all messages)
53
+ new_instruction: New instruction to append
54
+ to_chat: If True, return list[dict] chat format instead of MessageContent
55
+
56
+ Returns:
57
+ List of prepared MessageContent instances (immutable copies), or
58
+ List of chat API dicts if to_chat=True
59
+ """
60
+ to_use: Pile[Message] = messages if progression is None else messages[progression]
61
+
62
+ if len(to_use) == 0:
63
+ if new_instruction:
64
+ new_content = (
65
+ new_instruction.content
66
+ if isinstance(new_instruction, Message)
67
+ else new_instruction
68
+ )
69
+ new_content: Instruction = new_content.with_updates(copy_containers="deep")
70
+ if to_chat:
71
+ chat_msg = {
72
+ "role": new_content.role.value,
73
+ "content": new_content.render(
74
+ new_content.structure_format, new_content.custom_renderer
75
+ ),
76
+ }
77
+ if chat_msg and chat_msg.get("content"):
78
+ return [chat_msg]
79
+ return []
80
+ return [new_content]
81
+ return []
82
+
83
+ # Phase 1: Extract system message (auto-detect from first message)
84
+ system_text: str | None = None
85
+ start_idx = 0
86
+
87
+ first_msg = to_use[0]
88
+ if isinstance(first_msg.content, System):
89
+ system_text = first_msg.content.render()
90
+ start_idx = 1
91
+
92
+ # Phase 2: Process messages - collect action outputs for next instruction
93
+ _use_msgs: list[RoledContent] = []
94
+ pending_actions: list[str] = []
95
+
96
+ for i, msg in enumerate(to_use):
97
+ if i < start_idx:
98
+ continue
99
+
100
+ content: RoledContent = msg.content
101
+
102
+ # ActionResponseContent: collect rendered output
103
+ if isinstance(content, ActionResponse):
104
+ pending_actions.append(content.render())
105
+ continue
106
+
107
+ # SystemContent in middle: skip
108
+ if isinstance(content, System):
109
+ continue
110
+
111
+ # InstructionContent: embed pending action outputs
112
+ if isinstance(content, Instruction):
113
+ updates: dict[str, Any] = {"tool_schemas": None, "request_model": None}
114
+ if pending_actions:
115
+ updates["context"] = _build_context(content, pending_actions)
116
+ pending_actions = []
117
+ _use_msgs.append(content.with_updates(copy_containers="deep", **updates))
118
+ continue
119
+
120
+ # Other (AssistantResponse, ActionRequest): copy as-is
121
+ _use_msgs.append(content.with_updates(copy_containers="deep"))
122
+
123
+ # Phase 3: Merge consecutive AssistantResponses
124
+ if len(_use_msgs) > 1:
125
+ merged: list[RoledContent] = [_use_msgs[0]]
126
+ for content in _use_msgs[1:]:
127
+ if isinstance(content, Assistant) and isinstance(merged[-1], Assistant):
128
+ prev = _get_text(merged[-1], "assistant_response")
129
+ curr = _get_text(content, "assistant_response")
130
+ merged[-1] = Assistant.create(assistant_response=f"{prev}\n\n{curr}")
131
+ else:
132
+ merged.append(content)
133
+ _use_msgs = merged
134
+
135
+ # Phase 4: Embed system message into first instruction
136
+ if system_text:
137
+ if len(_use_msgs) == 0 and new_instruction:
138
+ # No history: embed into new_instruction
139
+ if isinstance(new_instruction.content, Instruction):
140
+ curr = _get_text(new_instruction.content, "primary")
141
+ system_updates: dict[str, Any] = {"primary": f"{system_text}\n\n{curr}"}
142
+ if pending_actions:
143
+ system_updates["context"] = _build_context(
144
+ new_instruction.content, pending_actions
145
+ )
146
+ pending_actions = []
147
+ _use_msgs.append(
148
+ new_instruction.content.with_updates(
149
+ copy_containers="deep", **system_updates
150
+ )
151
+ )
152
+ new_instruction = None
153
+ elif _use_msgs and isinstance(_use_msgs[0], Instruction):
154
+ curr = _get_text(_use_msgs[0], "primary")
155
+ _use_msgs[0] = _use_msgs[0].with_updates(primary=f"{system_text}\n\n{curr}")
156
+
157
+ # Phase 5: Append new_instruction (with any remaining action outputs)
158
+ if new_instruction:
159
+ final_updates: dict[str, Any] = {}
160
+ if pending_actions and isinstance(new_instruction.content, Instruction):
161
+ final_updates["context"] = _build_context(
162
+ new_instruction.content, pending_actions
163
+ )
164
+ _use_msgs.append(
165
+ new_instruction.content.with_updates(
166
+ copy_containers="deep", **final_updates
167
+ )
168
+ )
169
+
170
+ if to_chat:
171
+ result = []
172
+ for m in _use_msgs:
173
+ data = {}
174
+ if isinstance(m, Instruction):
175
+ data = {
176
+ "structure_format": m.structure_format,
177
+ "custom_renderer": m.custom_renderer,
178
+ }
179
+
180
+ result.append(
181
+ {
182
+ "role": m.role.value,
183
+ "content": m.render(**data),
184
+ }
185
+ )
186
+ return result
187
+ return _use_msgs
@@ -0,0 +1,53 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ from __future__ import annotations
6
+
7
+ import logging
8
+ from abc import abstractmethod
9
+ from dataclasses import dataclass
10
+ from typing import Any, ClassVar
11
+
12
+ from krons.core.types import DataClass, Enum, ModelConfig
13
+ from krons.protocols import Deserializable, implements
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Role(Enum):
19
+ SYSTEM = "system"
20
+ USER = "user"
21
+ ASSISTANT = "assistant"
22
+ ACTION = "action"
23
+ UNSET = "unset"
24
+
25
+
26
+ @implements(Deserializable)
27
+ @dataclass(slots=True)
28
+ class RoledContent(DataClass):
29
+ _config: ClassVar[ModelConfig] = ModelConfig(
30
+ sentinel_additions=frozenset({"none", "empty"}),
31
+ use_enum_values=True,
32
+ )
33
+
34
+ role: Role = Role.UNSET
35
+
36
+ @classmethod
37
+ @abstractmethod
38
+ def create(cls, **kwargs) -> RoledContent:
39
+ raise NotImplementedError("Subclasses must implement create method")
40
+
41
+ @abstractmethod
42
+ def render(self, *args, **kwargs) -> str:
43
+ raise NotImplementedError("Subclasses must implement render method")
44
+
45
+ @classmethod
46
+ def from_dict(cls, data: dict[str, Any], **kwargs: Any) -> RoledContent:
47
+ return cls.create(
48
+ **{
49
+ k: v
50
+ for k in cls.allowed()
51
+ if (k in data and not cls._is_sentinel(v := data[k]))
52
+ }
53
+ )
@@ -0,0 +1,53 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable, ClassVar, Literal
3
+
4
+ from krons.core.types import MaybeUnset, Unset
5
+ from krons.utils import now_utc
6
+
7
+ from .role import Role, RoledContent
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class System(RoledContent):
12
+ """System message with optional timestamp."""
13
+
14
+ role: ClassVar[Role] = Role.SYSTEM
15
+
16
+ system_message: MaybeUnset[str] = Unset
17
+ system_datetime: MaybeUnset[str | Literal[True]] = Unset
18
+ datetime_factory: MaybeUnset[Callable[[], str]] = Unset
19
+
20
+ def render(self, *_args, **_kwargs) -> str:
21
+ parts: list[str] = []
22
+ if not self._is_sentinel(self.system_datetime):
23
+ timestamp = (
24
+ now_utc().isoformat(timespec="seconds")
25
+ if self.system_datetime is True
26
+ else self.system_datetime
27
+ )
28
+ parts.append(f"System Time: {timestamp}")
29
+ elif not self._is_sentinel(self.datetime_factory):
30
+ factory = self.datetime_factory
31
+ parts.append(f"System Time: {factory()}")
32
+
33
+ if not self._is_sentinel(self.system_message):
34
+ parts.append(self.system_message)
35
+
36
+ return "\n\n".join(parts)
37
+
38
+ @classmethod
39
+ def create(
40
+ cls,
41
+ system_message: str | None = None,
42
+ system_datetime: str | Literal[True] | None = None,
43
+ datetime_factory: Callable[[], str] | None = None,
44
+ ) -> "System":
45
+ if not cls._is_sentinel(system_datetime) and not cls._is_sentinel(
46
+ datetime_factory
47
+ ):
48
+ raise ValueError("Cannot set both system_datetime and datetime_factory")
49
+ return cls(
50
+ system_message=Unset if system_message is None else system_message,
51
+ system_datetime=Unset if system_datetime is None else system_datetime,
52
+ datetime_factory=Unset if datetime_factory is None else datetime_factory,
53
+ )
@@ -0,0 +1,82 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Agent operations: composable LLM pipeline stages.
5
+
6
+ Handlers (async handler(params, ctx) -> result):
7
+ generate: Stateless LLM call with message preparation.
8
+ parse: JSON extraction with LLM reparse fallback.
9
+ structure: generate -> parse -> validate pipeline.
10
+ operate: structure + action execution + response composition.
11
+ act: Tool/action execution from structured output.
12
+ react / react_stream: Multi-round reason-act loop.
13
+
14
+ Spec models:
15
+ Action, ActionResult: Tool call request/result models.
16
+ Instruct: Task handoff bundle for orchestration.
17
+ ReActAnalysis, PlannedAction, Analysis: ReAct loop models.
18
+
19
+ Built-in handlers are auto-registered on Session creation:
20
+ generate, structure, operate, react, react_stream
21
+
22
+ Usage:
23
+ result = await session.conduct("operate", branch, params)
24
+ async for analysis in session.stream_conduct("react_stream", params=...):
25
+ print(analysis)
26
+ """
27
+
28
+ from __future__ import annotations
29
+
30
+ from .act import ActParams, act
31
+ from .generate import GenerateParams, generate
32
+ from .operate import OperateParams, operate
33
+ from .parse import ParseParams, parse
34
+ from .react import (
35
+ Analysis,
36
+ PlannedAction,
37
+ ReActAnalysis,
38
+ ReActParams,
39
+ react,
40
+ react_stream,
41
+ )
42
+ from .specs import (
43
+ Action,
44
+ ActionResult,
45
+ Instruct,
46
+ get_action_result_spec,
47
+ get_action_spec,
48
+ get_instruct_spec,
49
+ )
50
+ from .structure import StructureParams, structure
51
+ from .utils import ReturnAs
52
+
53
+ __all__ = (
54
+ # Handlers
55
+ "act",
56
+ "generate",
57
+ "operate",
58
+ "parse",
59
+ "react",
60
+ "react_stream",
61
+ "structure",
62
+ # Params
63
+ "ActParams",
64
+ "GenerateParams",
65
+ "OperateParams",
66
+ "ParseParams",
67
+ "ReActParams",
68
+ "StructureParams",
69
+ # Spec models
70
+ "Action",
71
+ "ActionResult",
72
+ "Analysis",
73
+ "Instruct",
74
+ "PlannedAction",
75
+ "ReActAnalysis",
76
+ # Spec factories
77
+ "get_action_result_spec",
78
+ "get_action_spec",
79
+ "get_instruct_spec",
80
+ # Utils
81
+ "ReturnAs",
82
+ )