macp-sdk-python 0.2.1__tar.gz → 0.2.3__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.
- {macp_sdk_python-0.2.1/src/macp_sdk_python.egg-info → macp_sdk_python-0.2.3}/PKG-INFO +3 -3
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/README.md +1 -1
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/pyproject.toml +7 -5
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/agent/participant.py +80 -4
- macp_sdk_python-0.2.3/src/macp_sdk/agent/runner.py +140 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/agent/transports.py +23 -6
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/base_session.py +4 -2
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/client.py +42 -17
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/envelope.py +4 -13
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3/src/macp_sdk_python.egg-info}/PKG-INFO +3 -3
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk_python.egg-info/requires.txt +1 -1
- macp_sdk_python-0.2.1/src/macp_sdk/agent/runner.py +0 -100
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/LICENSE +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/setup.cfg +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/__init__.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/_logging.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/agent/__init__.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/agent/dispatcher.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/agent/strategies.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/agent/types.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/auth.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/base_projection.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/constants.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/decision.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/errors.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/handoff.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/policy.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/projections.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/proposal.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/proto_registry.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/py.typed +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/quorum.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/retry.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/task.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/validation.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk/watchers.py +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk_python.egg-info/SOURCES.txt +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk_python.egg-info/dependency_links.txt +0 -0
- {macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk_python.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: macp-sdk-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python SDK for the MACP Rust runtime
|
|
5
5
|
Author-email: OpenAI <support@openai.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: grpcio>=1.74.0
|
|
19
19
|
Requires-Dist: protobuf>=5.27.0
|
|
20
|
-
Requires-Dist: macp-proto<0.2.0,>=0.1.
|
|
20
|
+
Requires-Dist: macp-proto<0.2.0,>=0.1.2
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: grpcio-tools>=1.74.0; extra == "dev"
|
|
23
23
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -161,5 +161,5 @@ Business logic — voting rules, AI decision heuristics, policy enforcement —
|
|
|
161
161
|
## Known runtime limitations
|
|
162
162
|
|
|
163
163
|
- `GetSession` returns metadata only (not mode state/transcript) — hence the local projection pattern
|
|
164
|
-
- `StreamSession`
|
|
164
|
+
- `StreamSession` is scoped to one session per stream; use `MacpStream.send_subscribe(session_id)` (RFC-MACP-0006-A1, since SDK 0.2.3 / `macp-proto 0.1.2`) to replay accepted history before live broadcast
|
|
165
165
|
- Business policy (majority, quorum, veto) belongs in your orchestrator/policy layer
|
|
@@ -127,5 +127,5 @@ Business logic — voting rules, AI decision heuristics, policy enforcement —
|
|
|
127
127
|
## Known runtime limitations
|
|
128
128
|
|
|
129
129
|
- `GetSession` returns metadata only (not mode state/transcript) — hence the local projection pattern
|
|
130
|
-
- `StreamSession`
|
|
130
|
+
- `StreamSession` is scoped to one session per stream; use `MacpStream.send_subscribe(session_id)` (RFC-MACP-0006-A1, since SDK 0.2.3 / `macp-proto 0.1.2`) to replay accepted history before live broadcast
|
|
131
131
|
- Business policy (majority, quorum, veto) belongs in your orchestrator/policy layer
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "macp-sdk-python"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "Python SDK for the MACP Rust runtime"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -25,10 +25,12 @@ classifiers = [
|
|
|
25
25
|
dependencies = [
|
|
26
26
|
"grpcio>=1.74.0",
|
|
27
27
|
"protobuf>=5.27.0",
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
|
|
28
|
+
# Requires 0.1.2+ for the RFC-MACP-0006-A1 subscribe_session_id /
|
|
29
|
+
# after_sequence fields on StreamSessionRequest (used by MacpStream
|
|
30
|
+
# .send_subscribe and the gRPC agent transport adapter). 0.1.1 is
|
|
31
|
+
# the earlier baseline for the context_id / extensions fields on
|
|
32
|
+
# SessionStartPayload. See docs/contributing.md "Bumping macp-proto".
|
|
33
|
+
"macp-proto>=0.1.2,<0.2.0",
|
|
32
34
|
]
|
|
33
35
|
|
|
34
36
|
[project.optional-dependencies]
|
|
@@ -70,6 +70,48 @@ class ParticipantActions:
|
|
|
70
70
|
"""Cancel the session."""
|
|
71
71
|
return self._client.cancel_session(self._session_id, reason=reason, auth=self._auth)
|
|
72
72
|
|
|
73
|
+
def start_session(
|
|
74
|
+
self,
|
|
75
|
+
intent: str,
|
|
76
|
+
participants: list[str],
|
|
77
|
+
ttl_ms: int,
|
|
78
|
+
context_id: str = "",
|
|
79
|
+
extensions: dict[str, bytes] | None = None,
|
|
80
|
+
mode_version: str | None = None,
|
|
81
|
+
configuration_version: str | None = None,
|
|
82
|
+
policy_version: str | None = None,
|
|
83
|
+
) -> Any:
|
|
84
|
+
"""Send a SessionStart envelope to open the session."""
|
|
85
|
+
from ..constants import (
|
|
86
|
+
DEFAULT_CONFIGURATION_VERSION,
|
|
87
|
+
DEFAULT_MODE_VERSION,
|
|
88
|
+
DEFAULT_POLICY_VERSION,
|
|
89
|
+
)
|
|
90
|
+
from ..envelope import (
|
|
91
|
+
build_envelope,
|
|
92
|
+
build_session_start_payload,
|
|
93
|
+
serialize_message,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
payload = build_session_start_payload(
|
|
97
|
+
intent=intent,
|
|
98
|
+
participants=participants,
|
|
99
|
+
ttl_ms=ttl_ms,
|
|
100
|
+
context_id=context_id,
|
|
101
|
+
extensions=extensions,
|
|
102
|
+
mode_version=mode_version or DEFAULT_MODE_VERSION,
|
|
103
|
+
configuration_version=configuration_version or DEFAULT_CONFIGURATION_VERSION,
|
|
104
|
+
policy_version=policy_version or DEFAULT_POLICY_VERSION,
|
|
105
|
+
)
|
|
106
|
+
envelope = build_envelope(
|
|
107
|
+
mode=self._mode,
|
|
108
|
+
message_type="SessionStart",
|
|
109
|
+
session_id=self._session_id,
|
|
110
|
+
sender=self._participant_id,
|
|
111
|
+
payload=serialize_message(payload),
|
|
112
|
+
)
|
|
113
|
+
return self.send_envelope(envelope)
|
|
114
|
+
|
|
73
115
|
def evaluate(
|
|
74
116
|
self,
|
|
75
117
|
proposal_id: str,
|
|
@@ -222,6 +264,7 @@ class Participant:
|
|
|
222
264
|
configuration_version: str | None = None,
|
|
223
265
|
policy_version: str | None = None,
|
|
224
266
|
transport: TransportAdapter | None = None,
|
|
267
|
+
initiator_config: Any | None = None,
|
|
225
268
|
) -> None:
|
|
226
269
|
self._participant_id = participant_id
|
|
227
270
|
self._session_id = session_id
|
|
@@ -229,6 +272,7 @@ class Participant:
|
|
|
229
272
|
self._client = client
|
|
230
273
|
self._auth = auth
|
|
231
274
|
self._stopped = False
|
|
275
|
+
self._initiator_config = initiator_config
|
|
232
276
|
|
|
233
277
|
self._dispatcher = Dispatcher()
|
|
234
278
|
self._session = SessionInfo(
|
|
@@ -365,19 +409,23 @@ class Participant:
|
|
|
365
409
|
def run(self) -> None:
|
|
366
410
|
"""Enter the blocking event loop.
|
|
367
411
|
|
|
368
|
-
If
|
|
369
|
-
|
|
412
|
+
If this participant is the initiator (``initiator_config`` is set),
|
|
413
|
+
emits SessionStart + kickoff before opening the stream.
|
|
370
414
|
|
|
371
415
|
Dispatches received events to registered handlers until the session
|
|
372
416
|
reaches a terminal state or ``stop()`` is called.
|
|
373
417
|
"""
|
|
374
418
|
logger.info(
|
|
375
|
-
"participant %s joining session %s (mode=%s)",
|
|
419
|
+
"participant %s joining session %s (mode=%s, initiator=%s)",
|
|
376
420
|
self._participant_id,
|
|
377
421
|
self._session_id,
|
|
378
422
|
self._mode,
|
|
423
|
+
self._initiator_config is not None,
|
|
379
424
|
)
|
|
380
425
|
|
|
426
|
+
if self._initiator_config is not None:
|
|
427
|
+
self._emit_initiator_envelopes()
|
|
428
|
+
|
|
381
429
|
transport = self._transport or GrpcTransportAdapter(
|
|
382
430
|
self._client,
|
|
383
431
|
self._session_id,
|
|
@@ -387,7 +435,6 @@ class Participant:
|
|
|
387
435
|
for message in transport.start():
|
|
388
436
|
if self._stopped:
|
|
389
437
|
break
|
|
390
|
-
# If the transport yields raw envelopes (gRPC), process as envelope
|
|
391
438
|
if message.raw is not None:
|
|
392
439
|
self._process_envelope(message.raw)
|
|
393
440
|
else:
|
|
@@ -395,6 +442,35 @@ class Participant:
|
|
|
395
442
|
finally:
|
|
396
443
|
transport.stop()
|
|
397
444
|
|
|
445
|
+
def _emit_initiator_envelopes(self) -> None:
|
|
446
|
+
"""Emit SessionStart + kickoff envelope as the initiator."""
|
|
447
|
+
cfg = self._initiator_config
|
|
448
|
+
if cfg is None:
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
self._actions.start_session(
|
|
452
|
+
intent=cfg.intent,
|
|
453
|
+
participants=cfg.participants,
|
|
454
|
+
ttl_ms=cfg.ttl_ms,
|
|
455
|
+
context_id=cfg.context_id,
|
|
456
|
+
mode_version=cfg.mode_version,
|
|
457
|
+
configuration_version=cfg.configuration_version,
|
|
458
|
+
policy_version=cfg.policy_version,
|
|
459
|
+
)
|
|
460
|
+
logger.info("SessionStart emitted (session=%s)", self._session_id)
|
|
461
|
+
|
|
462
|
+
if cfg.kickoff_message_type == "Proposal":
|
|
463
|
+
payload = cfg.kickoff_payload or {}
|
|
464
|
+
proposal_id = str(
|
|
465
|
+
payload.get("proposalId")
|
|
466
|
+
or payload.get("proposal_id")
|
|
467
|
+
or f"{self._session_id}-kickoff"
|
|
468
|
+
)
|
|
469
|
+
option = str(payload.get("option", "decide"))
|
|
470
|
+
rationale = str(payload.get("rationale", ""))
|
|
471
|
+
self._actions.propose(proposal_id, option, rationale=rationale)
|
|
472
|
+
logger.info("Kickoff proposal emitted (proposalId=%s)", proposal_id)
|
|
473
|
+
|
|
398
474
|
def process_event(self, envelope: Any) -> None:
|
|
399
475
|
"""Manually process a single envelope (for testing or polling transports)."""
|
|
400
476
|
self._process_envelope(envelope)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from ..auth import AuthConfig
|
|
9
|
+
from ..client import MacpClient
|
|
10
|
+
from ..constants import DEFAULT_POLICY_VERSION
|
|
11
|
+
from .participant import Participant
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class InitiatorConfig:
|
|
16
|
+
"""Configuration for the initiator agent's SessionStart + kickoff."""
|
|
17
|
+
|
|
18
|
+
intent: str
|
|
19
|
+
participants: list[str]
|
|
20
|
+
ttl_ms: int
|
|
21
|
+
context_id: str = ""
|
|
22
|
+
roots: list[dict[str, str]] | None = None
|
|
23
|
+
mode_version: str | None = None
|
|
24
|
+
configuration_version: str | None = None
|
|
25
|
+
policy_version: str | None = None
|
|
26
|
+
kickoff_message_type: str | None = None
|
|
27
|
+
kickoff_payload: dict[str, Any] = field(default_factory=dict)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def from_bootstrap(bootstrap_path: str | None = None) -> Participant:
|
|
31
|
+
"""Create a Participant from a bootstrap context file.
|
|
32
|
+
|
|
33
|
+
Reads the flat bootstrap format produced by the examples-service::
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
"participant_id": "...",
|
|
37
|
+
"session_id": "...",
|
|
38
|
+
"mode": "macp.mode.decision.v1",
|
|
39
|
+
"runtime_url": "localhost:50051",
|
|
40
|
+
"auth_token": "...",
|
|
41
|
+
"participants": ["agent-a", "agent-b"],
|
|
42
|
+
"secure": false,
|
|
43
|
+
"allow_insecure": true,
|
|
44
|
+
"initiator": { ... }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Also accepts ``auth.bearer_token`` for backwards compatibility.
|
|
48
|
+
"""
|
|
49
|
+
path = bootstrap_path or os.environ.get("MACP_BOOTSTRAP_FILE")
|
|
50
|
+
if not path:
|
|
51
|
+
raise ValueError("No bootstrap path provided and MACP_BOOTSTRAP_FILE not set")
|
|
52
|
+
|
|
53
|
+
with open(path) as f:
|
|
54
|
+
ctx: dict[str, Any] = json.load(f)
|
|
55
|
+
|
|
56
|
+
participant_id = str(ctx["participant_id"])
|
|
57
|
+
session_id = str(ctx["session_id"])
|
|
58
|
+
mode = str(ctx["mode"])
|
|
59
|
+
runtime_url = str(ctx.get("runtime_url") or ctx.get("runtime_address") or "localhost:50051")
|
|
60
|
+
secure = bool(ctx.get("secure", True))
|
|
61
|
+
allow_insecure = bool(ctx.get("allow_insecure", False))
|
|
62
|
+
|
|
63
|
+
auth: AuthConfig | None = None
|
|
64
|
+
auth_token = ctx.get("auth_token")
|
|
65
|
+
agent_id = ctx.get("agent_id")
|
|
66
|
+
auth_data = ctx.get("auth")
|
|
67
|
+
|
|
68
|
+
if auth_token:
|
|
69
|
+
auth = AuthConfig.for_bearer(
|
|
70
|
+
str(auth_token),
|
|
71
|
+
sender_hint=participant_id,
|
|
72
|
+
expected_sender=participant_id,
|
|
73
|
+
)
|
|
74
|
+
elif isinstance(auth_data, dict) and auth_data.get("bearer_token"):
|
|
75
|
+
auth = AuthConfig.for_bearer(
|
|
76
|
+
str(auth_data["bearer_token"]),
|
|
77
|
+
sender_hint=participant_id,
|
|
78
|
+
expected_sender=str(auth_data.get("expected_sender") or participant_id),
|
|
79
|
+
)
|
|
80
|
+
elif agent_id:
|
|
81
|
+
auth = AuthConfig.for_dev_agent(str(agent_id), expected_sender=participant_id)
|
|
82
|
+
elif isinstance(auth_data, dict) and auth_data.get("agent_id"):
|
|
83
|
+
auth = AuthConfig.for_dev_agent(str(auth_data["agent_id"]), expected_sender=participant_id)
|
|
84
|
+
|
|
85
|
+
client = MacpClient(
|
|
86
|
+
target=runtime_url,
|
|
87
|
+
secure=secure,
|
|
88
|
+
allow_insecure=allow_insecure,
|
|
89
|
+
auth=auth,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
raw_participants = ctx.get("participants")
|
|
93
|
+
participants: list[str] = (
|
|
94
|
+
[str(p) for p in raw_participants] if isinstance(raw_participants, list) else []
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
mode_version = ctx.get("mode_version")
|
|
98
|
+
configuration_version = ctx.get("configuration_version")
|
|
99
|
+
policy_version = ctx.get("policy_version")
|
|
100
|
+
|
|
101
|
+
initiator_config: InitiatorConfig | None = None
|
|
102
|
+
initiator_data = ctx.get("initiator")
|
|
103
|
+
if isinstance(initiator_data, dict):
|
|
104
|
+
ss = initiator_data.get("session_start", {})
|
|
105
|
+
kickoff = initiator_data.get("kickoff")
|
|
106
|
+
|
|
107
|
+
def _str_or(key: str, fallback: object) -> str | None:
|
|
108
|
+
"""Pick value from session_start, then fallback, coercing to str."""
|
|
109
|
+
val = ss.get(key)
|
|
110
|
+
if val is not None:
|
|
111
|
+
return str(val)
|
|
112
|
+
return str(fallback) if fallback else None
|
|
113
|
+
|
|
114
|
+
initiator_config = InitiatorConfig(
|
|
115
|
+
intent=str(ss.get("intent", "")),
|
|
116
|
+
participants=[str(p) for p in ss.get("participants", participants)],
|
|
117
|
+
ttl_ms=int(ss.get("ttl_ms", 300000)),
|
|
118
|
+
context_id=str(ss.get("context_id", "")),
|
|
119
|
+
roots=ss.get("roots"),
|
|
120
|
+
mode_version=_str_or("mode_version", mode_version),
|
|
121
|
+
configuration_version=_str_or("configuration_version", configuration_version),
|
|
122
|
+
policy_version=_str_or("policy_version", policy_version),
|
|
123
|
+
kickoff_message_type=(
|
|
124
|
+
str(kickoff["message_type"]) if kickoff and "message_type" in kickoff else None
|
|
125
|
+
),
|
|
126
|
+
kickoff_payload=kickoff.get("payload", {}) if kickoff else {},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return Participant(
|
|
130
|
+
participant_id=participant_id,
|
|
131
|
+
session_id=session_id,
|
|
132
|
+
mode=mode,
|
|
133
|
+
client=client,
|
|
134
|
+
auth=auth,
|
|
135
|
+
participants=participants,
|
|
136
|
+
mode_version=str(mode_version) if mode_version else None,
|
|
137
|
+
configuration_version=str(configuration_version) if configuration_version else None,
|
|
138
|
+
policy_version=str(policy_version) if policy_version else DEFAULT_POLICY_VERSION,
|
|
139
|
+
initiator_config=initiator_config,
|
|
140
|
+
)
|
|
@@ -53,6 +53,12 @@ class GrpcTransportAdapter:
|
|
|
53
53
|
"""Open a stream and yield messages for the target session."""
|
|
54
54
|
self._stream = self._client.open_stream(auth=self._auth, timeout=self._timeout)
|
|
55
55
|
try:
|
|
56
|
+
# RFC-MACP-0006-A1: Subscribe to the session with history replay.
|
|
57
|
+
# The runtime replays accepted envelopes then switches to live
|
|
58
|
+
# broadcast, ensuring non-initiator agents receive SessionStart +
|
|
59
|
+
# Proposal regardless of spawn order or connection timing.
|
|
60
|
+
self._stream.send_subscribe(self._session_id)
|
|
61
|
+
|
|
56
62
|
for envelope in self._stream.responses():
|
|
57
63
|
if self._stopped:
|
|
58
64
|
break
|
|
@@ -133,16 +139,27 @@ class HttpTransportAdapter:
|
|
|
133
139
|
|
|
134
140
|
def _envelope_to_message(envelope: Any) -> IncomingMessage:
|
|
135
141
|
"""Convert a protobuf Envelope to an IncomingMessage."""
|
|
142
|
+
from ..proto_registry import ProtoRegistry
|
|
143
|
+
|
|
136
144
|
payload_dict: dict[str, Any] = {}
|
|
137
145
|
if envelope.payload:
|
|
138
146
|
try:
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
147
|
+
registry = ProtoRegistry()
|
|
148
|
+
decoded = registry.decode_known_payload(
|
|
149
|
+
envelope.mode, envelope.message_type, envelope.payload
|
|
150
|
+
)
|
|
151
|
+
payload_dict = decoded if decoded is not None else json.loads(envelope.payload)
|
|
152
|
+
except Exception:
|
|
153
|
+
try:
|
|
154
|
+
payload_dict = json.loads(envelope.payload)
|
|
155
|
+
except Exception:
|
|
156
|
+
payload_dict = {}
|
|
142
157
|
|
|
143
|
-
proposal_id: str | None =
|
|
144
|
-
|
|
145
|
-
|
|
158
|
+
proposal_id: str | None = (
|
|
159
|
+
payload_dict.get("proposal_id") or payload_dict.get("proposalId") or None
|
|
160
|
+
)
|
|
161
|
+
if proposal_id is not None:
|
|
162
|
+
proposal_id = str(proposal_id)
|
|
146
163
|
|
|
147
164
|
return IncomingMessage(
|
|
148
165
|
message_type=envelope.message_type,
|
|
@@ -99,7 +99,8 @@ class BaseSession(ABC):
|
|
|
99
99
|
intent: str,
|
|
100
100
|
participants: list[str],
|
|
101
101
|
ttl_ms: int,
|
|
102
|
-
|
|
102
|
+
context_id: str = "",
|
|
103
|
+
extensions: Mapping[str, bytes] | None = None,
|
|
103
104
|
roots: Iterable[Any] | None = None,
|
|
104
105
|
sender: str | None = None,
|
|
105
106
|
auth: AuthConfig | None = None,
|
|
@@ -113,7 +114,8 @@ class BaseSession(ABC):
|
|
|
113
114
|
mode_version=self.mode_version,
|
|
114
115
|
configuration_version=self.configuration_version,
|
|
115
116
|
policy_version=self.policy_version,
|
|
116
|
-
|
|
117
|
+
context_id=context_id,
|
|
118
|
+
extensions=extensions,
|
|
117
119
|
roots=roots,
|
|
118
120
|
)
|
|
119
121
|
envelope = build_envelope(
|
|
@@ -101,30 +101,42 @@ class MacpStream:
|
|
|
101
101
|
item = self._requests.get()
|
|
102
102
|
if item is self._END:
|
|
103
103
|
return
|
|
104
|
-
|
|
105
|
-
|
|
104
|
+
# RFC-MACP-0006-A1: support both envelope sends and subscribe frames
|
|
105
|
+
if isinstance(item, core_pb2.StreamSessionRequest):
|
|
106
|
+
yield item
|
|
107
|
+
else:
|
|
108
|
+
assert isinstance(item, envelope_pb2.Envelope)
|
|
109
|
+
yield core_pb2.StreamSessionRequest(envelope=item)
|
|
106
110
|
|
|
107
111
|
def _pump_responses(self) -> None:
|
|
108
112
|
try:
|
|
109
113
|
for response in self._call:
|
|
110
|
-
#
|
|
111
|
-
|
|
112
|
-
|
|
114
|
+
# StreamSessionResponse has envelope + error at the top level.
|
|
115
|
+
envelope = getattr(response, "envelope", None)
|
|
116
|
+
error = getattr(response, "error", None)
|
|
117
|
+
|
|
118
|
+
if error is not None and hasattr(error, "ByteSize") and error.ByteSize() > 0:
|
|
119
|
+
for cb in self._inline_error_callbacks:
|
|
120
|
+
cb(error)
|
|
121
|
+
logger.warning("inline stream error: %s", error)
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
if envelope is not None and envelope.ByteSize() > 0:
|
|
125
|
+
self._responses.put(envelope)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
# Fallback: try a nested .response wrapper (legacy proto shape).
|
|
113
129
|
inner = getattr(response, "response", None)
|
|
114
130
|
if inner is not None and hasattr(inner, "ByteSize") and inner.ByteSize() > 0:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if
|
|
118
|
-
self._responses.put(
|
|
119
|
-
elif
|
|
120
|
-
# Inline application-level error — notify callbacks, keep stream open
|
|
131
|
+
inner_env = getattr(inner, "envelope", None)
|
|
132
|
+
inner_err = getattr(inner, "error", None)
|
|
133
|
+
if inner_env is not None and inner_env.ByteSize() > 0:
|
|
134
|
+
self._responses.put(inner_env)
|
|
135
|
+
elif inner_err is not None:
|
|
121
136
|
for cb in self._inline_error_callbacks:
|
|
122
|
-
cb(
|
|
123
|
-
logger.warning("inline stream error: %s",
|
|
137
|
+
cb(inner_err)
|
|
138
|
+
logger.warning("inline stream error: %s", inner_err)
|
|
124
139
|
continue
|
|
125
|
-
else:
|
|
126
|
-
# Flat format: response.envelope
|
|
127
|
-
self._responses.put(response.envelope)
|
|
128
140
|
except grpc.RpcError as exc:
|
|
129
141
|
self._responses.put(exc)
|
|
130
142
|
finally:
|
|
@@ -139,6 +151,19 @@ class MacpStream:
|
|
|
139
151
|
raise MacpSdkError("stream is already closed")
|
|
140
152
|
self._requests.put(envelope)
|
|
141
153
|
|
|
154
|
+
def send_subscribe(self, session_id: str, after_sequence: int = 0) -> None:
|
|
155
|
+
"""RFC-MACP-0006-A1: Send a subscribe-only frame to receive session
|
|
156
|
+
history + live broadcast. The runtime replays accepted envelopes from
|
|
157
|
+
``after_sequence`` onwards, then continues with live broadcast.
|
|
158
|
+
"""
|
|
159
|
+
if self._closed:
|
|
160
|
+
raise MacpSdkError("stream is already closed")
|
|
161
|
+
req = core_pb2.StreamSessionRequest(
|
|
162
|
+
subscribe_session_id=session_id,
|
|
163
|
+
after_sequence=after_sequence,
|
|
164
|
+
)
|
|
165
|
+
self._requests.put(req)
|
|
166
|
+
|
|
142
167
|
def read(self, timeout: float | None = None) -> envelope_pb2.Envelope | None:
|
|
143
168
|
item = self._responses.get(timeout=timeout)
|
|
144
169
|
if item is self._END:
|
|
@@ -182,7 +207,7 @@ class MacpClient:
|
|
|
182
207
|
root_certificates: bytes | None = None,
|
|
183
208
|
default_timeout: float | None = None,
|
|
184
209
|
client_name: str = "macp-sdk-python",
|
|
185
|
-
client_version: str = "0.2.
|
|
210
|
+
client_version: str = "0.2.3",
|
|
186
211
|
) -> None:
|
|
187
212
|
if secure is None:
|
|
188
213
|
secure = not allow_insecure
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import json
|
|
4
3
|
import time
|
|
5
4
|
import uuid
|
|
6
5
|
from collections.abc import Iterable, Mapping, Sequence
|
|
@@ -47,16 +46,6 @@ def now_unix_ms() -> int:
|
|
|
47
46
|
return int(time.time() * 1000)
|
|
48
47
|
|
|
49
48
|
|
|
50
|
-
def encode_context(context: bytes | str | Mapping[str, object] | None) -> bytes:
|
|
51
|
-
if context is None:
|
|
52
|
-
return b""
|
|
53
|
-
if isinstance(context, bytes):
|
|
54
|
-
return context
|
|
55
|
-
if isinstance(context, str):
|
|
56
|
-
return context.encode("utf-8")
|
|
57
|
-
return json.dumps(context).encode("utf-8")
|
|
58
|
-
|
|
59
|
-
|
|
60
49
|
def build_root(uri: str, name: str = "") -> core_pb2.Root:
|
|
61
50
|
return core_pb2.Root(uri=uri, name=name)
|
|
62
51
|
|
|
@@ -69,7 +58,8 @@ def build_session_start_payload(
|
|
|
69
58
|
mode_version: str = DEFAULT_MODE_VERSION,
|
|
70
59
|
configuration_version: str = DEFAULT_CONFIGURATION_VERSION,
|
|
71
60
|
policy_version: str = DEFAULT_POLICY_VERSION,
|
|
72
|
-
|
|
61
|
+
context_id: str = "",
|
|
62
|
+
extensions: Mapping[str, bytes] | None = None,
|
|
73
63
|
roots: Iterable[core_pb2.Root] | None = None,
|
|
74
64
|
) -> core_pb2.SessionStartPayload:
|
|
75
65
|
return core_pb2.SessionStartPayload(
|
|
@@ -79,7 +69,8 @@ def build_session_start_payload(
|
|
|
79
69
|
configuration_version=configuration_version,
|
|
80
70
|
policy_version=policy_version,
|
|
81
71
|
ttl_ms=ttl_ms,
|
|
82
|
-
|
|
72
|
+
context_id=context_id,
|
|
73
|
+
extensions=dict(extensions) if extensions else {},
|
|
83
74
|
roots=list(roots or []),
|
|
84
75
|
)
|
|
85
76
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: macp-sdk-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python SDK for the MACP Rust runtime
|
|
5
5
|
Author-email: OpenAI <support@openai.com>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -17,7 +17,7 @@ Description-Content-Type: text/markdown
|
|
|
17
17
|
License-File: LICENSE
|
|
18
18
|
Requires-Dist: grpcio>=1.74.0
|
|
19
19
|
Requires-Dist: protobuf>=5.27.0
|
|
20
|
-
Requires-Dist: macp-proto<0.2.0,>=0.1.
|
|
20
|
+
Requires-Dist: macp-proto<0.2.0,>=0.1.2
|
|
21
21
|
Provides-Extra: dev
|
|
22
22
|
Requires-Dist: grpcio-tools>=1.74.0; extra == "dev"
|
|
23
23
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
@@ -161,5 +161,5 @@ Business logic — voting rules, AI decision heuristics, policy enforcement —
|
|
|
161
161
|
## Known runtime limitations
|
|
162
162
|
|
|
163
163
|
- `GetSession` returns metadata only (not mode state/transcript) — hence the local projection pattern
|
|
164
|
-
- `StreamSession`
|
|
164
|
+
- `StreamSession` is scoped to one session per stream; use `MacpStream.send_subscribe(session_id)` (RFC-MACP-0006-A1, since SDK 0.2.3 / `macp-proto 0.1.2`) to replay accepted history before live broadcast
|
|
165
165
|
- Business policy (majority, quorum, veto) belongs in your orchestrator/policy layer
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
import os
|
|
5
|
-
|
|
6
|
-
from ..auth import AuthConfig
|
|
7
|
-
from ..client import MacpClient
|
|
8
|
-
from ..constants import DEFAULT_POLICY_VERSION
|
|
9
|
-
from .participant import Participant
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def from_bootstrap(bootstrap_path: str | None = None) -> Participant:
|
|
13
|
-
"""Create a Participant from a bootstrap context file.
|
|
14
|
-
|
|
15
|
-
The bootstrap file is a JSON document produced by the MACP hosting
|
|
16
|
-
infrastructure containing the information needed to connect and
|
|
17
|
-
participate in a session.
|
|
18
|
-
|
|
19
|
-
If ``bootstrap_path`` is not provided, the ``MACP_BOOTSTRAP_FILE``
|
|
20
|
-
environment variable is used.
|
|
21
|
-
|
|
22
|
-
Expected bootstrap JSON structure::
|
|
23
|
-
|
|
24
|
-
{
|
|
25
|
-
"participant_id": "...",
|
|
26
|
-
"session_id": "...",
|
|
27
|
-
"mode": "macp.mode.decision.v1",
|
|
28
|
-
"runtime_url": "localhost:50051",
|
|
29
|
-
"auth": {
|
|
30
|
-
"bearer_token": "...", // or "agent_id": "..."
|
|
31
|
-
},
|
|
32
|
-
"participants": ["agent-a", "agent-b"],
|
|
33
|
-
"policy_version": "policy.default",
|
|
34
|
-
"secure": true, // default; set false only for local dev
|
|
35
|
-
"allow_insecure": false // required if secure=false
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
Transport security follows RFC-MACP-0006 §3: ``secure`` defaults to
|
|
39
|
-
``true``. Setting ``secure: false`` additionally requires
|
|
40
|
-
``allow_insecure: true`` in the bootstrap (opt-in for local dev only).
|
|
41
|
-
"""
|
|
42
|
-
path = bootstrap_path or os.environ.get("MACP_BOOTSTRAP_FILE")
|
|
43
|
-
if not path:
|
|
44
|
-
raise ValueError("No bootstrap path provided and MACP_BOOTSTRAP_FILE not set")
|
|
45
|
-
|
|
46
|
-
with open(path) as f:
|
|
47
|
-
ctx: dict[str, object] = json.load(f)
|
|
48
|
-
|
|
49
|
-
participant_id = str(ctx["participant_id"])
|
|
50
|
-
session_id = str(ctx["session_id"])
|
|
51
|
-
mode = str(ctx["mode"])
|
|
52
|
-
runtime_url = str(ctx.get("runtime_url", "localhost:50051"))
|
|
53
|
-
secure = bool(ctx.get("secure", True))
|
|
54
|
-
allow_insecure = bool(ctx.get("allow_insecure", False))
|
|
55
|
-
|
|
56
|
-
# Build auth config
|
|
57
|
-
auth_data = ctx.get("auth")
|
|
58
|
-
auth: AuthConfig | None = None
|
|
59
|
-
if isinstance(auth_data, dict):
|
|
60
|
-
bearer = auth_data.get("bearer_token")
|
|
61
|
-
agent_id = auth_data.get("agent_id")
|
|
62
|
-
expected_sender = auth_data.get("expected_sender") or participant_id
|
|
63
|
-
if bearer:
|
|
64
|
-
auth = AuthConfig.for_bearer(
|
|
65
|
-
str(bearer),
|
|
66
|
-
sender_hint=participant_id,
|
|
67
|
-
expected_sender=str(expected_sender),
|
|
68
|
-
)
|
|
69
|
-
elif agent_id:
|
|
70
|
-
auth = AuthConfig.for_dev_agent(str(agent_id), expected_sender=str(expected_sender))
|
|
71
|
-
|
|
72
|
-
# Build client
|
|
73
|
-
client = MacpClient(
|
|
74
|
-
target=runtime_url,
|
|
75
|
-
secure=secure,
|
|
76
|
-
allow_insecure=allow_insecure,
|
|
77
|
-
auth=auth,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
# Extract optional fields
|
|
81
|
-
raw_participants = ctx.get("participants")
|
|
82
|
-
participants: list[str] = []
|
|
83
|
-
if isinstance(raw_participants, list):
|
|
84
|
-
participants = [str(p) for p in raw_participants]
|
|
85
|
-
|
|
86
|
-
mode_version = ctx.get("mode_version")
|
|
87
|
-
configuration_version = ctx.get("configuration_version")
|
|
88
|
-
policy_version = ctx.get("policy_version")
|
|
89
|
-
|
|
90
|
-
return Participant(
|
|
91
|
-
participant_id=participant_id,
|
|
92
|
-
session_id=session_id,
|
|
93
|
-
mode=mode,
|
|
94
|
-
client=client,
|
|
95
|
-
auth=auth,
|
|
96
|
-
participants=participants,
|
|
97
|
-
mode_version=str(mode_version) if mode_version else None,
|
|
98
|
-
configuration_version=str(configuration_version) if configuration_version else None,
|
|
99
|
-
policy_version=str(policy_version) if policy_version else DEFAULT_POLICY_VERSION,
|
|
100
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{macp_sdk_python-0.2.1 → macp_sdk_python-0.2.3}/src/macp_sdk_python.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|