macp-sdk-python 0.10.2__tar.gz → 0.12.1__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.
Files changed (41) hide show
  1. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/PKG-INFO +1 -1
  2. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/pyproject.toml +1 -1
  3. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/__init__.py +58 -8
  4. macp_sdk_python-0.12.1/src/macp_sdk/agent/__init__.py +110 -0
  5. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/agent/runner.py +3 -3
  6. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/agent/strategies.py +49 -17
  7. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/base_projection.py +19 -21
  8. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/handoff.py +10 -7
  9. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/proposal.py +50 -7
  10. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/task.py +99 -35
  11. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/watchers.py +37 -10
  12. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk_python.egg-info/PKG-INFO +1 -1
  13. macp_sdk_python-0.10.2/src/macp_sdk/agent/__init__.py +0 -68
  14. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/LICENSE +0 -0
  15. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/README.md +0 -0
  16. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/setup.cfg +0 -0
  17. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/_logging.py +0 -0
  18. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/agent/cancel_callback.py +0 -0
  19. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/agent/dispatcher.py +0 -0
  20. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/agent/participant.py +0 -0
  21. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/agent/transports.py +0 -0
  22. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/agent/types.py +0 -0
  23. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/auth.py +0 -0
  24. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/base_session.py +0 -0
  25. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/client.py +0 -0
  26. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/commitment_hash.py +0 -0
  27. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/constants.py +0 -0
  28. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/decision.py +0 -0
  29. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/envelope.py +0 -0
  30. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/errors.py +0 -0
  31. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/policy.py +0 -0
  32. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/projections.py +0 -0
  33. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/proto_registry.py +0 -0
  34. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/py.typed +0 -0
  35. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/quorum.py +0 -0
  36. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/retry.py +0 -0
  37. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk/validation.py +0 -0
  38. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk_python.egg-info/SOURCES.txt +0 -0
  39. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk_python.egg-info/dependency_links.txt +0 -0
  40. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/src/macp_sdk_python.egg-info/requires.txt +0 -0
  41. {macp_sdk_python-0.10.2 → macp_sdk_python-0.12.1}/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.10.2
3
+ Version: 0.12.1
4
4
  Summary: Python SDK for the MACP Rust runtime
5
5
  Author-email: Multi-Agent Coordination Protocol <macp@multiagentcoordinationprotocol.org>
6
6
  License: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "macp-sdk-python"
7
- version = "0.10.2"
7
+ version = "0.12.1"
8
8
  description = "Python SDK for the MACP Rust runtime"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1,4 +1,6 @@
1
+ import warnings
1
2
  from importlib.metadata import version as _version
3
+ from typing import Any as _Any
2
4
 
3
5
  from ._logging import configure_logging
4
6
  from .auth import AuthConfig
@@ -97,11 +99,11 @@ from .projections import (
97
99
  DecisionVoteRecord,
98
100
  )
99
101
  from .proposal import (
100
- AcceptRecord,
102
+ ProposalAcceptRecord,
101
103
  ProposalProjection,
102
104
  ProposalRecord,
105
+ ProposalRejectRecord,
103
106
  ProposalSession,
104
- RejectRecord,
105
107
  )
106
108
  from .proto_registry import ProtoRegistry
107
109
  from .quorum import ApprovalRequestRecord, BallotRecord, QuorumProjection, QuorumSession
@@ -110,8 +112,8 @@ from .task import (
110
112
  TaskCompleteRecord,
111
113
  TaskFailRecord,
112
114
  TaskProjection,
115
+ TaskRecord,
113
116
  TaskRejectRecord,
114
- TaskRequestRecord,
115
117
  TaskSession,
116
118
  TaskUpdateRecord,
117
119
  )
@@ -135,7 +137,7 @@ from .watchers import (
135
137
  PolicyChange,
136
138
  PolicyWatcher,
137
139
  RootsWatcher,
138
- SessionLifecycle,
140
+ SessionLifecycleEvent,
139
141
  SessionLifecycleWatcher,
140
142
  SignalWatcher,
141
143
  )
@@ -176,7 +178,6 @@ __all__ = [
176
178
  "UNKNOWN_POLICY_VERSION",
177
179
  "UNSUPPORTED_PROTOCOL_VERSION",
178
180
  "AbstentionRules",
179
- "AcceptRecord",
180
181
  "AckFailure",
181
182
  "ApprovalRequestRecord",
182
183
  "AuthConfig",
@@ -211,19 +212,20 @@ __all__ = [
211
212
  "PolicyChange",
212
213
  "PolicyWatcher",
213
214
  "ProjectionAnomaly",
215
+ "ProposalAcceptRecord",
214
216
  "ProposalAcceptanceRules",
215
217
  "ProposalProjection",
216
218
  "ProposalRecord",
219
+ "ProposalRejectRecord",
217
220
  "ProposalSession",
218
221
  "ProtoRegistry",
219
222
  "QuorumProjection",
220
223
  "QuorumSession",
221
224
  "QuorumThreshold",
222
- "RejectRecord",
223
225
  "RejectionRules",
224
226
  "RetryPolicy",
225
227
  "RootsWatcher",
226
- "SessionLifecycle",
228
+ "SessionLifecycleEvent",
227
229
  "SessionLifecycleWatcher",
228
230
  "SignalWatcher",
229
231
  "TaskAssignmentRules",
@@ -231,8 +233,8 @@ __all__ = [
231
233
  "TaskCompletionRules",
232
234
  "TaskFailRecord",
233
235
  "TaskProjection",
236
+ "TaskRecord",
234
237
  "TaskRejectRecord",
235
- "TaskRequestRecord",
236
238
  "TaskSession",
237
239
  "TaskUpdateRecord",
238
240
  "VotingRules",
@@ -273,3 +275,51 @@ __all__ = [
273
275
  "validate_ttl_ms",
274
276
  "validate_vote",
275
277
  ]
278
+
279
+ # ── Deprecated aliases (issue #103 / multiagentcoordinationprotocol#135) ─────
280
+ #
281
+ # Kept out of __all__ deliberately -- a deprecated name should not appear in
282
+ # `from macp_sdk import *` or in generated API docs. Does not delegate to the
283
+ # defining submodule's own __getattr__ (proposal.py's, watchers.py's): doing
284
+ # so would point the warning's stacklevel at this module's frame instead of
285
+ # the caller's, since `from macp_sdk import OldName` (this top-level package)
286
+ # is the far more common import path -- same reasoning as agent/__init__.py's
287
+ # own alias dict.
288
+ #
289
+ # This module IS a package (`macp_sdk/`, has __path__), like agent/__init__.py.
290
+ # CPython's import machinery (`importlib._bootstrap._handle_fromlist`) probes
291
+ # any fromlist name against a package with `hasattr(module, name)` *before*
292
+ # the `from ... import` statement's own bytecode-level getattr -- so a
293
+ # deprecated name accessed via `from macp_sdk import RejectRecord` triggers
294
+ # this __getattr__ twice (confirmed empirically in Phase 1), not once. Under
295
+ # Python's default warning filters the two are deduplicated by (message,
296
+ # category, location) and a caller sees a single printed line regardless;
297
+ # under a strict `error` filter (as this repo's own test suite runs under)
298
+ # the first `warnings.warn()` call raises immediately, so the second never
299
+ # happens either. Only an explicit `simplefilter("always")` capture (as in
300
+ # this repo's own deprecation tests) observes both. By contrast, resolving
301
+ # the same old name from its *defining* submodule (`macp_sdk.proposal`,
302
+ # `macp_sdk.watchers`, `macp_sdk.task` -- plain files, no __path__) fires
303
+ # this pattern once.
304
+ #
305
+ # One dict, one __getattr__, covering all renamed top-level exports (issue
306
+ # #103 items 3-5, issue #108) -- each entry's defining submodule (proposal.py,
307
+ # watchers.py, task.py) owns its own identical-shaped __getattr__ for direct
308
+ # submodule imports; this one is only reached via the `macp_sdk` top-level
309
+ # package path.
310
+ _DEPRECATED_ALIASES = {
311
+ "RejectRecord": "ProposalRejectRecord",
312
+ "AcceptRecord": "ProposalAcceptRecord",
313
+ "SessionLifecycle": "SessionLifecycleEvent",
314
+ "TaskRequestRecord": "TaskRecord",
315
+ }
316
+
317
+
318
+ def __getattr__(name: str) -> _Any:
319
+ new_name = _DEPRECATED_ALIASES.get(name)
320
+ if new_name is None:
321
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
322
+ warnings.warn(
323
+ f"{name} is deprecated; use {new_name} instead.", DeprecationWarning, stacklevel=2
324
+ )
325
+ return globals()[new_name]
@@ -0,0 +1,110 @@
1
+ import warnings
2
+ from typing import Any
3
+
4
+ from .cancel_callback import CancelCallbackServer, start_cancel_callback_server
5
+ from .dispatcher import Dispatcher
6
+ from .participant import InitiatorConfig, Participant, ParticipantActions
7
+ from .runner import from_bootstrap
8
+ from .strategies import (
9
+ CommitmentResult,
10
+ CommitmentStrategy,
11
+ EvaluationResult,
12
+ EvaluationStrategy,
13
+ VoteResult,
14
+ VotingStrategy,
15
+ commitment_handler,
16
+ evaluation_handler,
17
+ function_committer,
18
+ function_evaluator,
19
+ function_voter,
20
+ majority_committer,
21
+ majority_voter,
22
+ voting_handler,
23
+ )
24
+ from .transports import (
25
+ GrpcTransportAdapter,
26
+ HttpTransportAdapter,
27
+ TransportAdapter,
28
+ )
29
+ from .types import (
30
+ HandlerContext,
31
+ IncomingMessage,
32
+ MessageHandler,
33
+ PhaseChangeHandler,
34
+ SessionInfo,
35
+ TerminalHandler,
36
+ TerminalResult,
37
+ )
38
+
39
+ __all__ = [
40
+ "CancelCallbackServer",
41
+ "CommitmentResult",
42
+ "CommitmentStrategy",
43
+ "Dispatcher",
44
+ "EvaluationResult",
45
+ "EvaluationStrategy",
46
+ "GrpcTransportAdapter",
47
+ "HandlerContext",
48
+ "HttpTransportAdapter",
49
+ "IncomingMessage",
50
+ "InitiatorConfig",
51
+ "MessageHandler",
52
+ "Participant",
53
+ "ParticipantActions",
54
+ "PhaseChangeHandler",
55
+ "SessionInfo",
56
+ "TerminalHandler",
57
+ "TerminalResult",
58
+ "TransportAdapter",
59
+ "VoteResult",
60
+ "VotingStrategy",
61
+ "commitment_handler",
62
+ "evaluation_handler",
63
+ "from_bootstrap",
64
+ "function_committer",
65
+ "function_evaluator",
66
+ "function_voter",
67
+ "majority_committer",
68
+ "majority_voter",
69
+ "start_cancel_callback_server",
70
+ "voting_handler",
71
+ ]
72
+
73
+ # ── Deprecated aliases (issue #103 / multiagentcoordinationprotocol#135) ─────
74
+ #
75
+ # Kept out of __all__ deliberately -- a deprecated name should not appear in
76
+ # `from macp_sdk.agent import *` or in generated API docs. Does not delegate
77
+ # to strategies.py's own __getattr__: doing so would point the warning's
78
+ # stacklevel at this module's frame instead of the caller's, since that's the
79
+ # far more common `from macp_sdk.agent import VoteDecision` import path.
80
+ #
81
+ # This module IS a package (`agent/`, has `__path__`), unlike strategies.py.
82
+ # CPython's import machinery (`importlib._bootstrap._handle_fromlist`) probes
83
+ # any fromlist name against a package with `hasattr(module, name)` *before*
84
+ # the `from ... import` statement's own bytecode-level getattr -- so a
85
+ # deprecated name accessed via `from macp_sdk.agent import VoteDecision`
86
+ # triggers this __getattr__ twice (confirmed empirically), not once. Under
87
+ # Python's default warning filters the two are deduplicated by (message,
88
+ # category, location) and a caller sees a single printed line regardless;
89
+ # under a strict `error` filter (as this repo's own test suite runs under)
90
+ # the first `warnings.warn()` call raises immediately, so the second never
91
+ # happens either. Only an explicit `simplefilter("always")` capture (as in
92
+ # this repo's own deprecation tests) observes both. This is inherent to
93
+ # package-level `__getattr__` and not specific to this alias -- there is no
94
+ # fix that preserves the "re-warn on every plain attribute access" property
95
+ # documented in strategies.py's own alias comment without also suppressing
96
+ # this.
97
+ _DEPRECATED_ALIASES = {
98
+ "VoteDecision": "VoteResult",
99
+ "CommitmentDecision": "CommitmentResult",
100
+ }
101
+
102
+
103
+ def __getattr__(name: str) -> Any:
104
+ new_name = _DEPRECATED_ALIASES.get(name)
105
+ if new_name is None:
106
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
107
+ warnings.warn(
108
+ f"{name} is deprecated; use {new_name} instead.", DeprecationWarning, stacklevel=2
109
+ )
110
+ return globals()[new_name]
@@ -155,9 +155,9 @@ def from_bootstrap(bootstrap_path: str | None = None) -> Participant:
155
155
  participants=[str(p) for p in ss.get("participants", participants)],
156
156
  ttl_ms=int(ss.get("ttl_ms", 300000)),
157
157
  # Runtime v0.5.0 per-session suspension cap; absent → 0 (runtime
158
- # default). The TypeScript bootstrap schema does not yet map this
159
- # key, so it is Python-only for now; a bootstrap that omits it
160
- # behaves identically in both SDKs.
158
+ # default). macp-sdk-typescript's BootstrapPayload also declares
159
+ # and maps this key (src/agent/runner.ts:28,103); a bootstrap
160
+ # that omits it behaves identically in both SDKs.
161
161
  max_suspend_ms=int(ss.get("max_suspend_ms", 0)),
162
162
  context_id=str(ss.get("context_id", "")),
163
163
  extensions=_decode_extensions(ss.get("extensions")),
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import warnings
3
4
  from collections.abc import Callable
4
5
  from dataclasses import dataclass
5
6
  from typing import Any, Protocol
@@ -86,7 +87,7 @@ def function_evaluator(
86
87
 
87
88
 
88
89
  @dataclass(frozen=True, slots=True)
89
- class VoteDecision:
90
+ class VoteResult:
90
91
  """Result of a voting decision."""
91
92
 
92
93
  vote: str
@@ -98,7 +99,7 @@ class VotingStrategy(Protocol):
98
99
 
99
100
  def should_vote(self, projection: Any) -> bool: ...
100
101
 
101
- def decide_vote(self, projection: Any) -> VoteDecision: ...
102
+ def decide_vote(self, projection: Any) -> VoteResult: ...
102
103
 
103
104
 
104
105
  def voting_handler(strategy: VotingStrategy) -> MessageHandler:
@@ -132,7 +133,7 @@ def voting_handler(strategy: VotingStrategy) -> MessageHandler:
132
133
 
133
134
  def function_voter(
134
135
  should_vote_fn: Callable[[Any], bool],
135
- decide_fn: Callable[[Any], VoteDecision],
136
+ decide_fn: Callable[[Any], VoteResult],
136
137
  ) -> VotingStrategy:
137
138
  """Wrap plain functions as a VotingStrategy."""
138
139
 
@@ -142,7 +143,7 @@ def function_voter(
142
143
  def __init__(
143
144
  self,
144
145
  should_fn: Callable[[Any], bool],
145
- decide_fn: Callable[[Any], VoteDecision],
146
+ decide_fn: Callable[[Any], VoteResult],
146
147
  ) -> None:
147
148
  self._should_fn = should_fn
148
149
  self._decide_fn = decide_fn
@@ -150,7 +151,7 @@ def function_voter(
150
151
  def should_vote(self, projection: Any) -> bool:
151
152
  return self._should_fn(projection)
152
153
 
153
- def decide_vote(self, projection: Any) -> VoteDecision:
154
+ def decide_vote(self, projection: Any) -> VoteResult:
154
155
  return self._decide_fn(projection)
155
156
 
156
157
  return _FnVoter(should_vote_fn, decide_fn)
@@ -160,7 +161,7 @@ def function_voter(
160
161
 
161
162
 
162
163
  @dataclass(frozen=True, slots=True)
163
- class CommitmentDecision:
164
+ class CommitmentResult:
164
165
  """Result of a commitment decision."""
165
166
 
166
167
  action: str
@@ -174,7 +175,7 @@ class CommitmentStrategy(Protocol):
174
175
 
175
176
  def should_commit(self, projection: Any) -> bool: ...
176
177
 
177
- def decide_commitment(self, projection: Any) -> CommitmentDecision: ...
178
+ def decide_commitment(self, projection: Any) -> CommitmentResult: ...
178
179
 
179
180
 
180
181
  def commitment_handler(strategy: CommitmentStrategy) -> MessageHandler:
@@ -219,7 +220,7 @@ def commitment_handler(strategy: CommitmentStrategy) -> MessageHandler:
219
220
 
220
221
  def function_committer(
221
222
  should_commit_fn: Callable[[Any], bool],
222
- decide_fn: Callable[[Any], CommitmentDecision],
223
+ decide_fn: Callable[[Any], CommitmentResult],
223
224
  ) -> CommitmentStrategy:
224
225
  """Wrap plain functions as a CommitmentStrategy."""
225
226
 
@@ -229,7 +230,7 @@ def function_committer(
229
230
  def __init__(
230
231
  self,
231
232
  should_fn: Callable[[Any], bool],
232
- decide_fn: Callable[[Any], CommitmentDecision],
233
+ decide_fn: Callable[[Any], CommitmentResult],
233
234
  ) -> None:
234
235
  self._should_fn = should_fn
235
236
  self._decide_fn = decide_fn
@@ -237,7 +238,7 @@ def function_committer(
237
238
  def should_commit(self, projection: Any) -> bool:
238
239
  return self._should_fn(projection)
239
240
 
240
- def decide_commitment(self, projection: Any) -> CommitmentDecision:
241
+ def decide_commitment(self, projection: Any) -> CommitmentResult:
241
242
  return self._decide_fn(projection)
242
243
 
243
244
  return _FnCommitter(should_commit_fn, decide_fn)
@@ -286,10 +287,10 @@ def majority_voter(
286
287
  evaluations = getattr(projection, "evaluations", None)
287
288
  return bool(evaluations)
288
289
 
289
- def decide_vote(self, projection: Any) -> VoteDecision:
290
+ def decide_vote(self, projection: Any) -> VoteResult:
290
291
  evaluations = list(getattr(projection, "evaluations", None) or [])
291
292
  if not evaluations:
292
- return VoteDecision(vote="ABSTAIN", reason="no evaluations to decide from")
293
+ return VoteResult(vote="ABSTAIN", reason="no evaluations to decide from")
293
294
  # The most recently evaluated proposal is the one being voted on.
294
295
  proposal_id = evaluations[-1].proposal_id
295
296
  qualifying = [
@@ -298,21 +299,21 @@ def majority_voter(
298
299
  if e.proposal_id == proposal_id and e.recommendation.upper() != "REVIEW"
299
300
  ]
300
301
  if not qualifying:
301
- return VoteDecision(
302
+ return VoteResult(
302
303
  vote="ABSTAIN",
303
304
  reason=f"no qualifying evaluations for {proposal_id!r}",
304
305
  )
305
306
  approvals = sum(1 for e in qualifying if e.recommendation.upper() == "APPROVE")
306
307
  ratio = approvals / len(qualifying)
307
308
  if ratio >= self._threshold:
308
- return VoteDecision(
309
+ return VoteResult(
309
310
  vote="APPROVE",
310
311
  reason=(
311
312
  f"{approvals}/{len(qualifying)} evaluations approve "
312
313
  f"{proposal_id!r} (>= {self._threshold:.0%})"
313
314
  ),
314
315
  )
315
- return VoteDecision(
316
+ return VoteResult(
316
317
  vote="ABSTAIN",
317
318
  reason=(
318
319
  f"{approvals}/{len(qualifying)} evaluations approve "
@@ -355,9 +356,9 @@ def majority_committer(
355
356
  return False
356
357
  return projection.majority_winner() is not None
357
358
 
358
- def decide_commitment(self, projection: Any) -> CommitmentDecision:
359
+ def decide_commitment(self, projection: Any) -> CommitmentResult:
359
360
  winner = projection.majority_winner()
360
- return CommitmentDecision(
361
+ return CommitmentResult(
361
362
  action=self._action,
362
363
  authority_scope=self._scope,
363
364
  reason=f"majority winner: {winner}",
@@ -365,3 +366,34 @@ def majority_committer(
365
366
  )
366
367
 
367
368
  return _MajorityCommitter(quorum_size, action, authority_scope)
369
+
370
+
371
+ # ── Deprecated aliases (issue #103 / multiagentcoordinationprotocol#135) ─────
372
+ #
373
+ # ``VoteDecision``/``CommitmentDecision`` are the pre-rename names, kept as
374
+ # module-level lazy aliases (PEP 562) for one minor version, removed at this
375
+ # SDK's next major. A plain assignment (``VoteDecision = VoteResult``) would
376
+ # be silent; a wrapper subclass would fight ``frozen=True, slots=True``. This
377
+ # module has no ``__path__`` (a plain file, not a package), so CPython's
378
+ # ``from ... import VoteDecision`` resolves via a single ``getattr`` call --
379
+ # the warning fires exactly once per such import, not per call. (Contrast
380
+ # ``agent/__init__.py``'s own alias dict: a *package* import goes through an
381
+ # extra internal ``hasattr`` probe first, firing this pattern twice -- see
382
+ # the comment there.) Plain attribute access (``strategies.VoteDecision``,
383
+ # no ``from`` import) is not cached in ``globals()`` and re-warns on every
384
+ # such access -- the returned object ``is`` its ``*Result`` counterpart
385
+ # either way.
386
+ _DEPRECATED_ALIASES = {
387
+ "VoteDecision": "VoteResult",
388
+ "CommitmentDecision": "CommitmentResult",
389
+ }
390
+
391
+
392
+ def __getattr__(name: str) -> Any:
393
+ new_name = _DEPRECATED_ALIASES.get(name)
394
+ if new_name is None:
395
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
396
+ warnings.warn(
397
+ f"{name} is deprecated; use {new_name} instead.", DeprecationWarning, stacklevel=2
398
+ )
399
+ return globals()[new_name]
@@ -8,28 +8,27 @@ from macp.v1 import core_pb2, envelope_pb2
8
8
 
9
9
  from ._logging import logger
10
10
 
11
- # Cross-SDK contract with macp-sdk-typescript#55 — these two string values are
12
- # part of the wire-adjacent public API and must match the TypeScript SDK
13
- # byte-for-byte. They are also the two kinds pinned by the parity manifest
11
+ # Cross-SDK contract with macp-sdk-typescript#55 — these four string values
12
+ # are part of the wire-adjacent public API and must match the TypeScript SDK
13
+ # byte-for-byte. They are also the four kinds pinned by the parity manifest
14
14
  # (schemas/parity/contract.json's projection_anomaly.kinds) as of contract
15
- # 1.1.1 -- an implementation both SDKs already agreed on.
15
+ # 1.2.0 -- an implementation both SDKs now agree on.
16
16
  ANOMALY_DUPLICATE_VOTE = "duplicate_vote" # RFC-MACP-0007 §5.3
17
17
  ANOMALY_DUPLICATE_BALLOT = "duplicate_ballot" # RFC-MACP-0011 §5
18
18
 
19
19
  # Issue #94 (spec issue #148): whether a discarded competing TaskAccept or an
20
- # already-settled Handoff message should record an anomaly was, until now, an
21
- # open question the parity manifest deliberately left unpinned -- six call
22
- # sites in macp-sdk-typescript were marked "frozen pending cross-SDK
23
- # agreement with macp-sdk-python". This SDK adopts these two kinds
24
- # unilaterally as its side of that agreement (tracked via a matching issue
25
- # filed against macp-sdk-typescript, proposing the same two names) -- they
26
- # are NOT yet in the parity manifest's kinds list, and must not be asserted
27
- # against it until macp-sdk-typescript lands its matching side and the
28
- # manifest is bumped (a MINOR contract_version bump, per its versioning
29
- # rule). One kind per *mode* (not per cause): both cover only an
30
- # already-settled discard, never an unknown-subject-id discard -- an unknown
31
- # task_id/handoff_id can legitimately mean a projection that joined
32
- # mid-session, which is not caller misuse, so no kind is recorded for it.
20
+ # already-settled Handoff message should record an anomaly was an open
21
+ # question the parity manifest deliberately left unpinned -- six call sites
22
+ # in macp-sdk-typescript were marked "frozen pending cross-SDK agreement
23
+ # with macp-sdk-python". This SDK adopted these two kinds as its side of
24
+ # that agreement in PR #95; macp-sdk-typescript landed the matching side in
25
+ # its PR #134; the manifest bumped to contract 1.2.0 (spec PR #158) to
26
+ # mirror the now-settled agreement, and both kinds are pinned by
27
+ # tests/parity/test_contract.py accordingly. One kind per *mode* (not per
28
+ # cause): both cover only an already-settled discard, never an
29
+ # unknown-subject-id discard -- an unknown task_id/handoff_id can
30
+ # legitimately mean a projection that joined mid-session, which is not
31
+ # caller misuse, so no kind is recorded for it.
33
32
  ANOMALY_DUPLICATE_TASK_ACCEPT = "duplicate_task_accept" # RFC-MACP-0009 §5 rule 3a
34
33
  ANOMALY_SETTLED_HANDOFF = "settled_handoff" # RFC-MACP-0010 §5 rule 4 / §5.1(4)
35
34
 
@@ -43,10 +42,9 @@ class ProjectionAnomaly:
43
42
  Do not rename, reorder, or extend the field set without coordinating
44
43
  there first.
45
44
 
46
- ``kind`` values in the parity manifest (agreed by both SDKs):
47
- ``ANOMALY_DUPLICATE_VOTE``, ``ANOMALY_DUPLICATE_BALLOT``. ``kind`` values
48
- this SDK also records, pending macp-sdk-typescript landing a matching
49
- kind (issue #94): ``ANOMALY_DUPLICATE_TASK_ACCEPT``,
45
+ ``kind`` values, all four pinned by the parity manifest (agreed by both
46
+ SDKs) as of contract 1.2.0: ``ANOMALY_DUPLICATE_VOTE``,
47
+ ``ANOMALY_DUPLICATE_BALLOT``, ``ANOMALY_DUPLICATE_TASK_ACCEPT``,
50
48
  ``ANOMALY_SETTLED_HANDOFF`` -- see those constants' own comments.
51
49
 
52
50
  Honesty clause: this records an **observation**, not a spec-violation
@@ -30,9 +30,11 @@ class HandoffRecord:
30
30
  declined_by: str | None
31
31
  # True when the acceptance was an implicit accept synthesized by the
32
32
  # runtime (RFC-MACP-0010 §5.1) rather than an explicit client HandoffAccept.
33
- # Runtime >= 0.8.0 emits these automatically for every session (no
34
- # server-side opt-in exists) when an offer's implicit_accept_timeout_ms
35
- # elapses unactioned. Client-submitted accepts are always
33
+ # Runtime >= 0.8.0 emits these only for sessions whose bound governance
34
+ # policy declares a non-zero ``acceptance.implicit_accept_timeout_ms``
35
+ # (RFC-MACP-0010 §5.1, RFC-MACP-0012 §4.5), once that timeout elapses
36
+ # unactioned; with no policy bound the timeout resolves to 0 and no
37
+ # implicit accept ever fires. Client-submitted accepts are always
36
38
  # ``implicit=False`` (the runtime rejects a forged True).
37
39
  implicit: bool = False
38
40
 
@@ -177,9 +179,10 @@ class HandoffProjection(BaseProjection):
177
179
  """True if *handoff_id* was accepted by a runtime implicit accept.
178
180
 
179
181
  Distinguishes a timeout-driven implicit accept (RFC-MACP-0010 §5.1)
180
- from an explicit client ``HandoffAccept``. Runtime >= 0.8.0 emits
181
- these automatically for every session once an offer's
182
- ``implicit_accept_timeout_ms`` elapses unactioned.
182
+ from an explicit client ``HandoffAccept``. Runtime >= 0.8.0 emits one
183
+ only when the session's bound governance policy declares a non-zero
184
+ ``acceptance.implicit_accept_timeout_ms`` and that timeout elapses
185
+ unactioned; sessions with no such policy never see one.
183
186
  """
184
187
  handoff = self.handoffs.get(handoff_id)
185
188
  return handoff is not None and handoff.status == "accepted" and handoff.implicit
@@ -275,7 +278,7 @@ class HandoffSession(BaseSession):
275
278
  # ``implicit=true`` for runtime-synthesized accepts and the runtime
276
279
  # rejects a client-submitted True. Client accepts always leave the
277
280
  # field at its proto3 default (False). See the regression test in
278
- # tests/unit/test_handoff.py.
281
+ # tests/unit/test_absorb_runtime_v050.py.
279
282
  validate_required_field("handoff_id", handoff_id)
280
283
  payload = handoff_pb2.HandoffAcceptPayload(
281
284
  handoff_id=handoff_id,
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import warnings
3
4
  from dataclasses import dataclass
5
+ from typing import Any
4
6
 
5
7
  from macp.modes.proposal.v1 import proposal_pb2
6
8
  from macp.v1 import envelope_pb2
@@ -25,12 +27,27 @@ class ProposalRecord:
25
27
  summary: str
26
28
  proposer: str
27
29
  supersedes: str # "" if original
28
- status: str # "open" | "accepted" | "rejected" | "withdrawn"
30
+ # Reachable values only. An Accept is recorded on the projection's
31
+ # ``accepts`` list (and surfaced via ``accepted_proposal`` /
32
+ # ``is_accepted``), never on this field -- no code path assigns
33
+ # "accepted" here. A non-terminal Reject likewise leaves this "open";
34
+ # only ``terminal=True`` sets "rejected".
35
+ #
36
+ # That is by design, not an omission (issue #112). Acceptance is a
37
+ # per-sender, supersedable relation (RFC-MACP-0008 §5 rule 5), not a
38
+ # per-proposal fact, so a scalar field here cannot hold it: "alice
39
+ # accepts p2 while bob still accepts p1" is a legal state. This mirrors
40
+ # the runtime, whose ``ProposalDisposition`` is {Live, Withdrawn} with
41
+ # acceptance in a separate ``accepts`` map, and typescript-sdk's
42
+ # ``projections/proposal.ts``, which also never assigns "accepted".
43
+ # ``task.py``/``handoff.py`` do set "accepted" because their acceptance
44
+ # is one actor claiming one slot. See docs/modes/proposal.md.
45
+ status: str # "open" | "rejected" | "withdrawn"
29
46
  tags: list[str]
30
47
 
31
48
 
32
49
  @dataclass(slots=True)
33
- class RejectRecord:
50
+ class ProposalRejectRecord:
34
51
  proposal_id: str
35
52
  reason: str
36
53
  sender: str
@@ -38,7 +55,7 @@ class RejectRecord:
38
55
 
39
56
 
40
57
  @dataclass(slots=True)
41
- class AcceptRecord:
58
+ class ProposalAcceptRecord:
42
59
  proposal_id: str
43
60
  reason: str
44
61
  sender: str
@@ -58,8 +75,8 @@ class ProposalProjection(BaseProjection):
58
75
  super().__init__()
59
76
  self.phase = "Negotiating"
60
77
  self.proposals: dict[str, ProposalRecord] = {}
61
- self.accepts: list[AcceptRecord] = []
62
- self.rejections: list[RejectRecord] = []
78
+ self.accepts: list[ProposalAcceptRecord] = []
79
+ self.rejections: list[ProposalRejectRecord] = []
63
80
  # Tracks each sender's most recent Accept, so a later Accept from the
64
81
  # same sender supersedes an earlier one (RFC-MACP-0008 §5 rule 5).
65
82
  # `self.accepts` remains the full audit trail; this is the derived
@@ -102,7 +119,7 @@ class ProposalProjection(BaseProjection):
102
119
  p = proposal_pb2.AcceptPayload()
103
120
  p.ParseFromString(envelope.payload)
104
121
  self.accepts.append(
105
- AcceptRecord(
122
+ ProposalAcceptRecord(
106
123
  proposal_id=p.proposal_id,
107
124
  reason=p.reason,
108
125
  sender=envelope.sender,
@@ -115,7 +132,7 @@ class ProposalProjection(BaseProjection):
115
132
  p = proposal_pb2.RejectPayload()
116
133
  p.ParseFromString(envelope.payload)
117
134
  self.rejections.append(
118
- RejectRecord(
135
+ ProposalRejectRecord(
119
136
  proposal_id=p.proposal_id,
120
137
  reason=p.reason,
121
138
  sender=envelope.sender,
@@ -326,3 +343,29 @@ class ProposalSession(BaseSession):
326
343
  payload=serialize_message(payload),
327
344
  )
328
345
  return self._send_and_track(envelope, auth=auth)
346
+
347
+
348
+ # ── Deprecated aliases (issue #103 / multiagentcoordinationprotocol#135) ─────
349
+ #
350
+ # ``RejectRecord``/``AcceptRecord`` are the pre-rename names, kept as
351
+ # module-level lazy aliases (PEP 562) for one minor version, removed at this
352
+ # SDK's next major. Same mechanism and reasoning as ``agent/strategies.py``'s
353
+ # own alias dict -- see the comment there for the full rationale (plain
354
+ # assignment is silent; a wrapper subclass is unnecessary complexity here
355
+ # too). This module has no ``__path__`` (a plain file, not a package), so
356
+ # ``from macp_sdk.proposal import RejectRecord`` resolves via a single
357
+ # ``getattr`` call -- the warning fires exactly once per such import.
358
+ _DEPRECATED_ALIASES = {
359
+ "RejectRecord": "ProposalRejectRecord",
360
+ "AcceptRecord": "ProposalAcceptRecord",
361
+ }
362
+
363
+
364
+ def __getattr__(name: str) -> Any:
365
+ new_name = _DEPRECATED_ALIASES.get(name)
366
+ if new_name is None:
367
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
368
+ warnings.warn(
369
+ f"{name} is deprecated; use {new_name} instead.", DeprecationWarning, stacklevel=2
370
+ )
371
+ return globals()[new_name]
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import warnings
4
4
  from dataclasses import dataclass
5
+ from typing import Any
5
6
 
6
7
  from macp.modes.task.v1 import task_pb2
7
8
  from macp.v1 import envelope_pb2
@@ -19,12 +20,17 @@ from .validation import validate_required_field
19
20
 
20
21
 
21
22
  @dataclass(slots=True)
22
- class TaskRequestRecord:
23
+ class TaskRecord:
23
24
  task_id: str
24
25
  title: str
25
26
  instructions: str
26
27
  requested_assignee: str
27
28
  requester: str
29
+ status: str = "requested"
30
+ progress: float = 0.0
31
+ assignee: str | None = None
32
+ deadline_unix_ms: int = 0
33
+ input: bytes = b""
28
34
 
29
35
 
30
36
  @dataclass(slots=True)
@@ -78,18 +84,16 @@ class TaskProjection(BaseProjection):
78
84
  def __init__(self) -> None:
79
85
  super().__init__()
80
86
  self.phase = "Pending"
81
- self.tasks: dict[str, TaskRequestRecord] = {}
87
+ self.tasks: dict[str, TaskRecord] = {}
82
88
  self.updates: list[TaskUpdateRecord] = []
89
+ self.rejections: list[TaskRejectRecord] = []
83
90
  self.completions: list[TaskCompleteRecord] = []
84
91
  self.failures: list[TaskFailRecord] = []
85
- # Per-task mutable state (reporting view — what a caller reads)
86
- self._assignees: dict[str, str] = {} # task_id -> assignee
87
- self._statuses: dict[str, str] = {} # task_id -> status
88
- self._progress: dict[str, float] = {} # task_id -> progress
89
92
  # Session-scoped single-assignee slot (RFC-MACP-0009 §5 rule 3: "Only
90
93
  # one assignee may become active for the Session in base v1" — scoped
91
94
  # to the whole session, not to a task_id). This is the exclusivity
92
- # guard; ``_assignees`` above remains the per-task reporting view.
95
+ # guard; each task's own ``assignee`` field on ``self.tasks`` remains
96
+ # the per-task reporting view.
93
97
  # Mirrors typescript-sdk's ``activeAssignment`` field.
94
98
  self.active_assignment: tuple[str, str] | None = None # (sender, task_id)
95
99
 
@@ -99,15 +103,20 @@ class TaskProjection(BaseProjection):
99
103
  if mt == "TaskRequest":
100
104
  p = task_pb2.TaskRequestPayload()
101
105
  p.ParseFromString(envelope.payload)
102
- self.tasks[p.task_id] = TaskRequestRecord(
106
+ self.tasks[p.task_id] = TaskRecord(
103
107
  task_id=p.task_id,
104
108
  title=p.title,
105
109
  instructions=p.instructions,
106
110
  requested_assignee=p.requested_assignee,
107
111
  requester=envelope.sender,
112
+ status="requested",
113
+ progress=0.0,
114
+ assignee=None,
115
+ deadline_unix_ms=p.deadline_unix_ms,
116
+ input=p.input,
108
117
  )
109
- self._statuses[p.task_id] = "requested"
110
- self._progress[p.task_id] = 0.0
118
+ if self.active_assignment is not None and self.active_assignment[1] == p.task_id:
119
+ self.active_assignment = None
111
120
  self._set_phase("Requested")
112
121
  return
113
122
 
@@ -118,8 +127,8 @@ class TaskProjection(BaseProjection):
118
127
  if self.active_assignment is None:
119
128
  assignee = p.assignee or envelope.sender
120
129
  self.active_assignment = (envelope.sender, p.task_id)
121
- self._assignees[p.task_id] = assignee
122
- self._statuses[p.task_id] = "accepted"
130
+ self.tasks[p.task_id].assignee = assignee
131
+ self.tasks[p.task_id].status = "accepted"
123
132
  self._set_phase("InProgress")
124
133
  else:
125
134
  # RFC-MACP-0009 §5 rule 3a: a second TaskAccept while the
@@ -146,15 +155,27 @@ class TaskProjection(BaseProjection):
146
155
  if mt == "TaskReject":
147
156
  p = task_pb2.TaskRejectPayload()
148
157
  p.ParseFromString(envelope.payload)
149
- # Status write is gated on the task being known (task.ts:136's
150
- # `if (task)`); slot-freeing is a SEPARATE, unconditional check on
151
- # sender alone (task.ts:158-161) — a slot-holder's TaskReject
152
- # naming an unknown task_id still frees their held slot.
158
+ # The rejection record is kept unconditionally (mirrors updates/
159
+ # completions/failures' own audit-trail convention); only the
160
+ # per-task status write is gated on the task being known
161
+ # (task.ts:136's `if (task)`). Slot-freeing is a SEPARATE,
162
+ # unconditional check on sender alone (task.ts:158-161) — a
163
+ # slot-holder's TaskReject naming an unknown task_id still frees
164
+ # their held slot.
165
+ self.rejections.append(
166
+ TaskRejectRecord(
167
+ task_id=p.task_id,
168
+ assignee=p.assignee or envelope.sender,
169
+ reason=p.reason,
170
+ )
171
+ )
153
172
  if p.task_id in self.tasks:
154
- self._statuses[p.task_id] = "rejected"
173
+ self.tasks[p.task_id].status = "rejected"
155
174
  slot = self.active_assignment
156
175
  if slot is not None and slot[0] == envelope.sender:
157
- self._assignees.pop(slot[1], None)
176
+ held = self.tasks.get(slot[1])
177
+ if held is not None:
178
+ held.assignee = None
158
179
  self.active_assignment = None
159
180
  return
160
181
 
@@ -173,8 +194,8 @@ class TaskProjection(BaseProjection):
173
194
  )
174
195
  )
175
196
  if p.task_id in self.tasks:
176
- self._statuses[p.task_id] = "in_progress"
177
- self._progress[p.task_id] = p.progress
197
+ self.tasks[p.task_id].status = "in_progress"
198
+ self.tasks[p.task_id].progress = p.progress
178
199
  return
179
200
 
180
201
  if mt == "TaskComplete":
@@ -189,8 +210,8 @@ class TaskProjection(BaseProjection):
189
210
  )
190
211
  )
191
212
  if p.task_id in self.tasks:
192
- self._statuses[p.task_id] = "completed"
193
- self._progress[p.task_id] = 1.0
213
+ self.tasks[p.task_id].status = "completed"
214
+ self.tasks[p.task_id].progress = 1.0
194
215
  self._set_phase("Completed")
195
216
  return
196
217
 
@@ -207,32 +228,46 @@ class TaskProjection(BaseProjection):
207
228
  )
208
229
  )
209
230
  if p.task_id in self.tasks:
210
- self._statuses[p.task_id] = "failed"
231
+ self.tasks[p.task_id].status = "failed"
211
232
  self._set_phase("Failed")
212
233
 
213
234
  # -- State query helpers --
214
235
 
215
- def get_task(self, task_id: str) -> TaskRequestRecord | None:
216
- """Return the task request record for *task_id*, or None."""
236
+ def get_task(self, task_id: str) -> TaskRecord | None:
237
+ """Return the task's current record for *task_id*, or None.
238
+
239
+ Reflects live state, not just the original request: ``status``,
240
+ ``progress``, and ``assignee`` update in place as later messages
241
+ (TaskAccept/TaskUpdate/TaskComplete/TaskFail/TaskReject) arrive.
242
+
243
+ The returned object is this projection's own record, not a copy --
244
+ treat it as read-only. Mutating it mutates projection state directly
245
+ (same contract as :meth:`ProposalProjection.live_proposals`'s
246
+ returned records).
247
+ """
217
248
  return self.tasks.get(task_id)
218
249
 
219
250
  def current_assignee(self, task_id: str) -> str | None:
220
251
  """Return the current assignee for *task_id*, or None if unassigned."""
221
- return self._assignees.get(task_id)
252
+ rec = self.tasks.get(task_id)
253
+ return rec.assignee if rec is not None else None
222
254
 
223
255
  def current_status(self, task_id: str) -> str | None:
224
256
  """Return the current status for *task_id*, or None if unknown."""
225
- return self._statuses.get(task_id)
257
+ rec = self.tasks.get(task_id)
258
+ return rec.status if rec is not None else None
226
259
 
227
260
  def is_accepted(self, task_id: str) -> bool:
228
- status = self._statuses.get(task_id)
229
- return status == "accepted" or status == "in_progress"
261
+ rec = self.tasks.get(task_id)
262
+ return rec is not None and rec.status in ("accepted", "in_progress")
230
263
 
231
264
  def is_completed(self, task_id: str) -> bool:
232
- return self._statuses.get(task_id) == "completed"
265
+ rec = self.tasks.get(task_id)
266
+ return rec is not None and rec.status == "completed"
233
267
 
234
268
  def is_failed(self, task_id: str) -> bool:
235
- return self._statuses.get(task_id) == "failed"
269
+ rec = self.tasks.get(task_id)
270
+ return rec is not None and rec.status == "failed"
236
271
 
237
272
  def is_retryable(self, task_id: str) -> bool:
238
273
  """True if the task failed with ``retryable=True``."""
@@ -240,15 +275,20 @@ class TaskProjection(BaseProjection):
240
275
 
241
276
  def progress_of(self, task_id: str) -> float:
242
277
  """Return the latest progress value for *task_id*, or 0 if unknown."""
243
- return self._progress.get(task_id, 0.0)
278
+ rec = self.tasks.get(task_id)
279
+ return rec.progress if rec is not None else 0.0
244
280
 
245
281
  def latest_progress(self) -> float | None:
246
282
  return self.updates[-1].progress if self.updates else None
247
283
 
248
- def active_tasks(self) -> list[TaskRequestRecord]:
249
- """Return task records that are not in a terminal state."""
284
+ def active_tasks(self) -> list[TaskRecord]:
285
+ """Return task records that are not in a terminal state.
286
+
287
+ Returns this projection's own records, not copies -- see
288
+ :meth:`get_task`'s docstring for the read-only contract.
289
+ """
250
290
  active_statuses = {"requested", "accepted", "in_progress"}
251
- return [t for t in self.tasks.values() if self._statuses.get(t.task_id) in active_statuses]
291
+ return [t for t in self.tasks.values() if t.status in active_statuses]
252
292
 
253
293
 
254
294
  # ---------------------------------------------------------------------------
@@ -476,3 +516,27 @@ class TaskSession(BaseSession):
476
516
  stacklevel=2,
477
517
  )
478
518
  return self.fail_task(*args, **kwargs) # type: ignore[arg-type]
519
+
520
+
521
+ # ── Deprecated aliases (issue #108) ───────────────────────────────────────
522
+ #
523
+ # ``TaskRequestRecord`` is the pre-rename name, kept as a module-level lazy
524
+ # alias (PEP 562) for one minor version, removed at this SDK's next major.
525
+ # Same mechanism and reasoning as ``proposal.py``'s own alias dict -- see the
526
+ # comment there for the full rationale. This module has no ``__path__`` (a
527
+ # plain file, not a package), so ``from macp_sdk.task import
528
+ # TaskRequestRecord`` resolves via a single ``getattr`` call -- the warning
529
+ # fires exactly once per such import.
530
+ _DEPRECATED_ALIASES = {
531
+ "TaskRequestRecord": "TaskRecord",
532
+ }
533
+
534
+
535
+ def __getattr__(name: str) -> Any:
536
+ new_name = _DEPRECATED_ALIASES.get(name)
537
+ if new_name is None:
538
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
539
+ warnings.warn(
540
+ f"{name} is deprecated; use {new_name} instead.", DeprecationWarning, stacklevel=2
541
+ )
542
+ return globals()[new_name]
@@ -8,6 +8,7 @@ Each watcher provides three consumption patterns:
8
8
 
9
9
  from __future__ import annotations
10
10
 
11
+ import warnings
11
12
  from collections.abc import Callable, Iterator
12
13
  from dataclasses import dataclass, field
13
14
  from typing import TYPE_CHECKING, Any
@@ -26,11 +27,12 @@ class PolicyChange:
26
27
 
27
28
 
28
29
  @dataclass(slots=True)
29
- class SessionLifecycle:
30
+ class SessionLifecycleEvent:
30
31
  """A single session lifecycle event from ``WatchSessions``.
31
32
 
32
- Runtime event types (per ``SessionLifecycleEvent.EventType``, since
33
- macp-proto 0.1.3): ``CREATED`` on SessionStart acceptance (also emitted
33
+ Runtime event types (per the wire message's own
34
+ ``SessionLifecycleEvent.EventType`` enum, since macp-proto 0.1.3):
35
+ ``CREATED`` on SessionStart acceptance (also emitted
34
36
  for pre-existing sessions at subscribe time), ``RESOLVED`` on
35
37
  mode-determined terminal outcome, ``EXPIRED`` on TTL/policy expiry,
36
38
  ``CANCELLED`` on an accepted ``CancelSession`` (previously surfaced as
@@ -158,7 +160,8 @@ _SESSION_EVENT_PREFIX = "EVENT_TYPE_"
158
160
 
159
161
 
160
162
  def _session_event_name(event_type: int) -> str:
161
- """Map ``SessionLifecycleEvent.EventType`` enum ints to short string names.
163
+ """Map the wire message's ``SessionLifecycleEvent.EventType`` enum ints to
164
+ short string names.
162
165
 
163
166
  The proto enum spells values as ``EVENT_TYPE_CREATED``; strip the
164
167
  prefix so consumers can compare against ``"CREATED"`` without
@@ -176,7 +179,7 @@ class SessionLifecycleWatcher:
176
179
  """Watch for session lifecycle events from the runtime.
177
180
 
178
181
  Wraps ``MacpClient.watch_sessions()`` and normalises each response into
179
- a ``SessionLifecycle`` record carrying the event type as a short
182
+ a ``SessionLifecycleEvent`` record carrying the event type as a short
180
183
  string (``CREATED`` / ``RESOLVED`` / ``EXPIRED`` / ``CANCELLED`` /
181
184
  ``SUSPENDED`` / ``RESUMED``) and the full ``SessionMetadata``. The
182
185
  runtime emits an initial CREATED event for every already-open session at
@@ -188,24 +191,24 @@ class SessionLifecycleWatcher:
188
191
  self._client = client
189
192
  self._auth = auth
190
193
 
191
- def changes(self) -> Iterator[SessionLifecycle]:
192
- """Yield ``SessionLifecycle`` items from the runtime stream."""
194
+ def changes(self) -> Iterator[SessionLifecycleEvent]:
195
+ """Yield ``SessionLifecycleEvent`` items from the runtime stream."""
193
196
  for response in self._client.watch_sessions(auth=self._auth):
194
197
  event = getattr(response, "event", None)
195
198
  if event is None:
196
199
  continue
197
- yield SessionLifecycle(
200
+ yield SessionLifecycleEvent(
198
201
  event_type=_session_event_name(event.event_type),
199
202
  observed_at_unix_ms=event.observed_at_unix_ms,
200
203
  session=event.session,
201
204
  )
202
205
 
203
- def watch(self, handler: Callable[[SessionLifecycle], None]) -> None:
206
+ def watch(self, handler: Callable[[SessionLifecycleEvent], None]) -> None:
204
207
  """Block and invoke *handler* for each lifecycle event."""
205
208
  for change in self.changes():
206
209
  handler(change)
207
210
 
208
- def next_change(self) -> SessionLifecycle:
211
+ def next_change(self) -> SessionLifecycleEvent:
209
212
  """Pull a single lifecycle event from the stream and return it."""
210
213
  for change in self.changes():
211
214
  return change
@@ -236,3 +239,27 @@ class PolicyWatcher:
236
239
  for change in self.changes():
237
240
  return change
238
241
  raise RuntimeError("stream ended before receiving a policy change")
242
+
243
+
244
+ # ── Deprecated aliases (issue #103 / multiagentcoordinationprotocol#135) ─────
245
+ #
246
+ # ``SessionLifecycle`` is the pre-rename name, kept as a module-level lazy
247
+ # alias (PEP 562) for one minor version, removed at this SDK's next major.
248
+ # Same mechanism and reasoning as ``agent/strategies.py``'s own alias dict --
249
+ # see the comment there for the full rationale. This module has no
250
+ # ``__path__`` (a plain file, not a package), so
251
+ # ``from macp_sdk.watchers import SessionLifecycle`` resolves via a single
252
+ # ``getattr`` call -- the warning fires exactly once per such import.
253
+ _DEPRECATED_ALIASES = {
254
+ "SessionLifecycle": "SessionLifecycleEvent",
255
+ }
256
+
257
+
258
+ def __getattr__(name: str) -> Any:
259
+ new_name = _DEPRECATED_ALIASES.get(name)
260
+ if new_name is None:
261
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
262
+ warnings.warn(
263
+ f"{name} is deprecated; use {new_name} instead.", DeprecationWarning, stacklevel=2
264
+ )
265
+ return globals()[new_name]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: macp-sdk-python
3
- Version: 0.10.2
3
+ Version: 0.12.1
4
4
  Summary: Python SDK for the MACP Rust runtime
5
5
  Author-email: Multi-Agent Coordination Protocol <macp@multiagentcoordinationprotocol.org>
6
6
  License: Apache-2.0
@@ -1,68 +0,0 @@
1
- from .cancel_callback import CancelCallbackServer, start_cancel_callback_server
2
- from .dispatcher import Dispatcher
3
- from .participant import InitiatorConfig, Participant, ParticipantActions
4
- from .runner import from_bootstrap
5
- from .strategies import (
6
- CommitmentDecision,
7
- CommitmentStrategy,
8
- EvaluationResult,
9
- EvaluationStrategy,
10
- VoteDecision,
11
- VotingStrategy,
12
- commitment_handler,
13
- evaluation_handler,
14
- function_committer,
15
- function_evaluator,
16
- function_voter,
17
- majority_committer,
18
- majority_voter,
19
- voting_handler,
20
- )
21
- from .transports import (
22
- GrpcTransportAdapter,
23
- HttpTransportAdapter,
24
- TransportAdapter,
25
- )
26
- from .types import (
27
- HandlerContext,
28
- IncomingMessage,
29
- MessageHandler,
30
- PhaseChangeHandler,
31
- SessionInfo,
32
- TerminalHandler,
33
- TerminalResult,
34
- )
35
-
36
- __all__ = [
37
- "CancelCallbackServer",
38
- "CommitmentDecision",
39
- "CommitmentStrategy",
40
- "Dispatcher",
41
- "EvaluationResult",
42
- "EvaluationStrategy",
43
- "GrpcTransportAdapter",
44
- "HandlerContext",
45
- "HttpTransportAdapter",
46
- "IncomingMessage",
47
- "InitiatorConfig",
48
- "MessageHandler",
49
- "Participant",
50
- "ParticipantActions",
51
- "PhaseChangeHandler",
52
- "SessionInfo",
53
- "TerminalHandler",
54
- "TerminalResult",
55
- "TransportAdapter",
56
- "VoteDecision",
57
- "VotingStrategy",
58
- "commitment_handler",
59
- "evaluation_handler",
60
- "from_bootstrap",
61
- "function_committer",
62
- "function_evaluator",
63
- "function_voter",
64
- "majority_committer",
65
- "majority_voter",
66
- "start_cancel_callback_server",
67
- "voting_handler",
68
- ]