macp-sdk-python 0.10.2__py3-none-any.whl → 0.12.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- macp_sdk/__init__.py +58 -8
- macp_sdk/agent/__init__.py +46 -4
- macp_sdk/agent/runner.py +3 -3
- macp_sdk/agent/strategies.py +49 -17
- macp_sdk/base_projection.py +19 -21
- macp_sdk/handoff.py +10 -7
- macp_sdk/proposal.py +50 -7
- macp_sdk/task.py +99 -35
- macp_sdk/watchers.py +37 -10
- {macp_sdk_python-0.10.2.dist-info → macp_sdk_python-0.12.1.dist-info}/METADATA +1 -1
- {macp_sdk_python-0.10.2.dist-info → macp_sdk_python-0.12.1.dist-info}/RECORD +14 -14
- {macp_sdk_python-0.10.2.dist-info → macp_sdk_python-0.12.1.dist-info}/WHEEL +0 -0
- {macp_sdk_python-0.10.2.dist-info → macp_sdk_python-0.12.1.dist-info}/licenses/LICENSE +0 -0
- {macp_sdk_python-0.10.2.dist-info → macp_sdk_python-0.12.1.dist-info}/top_level.txt +0 -0
macp_sdk/__init__.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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]
|
macp_sdk/agent/__init__.py
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
1
4
|
from .cancel_callback import CancelCallbackServer, start_cancel_callback_server
|
|
2
5
|
from .dispatcher import Dispatcher
|
|
3
6
|
from .participant import InitiatorConfig, Participant, ParticipantActions
|
|
4
7
|
from .runner import from_bootstrap
|
|
5
8
|
from .strategies import (
|
|
6
|
-
|
|
9
|
+
CommitmentResult,
|
|
7
10
|
CommitmentStrategy,
|
|
8
11
|
EvaluationResult,
|
|
9
12
|
EvaluationStrategy,
|
|
10
|
-
|
|
13
|
+
VoteResult,
|
|
11
14
|
VotingStrategy,
|
|
12
15
|
commitment_handler,
|
|
13
16
|
evaluation_handler,
|
|
@@ -35,7 +38,7 @@ from .types import (
|
|
|
35
38
|
|
|
36
39
|
__all__ = [
|
|
37
40
|
"CancelCallbackServer",
|
|
38
|
-
"
|
|
41
|
+
"CommitmentResult",
|
|
39
42
|
"CommitmentStrategy",
|
|
40
43
|
"Dispatcher",
|
|
41
44
|
"EvaluationResult",
|
|
@@ -53,7 +56,7 @@ __all__ = [
|
|
|
53
56
|
"TerminalHandler",
|
|
54
57
|
"TerminalResult",
|
|
55
58
|
"TransportAdapter",
|
|
56
|
-
"
|
|
59
|
+
"VoteResult",
|
|
57
60
|
"VotingStrategy",
|
|
58
61
|
"commitment_handler",
|
|
59
62
|
"evaluation_handler",
|
|
@@ -66,3 +69,42 @@ __all__ = [
|
|
|
66
69
|
"start_cancel_callback_server",
|
|
67
70
|
"voting_handler",
|
|
68
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]
|
macp_sdk/agent/runner.py
CHANGED
|
@@ -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).
|
|
159
|
-
#
|
|
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")),
|
macp_sdk/agent/strategies.py
CHANGED
|
@@ -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
|
|
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) ->
|
|
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],
|
|
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],
|
|
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) ->
|
|
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
|
|
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) ->
|
|
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],
|
|
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],
|
|
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) ->
|
|
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) ->
|
|
290
|
+
def decide_vote(self, projection: Any) -> VoteResult:
|
|
290
291
|
evaluations = list(getattr(projection, "evaluations", None) or [])
|
|
291
292
|
if not evaluations:
|
|
292
|
-
return
|
|
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
|
|
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
|
|
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
|
|
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) ->
|
|
359
|
+
def decide_commitment(self, projection: Any) -> CommitmentResult:
|
|
359
360
|
winner = projection.majority_winner()
|
|
360
|
-
return
|
|
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]
|
macp_sdk/base_projection.py
CHANGED
|
@@ -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
|
|
12
|
-
# part of the wire-adjacent public API and must match the TypeScript SDK
|
|
13
|
-
# byte-for-byte. They are also the
|
|
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.
|
|
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
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
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
|
|
47
|
-
``ANOMALY_DUPLICATE_VOTE``,
|
|
48
|
-
|
|
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
|
macp_sdk/handoff.py
CHANGED
|
@@ -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
|
|
34
|
-
#
|
|
35
|
-
#
|
|
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
|
-
|
|
182
|
-
``implicit_accept_timeout_ms`` elapses
|
|
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/
|
|
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,
|
macp_sdk/proposal.py
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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[
|
|
62
|
-
self.rejections: list[
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
macp_sdk/task.py
CHANGED
|
@@ -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
|
|
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,
|
|
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;
|
|
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] =
|
|
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.
|
|
110
|
-
|
|
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.
|
|
122
|
-
self.
|
|
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
|
-
#
|
|
150
|
-
#
|
|
151
|
-
#
|
|
152
|
-
#
|
|
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.
|
|
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.
|
|
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.
|
|
177
|
-
self.
|
|
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.
|
|
193
|
-
self.
|
|
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.
|
|
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) ->
|
|
216
|
-
"""Return the task
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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[
|
|
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
|
|
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]
|
macp_sdk/watchers.py
CHANGED
|
@@ -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
|
|
30
|
+
class SessionLifecycleEvent:
|
|
30
31
|
"""A single session lifecycle event from ``WatchSessions``.
|
|
31
32
|
|
|
32
|
-
Runtime event types (per
|
|
33
|
-
macp-proto 0.1.3):
|
|
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
|
|
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 ``
|
|
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[
|
|
192
|
-
"""Yield ``
|
|
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
|
|
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[[
|
|
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) ->
|
|
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,7 +1,7 @@
|
|
|
1
|
-
macp_sdk/__init__.py,sha256=
|
|
1
|
+
macp_sdk/__init__.py,sha256=TsUKVkKC8ShuzIKtozE9v3tS_4zAhIJ1yANLuefNHB4,9314
|
|
2
2
|
macp_sdk/_logging.py,sha256=pHxyG89c9b_r8mcVSBA4E3e1GMAI5Jcc4_jslzKO6fM,431
|
|
3
3
|
macp_sdk/auth.py,sha256=ouqQW7P7LqUn6_0OilqDBmsrW1IrPQNJ-ofe1La2iqw,2856
|
|
4
|
-
macp_sdk/base_projection.py,sha256=
|
|
4
|
+
macp_sdk/base_projection.py,sha256=5wIPPb8YTmSSuZlvSI8WuezArLELQE-Xz2GVAQtRXEk,21344
|
|
5
5
|
macp_sdk/base_session.py,sha256=RCpLdSNLm3Voj1KCH__pTPbsQL2Xm4Q1zp3mp3UDE1o,8730
|
|
6
6
|
macp_sdk/client.py,sha256=AplJy86LNTKBQWX2OOAO7jUTq3Isp97DkFCUxVOPYs4,43987
|
|
7
7
|
macp_sdk/commitment_hash.py,sha256=TO5H3IQdYOCVXRvv6dxOed0D9qV6fAe3zadOjbqQ1bI,14709
|
|
@@ -9,27 +9,27 @@ macp_sdk/constants.py,sha256=7BaQC6qycmPEc1ChOQVDxlhEBsBdI-RQBT07BQ2Uiis,481
|
|
|
9
9
|
macp_sdk/decision.py,sha256=8yU5bv-DUU7eWgioc9CYVSB1sPcO9essiy-jsDQEunc,4792
|
|
10
10
|
macp_sdk/envelope.py,sha256=_MlJ43Dih3_FcJ4hyrirFqrOvb5RaK9ssQo9Y3kMlF0,9200
|
|
11
11
|
macp_sdk/errors.py,sha256=19dutKnnl-jtMOWpNBl2UEMCH_Sunz84G05G-wr7w1k,3836
|
|
12
|
-
macp_sdk/handoff.py,sha256=
|
|
12
|
+
macp_sdk/handoff.py,sha256=EEyQkG-TeTF3p09Lotnzm5kPQ-fpIYxWk9jt5VnoCv8,12775
|
|
13
13
|
macp_sdk/policy.py,sha256=MoyQ9l7zI0vkz_4Ewg8RRr-4DUhFBVPDQ-tn0r94sVw,19815
|
|
14
14
|
macp_sdk/projections.py,sha256=Vh101rBwtkrbW7WgOY0KJb-O_tS9TE9V41uRZS3xnWU,7278
|
|
15
|
-
macp_sdk/proposal.py,sha256=
|
|
15
|
+
macp_sdk/proposal.py,sha256=MKWGkQnRbYJ_Yzz54fVXpAD-adH0XIjLGEujmv_kQOg,13167
|
|
16
16
|
macp_sdk/proto_registry.py,sha256=jgGKE4maGbTejVhYotHFdUnN_ZjZgEqsDWpU8Hh1mN8,17628
|
|
17
17
|
macp_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
18
|
macp_sdk/quorum.py,sha256=Kk7UIs2X07weXNGT9hyVffOpu71_88-jXONyqb_VzzE,10686
|
|
19
19
|
macp_sdk/retry.py,sha256=KgRGC4Raz5Yto_LoRoPApmeERdTMP6fBQcQRpX_-w6g,1866
|
|
20
|
-
macp_sdk/task.py,sha256=
|
|
20
|
+
macp_sdk/task.py,sha256=plfuF9f8krKnbbNA8NxkM61jX6PbRLHLWtSYEMKjSIY,19946
|
|
21
21
|
macp_sdk/validation.py,sha256=nJUKx9hhrFtCIACe_TAzmzStloPCHYu8WGPXknPwAcE,8752
|
|
22
|
-
macp_sdk/watchers.py,sha256=
|
|
23
|
-
macp_sdk/agent/__init__.py,sha256=
|
|
22
|
+
macp_sdk/watchers.py,sha256=r0RayslkAKbwEI7ubfbnm_4eNq0HnUpWJugnDAsmfMM,9976
|
|
23
|
+
macp_sdk/agent/__init__.py,sha256=vrwgt4vaBGom2m45cm39GOTZ5QLya-h1f4IGKihLsTc,3673
|
|
24
24
|
macp_sdk/agent/cancel_callback.py,sha256=fCbAZ2LlmPtbNz_B6zn_be-m-g4CsKQLE_5x674KNzE,5541
|
|
25
25
|
macp_sdk/agent/dispatcher.py,sha256=t1wLYGpqJdm2nIaaEVStp_8zUIetHL8FT2wYNniLDTA,3972
|
|
26
26
|
macp_sdk/agent/participant.py,sha256=iC6I9gtdDuE_4o2G4nmVV0EjbIa8rZthnBkQ14XZ1E0,25738
|
|
27
|
-
macp_sdk/agent/runner.py,sha256=
|
|
28
|
-
macp_sdk/agent/strategies.py,sha256=
|
|
27
|
+
macp_sdk/agent/runner.py,sha256=jFZNVPm4tPuY_yDEsiFFB_FpaX5nMG6__L6xwuo52A8,9013
|
|
28
|
+
macp_sdk/agent/strategies.py,sha256=8o55FR5a-L2TCX2nCw8L8PO-ucswHniR2m8qVmuP4V0,14665
|
|
29
29
|
macp_sdk/agent/transports.py,sha256=xKM_cC2Qy3-PwktlZZMcTnbIwGKiXa3PvZmRQXXPDHo,14731
|
|
30
30
|
macp_sdk/agent/types.py,sha256=wtlF-tTRGnEGHhYknhu-5ZzOBZ1pRQ5GjYLKBR4YzQg,1610
|
|
31
|
-
macp_sdk_python-0.
|
|
32
|
-
macp_sdk_python-0.
|
|
33
|
-
macp_sdk_python-0.
|
|
34
|
-
macp_sdk_python-0.
|
|
35
|
-
macp_sdk_python-0.
|
|
31
|
+
macp_sdk_python-0.12.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
|
32
|
+
macp_sdk_python-0.12.1.dist-info/METADATA,sha256=X6j2ofU6LilgtcHUaifEyp4BsbRTLFE7re-PwwnyXd8,7478
|
|
33
|
+
macp_sdk_python-0.12.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
34
|
+
macp_sdk_python-0.12.1.dist-info/top_level.txt,sha256=AoSstnUrSaVCCQw4KLxLkpO-9BLoVZ230mPBR6z-jh8,9
|
|
35
|
+
macp_sdk_python-0.12.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|