macp-sdk-python 0.2.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 +207 -0
- macp_sdk/_logging.py +16 -0
- macp_sdk/agent/__init__.py +64 -0
- macp_sdk/agent/dispatcher.py +105 -0
- macp_sdk/agent/participant.py +404 -0
- macp_sdk/agent/runner.py +100 -0
- macp_sdk/agent/strategies.py +312 -0
- macp_sdk/agent/transports.py +153 -0
- macp_sdk/agent/types.py +64 -0
- macp_sdk/auth.py +74 -0
- macp_sdk/base_projection.py +52 -0
- macp_sdk/base_session.py +169 -0
- macp_sdk/client.py +625 -0
- macp_sdk/constants.py +20 -0
- macp_sdk/decision.py +154 -0
- macp_sdk/envelope.py +180 -0
- macp_sdk/errors.py +101 -0
- macp_sdk/handoff.py +241 -0
- macp_sdk/policy.py +292 -0
- macp_sdk/projections.py +183 -0
- macp_sdk/proposal.py +305 -0
- macp_sdk/proto_registry.py +150 -0
- macp_sdk/py.typed +0 -0
- macp_sdk/quorum.py +258 -0
- macp_sdk/retry.py +64 -0
- macp_sdk/task.py +368 -0
- macp_sdk/validation.py +117 -0
- macp_sdk/watchers.py +122 -0
- macp_sdk_python-0.2.1.dist-info/METADATA +165 -0
- macp_sdk_python-0.2.1.dist-info/RECORD +33 -0
- macp_sdk_python-0.2.1.dist-info/WHEEL +5 -0
- macp_sdk_python-0.2.1.dist-info/licenses/LICENSE +201 -0
- macp_sdk_python-0.2.1.dist-info/top_level.txt +1 -0
macp_sdk/proposal.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from macp.modes.proposal.v1 import proposal_pb2
|
|
6
|
+
from macp.v1 import envelope_pb2
|
|
7
|
+
|
|
8
|
+
from .auth import AuthConfig
|
|
9
|
+
from .base_projection import BaseProjection
|
|
10
|
+
from .base_session import BaseSession
|
|
11
|
+
from .constants import MODE_PROPOSAL
|
|
12
|
+
from .envelope import build_envelope, serialize_message
|
|
13
|
+
from .errors import MacpSessionError
|
|
14
|
+
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
# Projection records
|
|
17
|
+
# ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(slots=True)
|
|
21
|
+
class ProposalRecord:
|
|
22
|
+
proposal_id: str
|
|
23
|
+
title: str
|
|
24
|
+
summary: str
|
|
25
|
+
proposer: str
|
|
26
|
+
supersedes: str # "" if original
|
|
27
|
+
status: str # "open" | "accepted" | "rejected" | "withdrawn"
|
|
28
|
+
tags: list[str]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class RejectRecord:
|
|
33
|
+
proposal_id: str
|
|
34
|
+
reason: str
|
|
35
|
+
sender: str
|
|
36
|
+
terminal: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(slots=True)
|
|
40
|
+
class AcceptRecord:
|
|
41
|
+
proposal_id: str
|
|
42
|
+
reason: str
|
|
43
|
+
sender: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
# Projection
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ProposalProjection(BaseProjection):
|
|
52
|
+
"""In-process state tracking for Proposal mode sessions."""
|
|
53
|
+
|
|
54
|
+
MODE = MODE_PROPOSAL
|
|
55
|
+
|
|
56
|
+
def __init__(self) -> None:
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.phase = "Negotiating"
|
|
59
|
+
self.proposals: dict[str, ProposalRecord] = {}
|
|
60
|
+
self.accepts: list[AcceptRecord] = []
|
|
61
|
+
self.rejections: list[RejectRecord] = []
|
|
62
|
+
|
|
63
|
+
def _apply_mode_message(self, envelope: envelope_pb2.Envelope) -> None:
|
|
64
|
+
mt = envelope.message_type
|
|
65
|
+
|
|
66
|
+
if mt == "Proposal":
|
|
67
|
+
p = proposal_pb2.ProposalPayload()
|
|
68
|
+
p.ParseFromString(envelope.payload)
|
|
69
|
+
self.proposals[p.proposal_id] = ProposalRecord(
|
|
70
|
+
proposal_id=p.proposal_id,
|
|
71
|
+
title=p.title,
|
|
72
|
+
summary=p.summary,
|
|
73
|
+
proposer=envelope.sender,
|
|
74
|
+
supersedes="",
|
|
75
|
+
status="open",
|
|
76
|
+
tags=list(p.tags),
|
|
77
|
+
)
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
if mt == "CounterProposal":
|
|
81
|
+
p = proposal_pb2.CounterProposalPayload()
|
|
82
|
+
p.ParseFromString(envelope.payload)
|
|
83
|
+
self.proposals[p.proposal_id] = ProposalRecord(
|
|
84
|
+
proposal_id=p.proposal_id,
|
|
85
|
+
title=p.title,
|
|
86
|
+
summary=p.summary,
|
|
87
|
+
proposer=envelope.sender,
|
|
88
|
+
supersedes=p.supersedes_proposal_id,
|
|
89
|
+
status="open",
|
|
90
|
+
tags=[],
|
|
91
|
+
)
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
if mt == "Accept":
|
|
95
|
+
p = proposal_pb2.AcceptPayload()
|
|
96
|
+
p.ParseFromString(envelope.payload)
|
|
97
|
+
self.accepts.append(
|
|
98
|
+
AcceptRecord(
|
|
99
|
+
proposal_id=p.proposal_id,
|
|
100
|
+
reason=p.reason,
|
|
101
|
+
sender=envelope.sender,
|
|
102
|
+
)
|
|
103
|
+
)
|
|
104
|
+
return
|
|
105
|
+
|
|
106
|
+
if mt == "Reject":
|
|
107
|
+
p = proposal_pb2.RejectPayload()
|
|
108
|
+
p.ParseFromString(envelope.payload)
|
|
109
|
+
self.rejections.append(
|
|
110
|
+
RejectRecord(
|
|
111
|
+
proposal_id=p.proposal_id,
|
|
112
|
+
reason=p.reason,
|
|
113
|
+
sender=envelope.sender,
|
|
114
|
+
terminal=p.terminal,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
if p.terminal:
|
|
118
|
+
rec = self.proposals.get(p.proposal_id)
|
|
119
|
+
if rec is not None:
|
|
120
|
+
rec.status = "rejected"
|
|
121
|
+
self.phase = "TerminalRejected"
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
if mt == "Withdraw":
|
|
125
|
+
p = proposal_pb2.WithdrawPayload()
|
|
126
|
+
p.ParseFromString(envelope.payload)
|
|
127
|
+
rec = self.proposals.get(p.proposal_id)
|
|
128
|
+
if rec is not None:
|
|
129
|
+
rec.status = "withdrawn"
|
|
130
|
+
|
|
131
|
+
# -- State query helpers --
|
|
132
|
+
|
|
133
|
+
def live_proposals(self) -> dict[str, ProposalRecord]:
|
|
134
|
+
"""Return proposals that have not been withdrawn."""
|
|
135
|
+
return {k: v for k, v in self.proposals.items() if v.status != "withdrawn"}
|
|
136
|
+
|
|
137
|
+
def accepted_proposal(self) -> str | None:
|
|
138
|
+
"""Return the proposal_id that all accepting senders agree on, or None."""
|
|
139
|
+
if not self.accepts:
|
|
140
|
+
return None
|
|
141
|
+
ids = {a.proposal_id for a in self.accepts}
|
|
142
|
+
if len(ids) == 1:
|
|
143
|
+
return ids.pop()
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
def has_terminal_rejection(self) -> bool:
|
|
147
|
+
return any(r.terminal for r in self.rejections)
|
|
148
|
+
|
|
149
|
+
def active_proposals(self) -> list[ProposalRecord]:
|
|
150
|
+
"""Return proposals whose status is 'open'."""
|
|
151
|
+
return [p for p in self.proposals.values() if p.status == "open"]
|
|
152
|
+
|
|
153
|
+
def latest_proposal(self) -> ProposalRecord | None:
|
|
154
|
+
"""Return the most recently added proposal, or None."""
|
|
155
|
+
if not self.proposals:
|
|
156
|
+
return None
|
|
157
|
+
return list(self.proposals.values())[-1]
|
|
158
|
+
|
|
159
|
+
def is_accepted(self, proposal_id: str) -> bool:
|
|
160
|
+
"""True if any accept record references *proposal_id*."""
|
|
161
|
+
return any(a.proposal_id == proposal_id for a in self.accepts)
|
|
162
|
+
|
|
163
|
+
def is_terminally_rejected(self, proposal_id: str) -> bool:
|
|
164
|
+
"""True if a terminal rejection exists for *proposal_id*."""
|
|
165
|
+
return any(r.proposal_id == proposal_id and r.terminal for r in self.rejections)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# ---------------------------------------------------------------------------
|
|
169
|
+
# Session helper
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class ProposalSession(BaseSession):
|
|
174
|
+
"""High-level helper for Proposal mode sessions."""
|
|
175
|
+
|
|
176
|
+
MODE = MODE_PROPOSAL
|
|
177
|
+
|
|
178
|
+
def _create_projection(self) -> BaseProjection:
|
|
179
|
+
return ProposalProjection()
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def proposal_projection(self) -> ProposalProjection:
|
|
183
|
+
assert isinstance(self.projection, ProposalProjection)
|
|
184
|
+
return self.projection
|
|
185
|
+
|
|
186
|
+
def propose(
|
|
187
|
+
self,
|
|
188
|
+
proposal_id: str,
|
|
189
|
+
title: str,
|
|
190
|
+
*,
|
|
191
|
+
summary: str = "",
|
|
192
|
+
details: bytes = b"",
|
|
193
|
+
tags: list[str] | None = None,
|
|
194
|
+
sender: str | None = None,
|
|
195
|
+
auth: AuthConfig | None = None,
|
|
196
|
+
) -> envelope_pb2.Ack:
|
|
197
|
+
payload = proposal_pb2.ProposalPayload(
|
|
198
|
+
proposal_id=proposal_id,
|
|
199
|
+
title=title,
|
|
200
|
+
summary=summary,
|
|
201
|
+
details=details,
|
|
202
|
+
tags=tags or [],
|
|
203
|
+
)
|
|
204
|
+
envelope = build_envelope(
|
|
205
|
+
mode=self.MODE,
|
|
206
|
+
message_type="Proposal",
|
|
207
|
+
session_id=self.session_id,
|
|
208
|
+
sender=self._sender_for(sender, auth=auth),
|
|
209
|
+
payload=serialize_message(payload),
|
|
210
|
+
)
|
|
211
|
+
return self._send_and_track(envelope, auth=auth)
|
|
212
|
+
|
|
213
|
+
def counter_propose(
|
|
214
|
+
self,
|
|
215
|
+
proposal_id: str,
|
|
216
|
+
supersedes_proposal_id: str,
|
|
217
|
+
title: str,
|
|
218
|
+
*,
|
|
219
|
+
summary: str = "",
|
|
220
|
+
details: bytes = b"",
|
|
221
|
+
sender: str | None = None,
|
|
222
|
+
auth: AuthConfig | None = None,
|
|
223
|
+
) -> envelope_pb2.Ack:
|
|
224
|
+
payload = proposal_pb2.CounterProposalPayload(
|
|
225
|
+
proposal_id=proposal_id,
|
|
226
|
+
supersedes_proposal_id=supersedes_proposal_id,
|
|
227
|
+
title=title,
|
|
228
|
+
summary=summary,
|
|
229
|
+
details=details,
|
|
230
|
+
)
|
|
231
|
+
envelope = build_envelope(
|
|
232
|
+
mode=self.MODE,
|
|
233
|
+
message_type="CounterProposal",
|
|
234
|
+
session_id=self.session_id,
|
|
235
|
+
sender=self._sender_for(sender, auth=auth),
|
|
236
|
+
payload=serialize_message(payload),
|
|
237
|
+
)
|
|
238
|
+
return self._send_and_track(envelope, auth=auth)
|
|
239
|
+
|
|
240
|
+
def accept(
|
|
241
|
+
self,
|
|
242
|
+
proposal_id: str,
|
|
243
|
+
*,
|
|
244
|
+
reason: str = "",
|
|
245
|
+
sender: str | None = None,
|
|
246
|
+
auth: AuthConfig | None = None,
|
|
247
|
+
) -> envelope_pb2.Ack:
|
|
248
|
+
payload = proposal_pb2.AcceptPayload(
|
|
249
|
+
proposal_id=proposal_id,
|
|
250
|
+
reason=reason,
|
|
251
|
+
)
|
|
252
|
+
envelope = build_envelope(
|
|
253
|
+
mode=self.MODE,
|
|
254
|
+
message_type="Accept",
|
|
255
|
+
session_id=self.session_id,
|
|
256
|
+
sender=self._sender_for(sender, auth=auth),
|
|
257
|
+
payload=serialize_message(payload),
|
|
258
|
+
)
|
|
259
|
+
return self._send_and_track(envelope, auth=auth)
|
|
260
|
+
|
|
261
|
+
def reject(
|
|
262
|
+
self,
|
|
263
|
+
proposal_id: str,
|
|
264
|
+
*,
|
|
265
|
+
terminal: bool = False,
|
|
266
|
+
reason: str = "",
|
|
267
|
+
sender: str | None = None,
|
|
268
|
+
auth: AuthConfig | None = None,
|
|
269
|
+
) -> envelope_pb2.Ack:
|
|
270
|
+
payload = proposal_pb2.RejectPayload(
|
|
271
|
+
proposal_id=proposal_id,
|
|
272
|
+
terminal=terminal,
|
|
273
|
+
reason=reason,
|
|
274
|
+
)
|
|
275
|
+
envelope = build_envelope(
|
|
276
|
+
mode=self.MODE,
|
|
277
|
+
message_type="Reject",
|
|
278
|
+
session_id=self.session_id,
|
|
279
|
+
sender=self._sender_for(sender, auth=auth),
|
|
280
|
+
payload=serialize_message(payload),
|
|
281
|
+
)
|
|
282
|
+
return self._send_and_track(envelope, auth=auth)
|
|
283
|
+
|
|
284
|
+
def withdraw(
|
|
285
|
+
self,
|
|
286
|
+
proposal_id: str,
|
|
287
|
+
*,
|
|
288
|
+
reason: str = "",
|
|
289
|
+
sender: str | None = None,
|
|
290
|
+
auth: AuthConfig | None = None,
|
|
291
|
+
) -> envelope_pb2.Ack:
|
|
292
|
+
if not proposal_id or not proposal_id.strip():
|
|
293
|
+
raise MacpSessionError("proposal_id must be non-empty for withdraw")
|
|
294
|
+
payload = proposal_pb2.WithdrawPayload(
|
|
295
|
+
proposal_id=proposal_id,
|
|
296
|
+
reason=reason,
|
|
297
|
+
)
|
|
298
|
+
envelope = build_envelope(
|
|
299
|
+
mode=self.MODE,
|
|
300
|
+
message_type="Withdraw",
|
|
301
|
+
session_id=self.session_id,
|
|
302
|
+
sender=self._sender_for(sender, auth=auth),
|
|
303
|
+
payload=serialize_message(payload),
|
|
304
|
+
)
|
|
305
|
+
return self._send_and_track(envelope, auth=auth)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Centralized protobuf encode/decode registry for MACP message types.
|
|
2
|
+
|
|
3
|
+
Uses compiled ``_pb2`` modules (via ``google.protobuf.symbol_database``) to
|
|
4
|
+
look up message classes by fully-qualified type name.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import importlib
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from google.protobuf import json_format, symbol_database # type: ignore[import-untyped]
|
|
15
|
+
|
|
16
|
+
from .constants import (
|
|
17
|
+
MODE_DECISION,
|
|
18
|
+
MODE_HANDOFF,
|
|
19
|
+
MODE_MULTI_ROUND,
|
|
20
|
+
MODE_PROPOSAL,
|
|
21
|
+
MODE_QUORUM,
|
|
22
|
+
MODE_TASK,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# ── Type-name mappings (mirrors TypeScript CORE_MAP / MODE_MAP) ──────
|
|
26
|
+
|
|
27
|
+
CORE_MAP: dict[str, str] = {
|
|
28
|
+
"SessionStart": "macp.v1.SessionStartPayload",
|
|
29
|
+
"Commitment": "macp.v1.CommitmentPayload",
|
|
30
|
+
"Signal": "macp.v1.SignalPayload",
|
|
31
|
+
"Progress": "macp.v1.ProgressPayload",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
MODE_MAP: dict[str, dict[str, str]] = {
|
|
35
|
+
MODE_DECISION: {
|
|
36
|
+
"Proposal": "macp.modes.decision.v1.ProposalPayload",
|
|
37
|
+
"Evaluation": "macp.modes.decision.v1.EvaluationPayload",
|
|
38
|
+
"Objection": "macp.modes.decision.v1.ObjectionPayload",
|
|
39
|
+
"Vote": "macp.modes.decision.v1.VotePayload",
|
|
40
|
+
},
|
|
41
|
+
MODE_PROPOSAL: {
|
|
42
|
+
"Proposal": "macp.modes.proposal.v1.ProposalPayload",
|
|
43
|
+
"CounterProposal": "macp.modes.proposal.v1.CounterProposalPayload",
|
|
44
|
+
"Accept": "macp.modes.proposal.v1.AcceptPayload",
|
|
45
|
+
"Reject": "macp.modes.proposal.v1.RejectPayload",
|
|
46
|
+
"Withdraw": "macp.modes.proposal.v1.WithdrawPayload",
|
|
47
|
+
},
|
|
48
|
+
MODE_TASK: {
|
|
49
|
+
"TaskRequest": "macp.modes.task.v1.TaskRequestPayload",
|
|
50
|
+
"TaskAccept": "macp.modes.task.v1.TaskAcceptPayload",
|
|
51
|
+
"TaskReject": "macp.modes.task.v1.TaskRejectPayload",
|
|
52
|
+
"TaskUpdate": "macp.modes.task.v1.TaskUpdatePayload",
|
|
53
|
+
"TaskComplete": "macp.modes.task.v1.TaskCompletePayload",
|
|
54
|
+
"TaskFail": "macp.modes.task.v1.TaskFailPayload",
|
|
55
|
+
},
|
|
56
|
+
MODE_HANDOFF: {
|
|
57
|
+
"HandoffOffer": "macp.modes.handoff.v1.HandoffOfferPayload",
|
|
58
|
+
"HandoffContext": "macp.modes.handoff.v1.HandoffContextPayload",
|
|
59
|
+
"HandoffAccept": "macp.modes.handoff.v1.HandoffAcceptPayload",
|
|
60
|
+
"HandoffDecline": "macp.modes.handoff.v1.HandoffDeclinePayload",
|
|
61
|
+
},
|
|
62
|
+
MODE_QUORUM: {
|
|
63
|
+
"ApprovalRequest": "macp.modes.quorum.v1.ApprovalRequestPayload",
|
|
64
|
+
"Approve": "macp.modes.quorum.v1.ApprovePayload",
|
|
65
|
+
"Reject": "macp.modes.quorum.v1.RejectPayload",
|
|
66
|
+
"Abstain": "macp.modes.quorum.v1.AbstainPayload",
|
|
67
|
+
},
|
|
68
|
+
MODE_MULTI_ROUND: {
|
|
69
|
+
"Contribute": "__json__",
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
# Ensure all _pb2 modules are imported so descriptors are registered.
|
|
74
|
+
_PB2_MODULES_LOADED = False
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ensure_pb2_imports() -> None:
|
|
78
|
+
global _PB2_MODULES_LOADED
|
|
79
|
+
if _PB2_MODULES_LOADED:
|
|
80
|
+
return
|
|
81
|
+
# Import all proto modules to register their descriptors in the global pool.
|
|
82
|
+
for _mod in (
|
|
83
|
+
"macp.v1.core_pb2",
|
|
84
|
+
"macp.v1.envelope_pb2",
|
|
85
|
+
"macp.v1.policy_pb2",
|
|
86
|
+
"macp.modes.decision.v1.decision_pb2",
|
|
87
|
+
"macp.modes.proposal.v1.proposal_pb2",
|
|
88
|
+
"macp.modes.task.v1.task_pb2",
|
|
89
|
+
"macp.modes.handoff.v1.handoff_pb2",
|
|
90
|
+
"macp.modes.quorum.v1.quorum_pb2",
|
|
91
|
+
):
|
|
92
|
+
importlib.import_module(_mod)
|
|
93
|
+
_PB2_MODULES_LOADED = True
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ProtoRegistry:
|
|
97
|
+
"""Registry for protobuf type-name-based encode/decode of MACP payloads."""
|
|
98
|
+
|
|
99
|
+
def __init__(self) -> None:
|
|
100
|
+
_ensure_pb2_imports()
|
|
101
|
+
self._db = symbol_database.Default()
|
|
102
|
+
|
|
103
|
+
def get_known_type_name(self, mode: str, message_type: str) -> str | None:
|
|
104
|
+
"""Return the fully-qualified protobuf type name, or None if unknown."""
|
|
105
|
+
return MODE_MAP.get(mode, {}).get(message_type) or CORE_MAP.get(message_type)
|
|
106
|
+
|
|
107
|
+
def encode_message(self, type_name: str, value: dict[str, Any]) -> bytes:
|
|
108
|
+
"""Encode *value* as a protobuf message identified by *type_name*."""
|
|
109
|
+
cls = self._db.GetSymbol(type_name)
|
|
110
|
+
msg = json_format.ParseDict(value, cls())
|
|
111
|
+
return msg.SerializeToString()
|
|
112
|
+
|
|
113
|
+
def decode_message(self, type_name: str, payload: bytes) -> dict[str, Any]:
|
|
114
|
+
"""Decode *payload* into a dict using the message class for *type_name*."""
|
|
115
|
+
cls = self._db.GetSymbol(type_name)
|
|
116
|
+
msg = cls()
|
|
117
|
+
msg.ParseFromString(payload)
|
|
118
|
+
return json_format.MessageToDict(msg, preserving_proto_field_name=True)
|
|
119
|
+
|
|
120
|
+
def encode_known_payload(self, mode: str, message_type: str, value: dict[str, Any]) -> bytes:
|
|
121
|
+
"""Encode *value* using the known type mapping for *mode*/*message_type*."""
|
|
122
|
+
type_name = self.get_known_type_name(mode, message_type)
|
|
123
|
+
if type_name is None:
|
|
124
|
+
raise ValueError(f"unknown payload mapping for {mode}/{message_type}")
|
|
125
|
+
if type_name == "__json__":
|
|
126
|
+
return json.dumps(value).encode("utf-8")
|
|
127
|
+
return self.encode_message(type_name, value)
|
|
128
|
+
|
|
129
|
+
def decode_known_payload(
|
|
130
|
+
self, mode: str, message_type: str, payload: bytes
|
|
131
|
+
) -> dict[str, Any] | None:
|
|
132
|
+
"""Decode *payload* using the known type mapping, or try UTF-8 fallback."""
|
|
133
|
+
type_name = self.get_known_type_name(mode, message_type)
|
|
134
|
+
if type_name is None or type_name == "__json__":
|
|
135
|
+
return self._try_decode_utf8(payload)
|
|
136
|
+
return self.decode_message(type_name, payload)
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def _try_decode_utf8(payload: bytes) -> dict[str, Any] | None:
|
|
140
|
+
if not payload:
|
|
141
|
+
return None
|
|
142
|
+
text = payload.decode("utf-8")
|
|
143
|
+
try:
|
|
144
|
+
return {"encoding": "json", "json": json.loads(text)}
|
|
145
|
+
except (json.JSONDecodeError, ValueError):
|
|
146
|
+
return {
|
|
147
|
+
"encoding": "text",
|
|
148
|
+
"text": text,
|
|
149
|
+
"payload_base64": base64.b64encode(payload).decode(),
|
|
150
|
+
}
|
macp_sdk/py.typed
ADDED
|
File without changes
|