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/client.py ADDED
@@ -0,0 +1,625 @@
1
+ from __future__ import annotations
2
+
3
+ import queue
4
+ import threading
5
+ from collections.abc import Callable, Iterator, Sequence
6
+ from typing import Any
7
+
8
+ import grpc
9
+ from macp.v1 import core_pb2, core_pb2_grpc, envelope_pb2, policy_pb2
10
+
11
+ from ._logging import logger
12
+ from .auth import AuthConfig
13
+ from .envelope import (
14
+ build_envelope,
15
+ build_progress_payload,
16
+ build_signal_payload,
17
+ serialize_message,
18
+ )
19
+ from .errors import (
20
+ AckFailure,
21
+ MacpAckError,
22
+ MacpIdentityMismatchError,
23
+ MacpSdkError,
24
+ MacpTransportError,
25
+ )
26
+
27
+
28
+ def _parse_ack_reasons(ack: object) -> list[str]:
29
+ """Extract structured denial reasons from an ACK error's details."""
30
+ import json as _json
31
+
32
+ error = getattr(ack, "error", None)
33
+ if not error:
34
+ return []
35
+ details_bytes = getattr(error, "details", None) or b""
36
+ if not details_bytes:
37
+ return []
38
+ try:
39
+ parsed = _json.loads(details_bytes)
40
+ reasons = parsed.get("reasons", [])
41
+ return list(reasons) if isinstance(reasons, list) else []
42
+ except Exception:
43
+ return []
44
+
45
+
46
+ def _parse_grpc_metadata_reasons(rpc_error: grpc.RpcError) -> list[str]:
47
+ """Extract structured reasons from gRPC trailing metadata."""
48
+ import json as _json
49
+
50
+ try:
51
+ metadata = rpc_error.trailing_metadata()
52
+ if not metadata:
53
+ return []
54
+ for item in metadata:
55
+ key, value = item.key, item.value
56
+ if key == "macp-error-details-bin":
57
+ data = value if isinstance(value, bytes) else value.encode("utf-8")
58
+ parsed = _json.loads(data)
59
+ reasons = parsed.get("reasons", [])
60
+ return list(reasons) if isinstance(reasons, list) else []
61
+ except Exception:
62
+ pass
63
+ return []
64
+
65
+
66
+ def _default_capabilities() -> core_pb2.Capabilities:
67
+ return core_pb2.Capabilities(
68
+ sessions=core_pb2.SessionsCapability(stream=True),
69
+ cancellation=core_pb2.CancellationCapability(cancel_session=True),
70
+ progress=core_pb2.ProgressCapability(progress=True),
71
+ manifest=core_pb2.ManifestCapability(get_manifest=True),
72
+ mode_registry=core_pb2.ModeRegistryCapability(list_modes=True, list_changed=True),
73
+ roots=core_pb2.RootsCapability(list_roots=True, list_changed=True),
74
+ policy_registry=policy_pb2.PolicyRegistryCapability(
75
+ register_policy=True, list_policies=True, list_changed=True
76
+ ),
77
+ experimental=core_pb2.ExperimentalCapabilities(features={}),
78
+ )
79
+
80
+
81
+ class MacpStream:
82
+ _END = object()
83
+
84
+ def __init__(
85
+ self,
86
+ stub: core_pb2_grpc.MACPRuntimeServiceStub,
87
+ *,
88
+ metadata: Sequence[tuple[str, str]],
89
+ timeout: float | None = None,
90
+ ) -> None:
91
+ self._requests: queue.Queue[object] = queue.Queue()
92
+ self._responses: queue.Queue[object] = queue.Queue()
93
+ self._closed = False
94
+ self._inline_error_callbacks: list[Callable[[Any], None]] = []
95
+ self._call = stub.StreamSession(self._request_iter(), metadata=metadata, timeout=timeout)
96
+ self._thread = threading.Thread(target=self._pump_responses, daemon=True)
97
+ self._thread.start()
98
+
99
+ def _request_iter(self) -> Iterator[core_pb2.StreamSessionRequest]:
100
+ while True:
101
+ item = self._requests.get()
102
+ if item is self._END:
103
+ return
104
+ assert isinstance(item, envelope_pb2.Envelope)
105
+ yield core_pb2.StreamSessionRequest(envelope=item)
106
+
107
+ def _pump_responses(self) -> None:
108
+ try:
109
+ for response in self._call:
110
+ # Support both formats:
111
+ # New: StreamSessionResponse { response: { envelope | error } }
112
+ # Old: StreamSessionResponse { envelope }
113
+ inner = getattr(response, "response", None)
114
+ if inner is not None and hasattr(inner, "ByteSize") and inner.ByteSize() > 0:
115
+ envelope = getattr(inner, "envelope", None)
116
+ error = getattr(inner, "error", None)
117
+ if envelope is not None and envelope.ByteSize() > 0:
118
+ self._responses.put(envelope)
119
+ elif error is not None:
120
+ # Inline application-level error — notify callbacks, keep stream open
121
+ for cb in self._inline_error_callbacks:
122
+ cb(error)
123
+ logger.warning("inline stream error: %s", error)
124
+ continue
125
+ else:
126
+ # Flat format: response.envelope
127
+ self._responses.put(response.envelope)
128
+ except grpc.RpcError as exc:
129
+ self._responses.put(exc)
130
+ finally:
131
+ self._responses.put(self._END)
132
+
133
+ def on_inline_error(self, callback: Callable[[Any], None]) -> None:
134
+ """Register a callback for inline application-level stream errors."""
135
+ self._inline_error_callbacks.append(callback)
136
+
137
+ def send(self, envelope: envelope_pb2.Envelope) -> None:
138
+ if self._closed:
139
+ raise MacpSdkError("stream is already closed")
140
+ self._requests.put(envelope)
141
+
142
+ def read(self, timeout: float | None = None) -> envelope_pb2.Envelope | None:
143
+ item = self._responses.get(timeout=timeout)
144
+ if item is self._END:
145
+ return None
146
+ if isinstance(item, grpc.RpcError):
147
+ raise MacpTransportError(item.details() or str(item))
148
+ assert isinstance(item, envelope_pb2.Envelope)
149
+ return item
150
+
151
+ def responses(self, timeout: float | None = None) -> Iterator[envelope_pb2.Envelope]:
152
+ while True:
153
+ envelope = self.read(timeout=timeout)
154
+ if envelope is None:
155
+ return
156
+ yield envelope
157
+
158
+ def close(self) -> None:
159
+ if self._closed:
160
+ return
161
+ self._closed = True
162
+ self._requests.put(self._END)
163
+
164
+
165
+ class MacpClient:
166
+ """gRPC client for the MACP runtime.
167
+
168
+ Transport security follows RFC-MACP-0006 §3: TLS 1.2+ is REQUIRED in
169
+ production, so ``secure`` defaults to ``True``. Plaintext gRPC is only
170
+ available via the explicit ``allow_insecure=True`` opt-in, which is
171
+ intended for local development against a runtime started with
172
+ ``MACP_ALLOW_INSECURE=1``.
173
+ """
174
+
175
+ def __init__(
176
+ self,
177
+ *,
178
+ target: str,
179
+ secure: bool | None = None,
180
+ allow_insecure: bool = False,
181
+ auth: AuthConfig | None = None,
182
+ root_certificates: bytes | None = None,
183
+ default_timeout: float | None = None,
184
+ client_name: str = "macp-sdk-python",
185
+ client_version: str = "0.2.1",
186
+ ) -> None:
187
+ if secure is None:
188
+ secure = not allow_insecure
189
+ if not secure and not allow_insecure:
190
+ raise MacpSdkError(
191
+ "secure=False requires allow_insecure=True; "
192
+ "TLS is required by RFC-MACP-0006 §3 in production. "
193
+ "For local dev only, pass allow_insecure=True."
194
+ )
195
+ self.target = target
196
+ self.secure = secure
197
+ self.auth = auth
198
+ self.default_timeout = default_timeout
199
+ self.client_name = client_name
200
+ self.client_version = client_version
201
+ if secure:
202
+ creds = grpc.ssl_channel_credentials(root_certificates=root_certificates)
203
+ self.channel = grpc.secure_channel(target, creds)
204
+ else:
205
+ logger.warning("MacpClient insecure channel to %s — allowed only for local dev", target)
206
+ self.channel = grpc.insecure_channel(target)
207
+ self.stub = core_pb2_grpc.MACPRuntimeServiceStub(self.channel)
208
+
209
+ def close(self) -> None:
210
+ self.channel.close()
211
+
212
+ def __enter__(self) -> MacpClient:
213
+ return self
214
+
215
+ def __exit__(self, exc_type: object, exc: object, tb: object) -> None:
216
+ self.close()
217
+
218
+ def _metadata(self, auth: AuthConfig | None = None) -> Sequence[tuple[str, str]]:
219
+ selected = auth or self.auth
220
+ return selected.metadata() if selected else []
221
+
222
+ def _require_auth(self, auth: AuthConfig | None = None) -> AuthConfig:
223
+ selected = auth or self.auth
224
+ if selected is None:
225
+ raise MacpSdkError("this operation requires auth; pass auth= or configure client.auth")
226
+ return selected
227
+
228
+ @staticmethod
229
+ def _resolve_sender(auth_cfg: AuthConfig, sender: str) -> str:
230
+ """Resolve and validate the envelope sender against auth.expected_sender.
231
+
232
+ Raises :class:`MacpIdentityMismatchError` when an explicit ``sender``
233
+ contradicts ``auth_cfg.expected_sender``. Returns the effective sender
234
+ string to place on the envelope (possibly the fallback from ``auth_cfg``).
235
+ """
236
+ expected = auth_cfg.expected_sender
237
+ if sender:
238
+ if expected is not None and sender != expected:
239
+ raise MacpIdentityMismatchError(expected=expected, actual=sender)
240
+ return sender
241
+ return auth_cfg.sender or ""
242
+
243
+ @staticmethod
244
+ def _failure_from_ack(ack: envelope_pb2.Ack) -> AckFailure:
245
+ """Build an :class:`AckFailure` from a NACK envelope, including reasons.
246
+
247
+ Used by every RPC that returns an ``Ack`` (``send``, ``cancel_session``)
248
+ so structured denial reasons (``POLICY_DENIED`` rule IDs, etc.) surface
249
+ uniformly in ``MacpAckError.reasons`` no matter which call produced
250
+ them.
251
+ """
252
+ error = ack.error
253
+ return AckFailure(
254
+ code=(error.code if error else "UNKNOWN"),
255
+ message=(error.message if error else "runtime returned nack"),
256
+ session_id=ack.session_id,
257
+ message_id=ack.message_id,
258
+ reasons=_parse_ack_reasons(ack),
259
+ )
260
+
261
+ def initialize(self, *, timeout: float | None = None) -> core_pb2.InitializeResponse:
262
+ request = core_pb2.InitializeRequest(
263
+ supported_protocol_versions=["1.0"],
264
+ client_info=core_pb2.ClientInfo(
265
+ name=self.client_name,
266
+ title=self.client_name,
267
+ version=self.client_version,
268
+ description="Python SDK for the MACP runtime",
269
+ website_url="",
270
+ ),
271
+ capabilities=_default_capabilities(),
272
+ )
273
+ return self.stub.Initialize(request, timeout=timeout or self.default_timeout)
274
+
275
+ def send(
276
+ self,
277
+ envelope: envelope_pb2.Envelope,
278
+ *,
279
+ auth: AuthConfig | None = None,
280
+ timeout: float | None = None,
281
+ raise_on_nack: bool = True,
282
+ ) -> envelope_pb2.Ack:
283
+ auth_cfg = self._require_auth(auth)
284
+ try:
285
+ response = self.stub.Send(
286
+ core_pb2.SendRequest(envelope=envelope),
287
+ metadata=self._metadata(auth_cfg),
288
+ timeout=timeout or self.default_timeout,
289
+ )
290
+ except grpc.RpcError as rpc_err:
291
+ code = rpc_err.code()
292
+ if code == grpc.StatusCode.ALREADY_EXISTS:
293
+ failure = AckFailure(
294
+ code="SESSION_ALREADY_EXISTS",
295
+ message=rpc_err.details() or "session already exists",
296
+ )
297
+ raise MacpAckError(failure) from rpc_err
298
+ if code == grpc.StatusCode.FAILED_PRECONDITION:
299
+ reasons = _parse_grpc_metadata_reasons(rpc_err)
300
+ failure = AckFailure(
301
+ code="POLICY_DENIED",
302
+ message=rpc_err.details() or "policy denied",
303
+ reasons=reasons,
304
+ )
305
+ raise MacpAckError(failure) from rpc_err
306
+ if code == grpc.StatusCode.INVALID_ARGUMENT:
307
+ raise MacpTransportError(rpc_err.details() or "invalid argument") from rpc_err
308
+ raise MacpTransportError(rpc_err.details() or str(rpc_err)) from rpc_err
309
+ ack = response.ack
310
+ # Duplicate acks are idempotent success — the message was already accepted.
311
+ # This matches TypeScript SDK behaviour and is correct for retry scenarios.
312
+ if ack.duplicate:
313
+ return ack
314
+ if raise_on_nack and not ack.ok:
315
+ raise MacpAckError(self._failure_from_ack(ack))
316
+ return ack
317
+
318
+ def get_session(
319
+ self,
320
+ session_id: str,
321
+ *,
322
+ auth: AuthConfig | None = None,
323
+ timeout: float | None = None,
324
+ ) -> core_pb2.GetSessionResponse:
325
+ auth_cfg = self._require_auth(auth)
326
+ return self.stub.GetSession(
327
+ core_pb2.GetSessionRequest(session_id=session_id),
328
+ metadata=self._metadata(auth_cfg),
329
+ timeout=timeout or self.default_timeout,
330
+ )
331
+
332
+ def cancel_session(
333
+ self,
334
+ session_id: str,
335
+ *,
336
+ reason: str,
337
+ cancelled_by: str = "",
338
+ auth: AuthConfig | None = None,
339
+ timeout: float | None = None,
340
+ raise_on_nack: bool = True,
341
+ ) -> envelope_pb2.Ack:
342
+ auth_cfg = self._require_auth(auth)
343
+ request_kwargs: dict[str, object] = {
344
+ "session_id": session_id,
345
+ "reason": reason,
346
+ }
347
+ # Forward-compatible: add cancelled_by if proto supports it
348
+ if cancelled_by:
349
+ has_field = any(
350
+ f.name == "cancelled_by" for f in core_pb2.CancelSessionRequest.DESCRIPTOR.fields
351
+ )
352
+ if has_field:
353
+ request_kwargs["cancelled_by"] = cancelled_by
354
+ try:
355
+ response = self.stub.CancelSession(
356
+ core_pb2.CancelSessionRequest(**request_kwargs),
357
+ metadata=self._metadata(auth_cfg),
358
+ timeout=timeout or self.default_timeout,
359
+ )
360
+ except grpc.RpcError as rpc_err:
361
+ raise MacpTransportError(rpc_err.details() or str(rpc_err)) from rpc_err
362
+ ack = response.ack
363
+ if raise_on_nack and not ack.ok:
364
+ raise MacpAckError(self._failure_from_ack(ack))
365
+ return ack
366
+
367
+ def get_manifest(
368
+ self, agent_id: str = "", *, timeout: float | None = None
369
+ ) -> core_pb2.GetManifestResponse:
370
+ return self.stub.GetManifest(
371
+ core_pb2.GetManifestRequest(agent_id=agent_id),
372
+ timeout=timeout or self.default_timeout,
373
+ )
374
+
375
+ def list_modes(self, *, timeout: float | None = None) -> core_pb2.ListModesResponse:
376
+ return self.stub.ListModes(
377
+ core_pb2.ListModesRequest(),
378
+ timeout=timeout or self.default_timeout,
379
+ )
380
+
381
+ def list_ext_modes(self, *, timeout: float | None = None) -> core_pb2.ListExtModesResponse:
382
+ return self.stub.ListExtModes(
383
+ core_pb2.ListExtModesRequest(),
384
+ timeout=timeout or self.default_timeout,
385
+ )
386
+
387
+ def list_roots(self, *, timeout: float | None = None) -> core_pb2.ListRootsResponse:
388
+ return self.stub.ListRoots(
389
+ core_pb2.ListRootsRequest(),
390
+ timeout=timeout or self.default_timeout,
391
+ )
392
+
393
+ def register_ext_mode(
394
+ self,
395
+ descriptor: core_pb2.ModeDescriptor,
396
+ *,
397
+ auth: AuthConfig | None = None,
398
+ timeout: float | None = None,
399
+ ) -> core_pb2.RegisterExtModeResponse:
400
+ auth_cfg = self._require_auth(auth)
401
+ return self.stub.RegisterExtMode(
402
+ core_pb2.RegisterExtModeRequest(mode_descriptor=descriptor),
403
+ metadata=self._metadata(auth_cfg),
404
+ timeout=timeout or self.default_timeout,
405
+ )
406
+
407
+ def unregister_ext_mode(
408
+ self,
409
+ mode: str,
410
+ *,
411
+ auth: AuthConfig | None = None,
412
+ timeout: float | None = None,
413
+ ) -> core_pb2.UnregisterExtModeResponse:
414
+ auth_cfg = self._require_auth(auth)
415
+ return self.stub.UnregisterExtMode(
416
+ core_pb2.UnregisterExtModeRequest(mode=mode),
417
+ metadata=self._metadata(auth_cfg),
418
+ timeout=timeout or self.default_timeout,
419
+ )
420
+
421
+ def promote_mode(
422
+ self,
423
+ mode: str,
424
+ promoted_mode_name: str = "",
425
+ *,
426
+ auth: AuthConfig | None = None,
427
+ timeout: float | None = None,
428
+ ) -> core_pb2.PromoteModeResponse:
429
+ auth_cfg = self._require_auth(auth)
430
+ return self.stub.PromoteMode(
431
+ core_pb2.PromoteModeRequest(mode=mode, promoted_mode_name=promoted_mode_name),
432
+ metadata=self._metadata(auth_cfg),
433
+ timeout=timeout or self.default_timeout,
434
+ )
435
+
436
+ # ── Governance policy lifecycle ───────────────────────────────────
437
+
438
+ def register_policy(
439
+ self,
440
+ descriptor: policy_pb2.PolicyDescriptor,
441
+ *,
442
+ auth: AuthConfig | None = None,
443
+ timeout: float | None = None,
444
+ ) -> policy_pb2.RegisterPolicyResponse:
445
+ """Register a governance policy with the runtime."""
446
+ auth_cfg = self._require_auth(auth)
447
+ return self.stub.RegisterPolicy(
448
+ policy_pb2.RegisterPolicyRequest(policy_descriptor=descriptor),
449
+ metadata=self._metadata(auth_cfg),
450
+ timeout=timeout or self.default_timeout,
451
+ )
452
+
453
+ def unregister_policy(
454
+ self,
455
+ policy_id: str,
456
+ *,
457
+ auth: AuthConfig | None = None,
458
+ timeout: float | None = None,
459
+ ) -> policy_pb2.UnregisterPolicyResponse:
460
+ """Unregister a governance policy from the runtime."""
461
+ auth_cfg = self._require_auth(auth)
462
+ return self.stub.UnregisterPolicy(
463
+ policy_pb2.UnregisterPolicyRequest(policy_id=policy_id),
464
+ metadata=self._metadata(auth_cfg),
465
+ timeout=timeout or self.default_timeout,
466
+ )
467
+
468
+ def get_policy(
469
+ self,
470
+ policy_id: str,
471
+ *,
472
+ auth: AuthConfig | None = None,
473
+ timeout: float | None = None,
474
+ ) -> policy_pb2.GetPolicyResponse:
475
+ """Retrieve a single governance policy by ID."""
476
+ auth_cfg = self._require_auth(auth)
477
+ return self.stub.GetPolicy(
478
+ policy_pb2.GetPolicyRequest(policy_id=policy_id),
479
+ metadata=self._metadata(auth_cfg),
480
+ timeout=timeout or self.default_timeout,
481
+ )
482
+
483
+ def list_policies(
484
+ self,
485
+ mode: str | None = None,
486
+ *,
487
+ auth: AuthConfig | None = None,
488
+ timeout: float | None = None,
489
+ ) -> policy_pb2.ListPoliciesResponse:
490
+ """List registered governance policies, optionally filtered by mode."""
491
+ auth_cfg = self._require_auth(auth)
492
+ return self.stub.ListPolicies(
493
+ policy_pb2.ListPoliciesRequest(mode=mode or ""),
494
+ metadata=self._metadata(auth_cfg),
495
+ timeout=timeout or self.default_timeout,
496
+ )
497
+
498
+ def watch_policies(
499
+ self, *, timeout: float | None = None
500
+ ) -> Iterator[policy_pb2.WatchPoliciesResponse]:
501
+ """Server-streaming RPC: yields governance policy change events."""
502
+ logger.debug("watch_policies starting")
503
+ call = self.stub.WatchPolicies(
504
+ policy_pb2.WatchPoliciesRequest(),
505
+ timeout=timeout or self.default_timeout,
506
+ )
507
+ try:
508
+ yield from call
509
+ except grpc.RpcError as exc:
510
+ raise MacpTransportError(str(exc)) from exc
511
+
512
+ def open_stream(
513
+ self, *, auth: AuthConfig | None = None, timeout: float | None = None
514
+ ) -> MacpStream:
515
+ auth_cfg = self._require_auth(auth)
516
+ return MacpStream(
517
+ self.stub,
518
+ metadata=self._metadata(auth_cfg),
519
+ timeout=timeout or self.default_timeout,
520
+ )
521
+
522
+ def watch_mode_registry(
523
+ self, *, timeout: float | None = None
524
+ ) -> Iterator[core_pb2.WatchModeRegistryResponse]:
525
+ """Server-streaming RPC: yields mode registry change events."""
526
+ logger.debug("watch_mode_registry starting")
527
+ call = self.stub.WatchModeRegistry(
528
+ core_pb2.WatchModeRegistryRequest(),
529
+ timeout=timeout or self.default_timeout,
530
+ )
531
+ try:
532
+ yield from call
533
+ except grpc.RpcError as exc:
534
+ raise MacpTransportError(str(exc)) from exc
535
+
536
+ def watch_roots(self, *, timeout: float | None = None) -> Iterator[core_pb2.WatchRootsResponse]:
537
+ """Server-streaming RPC: yields root change events."""
538
+ logger.debug("watch_roots starting")
539
+ call = self.stub.WatchRoots(
540
+ core_pb2.WatchRootsRequest(),
541
+ timeout=timeout or self.default_timeout,
542
+ )
543
+ try:
544
+ yield from call
545
+ except grpc.RpcError as exc:
546
+ raise MacpTransportError(str(exc)) from exc
547
+
548
+ def watch_signals(
549
+ self, *, timeout: float | None = None
550
+ ) -> Iterator[core_pb2.WatchSignalsResponse]:
551
+ """Server-streaming RPC: yields ambient signal envelopes."""
552
+ logger.debug("watch_signals starting")
553
+ call = self.stub.WatchSignals(
554
+ core_pb2.WatchSignalsRequest(),
555
+ timeout=timeout or self.default_timeout,
556
+ )
557
+ try:
558
+ yield from call
559
+ except grpc.RpcError as exc:
560
+ raise MacpTransportError(str(exc)) from exc
561
+
562
+ def send_signal(
563
+ self,
564
+ *,
565
+ signal_type: str,
566
+ data: bytes = b"",
567
+ confidence: float = 0.0,
568
+ correlation_session_id: str = "",
569
+ sender: str = "",
570
+ auth: AuthConfig | None = None,
571
+ timeout: float | None = None,
572
+ ) -> envelope_pb2.Ack:
573
+ """Send an ambient (non-session) signal to the runtime."""
574
+ auth_cfg = self._require_auth(auth)
575
+ payload = build_signal_payload(
576
+ signal_type=signal_type,
577
+ data=data,
578
+ confidence=confidence,
579
+ correlation_session_id=correlation_session_id,
580
+ )
581
+ envelope = build_envelope(
582
+ mode="",
583
+ message_type="Signal",
584
+ session_id="",
585
+ payload=serialize_message(payload),
586
+ sender=self._resolve_sender(auth_cfg, sender),
587
+ )
588
+ return self.send(envelope, auth=auth_cfg, timeout=timeout)
589
+
590
+ def send_progress(
591
+ self,
592
+ *,
593
+ session_id: str = "",
594
+ mode: str = "",
595
+ progress_token: str,
596
+ progress: float,
597
+ total: float,
598
+ message: str = "",
599
+ target_message_id: str = "",
600
+ sender: str = "",
601
+ auth: AuthConfig | None = None,
602
+ timeout: float | None = None,
603
+ ) -> envelope_pb2.Ack:
604
+ """Send a progress update.
605
+
606
+ When ``session_id`` and ``mode`` are empty, the progress is treated
607
+ as an *ambient* progress message routed through the signal broadcast
608
+ path.
609
+ """
610
+ auth_cfg = self._require_auth(auth)
611
+ payload = build_progress_payload(
612
+ progress_token=progress_token,
613
+ progress=progress,
614
+ total=total,
615
+ message=message,
616
+ target_message_id=target_message_id,
617
+ )
618
+ envelope = build_envelope(
619
+ mode=mode,
620
+ message_type="Progress",
621
+ session_id=session_id,
622
+ payload=serialize_message(payload),
623
+ sender=self._resolve_sender(auth_cfg, sender),
624
+ )
625
+ return self.send(envelope, auth=auth_cfg, timeout=timeout)
macp_sdk/constants.py ADDED
@@ -0,0 +1,20 @@
1
+ MACP_VERSION = "1.0"
2
+
3
+ DEFAULT_MODE_VERSION = "1.0.0"
4
+ DEFAULT_CONFIGURATION_VERSION = "config.default"
5
+ DEFAULT_POLICY_VERSION = "policy.default"
6
+
7
+ MODE_DECISION = "macp.mode.decision.v1"
8
+ MODE_PROPOSAL = "macp.mode.proposal.v1"
9
+ MODE_TASK = "macp.mode.task.v1"
10
+ MODE_HANDOFF = "macp.mode.handoff.v1"
11
+ MODE_QUORUM = "macp.mode.quorum.v1"
12
+ MODE_MULTI_ROUND = "ext.multi_round.v1"
13
+
14
+ STANDARD_MODES = (
15
+ MODE_DECISION,
16
+ MODE_PROPOSAL,
17
+ MODE_TASK,
18
+ MODE_HANDOFF,
19
+ MODE_QUORUM,
20
+ )