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.
- krons/__init__.py +49 -0
- krons/agent/__init__.py +144 -0
- krons/agent/mcps/__init__.py +14 -0
- krons/agent/mcps/loader.py +287 -0
- krons/agent/mcps/wrapper.py +799 -0
- krons/agent/message/__init__.py +20 -0
- krons/agent/message/action.py +69 -0
- krons/agent/message/assistant.py +52 -0
- krons/agent/message/common.py +49 -0
- krons/agent/message/instruction.py +130 -0
- krons/agent/message/prepare_msg.py +187 -0
- krons/agent/message/role.py +53 -0
- krons/agent/message/system.py +53 -0
- krons/agent/operations/__init__.py +82 -0
- krons/agent/operations/act.py +100 -0
- krons/agent/operations/generate.py +145 -0
- krons/agent/operations/llm_reparse.py +89 -0
- krons/agent/operations/operate.py +247 -0
- krons/agent/operations/parse.py +243 -0
- krons/agent/operations/react.py +286 -0
- krons/agent/operations/specs.py +235 -0
- krons/agent/operations/structure.py +151 -0
- krons/agent/operations/utils.py +79 -0
- krons/agent/providers/__init__.py +17 -0
- krons/agent/providers/anthropic_messages.py +146 -0
- krons/agent/providers/claude_code.py +276 -0
- krons/agent/providers/gemini.py +268 -0
- krons/agent/providers/match.py +75 -0
- krons/agent/providers/oai_chat.py +174 -0
- krons/agent/third_party/__init__.py +2 -0
- krons/agent/third_party/anthropic_models.py +154 -0
- krons/agent/third_party/claude_code.py +682 -0
- krons/agent/third_party/gemini_models.py +508 -0
- krons/agent/third_party/openai_models.py +295 -0
- krons/agent/tool.py +291 -0
- krons/core/__init__.py +127 -0
- krons/core/base/__init__.py +121 -0
- {kronos/core → krons/core/base}/broadcaster.py +7 -3
- {kronos/core → krons/core/base}/element.py +15 -7
- {kronos/core → krons/core/base}/event.py +41 -8
- {kronos/core → krons/core/base}/eventbus.py +4 -2
- {kronos/core → krons/core/base}/flow.py +14 -7
- {kronos/core → krons/core/base}/graph.py +27 -11
- {kronos/core → krons/core/base}/node.py +47 -22
- {kronos/core → krons/core/base}/pile.py +26 -12
- {kronos/core → krons/core/base}/processor.py +23 -9
- {kronos/core → krons/core/base}/progression.py +5 -3
- {kronos → krons/core}/specs/__init__.py +0 -5
- {kronos → krons/core}/specs/adapters/dataclass_field.py +16 -8
- {kronos → krons/core}/specs/adapters/pydantic_adapter.py +11 -5
- {kronos → krons/core}/specs/adapters/sql_ddl.py +16 -10
- {kronos → krons/core}/specs/catalog/__init__.py +2 -2
- {kronos → krons/core}/specs/catalog/_audit.py +3 -3
- {kronos → krons/core}/specs/catalog/_common.py +2 -2
- {kronos → krons/core}/specs/catalog/_content.py +5 -5
- {kronos → krons/core}/specs/catalog/_enforcement.py +4 -4
- {kronos → krons/core}/specs/factory.py +7 -7
- {kronos → krons/core}/specs/operable.py +9 -3
- {kronos → krons/core}/specs/protocol.py +4 -2
- {kronos → krons/core}/specs/spec.py +25 -13
- {kronos → krons/core}/types/base.py +7 -5
- {kronos → krons/core}/types/db_types.py +2 -2
- {kronos → krons/core}/types/identity.py +1 -1
- {kronos → krons}/errors.py +13 -13
- {kronos → krons}/protocols.py +9 -4
- krons/resource/__init__.py +89 -0
- {kronos/services → krons/resource}/backend.py +50 -24
- {kronos/services → krons/resource}/endpoint.py +28 -14
- {kronos/services → krons/resource}/hook.py +22 -9
- {kronos/services → krons/resource}/imodel.py +50 -32
- {kronos/services → krons/resource}/registry.py +27 -25
- {kronos/services → krons/resource}/utilities/rate_limited_executor.py +10 -6
- {kronos/services → krons/resource}/utilities/rate_limiter.py +4 -2
- {kronos/services → krons/resource}/utilities/resilience.py +17 -7
- krons/resource/utilities/token_calculator.py +185 -0
- {kronos → krons}/session/__init__.py +12 -17
- krons/session/constraints.py +70 -0
- {kronos → krons}/session/exchange.py +14 -6
- {kronos → krons}/session/message.py +4 -2
- krons/session/registry.py +35 -0
- {kronos → krons}/session/session.py +165 -174
- krons/utils/__init__.py +85 -0
- krons/utils/_function_arg_parser.py +99 -0
- krons/utils/_pythonic_function_call.py +249 -0
- {kronos → krons}/utils/_to_list.py +9 -3
- {kronos → krons}/utils/_utils.py +9 -5
- {kronos → krons}/utils/concurrency/__init__.py +38 -38
- {kronos → krons}/utils/concurrency/_async_call.py +6 -4
- {kronos → krons}/utils/concurrency/_errors.py +3 -1
- {kronos → krons}/utils/concurrency/_patterns.py +3 -1
- {kronos → krons}/utils/concurrency/_resource_tracker.py +6 -2
- krons/utils/display.py +257 -0
- {kronos → krons}/utils/fuzzy/__init__.py +6 -1
- {kronos → krons}/utils/fuzzy/_fuzzy_match.py +14 -8
- {kronos → krons}/utils/fuzzy/_string_similarity.py +3 -1
- {kronos → krons}/utils/fuzzy/_to_dict.py +3 -1
- krons/utils/schemas/__init__.py +26 -0
- krons/utils/schemas/_breakdown_pydantic_annotation.py +131 -0
- krons/utils/schemas/_formatter.py +72 -0
- krons/utils/schemas/_minimal_yaml.py +151 -0
- krons/utils/schemas/_typescript.py +153 -0
- {kronos → krons}/utils/sql/_sql_validation.py +1 -1
- krons/utils/validators/__init__.py +3 -0
- krons/utils/validators/_validate_image_url.py +56 -0
- krons/work/__init__.py +126 -0
- krons/work/engine.py +333 -0
- krons/work/form.py +305 -0
- {kronos → krons/work}/operations/__init__.py +7 -4
- {kronos → krons/work}/operations/builder.py +4 -4
- {kronos/enforcement → krons/work/operations}/context.py +37 -6
- {kronos → krons/work}/operations/flow.py +17 -9
- krons/work/operations/node.py +103 -0
- krons/work/operations/registry.py +103 -0
- {kronos/specs → krons/work}/phrase.py +131 -14
- {kronos/enforcement → krons/work}/policy.py +3 -3
- krons/work/report.py +268 -0
- krons/work/rules/__init__.py +47 -0
- {kronos/enforcement → krons/work/rules}/common/boolean.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/choice.py +9 -3
- {kronos/enforcement → krons/work/rules}/common/number.py +3 -1
- {kronos/enforcement → krons/work/rules}/common/string.py +9 -3
- {kronos/enforcement → krons/work/rules}/rule.py +2 -2
- {kronos/enforcement → krons/work/rules}/validator.py +21 -6
- {kronos/enforcement → krons/work}/service.py +16 -7
- krons/work/worker.py +266 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/METADATA +19 -5
- krons-0.2.0.dist-info/RECORD +154 -0
- kronos/core/__init__.py +0 -145
- kronos/enforcement/__init__.py +0 -57
- kronos/operations/node.py +0 -101
- kronos/operations/registry.py +0 -92
- kronos/services/__init__.py +0 -81
- kronos/specs/adapters/__init__.py +0 -0
- kronos/utils/__init__.py +0 -40
- krons-0.1.0.dist-info/RECORD +0 -101
- {kronos → krons/core/specs/adapters}/__init__.py +0 -0
- {kronos → krons/core}/specs/adapters/_utils.py +0 -0
- {kronos → krons/core}/specs/adapters/factory.py +0 -0
- {kronos → krons/core}/types/__init__.py +0 -0
- {kronos → krons/core}/types/_sentinel.py +0 -0
- {kronos → krons}/py.typed +0 -0
- {kronos/services → krons/resource}/utilities/__init__.py +0 -0
- {kronos/services → krons/resource}/utilities/header_factory.py +0 -0
- {kronos → krons}/utils/_hash.py +0 -0
- {kronos → krons}/utils/_json_dump.py +0 -0
- {kronos → krons}/utils/_lazy_init.py +0 -0
- {kronos → krons}/utils/_to_num.py +0 -0
- {kronos → krons}/utils/concurrency/_cancel.py +0 -0
- {kronos → krons}/utils/concurrency/_primitives.py +0 -0
- {kronos → krons}/utils/concurrency/_priority_queue.py +0 -0
- {kronos → krons}/utils/concurrency/_run_async.py +0 -0
- {kronos → krons}/utils/concurrency/_task.py +0 -0
- {kronos → krons}/utils/concurrency/_utils.py +0 -0
- {kronos → krons}/utils/fuzzy/_extract_json.py +0 -0
- {kronos → krons}/utils/fuzzy/_fuzzy_json.py +0 -0
- {kronos → krons}/utils/sql/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/__init__.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/mapping.py +0 -0
- {kronos/enforcement → krons/work/rules}/common/model.py +0 -0
- {kronos/enforcement → krons/work/rules}/registry.py +0 -0
- {krons-0.1.0.dist-info → krons-0.2.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
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
|
|
18
|
-
from
|
|
19
|
-
from
|
|
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
|
|
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
|
-
*[
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
20
|
-
from
|
|
21
|
-
from
|
|
22
|
-
from
|
|
23
|
-
from
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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] =
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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}
|
|
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
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
319
|
-
|
|
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
|
-
**
|
|
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
|
-
|
|
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.
|
|
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_
|