ldp-protocol 0.1.0__tar.gz
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.
- ldp_protocol-0.1.0/.gitignore +10 -0
- ldp_protocol-0.1.0/PKG-INFO +110 -0
- ldp_protocol-0.1.0/README.md +77 -0
- ldp_protocol-0.1.0/pyproject.toml +51 -0
- ldp_protocol-0.1.0/src/ldp_protocol/__init__.py +49 -0
- ldp_protocol-0.1.0/src/ldp_protocol/client.py +203 -0
- ldp_protocol-0.1.0/src/ldp_protocol/delegate.py +253 -0
- ldp_protocol-0.1.0/src/ldp_protocol/router.py +192 -0
- ldp_protocol-0.1.0/src/ldp_protocol/types/__init__.py +25 -0
- ldp_protocol-0.1.0/src/ldp_protocol/types/capability.py +29 -0
- ldp_protocol-0.1.0/src/ldp_protocol/types/identity.py +64 -0
- ldp_protocol-0.1.0/src/ldp_protocol/types/messages.py +135 -0
- ldp_protocol-0.1.0/src/ldp_protocol/types/payload.py +63 -0
- ldp_protocol-0.1.0/src/ldp_protocol/types/provenance.py +35 -0
- ldp_protocol-0.1.0/src/ldp_protocol/types/session.py +59 -0
- ldp_protocol-0.1.0/src/ldp_protocol/types/trust.py +19 -0
- ldp_protocol-0.1.0/tests/__init__.py +0 -0
- ldp_protocol-0.1.0/tests/test_router.py +137 -0
- ldp_protocol-0.1.0/tests/test_types.py +172 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ldp-protocol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LDP — LLM Delegate Protocol: identity-aware communication for multi-agent LLM systems
|
|
5
|
+
Project-URL: Homepage, https://github.com/sunilp/ldp-protocol
|
|
6
|
+
Project-URL: Documentation, https://github.com/sunilp/ldp-protocol/tree/main/sdk/python
|
|
7
|
+
Project-URL: Repository, https://github.com/sunilp/ldp-protocol
|
|
8
|
+
Project-URL: Issues, https://github.com/sunilp/ldp-protocol/issues
|
|
9
|
+
Author: Sunil Prakash
|
|
10
|
+
License-Expression: Apache-2.0
|
|
11
|
+
Keywords: a2a,agents,delegation,llm,mcp,multi-agent,protocol
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: httpx>=0.25
|
|
24
|
+
Requires-Dist: pydantic>=2.0
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.1; extra == 'dev'
|
|
29
|
+
Provides-Extra: server
|
|
30
|
+
Requires-Dist: starlette>=0.27; extra == 'server'
|
|
31
|
+
Requires-Dist: uvicorn>=0.23; extra == 'server'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# LDP Protocol — Python SDK
|
|
35
|
+
|
|
36
|
+
Identity-aware communication protocol for multi-agent LLM systems.
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install ldp-protocol
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### Create a delegate
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from ldp_protocol import LdpDelegate, LdpCapability, QualityMetrics
|
|
48
|
+
|
|
49
|
+
class MyDelegate(LdpDelegate):
|
|
50
|
+
async def handle_task(self, skill, input_data, task_id):
|
|
51
|
+
return {"answer": "42"}, 0.95
|
|
52
|
+
|
|
53
|
+
delegate = MyDelegate(
|
|
54
|
+
delegate_id="ldp:delegate:my-agent",
|
|
55
|
+
name="My Agent",
|
|
56
|
+
model_family="claude",
|
|
57
|
+
model_version="claude-sonnet-4-6",
|
|
58
|
+
capabilities=[
|
|
59
|
+
LdpCapability(
|
|
60
|
+
name="reasoning",
|
|
61
|
+
quality=QualityMetrics(quality_score=0.85, cost_per_call_usd=0.01),
|
|
62
|
+
),
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
delegate.run(port=8090) # requires: pip install ldp-protocol[server]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Discover and invoke
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
from ldp_protocol import LdpClient
|
|
72
|
+
|
|
73
|
+
async with LdpClient() as client:
|
|
74
|
+
identity = await client.discover("http://localhost:8090")
|
|
75
|
+
print(f"Found: {identity.name} ({identity.model_family})")
|
|
76
|
+
|
|
77
|
+
result = await client.submit_task(
|
|
78
|
+
"http://localhost:8090",
|
|
79
|
+
skill="reasoning",
|
|
80
|
+
input_data={"prompt": "Analyze the tradeoffs..."},
|
|
81
|
+
)
|
|
82
|
+
print(f"Output: {result['output']}")
|
|
83
|
+
print(f"Provenance: {result['provenance']}")
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Multi-delegate routing
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from ldp_protocol import LdpRouter, RoutingStrategy
|
|
90
|
+
|
|
91
|
+
async with LdpRouter() as router:
|
|
92
|
+
await router.discover_delegates([
|
|
93
|
+
"http://fast-model:8091",
|
|
94
|
+
"http://deep-model:8092",
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
# Route by quality, cost, latency, or balanced score
|
|
98
|
+
result = await router.route_and_submit(
|
|
99
|
+
skill="reasoning",
|
|
100
|
+
input_data={"prompt": "Complex analysis..."},
|
|
101
|
+
strategy=RoutingStrategy.QUALITY,
|
|
102
|
+
)
|
|
103
|
+
print(f"Routed to: {result['routed_to']['name']}")
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Links
|
|
107
|
+
|
|
108
|
+
- [Protocol specification](https://github.com/sunilp/ldp-protocol)
|
|
109
|
+
- [Research paper](https://arxiv.org/abs/2603.08852)
|
|
110
|
+
- [Rust reference implementation](https://github.com/sunilp/ldp-protocol)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# LDP Protocol — Python SDK
|
|
2
|
+
|
|
3
|
+
Identity-aware communication protocol for multi-agent LLM systems.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install ldp-protocol
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
### Create a delegate
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from ldp_protocol import LdpDelegate, LdpCapability, QualityMetrics
|
|
15
|
+
|
|
16
|
+
class MyDelegate(LdpDelegate):
|
|
17
|
+
async def handle_task(self, skill, input_data, task_id):
|
|
18
|
+
return {"answer": "42"}, 0.95
|
|
19
|
+
|
|
20
|
+
delegate = MyDelegate(
|
|
21
|
+
delegate_id="ldp:delegate:my-agent",
|
|
22
|
+
name="My Agent",
|
|
23
|
+
model_family="claude",
|
|
24
|
+
model_version="claude-sonnet-4-6",
|
|
25
|
+
capabilities=[
|
|
26
|
+
LdpCapability(
|
|
27
|
+
name="reasoning",
|
|
28
|
+
quality=QualityMetrics(quality_score=0.85, cost_per_call_usd=0.01),
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
)
|
|
32
|
+
delegate.run(port=8090) # requires: pip install ldp-protocol[server]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Discover and invoke
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from ldp_protocol import LdpClient
|
|
39
|
+
|
|
40
|
+
async with LdpClient() as client:
|
|
41
|
+
identity = await client.discover("http://localhost:8090")
|
|
42
|
+
print(f"Found: {identity.name} ({identity.model_family})")
|
|
43
|
+
|
|
44
|
+
result = await client.submit_task(
|
|
45
|
+
"http://localhost:8090",
|
|
46
|
+
skill="reasoning",
|
|
47
|
+
input_data={"prompt": "Analyze the tradeoffs..."},
|
|
48
|
+
)
|
|
49
|
+
print(f"Output: {result['output']}")
|
|
50
|
+
print(f"Provenance: {result['provenance']}")
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Multi-delegate routing
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from ldp_protocol import LdpRouter, RoutingStrategy
|
|
57
|
+
|
|
58
|
+
async with LdpRouter() as router:
|
|
59
|
+
await router.discover_delegates([
|
|
60
|
+
"http://fast-model:8091",
|
|
61
|
+
"http://deep-model:8092",
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
# Route by quality, cost, latency, or balanced score
|
|
65
|
+
result = await router.route_and_submit(
|
|
66
|
+
skill="reasoning",
|
|
67
|
+
input_data={"prompt": "Complex analysis..."},
|
|
68
|
+
strategy=RoutingStrategy.QUALITY,
|
|
69
|
+
)
|
|
70
|
+
print(f"Routed to: {result['routed_to']['name']}")
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Links
|
|
74
|
+
|
|
75
|
+
- [Protocol specification](https://github.com/sunilp/ldp-protocol)
|
|
76
|
+
- [Research paper](https://arxiv.org/abs/2603.08852)
|
|
77
|
+
- [Rust reference implementation](https://github.com/sunilp/ldp-protocol)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ldp-protocol"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LDP — LLM Delegate Protocol: identity-aware communication for multi-agent LLM systems"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "Apache-2.0"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Sunil Prakash" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["llm", "agents", "protocol", "delegation", "multi-agent", "a2a", "mcp"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: Apache Software License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"pydantic>=2.0",
|
|
30
|
+
"httpx>=0.25",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.optional-dependencies]
|
|
34
|
+
server = ["starlette>=0.27", "uvicorn>=0.23"]
|
|
35
|
+
dev = ["pytest>=7.0", "pytest-asyncio>=0.21", "ruff>=0.1"]
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/sunilp/ldp-protocol"
|
|
39
|
+
Documentation = "https://github.com/sunilp/ldp-protocol/tree/main/sdk/python"
|
|
40
|
+
Repository = "https://github.com/sunilp/ldp-protocol"
|
|
41
|
+
Issues = "https://github.com/sunilp/ldp-protocol/issues"
|
|
42
|
+
|
|
43
|
+
[tool.hatch.build.targets.wheel]
|
|
44
|
+
packages = ["src/ldp_protocol"]
|
|
45
|
+
|
|
46
|
+
[tool.ruff]
|
|
47
|
+
target-version = "py310"
|
|
48
|
+
line-length = 100
|
|
49
|
+
|
|
50
|
+
[tool.pytest.ini_options]
|
|
51
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""LDP — LLM Delegate Protocol.
|
|
2
|
+
|
|
3
|
+
Identity-aware communication protocol for multi-agent LLM systems.
|
|
4
|
+
LDP adds delegation intelligence on top of A2A and MCP: rich identity,
|
|
5
|
+
progressive payload modes, governed sessions, structured provenance,
|
|
6
|
+
and trust domains.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ldp_protocol.types import (
|
|
10
|
+
LdpCapability,
|
|
11
|
+
LdpEnvelope,
|
|
12
|
+
LdpIdentityCard,
|
|
13
|
+
LdpMessageBody,
|
|
14
|
+
LdpSession,
|
|
15
|
+
NegotiatedPayload,
|
|
16
|
+
PayloadMode,
|
|
17
|
+
Provenance,
|
|
18
|
+
QualityMetrics,
|
|
19
|
+
SessionConfig,
|
|
20
|
+
SessionState,
|
|
21
|
+
TrustDomain,
|
|
22
|
+
)
|
|
23
|
+
from ldp_protocol.client import LdpClient
|
|
24
|
+
from ldp_protocol.delegate import LdpDelegate
|
|
25
|
+
from ldp_protocol.router import LdpRouter
|
|
26
|
+
|
|
27
|
+
__version__ = "0.1.0"
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
# Types
|
|
31
|
+
"LdpIdentityCard",
|
|
32
|
+
"LdpCapability",
|
|
33
|
+
"QualityMetrics",
|
|
34
|
+
"TrustDomain",
|
|
35
|
+
"PayloadMode",
|
|
36
|
+
"NegotiatedPayload",
|
|
37
|
+
"SessionConfig",
|
|
38
|
+
"SessionState",
|
|
39
|
+
"LdpSession",
|
|
40
|
+
"LdpEnvelope",
|
|
41
|
+
"LdpMessageBody",
|
|
42
|
+
"Provenance",
|
|
43
|
+
# Client
|
|
44
|
+
"LdpClient",
|
|
45
|
+
# Delegate
|
|
46
|
+
"LdpDelegate",
|
|
47
|
+
# Router
|
|
48
|
+
"LdpRouter",
|
|
49
|
+
]
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""LDP HTTP client — discover delegates, manage sessions, submit tasks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
|
|
10
|
+
from ldp_protocol.types.identity import LdpIdentityCard
|
|
11
|
+
from ldp_protocol.types.messages import LdpEnvelope, LdpMessageBody
|
|
12
|
+
from ldp_protocol.types.payload import PayloadMode, negotiate_payload_mode
|
|
13
|
+
from ldp_protocol.types.provenance import Provenance
|
|
14
|
+
from ldp_protocol.types.session import LdpSession, SessionConfig, SessionState
|
|
15
|
+
from ldp_protocol.types.trust import TrustDomain
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LdpClient:
|
|
19
|
+
"""Async HTTP client for LDP protocol communication.
|
|
20
|
+
|
|
21
|
+
Handles discovery, session management, and task submission.
|
|
22
|
+
|
|
23
|
+
Usage:
|
|
24
|
+
async with LdpClient() as client:
|
|
25
|
+
identity = await client.discover("http://localhost:8090")
|
|
26
|
+
result = await client.submit_task(
|
|
27
|
+
"http://localhost:8090",
|
|
28
|
+
skill="reasoning",
|
|
29
|
+
input_data={"prompt": "Analyze..."},
|
|
30
|
+
)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
delegate_id: str = "ldp:client:default",
|
|
36
|
+
config: SessionConfig | None = None,
|
|
37
|
+
timeout: float = 30.0,
|
|
38
|
+
):
|
|
39
|
+
self.delegate_id = delegate_id
|
|
40
|
+
self.config = config or SessionConfig()
|
|
41
|
+
self._http = httpx.AsyncClient(timeout=timeout)
|
|
42
|
+
self._sessions: dict[str, LdpSession] = {}
|
|
43
|
+
|
|
44
|
+
async def __aenter__(self) -> LdpClient:
|
|
45
|
+
return self
|
|
46
|
+
|
|
47
|
+
async def __aexit__(self, *args) -> None:
|
|
48
|
+
await self.close()
|
|
49
|
+
|
|
50
|
+
async def close(self) -> None:
|
|
51
|
+
"""Close the HTTP client."""
|
|
52
|
+
await self._http.aclose()
|
|
53
|
+
|
|
54
|
+
async def discover(self, url: str) -> LdpIdentityCard:
|
|
55
|
+
"""Fetch a delegate's identity card.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
url: Base URL of the delegate (e.g., "http://localhost:8090").
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The delegate's identity card with full metadata.
|
|
62
|
+
"""
|
|
63
|
+
endpoint = f"{url.rstrip('/')}/ldp/identity"
|
|
64
|
+
resp = await self._http.get(endpoint)
|
|
65
|
+
resp.raise_for_status()
|
|
66
|
+
return LdpIdentityCard.model_validate(resp.json())
|
|
67
|
+
|
|
68
|
+
async def send_message(self, url: str, envelope: LdpEnvelope) -> LdpEnvelope:
|
|
69
|
+
"""Send an LDP message and receive a response."""
|
|
70
|
+
endpoint = f"{url.rstrip('/')}/ldp/messages"
|
|
71
|
+
resp = await self._http.post(
|
|
72
|
+
endpoint,
|
|
73
|
+
json=envelope.model_dump(by_alias=True),
|
|
74
|
+
)
|
|
75
|
+
resp.raise_for_status()
|
|
76
|
+
return LdpEnvelope.model_validate(resp.json())
|
|
77
|
+
|
|
78
|
+
async def establish_session(self, url: str) -> LdpSession:
|
|
79
|
+
"""Establish a new LDP session with a delegate.
|
|
80
|
+
|
|
81
|
+
Performs the full handshake: HELLO -> CAPABILITY_MANIFEST ->
|
|
82
|
+
SESSION_PROPOSE -> SESSION_ACCEPT.
|
|
83
|
+
"""
|
|
84
|
+
# Step 1: HELLO
|
|
85
|
+
hello = LdpEnvelope.create(
|
|
86
|
+
session_id="",
|
|
87
|
+
from_id=self.delegate_id,
|
|
88
|
+
to_id=url,
|
|
89
|
+
body=LdpMessageBody.hello(
|
|
90
|
+
delegate_id=self.delegate_id,
|
|
91
|
+
supported_modes=self.config.preferred_payload_modes,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
hello_resp = await self.send_message(url, hello)
|
|
95
|
+
|
|
96
|
+
# Step 2: SESSION_PROPOSE
|
|
97
|
+
session_id = str(uuid4())
|
|
98
|
+
propose = LdpEnvelope.create(
|
|
99
|
+
session_id=session_id,
|
|
100
|
+
from_id=self.delegate_id,
|
|
101
|
+
to_id=url,
|
|
102
|
+
body=LdpMessageBody.session_propose(
|
|
103
|
+
config={
|
|
104
|
+
"preferred_payload_modes": [
|
|
105
|
+
m.value for m in self.config.preferred_payload_modes
|
|
106
|
+
],
|
|
107
|
+
"ttl_secs": self.config.ttl_secs,
|
|
108
|
+
}
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
propose_resp = await self.send_message(url, propose)
|
|
112
|
+
|
|
113
|
+
if propose_resp.body.type == "SESSION_REJECT":
|
|
114
|
+
raise ConnectionError(
|
|
115
|
+
f"Session rejected: {propose_resp.body.reason}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Build session from response
|
|
119
|
+
negotiated_mode = propose_resp.body.negotiated_mode or PayloadMode.TEXT
|
|
120
|
+
identity = await self.discover(url)
|
|
121
|
+
|
|
122
|
+
session = LdpSession(
|
|
123
|
+
session_id=propose_resp.body.session_id or session_id,
|
|
124
|
+
remote_url=url,
|
|
125
|
+
remote_delegate_id=identity.delegate_id,
|
|
126
|
+
state=SessionState.ACTIVE,
|
|
127
|
+
trust_domain=identity.trust_domain,
|
|
128
|
+
ttl_secs=self.config.ttl_secs,
|
|
129
|
+
)
|
|
130
|
+
session.payload.mode = negotiated_mode
|
|
131
|
+
|
|
132
|
+
self._sessions[url] = session
|
|
133
|
+
return session
|
|
134
|
+
|
|
135
|
+
async def get_or_establish_session(self, url: str) -> LdpSession:
|
|
136
|
+
"""Get an existing session or establish a new one."""
|
|
137
|
+
session = self._sessions.get(url)
|
|
138
|
+
if session and session.is_active:
|
|
139
|
+
return session
|
|
140
|
+
return await self.establish_session(url)
|
|
141
|
+
|
|
142
|
+
async def submit_task(
|
|
143
|
+
self,
|
|
144
|
+
url: str,
|
|
145
|
+
*,
|
|
146
|
+
skill: str,
|
|
147
|
+
input_data: Any,
|
|
148
|
+
session: LdpSession | None = None,
|
|
149
|
+
) -> dict[str, Any]:
|
|
150
|
+
"""Submit a task to a delegate and get the result.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
url: Delegate URL.
|
|
154
|
+
skill: Skill to invoke.
|
|
155
|
+
input_data: Input data for the task.
|
|
156
|
+
session: Optional existing session (auto-establishes if None).
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Dict with 'output' and 'provenance' keys.
|
|
160
|
+
"""
|
|
161
|
+
if session is None:
|
|
162
|
+
session = await self.get_or_establish_session(url)
|
|
163
|
+
|
|
164
|
+
task_id = str(uuid4())
|
|
165
|
+
submit = LdpEnvelope.create(
|
|
166
|
+
session_id=session.session_id,
|
|
167
|
+
from_id=self.delegate_id,
|
|
168
|
+
to_id=session.remote_delegate_id,
|
|
169
|
+
body=LdpMessageBody.task_submit(
|
|
170
|
+
task_id=task_id,
|
|
171
|
+
skill=skill,
|
|
172
|
+
input=input_data,
|
|
173
|
+
),
|
|
174
|
+
payload_mode=session.payload.mode,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
response = await self.send_message(url, submit)
|
|
178
|
+
session.touch()
|
|
179
|
+
session.task_count += 1
|
|
180
|
+
|
|
181
|
+
if response.body.type == "TASK_RESULT":
|
|
182
|
+
return {
|
|
183
|
+
"task_id": response.body.task_id,
|
|
184
|
+
"output": response.body.output,
|
|
185
|
+
"provenance": (
|
|
186
|
+
response.body.provenance.model_dump()
|
|
187
|
+
if response.body.provenance
|
|
188
|
+
else None
|
|
189
|
+
),
|
|
190
|
+
}
|
|
191
|
+
elif response.body.type == "TASK_FAILED":
|
|
192
|
+
raise RuntimeError(f"Task failed: {response.body.error}")
|
|
193
|
+
else:
|
|
194
|
+
return {
|
|
195
|
+
"task_id": task_id,
|
|
196
|
+
"status": response.body.type,
|
|
197
|
+
"message": response.body.message,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def active_sessions(self) -> int:
|
|
202
|
+
"""Number of active sessions."""
|
|
203
|
+
return sum(1 for s in self._sessions.values() if s.is_active)
|