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,151 @@
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
+ from typing import Any
7
+
8
+ import orjson
9
+ import yaml # type: ignore[import-untyped]
10
+
11
+ __all__ = ("minimal_yaml",)
12
+
13
+ # Maximum recursion depth for pruning to prevent stack overflow
14
+ MAX_PRUNE_DEPTH = 100
15
+
16
+
17
+ class MinimalDumper(yaml.SafeDumper):
18
+ """YAML dumper with minimal, readable settings."""
19
+
20
+ def ignore_aliases(self, data: Any) -> bool: # type: ignore[override]
21
+ """Disable anchors/aliases (&id001, *id001) for repeated objects."""
22
+ return True
23
+
24
+
25
+ def _represent_str(dumper: yaml.SafeDumper, data: str):
26
+ """Use block scalars for multiline text; plain style otherwise."""
27
+ if "\n" in data:
28
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
29
+ return dumper.represent_scalar("tag:yaml.org,2002:str", data)
30
+
31
+
32
+ MinimalDumper.add_representer(str, _represent_str)
33
+
34
+
35
+ def _is_empty(x: Any) -> bool:
36
+ """Define 'empty' for pruning. Keeps 0 and False."""
37
+ if x is None:
38
+ return True
39
+ if isinstance(x, str):
40
+ return x.strip() == ""
41
+ if isinstance(x, dict):
42
+ return len(x) == 0
43
+ if isinstance(x, list | tuple | set):
44
+ return len(x) == 0
45
+ return False
46
+
47
+
48
+ def _prune(x: Any, *, _depth: int = 0, _max_depth: int = MAX_PRUNE_DEPTH) -> Any:
49
+ """Recursively remove empty leaves and empty containers.
50
+
51
+ Args:
52
+ x: Value to prune
53
+ _depth: Current recursion depth (internal use)
54
+ _max_depth: Maximum recursion depth (default: 100)
55
+
56
+ Returns:
57
+ Pruned value with empty containers removed
58
+
59
+ Raises:
60
+ RecursionError: If depth exceeds _max_depth
61
+ """
62
+ if _depth > _max_depth:
63
+ msg = f"Pruning depth exceeds maximum ({_max_depth})"
64
+ raise RecursionError(msg)
65
+
66
+ if isinstance(x, dict):
67
+ pruned = {
68
+ k: _prune(v, _depth=_depth + 1, _max_depth=_max_depth)
69
+ for k, v in x.items()
70
+ if not _is_empty(v)
71
+ }
72
+ return {k: v for k, v in pruned.items() if not _is_empty(v)}
73
+ if isinstance(x, list):
74
+ pruned_list = [
75
+ _prune(v, _depth=_depth + 1, _max_depth=_max_depth)
76
+ for v in x
77
+ if not _is_empty(v)
78
+ ]
79
+ return [v for v in pruned_list if not _is_empty(v)]
80
+ if isinstance(x, tuple):
81
+ pruned_list = [
82
+ _prune(v, _depth=_depth + 1, _max_depth=_max_depth)
83
+ for v in x
84
+ if not _is_empty(v)
85
+ ]
86
+ return tuple(v for v in pruned_list if not _is_empty(v))
87
+ if isinstance(x, set):
88
+ pruned_set = {
89
+ _prune(v, _depth=_depth + 1, _max_depth=_max_depth)
90
+ for v in x
91
+ if not _is_empty(v)
92
+ }
93
+ return {v for v in pruned_set if not _is_empty(v)}
94
+ return x
95
+
96
+
97
+ def minimal_yaml(
98
+ value: Any,
99
+ *,
100
+ drop_empties: bool = True,
101
+ indent: int = 2,
102
+ line_width: int = 2**31 - 1,
103
+ sort_keys: bool = False,
104
+ unescape_html: bool = False,
105
+ ) -> str:
106
+ """Convert value to minimal YAML string.
107
+
108
+ Args:
109
+ value: Value to convert (dict, list, or JSON string)
110
+ drop_empties: Remove empty/None values recursively (default: True)
111
+ indent: YAML indentation level (default: 2)
112
+ line_width: Maximum line width (default: unlimited)
113
+ sort_keys: Sort dictionary keys alphabetically (default: False)
114
+ unescape_html: Unescape HTML entities in output (default: False)
115
+
116
+ Returns:
117
+ YAML-formatted string
118
+
119
+ Raises:
120
+ RecursionError: If value nesting exceeds maximum depth (100)
121
+
122
+ Security Note:
123
+ This function only DUMPS (outputs) YAML using SafeDumper - it does
124
+ not load/parse YAML, so it is not vulnerable to YAML deserialization
125
+ attacks. However, if unescape_html=True is used and the output is
126
+ later rendered in HTML without proper escaping, XSS vulnerabilities
127
+ may occur. Use caution with unescape_html in web contexts.
128
+ """
129
+ # Auto-parse JSON strings for convenience (fails gracefully on invalid JSON)
130
+ if isinstance(value, str):
131
+ try:
132
+ value = orjson.loads(value)
133
+ except orjson.JSONDecodeError:
134
+ # Not valid JSON - treat as plain string
135
+ pass
136
+
137
+ data = _prune(value) if drop_empties else value
138
+ str_ = yaml.dump(
139
+ data,
140
+ Dumper=MinimalDumper,
141
+ default_flow_style=False,
142
+ sort_keys=sort_keys,
143
+ allow_unicode=True,
144
+ indent=indent,
145
+ width=line_width,
146
+ )
147
+ if unescape_html:
148
+ import html
149
+
150
+ return html.unescape(str_)
151
+ return str_
@@ -0,0 +1,153 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ def _type_map(json_type: str) -> str:
6
+ """Map JSON Schema types to TypeScript-like types."""
7
+ mapping = {
8
+ "string": "string",
9
+ "integer": "int",
10
+ "number": "float",
11
+ "boolean": "bool",
12
+ "array": "array", # Will be handled specially
13
+ "object": "object",
14
+ "null": "null",
15
+ }
16
+ return mapping.get(json_type, json_type)
17
+
18
+
19
+ def _format_enum_union(enum_values: list) -> str:
20
+ """Format enum values as TypeScript union of literals."""
21
+ formatted = []
22
+ for val in enum_values:
23
+ if isinstance(val, str):
24
+ formatted.append(f'"{val}"')
25
+ elif val is None:
26
+ formatted.append("null")
27
+ else:
28
+ formatted.append(str(val))
29
+ return " | ".join(formatted)
30
+
31
+
32
+ def _extract_type_signature(field_spec: dict, required: bool) -> tuple[str, bool]:
33
+ """Extract TypeScript-style type signature from JSON Schema field."""
34
+ # Handle enums first (most specific)
35
+ if "enum" in field_spec:
36
+ type_sig = _format_enum_union(field_spec["enum"])
37
+ # Check if enum includes null
38
+ has_null = None in field_spec["enum"]
39
+ is_optional = not required or has_null
40
+ return type_sig, is_optional
41
+
42
+ # Handle anyOf (unions)
43
+ if "anyOf" in field_spec:
44
+ options = field_spec["anyOf"]
45
+ type_parts = []
46
+ has_null = False
47
+
48
+ for opt in options:
49
+ if opt.get("type") == "null":
50
+ has_null = True
51
+ continue
52
+
53
+ if "type" in opt:
54
+ if opt["type"] == "array" and "items" in opt:
55
+ item_type = _type_map(opt["items"].get("type", "any"))
56
+ if "$ref" in opt["items"]:
57
+ item_type = opt["items"]["$ref"].split("/")[-1]
58
+ type_parts.append(f"{item_type}[]")
59
+ else:
60
+ type_parts.append(_type_map(opt["type"]))
61
+ elif "$ref" in opt:
62
+ ref_name = opt["$ref"].split("/")[-1]
63
+ type_parts.append(ref_name)
64
+ elif "enum" in opt:
65
+ type_parts.append(_format_enum_union(opt["enum"]))
66
+
67
+ if has_null:
68
+ type_parts.append("null")
69
+
70
+ type_sig = " | ".join(type_parts) if type_parts else "any"
71
+ is_optional = not required or has_null
72
+ return type_sig, is_optional
73
+
74
+ # Handle arrays
75
+ if "type" in field_spec and field_spec["type"] == "array":
76
+ if "items" in field_spec:
77
+ items_spec = field_spec["items"]
78
+ if "type" in items_spec:
79
+ item_type = _type_map(items_spec["type"])
80
+ elif "$ref" in items_spec:
81
+ item_type = items_spec["$ref"].split("/")[-1]
82
+ elif "enum" in items_spec:
83
+ item_type = f"({_format_enum_union(items_spec['enum'])})"
84
+ else:
85
+ item_type = "any"
86
+ else:
87
+ item_type = "any"
88
+ type_sig = f"{item_type}[]"
89
+ is_optional = not required
90
+ return type_sig, is_optional
91
+
92
+ # Handle $ref
93
+ if "$ref" in field_spec:
94
+ ref_name = field_spec["$ref"].split("/")[-1]
95
+ is_optional = not required
96
+ return ref_name, is_optional
97
+
98
+ # Handle simple types
99
+ if "type" in field_spec:
100
+ base_type = field_spec["type"]
101
+ # Handle nullable simple types (type? suffix from simplification)
102
+ if isinstance(base_type, str) and base_type.endswith("?"):
103
+ type_sig = _type_map(base_type[:-1])
104
+ is_optional = True
105
+ else:
106
+ type_sig = _type_map(base_type)
107
+ is_optional = not required
108
+ return type_sig, is_optional
109
+
110
+ # Fallback
111
+ return "any", not required
112
+
113
+
114
+ def typescript_schema(schema: dict, indent: int = 0) -> str:
115
+ """Convert JSON Schema to TypeScript-style notation for optimal LLM comprehension."""
116
+ lines = []
117
+ prefix = " " * indent
118
+
119
+ if "properties" not in schema:
120
+ return ""
121
+
122
+ required_fields = set(schema.get("required", []))
123
+
124
+ for field_name, field_spec in schema["properties"].items():
125
+ is_required = field_name in required_fields
126
+
127
+ # Extract type signature
128
+ type_sig, is_optional = _extract_type_signature(field_spec, is_required)
129
+
130
+ # Add default value if present
131
+ default_str = ""
132
+ if "default" in field_spec:
133
+ default_val = field_spec["default"]
134
+ if isinstance(default_val, str):
135
+ default_str = f' = "{default_val}"'
136
+ elif default_val is None:
137
+ default_str = " = null"
138
+ elif isinstance(default_val, bool):
139
+ default_str = f" = {'true' if default_val else 'false'}"
140
+ else:
141
+ default_str = f" = {default_val}"
142
+
143
+ # Build field definition
144
+ optional_marker = "?" if is_optional else ""
145
+ field_def = f"{prefix}{field_name}{optional_marker}: {type_sig}{default_str}"
146
+
147
+ # Add description if present
148
+ if field_spec.get("description"):
149
+ field_def += f" - {field_spec['description']}"
150
+
151
+ lines.append(field_def)
152
+
153
+ return "\n".join(lines)
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
 
11
11
  import re
12
12
 
13
- from kronos.errors import ValidationError
13
+ from krons.errors import ValidationError
14
14
 
15
15
  __all__ = (
16
16
  "validate_identifier",
@@ -0,0 +1,3 @@
1
+ from ._validate_image_url import validate_image_url
2
+
3
+ __all__ = ("validate_image_url",)
@@ -0,0 +1,56 @@
1
+ from urllib.parse import urlparse
2
+
3
+ __all__ = ("validate_image_url",)
4
+
5
+
6
+ def validate_image_url(url: str) -> None:
7
+ """Validate image URL to prevent security vulnerabilities.
8
+
9
+ Security checks:
10
+ - Reject null bytes (path truncation attacks)
11
+ - Reject file:// URLs (local file access)
12
+ - Reject javascript: URLs (XSS attacks)
13
+ - Reject data:// URLs (DoS via large embedded images)
14
+ - Only allow http:// and https:// schemes
15
+ - Validate URL format
16
+
17
+ Args:
18
+ url: URL to validate
19
+
20
+ Raises:
21
+ ValueError: If URL is invalid or uses disallowed scheme
22
+ """
23
+
24
+ if not url or not isinstance(url, str):
25
+ raise ValueError(
26
+ f"Image URL must be non-empty string, got: {type(url).__name__}"
27
+ )
28
+
29
+ # Reject null bytes (path truncation attacks)
30
+ # Check both literal null bytes and percent-encoded %00
31
+ if "\x00" in url:
32
+ raise ValueError(
33
+ "Image URL contains null byte - potential path truncation attack"
34
+ )
35
+ if "%00" in url.lower():
36
+ raise ValueError(
37
+ "Image URL contains percent-encoded null byte (%00) - potential path truncation attack"
38
+ )
39
+
40
+ try:
41
+ parsed = urlparse(url)
42
+ except Exception as e:
43
+ raise ValueError(f"Malformed image URL '{url}': {e}") from e
44
+
45
+ # Only allow http and https schemes
46
+ if parsed.scheme not in ("http", "https"):
47
+ raise ValueError(
48
+ f"Image URL must use http:// or https:// scheme, got: {parsed.scheme}://"
49
+ f"\nRejected URL: {url}"
50
+ f"\nReason: Disallowed schemes (file://, javascript://, data://) pose "
51
+ f"security risks (local file access, XSS, DoS)"
52
+ )
53
+
54
+ # Ensure netloc (domain) is present for http/https
55
+ if not parsed.netloc:
56
+ raise ValueError(f"Image URL missing domain: {url}")
krons/work/__init__.py ADDED
@@ -0,0 +1,126 @@
1
+ # Copyright (c) 2025 - 2026, HaiyangLi <quantocean.li at gmail dot com>
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Work system - Declarative workflow orchestration.
5
+
6
+ Two complementary patterns at different abstraction levels:
7
+
8
+ **Report** (artifact state):
9
+ Declarative workflow definition via form_assignments DSL.
10
+ Tracks one specific job's progress through the workflow.
11
+ Dependencies implicit from field names.
12
+
13
+ class HiringBriefReport(Report):
14
+ role_classification: RoleClassification | None = None
15
+ strategic_context: StrategicContext | None = None
16
+
17
+ assignment: str = "job_input -> executive_summary"
18
+
19
+ form_assignments: list[str] = [
20
+ "classifier: job_input -> role_classification | api:fast",
21
+ "strategist: job_input, role_classification -> strategic_context | api:synthesis",
22
+ ]
23
+
24
+ **Worker** (execution capability):
25
+ Functional station that can execute forms.
26
+ Has internal DAG for retries/error handling.
27
+ Matches to forms via resource hints.
28
+
29
+ class ClassifierWorker(Worker):
30
+ @work(assignment="job_input -> role_classification")
31
+ async def classify(self, job_input, **kwargs):
32
+ return await self.llm.chat(**kwargs)
33
+
34
+ Core concepts:
35
+ - Phrase: Typed operation signature (inputs -> outputs)
36
+ - Form: Data binding + scheduling (stateful artifact)
37
+ - Report: Multi-step workflow declaration (stateful artifact)
38
+ - Worker: Execution capability (stateless station)
39
+ - WorkerEngine: Execution driver
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ from typing import TYPE_CHECKING
45
+
46
+ # Lazy import mapping
47
+ _LAZY_IMPORTS: dict[str, tuple[str, str]] = {
48
+ # engine
49
+ "WorkerEngine": ("krons.work.engine", "WorkerEngine"),
50
+ "WorkerTask": ("krons.work.engine", "WorkerTask"),
51
+ # form
52
+ "Form": ("krons.work.form", "Form"),
53
+ "ParsedAssignment": ("krons.work.form", "ParsedAssignment"),
54
+ "parse_assignment": ("krons.work.form", "parse_assignment"),
55
+ "parse_full_assignment": ("krons.work.form", "parse_full_assignment"),
56
+ # phrase
57
+ "CrudOperation": ("krons.work.phrase", "CrudOperation"),
58
+ "CrudPattern": ("krons.work.phrase", "CrudPattern"),
59
+ "Phrase": ("krons.work.phrase", "Phrase"),
60
+ "phrase": ("krons.work.phrase", "phrase"),
61
+ # report
62
+ "Report": ("krons.work.report", "Report"),
63
+ # worker
64
+ "Worker": ("krons.work.worker", "Worker"),
65
+ "WorkConfig": ("krons.work.worker", "WorkConfig"),
66
+ "WorkLink": ("krons.work.worker", "WorkLink"),
67
+ "work": ("krons.work.worker", "work"),
68
+ "worklink": ("krons.work.worker", "worklink"),
69
+ }
70
+
71
+ _LOADED: dict[str, object] = {}
72
+
73
+
74
+ def __getattr__(name: str) -> object:
75
+ """Lazy import attributes on first access."""
76
+ if name in _LOADED:
77
+ return _LOADED[name]
78
+
79
+ if name in _LAZY_IMPORTS:
80
+ from importlib import import_module
81
+
82
+ module_name, attr_name = _LAZY_IMPORTS[name]
83
+ module = import_module(module_name)
84
+ value = getattr(module, attr_name)
85
+ _LOADED[name] = value
86
+ return value
87
+
88
+ raise AttributeError(f"module 'krons.work' has no attribute {name!r}")
89
+
90
+
91
+ def __dir__() -> list[str]:
92
+ """Return all available attributes for autocomplete."""
93
+ return list(__all__)
94
+
95
+
96
+ # TYPE_CHECKING block for static analysis
97
+ if TYPE_CHECKING:
98
+ from krons.work.engine import WorkerEngine, WorkerTask
99
+ from krons.work.form import (
100
+ Form,
101
+ ParsedAssignment,
102
+ parse_assignment,
103
+ parse_full_assignment,
104
+ )
105
+ from krons.work.phrase import CrudOperation, CrudPattern, Phrase, phrase
106
+ from krons.work.report import Report
107
+ from krons.work.worker import WorkConfig, Worker, WorkLink, work, worklink
108
+
109
+ __all__ = (
110
+ "CrudOperation",
111
+ "CrudPattern",
112
+ "Form",
113
+ "ParsedAssignment",
114
+ "Phrase",
115
+ "Report",
116
+ "WorkConfig",
117
+ "WorkLink",
118
+ "Worker",
119
+ "WorkerEngine",
120
+ "WorkerTask",
121
+ "parse_assignment",
122
+ "parse_full_assignment",
123
+ "phrase",
124
+ "work",
125
+ "worklink",
126
+ )