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
@@ -10,23 +10,19 @@ Core types:
10
10
  Exchange: Async message router between entity mailboxes.
11
11
 
12
12
  Validators (raise on failure):
13
- resource_must_exist_in_session
14
- resource_must_be_accessible_by_branch
15
- capabilities_must_be_subset_of_branch
16
- resolve_branch_exists_in_session
13
+ resource_must_exist
14
+ resource_must_be_accessible
15
+ capabilities_must_be_granted
17
16
  """
18
17
 
18
+ from .constraints import (
19
+ capabilities_must_be_granted,
20
+ resource_must_be_accessible,
21
+ resource_must_exist,
22
+ )
19
23
  from .exchange import Exchange
20
24
  from .message import Message
21
- from .session import (
22
- Branch,
23
- Session,
24
- SessionConfig,
25
- capabilities_must_be_subset_of_branch,
26
- resolve_branch_exists_in_session,
27
- resource_must_be_accessible_by_branch,
28
- resource_must_exist_in_session,
29
- )
25
+ from .session import Branch, Session, SessionConfig
30
26
 
31
27
  __all__ = (
32
28
  "Branch",
@@ -34,8 +30,7 @@ __all__ = (
34
30
  "Message",
35
31
  "Session",
36
32
  "SessionConfig",
37
- "capabilities_must_be_subset_of_branch",
38
- "resolve_branch_exists_in_session",
39
- "resource_must_be_accessible_by_branch",
40
- "resource_must_exist_in_session",
33
+ "capabilities_must_be_granted",
34
+ "resource_must_be_accessible",
35
+ "resource_must_exist",
41
36
  )
@@ -0,0 +1,70 @@
1
+ from krons.errors import AccessError, ConfigurationError, ExistsError, NotFoundError
2
+
3
+ __all__ = (
4
+ "resource_must_exist",
5
+ "resource_must_be_accessible",
6
+ "capabilities_must_be_granted",
7
+ "branch_name_must_be_unique",
8
+ "genai_model_must_be_configured",
9
+ )
10
+
11
+
12
+ def resource_must_exist(session, name: str):
13
+ """Validate resource exists in session. Raise NotFoundError if not."""
14
+ if not session.resources.has(name):
15
+ raise NotFoundError(
16
+ f"Service '{name}' not found in session services",
17
+ details={"available": session.resources.list_names()},
18
+ )
19
+
20
+
21
+ def resource_must_be_accessible(branch, name: str) -> None:
22
+ """Validate branch has resource access. Raise AccessError if not."""
23
+ if name not in branch.resources:
24
+ raise AccessError(
25
+ f"Branch '{branch.name}' has no access to resource '{name}'",
26
+ details={
27
+ "branch": branch.name,
28
+ "resource": name,
29
+ "available": list(branch.resources),
30
+ },
31
+ )
32
+
33
+
34
+ def capabilities_must_be_granted(branch, capabilities: set[str]) -> None:
35
+ """Validate branch has all capabilities. Raise AccessError listing missing."""
36
+ if not capabilities.issubset(branch.capabilities):
37
+ missing = capabilities - branch.capabilities
38
+ raise AccessError(
39
+ f"Branch '{branch.name}' missing capabilities: {missing}",
40
+ details={
41
+ "requested": sorted(capabilities),
42
+ "available": sorted(branch.capabilities),
43
+ },
44
+ )
45
+
46
+
47
+ def branch_name_must_be_unique(session, name: str) -> None:
48
+ try:
49
+ session.communications.get_progression(name)
50
+ # If we get here, the name exists - not unique
51
+ raise ExistsError(f"Branch with name '{name}' already exists")
52
+ except KeyError:
53
+ # KeyError means name not found - it's unique, which is good
54
+ pass
55
+
56
+
57
+ def genai_model_must_be_configured(session) -> None:
58
+ """Validate session has a default GenAI model configured.
59
+
60
+ Args:
61
+ session: Session to check
62
+
63
+ Raises:
64
+ ConfigurationError: If no default model configured
65
+ """
66
+ if session.default_gen_model is None:
67
+ raise ConfigurationError(
68
+ "Session has no default_gen_model configured",
69
+ details={"session_id": str(session.id)},
70
+ )
@@ -14,9 +14,9 @@ from uuid import UUID
14
14
 
15
15
  from pydantic import PrivateAttr
16
16
 
17
- from kronos.core import Element, Flow, Pile, Progression
18
- from kronos.errors import ExistsError
19
- from kronos.utils import concurrency
17
+ from krons.core import Element, Flow, Pile, Progression
18
+ from krons.errors import ExistsError
19
+ from krons.utils import concurrency
20
20
 
21
21
  from .message import Message
22
22
 
@@ -134,11 +134,17 @@ class Exchange(Element):
134
134
  except Exception:
135
135
  message_copy = message.model_copy()
136
136
  deliveries.append((other_id, message_copy))
137
- elif message.recipient is not None and message.recipient in self._owner_index:
137
+ elif (
138
+ message.recipient is not None
139
+ and message.recipient in self._owner_index
140
+ ):
138
141
  deliveries.append((message.recipient, message))
139
142
  if deliveries:
140
143
  await concurrency.gather(
141
- *[self._deliver_to(recipient_id, message) for recipient_id, message in deliveries],
144
+ *[
145
+ self._deliver_to(recipient_id, message)
146
+ for recipient_id, message in deliveries
147
+ ],
142
148
  return_exceptions=True,
143
149
  )
144
150
 
@@ -196,7 +202,9 @@ class Exchange(Element):
196
202
  if flow is None:
197
203
  raise ValueError(f"Sender {sender} not registered")
198
204
 
199
- message = Message(sender=sender, recipient=recipient, content=content, channel=channel)
205
+ message = Message(
206
+ sender=sender, recipient=recipient, content=content, channel=channel
207
+ )
200
208
  flow.add_item(message, progressions=OUTBOX)
201
209
  return message
202
210
 
@@ -8,7 +8,7 @@ from uuid import UUID
8
8
 
9
9
  from pydantic import Field, field_validator
10
10
 
11
- from kronos.core import Node
11
+ from krons.core import Node
12
12
 
13
13
 
14
14
  class Message(Node):
@@ -27,7 +27,9 @@ class Message(Node):
27
27
  content: Any
28
28
  sender: UUID | None = None
29
29
  recipient: UUID | None = None
30
- channel: str | None = Field(None, description="Optional namespace for message grouping")
30
+ channel: str | None = Field(
31
+ None, description="Optional namespace for message grouping"
32
+ )
31
33
 
32
34
  @property
33
35
  def is_broadcast(self) -> bool:
@@ -0,0 +1,35 @@
1
+ from krons.core import Pile
2
+ from krons.core.types import ID
3
+
4
+ from .session import Session
5
+
6
+ SESSION_REGISTRY: Pile[Session] = Pile(item_type=Session, strict_type=True)
7
+
8
+
9
+ async def get_session(session_id: ID[Session]) -> Session:
10
+ if session_id not in SESSION_REGISTRY:
11
+ raise ValueError(f"Session with id {session_id} not found in registry.")
12
+ async with SESSION_REGISTRY:
13
+ return SESSION_REGISTRY[session_id]
14
+
15
+
16
+ async def create_session():
17
+ session = Session()
18
+ async with SESSION_REGISTRY:
19
+ SESSION_REGISTRY.add(session)
20
+
21
+
22
+ async def delete_session(session_id: ID[Session]):
23
+ if session_id not in SESSION_REGISTRY:
24
+ raise ValueError(f"Session with id {session_id} not found in registry.")
25
+ async with SESSION_REGISTRY:
26
+ SESSION_REGISTRY.remove(session_id)
27
+
28
+
29
+ async def list_sessions_ids() -> list[ID[Session]]:
30
+ return list(SESSION_REGISTRY.keys())
31
+
32
+
33
+ async def clear_sessions():
34
+ async with SESSION_REGISTRY:
35
+ SESSION_REGISTRY.clear()
@@ -10,50 +10,27 @@ Branch is a named message progression with capability/resource access control.
10
10
  from __future__ import annotations
11
11
 
12
12
  import contextlib
13
- from collections.abc import Iterable
14
- from typing import TYPE_CHECKING, Any, Literal
13
+ from collections.abc import AsyncGenerator, Iterable
14
+ from typing import Any, Literal
15
15
  from uuid import UUID
16
16
 
17
- from pydantic import Field
17
+ from pydantic import Field, model_validator
18
18
 
19
- from kronos.core import Element, Flow, Progression
20
- from kronos.errors import AccessError, NotFoundError
21
- from kronos.operations.node import Operation
22
- from kronos.operations.registry import OperationRegistry
23
- from kronos.services import ServiceRegistry
24
- from kronos.types import HashableModel, Unset, UnsetType, not_sentinel
19
+ from krons.core import Element, Flow, Pile, Progression
20
+ from krons.core.types import HashableModel, Unset, UnsetType, not_sentinel
21
+ from krons.errors import NotFoundError
22
+ from krons.resource import Calling, ResourceRegistry, iModel
23
+ from krons.work.operations import Operation, OperationRegistry, RequestContext
25
24
 
26
25
  from .message import Message
27
26
 
28
- if TYPE_CHECKING:
29
- from kronos.services.backend import Calling
30
-
31
27
  __all__ = (
32
28
  "Branch",
33
29
  "Session",
34
30
  "SessionConfig",
35
- "capabilities_must_be_subset_of_branch",
36
- "resource_must_be_accessible_by_branch",
37
- "resource_must_exist_in_session",
38
31
  )
39
32
 
40
33
 
41
- class SessionConfig(HashableModel):
42
- """Session initialization configuration.
43
-
44
- Attributes:
45
- default_branch_name: Name for auto-created default branch.
46
- default_capabilities: Capabilities granted to default branch.
47
- default_resources: Resources accessible by default branch.
48
- auto_create_default_branch: Create "main" branch on init.
49
- """
50
-
51
- default_branch_name: str | None = None
52
- default_capabilities: set[str] = Field(default_factory=set)
53
- default_resources: set[str] = Field(default_factory=set)
54
- auto_create_default_branch: bool = True
55
-
56
-
57
34
  class Branch(Progression):
58
35
  """Message progression with capability and resource access control.
59
36
 
@@ -67,95 +44,85 @@ class Branch(Progression):
67
44
  """
68
45
 
69
46
  session_id: UUID = Field(..., frozen=True)
70
- capabilities: set[str] = Field(default_factory=set)
71
- resources: set[str] = Field(default_factory=set)
47
+ capabilities: set[str] = Field(default_factory=set, frozen=True)
48
+ resources: set[str] = Field(default_factory=set, frozen=True)
72
49
 
73
50
  def __repr__(self) -> str:
74
51
  name_str = f" name='{self.name}'" if self.name else ""
75
52
  return f"Branch(messages={len(self)}, session={str(self.session_id)[:8]}{name_str})"
76
53
 
77
54
 
78
- class Session(Element):
79
- """Central orchestrator: messages, branches, services, operations.
80
-
81
- Lifecycle:
82
- 1. Create session (auto-creates default branch unless disabled)
83
- 2. Register services and operations
84
- 3. Create branches for different contexts/users
85
- 4. Add messages, conduct operations, make service requests
55
+ class SessionConfig(HashableModel):
56
+ default_branch_name: str | None = None
57
+ shared_capabilities: set[str] = Field(default_factory=set)
58
+ shared_resources: set[str] = Field(default_factory=set)
59
+ default_gen_model: str | None = None
60
+ default_parse_model: str | None = None
61
+ auto_create_default_branch: bool = True
86
62
 
87
- Attributes:
88
- user: Optional user identifier.
89
- communications: Flow containing messages and branches.
90
- services: Service registry for backend access.
91
- operations: Operation factory registry.
92
- config: Session configuration.
93
- default_branch_id: Branch used when none specified.
94
-
95
- Example:
96
- session = Session(user="agent-1")
97
- session.services.register("openai", openai_service)
98
- session.operations.register("chat", chat_factory)
99
- result = await session.conduct("chat", params={"prompt": "hello"})
100
- """
101
63
 
64
+ class Session(Element):
102
65
  user: str | None = None
103
- communications: Flow[Message, Branch] = None # type: ignore
104
- services: ServiceRegistry = None # type: ignore
105
- operations: OperationRegistry = None # type: ignore
106
- config: SessionConfig = None # type: ignore
66
+ communications: Flow[Message, Branch] = Field(
67
+ default_factory=lambda: Flow(item_type=Message)
68
+ )
69
+ resources: ResourceRegistry = Field(default_factory=ResourceRegistry)
70
+ operations: OperationRegistry = Field(default_factory=OperationRegistry)
71
+ config: SessionConfig = Field(default_factory=SessionConfig)
107
72
  default_branch_id: UUID | None = None
108
73
 
109
- def __init__(
110
- self,
111
- user: str | None = None,
112
- communications: Flow[Message, Branch] | None = None,
113
- services: ServiceRegistry | None = None,
114
- operations: OperationRegistry | None = None,
115
- config: SessionConfig | dict | None = None,
116
- default_branch_id: UUID | None = None,
117
- **data,
118
- ):
119
- """Initialize session with optional pre-configured components.
74
+ @model_validator(mode="after")
75
+ def _validate_default_branch(self) -> Session:
76
+ """Auto-create default branch and register built-in operations."""
77
+ if self.config.auto_create_default_branch and self.default_branch is None:
78
+ default_branch_name = self.config.default_branch_name or "main"
79
+ self.create_branch(
80
+ name=default_branch_name,
81
+ capabilities=self.config.shared_capabilities,
82
+ resources=self.config.shared_resources,
83
+ )
84
+ self.set_default_branch(default_branch_name)
85
+
86
+ # Register built-in operations (lazy import avoids circular)
87
+ from krons.agent.operations import (
88
+ generate,
89
+ operate,
90
+ react,
91
+ react_stream,
92
+ structure,
93
+ )
120
94
 
121
- Args:
122
- user: User identifier.
123
- communications: Pre-existing Flow (creates new if None).
124
- services: Pre-existing ServiceRegistry (creates new if None).
125
- operations: Pre-existing OperationRegistry (creates new if None).
126
- config: SessionConfig or dict (uses defaults if None).
127
- default_branch_id: Pre-set default branch.
128
- **data: Additional Element fields.
129
- """
130
- super().__init__(**data)
131
- self.user = user
132
- self.communications = communications or Flow(item_type=Message)
133
- self.services = services or ServiceRegistry()
134
- self.operations = operations or OperationRegistry()
135
- self.default_branch_id = default_branch_id
136
-
137
- if config is None:
138
- self.config = SessionConfig()
139
- elif isinstance(config, dict):
140
- self.config = SessionConfig(**config)
141
- else:
142
- self.config = config
95
+ for name, handler in (
96
+ ("generate", generate),
97
+ ("structure", structure),
98
+ ("operate", operate),
99
+ ("react", react),
100
+ ("react_stream", react_stream),
101
+ ):
102
+ if not self.operations.has(name):
103
+ self.operations.register(name, handler)
143
104
 
144
- if self.config.auto_create_default_branch and self.default_branch_id is None:
145
- branch = self.create_branch(
146
- name=self.config.default_branch_name or "main",
147
- capabilities=self.config.default_capabilities,
148
- resources=self.config.default_resources,
149
- )
150
- self.default_branch_id = branch.id
105
+ return self
106
+
107
+ @property
108
+ def default_gen_model(self) -> iModel | None:
109
+ if self.config.default_gen_model is None:
110
+ return None
111
+ return self.resources.get(self.config.default_gen_model)
112
+
113
+ @property
114
+ def default_parse_model(self) -> iModel | None:
115
+ if self.config.default_parse_model is None:
116
+ return None
117
+ return self.resources.get(self.config.default_parse_model)
151
118
 
152
119
  @property
153
- def messages(self):
120
+ def messages(self) -> Pile[Message]:
154
121
  """All messages in session (Pile[Message])."""
155
122
  return self.communications.items
156
123
 
157
124
  @property
158
- def branches(self):
125
+ def branches(self) -> Pile[Branch]:
159
126
  """All branches in session (Pile[Branch])."""
160
127
  return self.communications.progressions
161
128
 
@@ -187,10 +154,14 @@ class Session(Element):
187
154
  Returns:
188
155
  Created Branch added to session.
189
156
  """
157
+ if name:
158
+ from .constraints import branch_name_must_be_unique
159
+
160
+ branch_name_must_be_unique(self, name)
161
+
190
162
  order: list[UUID] = []
191
163
  if messages:
192
- for msg in messages:
193
- order.append(msg.id if isinstance(msg, Message) else msg)
164
+ order.extend([self._coerce_id(msg) for msg in messages])
194
165
 
195
166
  branch = Branch(
196
167
  session_id=self.id,
@@ -265,9 +236,13 @@ class Session(Element):
265
236
  name=name or f"{source.name}_fork",
266
237
  messages=source.order,
267
238
  capabilities=(
268
- {*source.capabilities} if capabilities is True else (capabilities or set())
239
+ {*source.capabilities}
240
+ if capabilities is True
241
+ else (capabilities or set())
242
+ ),
243
+ resources=(
244
+ {*source.resources} if resources is True else (resources or set())
269
245
  ),
270
- resources=({*source.resources} if resources is True else (resources or set())),
271
246
  )
272
247
 
273
248
  forked.metadata["forked_from"] = {
@@ -288,38 +263,25 @@ class Session(Element):
288
263
 
289
264
  async def request(
290
265
  self,
291
- service_name: str,
292
- *,
266
+ name: str,
267
+ /,
293
268
  branch: Branch | UUID | str | None = None,
294
269
  poll_timeout: float | None = None,
295
270
  poll_interval: float | None = None,
296
- **kwargs,
271
+ **options,
297
272
  ) -> Calling:
298
- """Direct service invocation with optional access control.
299
-
300
- Args:
301
- service_name: Registered service name.
302
- branch: If provided, checks service in branch.resources.
303
- poll_timeout: Max wait seconds.
304
- poll_interval: Poll interval seconds.
305
- **kwargs: Service-specific arguments.
306
-
307
- Returns:
308
- Calling with execution results.
309
-
310
- Raises:
311
- AccessError: If branch lacks access to service.
312
- NotFoundError: If service not registered.
313
- """
314
273
  if branch is not None:
315
274
  resolved_branch = self.get_branch(branch)
316
- resource_must_be_accessible_by_branch(resolved_branch, service_name)
317
275
 
318
- service = self.services.get(service_name)
319
- return await service.invoke(
276
+ from .constraints import resource_must_be_accessible
277
+
278
+ resource_must_be_accessible(resolved_branch, name)
279
+
280
+ resource = self.resources.get(name)
281
+ return await resource.invoke(
320
282
  poll_timeout=poll_timeout,
321
283
  poll_interval=poll_interval,
322
- **kwargs,
284
+ **options,
323
285
  )
324
286
 
325
287
  async def conduct(
@@ -327,6 +289,7 @@ class Session(Element):
327
289
  operation_type: str,
328
290
  branch: Branch | UUID | str | None = None,
329
291
  params: Any | None = None,
292
+ verbose: bool = False,
330
293
  ) -> Operation:
331
294
  """Execute operation via registry.
332
295
 
@@ -334,6 +297,7 @@ class Session(Element):
334
297
  operation_type: Registry key.
335
298
  branch: Target branch (default if None).
336
299
  params: Operation parameters.
300
+ verbose: Print real-time status updates.
337
301
 
338
302
  Returns:
339
303
  Invoked Operation (result in op.execution.response).
@@ -343,16 +307,85 @@ class Session(Element):
343
307
  KeyError: Operation not registered.
344
308
  """
345
309
  resolved = self._resolve_branch(branch)
310
+
311
+ if verbose:
312
+ from krons.utils.display import Timer, status
313
+
314
+ branch_name = resolved.name or str(resolved.id)[:8]
315
+ status(
316
+ f"conduct({operation_type}) on branch={branch_name}",
317
+ style="info",
318
+ )
319
+
346
320
  op = Operation(
347
321
  operation_type=operation_type,
348
322
  parameters=params,
349
- timeout=None,
350
- streaming=False,
351
323
  )
324
+ op._verbose = verbose
352
325
  op.bind(self, resolved)
353
- await op.invoke()
326
+
327
+ if verbose:
328
+ with Timer(f"{operation_type} completed"):
329
+ await op.invoke()
330
+
331
+ resp = op.execution.response
332
+ if op.execution.error:
333
+ status(f"ERROR: {op.execution.error}", style="error")
334
+ elif isinstance(resp, str):
335
+ status(f"response: {len(resp)} chars", style="success")
336
+ else:
337
+ status(f"response: {type(resp).__name__}", style="success")
338
+ else:
339
+ await op.invoke()
340
+
354
341
  return op
355
342
 
343
+ async def stream_conduct(
344
+ self,
345
+ operation_type: str,
346
+ branch: Branch | UUID | str | None = None,
347
+ params: Any | None = None,
348
+ verbose: bool = False,
349
+ ) -> AsyncGenerator[Any, None]:
350
+ """Stream operation results via async generator.
351
+
352
+ For streaming handlers like react_stream that yield intermediate
353
+ results per round. Bypasses the Operation wrapper since streaming
354
+ handlers produce multiple values, not a single response.
355
+
356
+ Args:
357
+ operation_type: Registry key (e.g. "react_stream").
358
+ branch: Target branch (default if None).
359
+ params: Operation parameters.
360
+ verbose: Enable real-time streaming output.
361
+
362
+ Yields:
363
+ Handler results (e.g., ReActAnalysis per round).
364
+ """
365
+ resolved = self._resolve_branch(branch)
366
+ handler = self.operations.get(operation_type)
367
+
368
+ ctx = RequestContext(
369
+ name=operation_type,
370
+ session_id=self.id,
371
+ branch=resolved.name or str(resolved.id),
372
+ _bound_session=self,
373
+ _bound_branch=resolved,
374
+ _verbose=verbose,
375
+ )
376
+
377
+ if verbose:
378
+ from krons.utils.display import status
379
+
380
+ branch_name = resolved.name or str(resolved.id)[:8]
381
+ status(
382
+ f"stream_conduct({operation_type}) on branch={branch_name}",
383
+ style="info",
384
+ )
385
+
386
+ async for result in handler(params, ctx):
387
+ yield result
388
+
356
389
  def _resolve_branch(self, branch: Branch | UUID | str | None) -> Branch:
357
390
  """Resolve to Branch, falling back to default. Raises if neither available."""
358
391
  if branch is not None:
@@ -365,47 +398,5 @@ class Session(Element):
365
398
  return (
366
399
  f"Session(messages={len(self.messages)}, "
367
400
  f"branches={len(self.branches)}, "
368
- f"services={len(self.services)})"
369
- )
370
-
371
-
372
- def resource_must_exist_in_session(session: Session, name: str) -> None:
373
- """Validate service exists. Raise NotFoundError with available names if not."""
374
- if not session.services.has(name):
375
- raise NotFoundError(
376
- f"Service '{name}' not found in session services",
377
- details={"available": session.services.list_names()},
378
- )
379
-
380
-
381
- def resource_must_be_accessible_by_branch(branch: Branch, name: str) -> None:
382
- """Validate branch has resource access. Raise AccessError if not."""
383
- if name not in branch.resources:
384
- raise AccessError(
385
- f"Branch '{branch.name}' has no access to resource '{name}'",
386
- details={
387
- "branch": branch.name,
388
- "resource": name,
389
- "available": list(branch.resources),
390
- },
401
+ f"services={len(self.resources)})"
391
402
  )
392
-
393
-
394
- def capabilities_must_be_subset_of_branch(branch: Branch, capabilities: set[str]) -> None:
395
- """Validate branch has all capabilities. Raise AccessError listing missing."""
396
- if not capabilities.issubset(branch.capabilities):
397
- missing = capabilities - branch.capabilities
398
- raise AccessError(
399
- f"Branch '{branch.name}' missing capabilities: {missing}",
400
- details={
401
- "requested": sorted(capabilities),
402
- "available": sorted(branch.capabilities),
403
- },
404
- )
405
-
406
-
407
- def resolve_branch_exists_in_session(session: Session, branch: Branch | str) -> Branch:
408
- """Get branch from session. Raise NotFoundError if not found."""
409
- if (b_ := session.get_branch(branch, None)) is None:
410
- raise NotFoundError(f"Branch '{branch}' does not exist in session")
411
- return b_