ailogger-client-dev 0.1.51__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.
@@ -0,0 +1,610 @@
1
+ """
2
+ Typed error vocabulary for the ai-logger client.
3
+
4
+ This module declares the typed error vocabulary through which the ai-logger
5
+ client communicates failure to its callers. It is a MAPPING OVER the framework
6
+ client's hierarchy, not an invention: every kind declared here translates a
7
+ named member of the adapter framework's ClientError hierarchy
8
+ (mcp_proxy_adapter.client.jsonrpc_client.exceptions), and every member of that
9
+ hierarchy is covered by a kind declared here or by the family base that
10
+ contains it.
11
+
12
+ EXHAUSTIVENESS BY CONSTRUCTION. The module names ClientError from the adapter
13
+ framework only in this docstring and in the class docstrings that name it.
14
+ The call site (G-012/T-003/A-002) enforces the mapping at runtime by walking
15
+ the framework's hierarchy at import time and asserting that every subclass is
16
+ covered. This design ensures that if the framework adds a new exception member,
17
+ the runtime walk catches it rather than passing silently. SchemaGeneratorError,
18
+ an intermediate base in the framework's hierarchy, is covered by the
19
+ RequestRejectedError family base (which holds three of its four children); the
20
+ fourth child (MethodNotFoundError) is UnknownCommandError. Together, the
21
+ RequestRejectedError family and UnknownCommandError exhaust SchemaGeneratorError
22
+ and all its children.
23
+
24
+ NO TRANSPORT DETAIL ON THE CALLER'S SURFACE. Under HRS {loe8}, a caller never
25
+ sees a host, connection, retry count, status code or transport envelope. No
26
+ kind declared here carries any of them: no attribute holding a host or
27
+ connection, no retry count, no status code, and no request or response
28
+ envelope. The framework's ClientRequestError carries a status code and response
29
+ dictionary; neither is admitted to this client's error surface.
30
+
31
+ STRUCTURE. One base error (AiLoggerClientError) from which all others descend,
32
+ so a caller that wants to catch everything this client can raise catches one
33
+ name. Beneath it: (1) ServiceUnreachableError, translating
34
+ ClientConnectionError (transport failure; caller retries or corrects config);
35
+ (2) ServiceRefusalError, translating ClientRequestError (service answered in
36
+ refusal; caller corrects request or accepts). Beneath base: (3) UnknownCommandError,
37
+ translating MethodNotFoundError (service does not publish this command).
38
+ (4) RequestRejectedError family base with three members - MissingArgumentError
39
+ (RequiredParameterMissingError), WrongArgumentTypeError (InvalidParameterTypeError),
40
+ InadmissibleArgumentValueError (InvalidParameterValueError) - caller corrects
41
+ its arguments. (5) SchemaDisagreementError, translating SchemaValidationError
42
+ (client and service are mismatched). (6) QueuedCompletionError family base with
43
+ four members - CompletionTimeoutError (CommandSessionTimeoutError),
44
+ CompletionFailedError (CommandSessionFailedError), ResultUnavailableError
45
+ (CommandSessionResultError), QueuedProtocolError (CommandSessionProtocolError)
46
+ - distinguishes a job still running from a job that failed, for polled
47
+ completion. (7) DialogueFileTransferError, translating the TransferClientError
48
+ family as a whole (translates all seven members: session-not-found,
49
+ resume-conflict, checksum-mismatch, compression, too-large,
50
+ range-not-satisfiable, expired).
51
+
52
+ THIS MODULE DOES NOT: perform translation (that is A-002), inspect answers,
53
+ know domain error codes, declare the client class or configuration reading
54
+ (those are other steps), or hold a copy of the service's stable error-code
55
+ vocabulary (that lives in G-009 inside the service).
56
+
57
+ Step citation: G-012/T-003/A-001.
58
+ Translation (A-002) and refusal payload (A-003) extend this same file.
59
+
60
+ A-002 SECTION: Framework Failure Translation
61
+
62
+ The module provides a single immutable translation table (_TRANSLATIONS) that
63
+ maps each named member of the framework's ClientError hierarchy to the
64
+ corresponding kind in this client's vocabulary. The table is declared as data
65
+ (types.MappingProxyType over a dict literal) and never modified.
66
+
67
+ Translation is performed by a single entry point function (translate_framework_failure)
68
+ that takes a raised framework failure and returns the corresponding error of this
69
+ client's vocabulary. The function resolves the most-specific matching member first
70
+ by walking type(failure).__mro__, ensuring that a family member is never swallowed
71
+ by the family base above it. If no MRO member is in the table, the function returns
72
+ an instance of AiLoggerClientError (the fallback) so that no framework-typed failure
73
+ escapes.
74
+
75
+ A reusable guard (translated_failures) wraps every call the client makes to the
76
+ privately held framework client. It catches ClientError and only ClientError (never
77
+ bare Exception or BaseException), translates it through the entry point, and raises
78
+ the translated error with the original as its cause, so that a maintainer can still
79
+ diagnose via the cause chain while a caller reads only this client's vocabulary.
80
+
81
+ The translated error is constructed with no arguments: its type carries the meaning,
82
+ the __cause__ chain carries the diagnosis, and its string form is empty. No host,
83
+ connection, retry count, numeric status or envelope is copied from the original.
84
+ This posture ensures that transport details remain behind the client's boundary.
85
+
86
+ A-003 SECTION: Refusal Payload and Answer Recognition
87
+
88
+ A refusal reaches this client by two routes. The first is via a raised framework
89
+ failure (ClientRequestError), which is translated by A-002 to ServiceRefusalError.
90
+ The second is via an answer dict that came back intact over a working transport
91
+ and carries the service's own refusal in its payload (success=False, with domain
92
+ code and statement in the data dict).
93
+
94
+ The recogniser function (refusal_from_answer) is the only place in the client
95
+ where an answer is examined for a refusal. It reads the stable domain code from
96
+ the payload position (data["code"]), never from the framework's numeric code,
97
+ and produces a ServiceRefusalError when it finds the structural refusal marker
98
+ (success=False). Both routes produce the same kind, so a caller catches one name
99
+ and never has to inspect transport markers to tell a transport failure from a
100
+ service refusal.
101
+
102
+ ServiceRefusalError now carries a payload: the service's stable domain code as
103
+ a plain string (opaque carry-through, never invented), the human-readable
104
+ statement the service sent with it, and the command the refusal answered. A
105
+ caller compares the code, never the prose. The str form of the error contains
106
+ the code and statement only, never transport detail.
107
+
108
+ No error kind is added; the vocabulary is closed by A-001. No translation entry
109
+ is changed; A-002 is unchanged. The domain code is declared once in the service
110
+ and never replicated here.
111
+ """
112
+
113
+ from __future__ import annotations
114
+
115
+ import contextlib
116
+ import types
117
+ from collections.abc import Iterator, Mapping
118
+ from typing import Any
119
+
120
+ from mcp_proxy_adapter.client.jsonrpc_client.exceptions import (
121
+ ClientConnectionError,
122
+ ClientConnectionRefusedError,
123
+ ClientConnectTimeoutError,
124
+ ClientDnsResolutionError,
125
+ ClientError,
126
+ ClientRemoteResponseError,
127
+ ClientRequestError,
128
+ CommandSessionClientError,
129
+ CommandSessionFailedError,
130
+ CommandSessionProtocolError,
131
+ CommandSessionResultError,
132
+ CommandSessionTimeoutError,
133
+ InvalidParameterTypeError,
134
+ InvalidParameterValueError,
135
+ MethodNotFoundError,
136
+ RequiredParameterMissingError,
137
+ SchemaGeneratorError,
138
+ SchemaValidationError,
139
+ TransferClientError,
140
+ )
141
+
142
+
143
+ class AiLoggerClientError(Exception):
144
+ """
145
+ Base exception for all errors raised by the ai-logger client.
146
+
147
+ All exceptions raised by this client descend from this class.
148
+ Callers may catch this to catch everything the client can raise.
149
+ """
150
+
151
+
152
+ class ServiceUnreachableError(AiLoggerClientError):
153
+ """
154
+ The service could not be reached at all.
155
+
156
+ Translates ``ClientConnectionError`` from the adapter framework. Indicates
157
+ a transport failure before the service could be contacted. Caller should
158
+ retry later or correct the connection material it configured.
159
+ """
160
+
161
+
162
+ class ServiceRefusalError(AiLoggerClientError):
163
+ """
164
+ The service was reached and answered in refusal.
165
+
166
+ Translates ``ClientRequestError`` from the adapter framework. The service
167
+ rejected the request at the HTTP level. Caller should correct its request
168
+ or accept the refusal.
169
+
170
+ Carries the service's stable domain error code as a plain string (the
171
+ service's own code from the closed vocabulary), the human-readable
172
+ statement that accompanied it, and the command the refusal answered. A
173
+ caller compares the code, never the prose. The code is opaque to this
174
+ client: it carries through unchanged so that a code the service adds later
175
+ reaches a caller without a client release. Details added by A-003.
176
+
177
+ A refusal of the request's own shape also carries the three fields the
178
+ service states in ``data.details``: ``violation`` (``unknown``,
179
+ ``missing``, ``type`` or ``value``), ``expected`` (what the declaration
180
+ requires) and ``got`` (what the request carried). They are copied from
181
+ the answer, never recomputed, and are ``None`` when the answer carries no
182
+ such datum - always present as attributes.
183
+ """
184
+
185
+ def __init__(
186
+ self,
187
+ code: str | None = None,
188
+ statement: str | None = None,
189
+ command: str | None = None,
190
+ violation: str | None = None,
191
+ expected: object = None,
192
+ got: object = None,
193
+ ) -> None:
194
+ """
195
+ Args:
196
+ code: The service's stable domain error code as a plain string.
197
+ Opaque to this client; carries through unchanged.
198
+ statement: The human-readable statement the service sent with it.
199
+ command: The command the refusal answered.
200
+ violation: The service's ``data.details.violation`` marker, if any.
201
+ expected: The service's ``data.details.expected``, if any.
202
+ got: The service's ``data.details.got``, if any.
203
+
204
+ The error is constructible with no arguments; every argument is
205
+ optional to support the guard route (A-002) which instantiates kinds
206
+ no-arg.
207
+ """
208
+ self.code: str | None = code
209
+ self.statement: str | None = statement
210
+ self.command: str | None = command
211
+ self.violation: str | None = violation
212
+ self.expected: object = expected
213
+ self.got: object = got
214
+ message = ""
215
+ if code is not None and statement is not None:
216
+ message = f"{code}: {statement}"
217
+ elif code is not None:
218
+ message = code
219
+ elif statement is not None:
220
+ message = statement
221
+ super().__init__(message)
222
+
223
+
224
+ class UnknownCommandError(AiLoggerClientError):
225
+ """
226
+ The client asked for a command this service does not publish.
227
+
228
+ Translates ``MethodNotFoundError`` from the adapter framework. One of the
229
+ two kinds that together (with the RequestRejectedError family) exhausts
230
+ ``SchemaGeneratorError`` and all its children. Indicates client-and-service
231
+ drift. The named pipeline check client-surface-agreement exists to catch
232
+ this before a consumer meets it.
233
+ """
234
+
235
+
236
+ class RequestRejectedError(AiLoggerClientError):
237
+ """
238
+ Base for the request-rejected family.
239
+
240
+ Gathered base so a caller may catch the whole family at once.
241
+ Members indicate that arguments were decided invalid before the call left
242
+ the caller's process. Caller remedy is to correct its own arguments.
243
+ Together with UnknownCommandError, exhausts ``SchemaGeneratorError`` and
244
+ all its children from the adapter framework; SchemaGeneratorError itself
245
+ is covered by this family base as the nearest named coverage.
246
+ """
247
+
248
+
249
+ class MissingArgumentError(RequestRejectedError):
250
+ """
251
+ A required argument was missing.
252
+
253
+ Translates ``RequiredParameterMissingError`` from the adapter framework.
254
+ Caller must supply the missing argument.
255
+ """
256
+
257
+
258
+ class WrongArgumentTypeError(RequestRejectedError):
259
+ """
260
+ An argument had the wrong type.
261
+
262
+ Translates ``InvalidParameterTypeError`` from the adapter framework.
263
+ Caller must pass a value of the correct type.
264
+ """
265
+
266
+
267
+ class InadmissibleArgumentValueError(RequestRejectedError):
268
+ """
269
+ An argument's value was not admitted.
270
+
271
+ Translates ``InvalidParameterValueError`` from the adapter framework.
272
+ Caller must pass a value that satisfies the constraint.
273
+ """
274
+
275
+
276
+ class SchemaDisagreementError(AiLoggerClientError):
277
+ """
278
+ What was sent or received does not match the service's schema.
279
+
280
+ Translates ``SchemaValidationError`` from the adapter framework. Indicates
281
+ that the pairing of client and service is wrong rather than the caller's
282
+ own arguments. Caller should verify client and service versions match.
283
+ """
284
+
285
+
286
+ class QueuedCompletionError(AiLoggerClientError):
287
+ """
288
+ Base for the queued-completion family.
289
+
290
+ Translates the entire ``CommandSessionClientError`` family from the adapter
291
+ framework as a whole, covering all four members: CommandSessionTimeoutError,
292
+ CommandSessionFailedError, CommandSessionResultError, and
293
+ CommandSessionProtocolError. Gathered base so a caller may distinguish a
294
+ job that is still running from a job that failed. Members indicate
295
+ different failure modes of polled completion (the default of G-012/T-002).
296
+ """
297
+
298
+
299
+ class CompletionTimeoutError(QueuedCompletionError):
300
+ """
301
+ Waiting for a queued completion timed out.
302
+
303
+ Translates ``CommandSessionTimeoutError`` from the adapter framework.
304
+ Indicates the job did not finish within the configured timeout.
305
+ """
306
+
307
+
308
+ class CompletionFailedError(QueuedCompletionError):
309
+ """
310
+ A queued completion reported failure.
311
+
312
+ Translates ``CommandSessionFailedError`` from the adapter framework.
313
+ Indicates the service reported that the job failed after being queued.
314
+ """
315
+
316
+
317
+ class ResultUnavailableError(QueuedCompletionError):
318
+ """
319
+ The result of a queued completion could not be fetched.
320
+
321
+ Translates ``CommandSessionResultError`` from the adapter framework.
322
+ Indicates the job was accepted and queued but the result became
323
+ unavailable, perhaps due to session expiry or server state loss.
324
+ """
325
+
326
+
327
+ class QueuedProtocolError(QueuedCompletionError):
328
+ """
329
+ A violation of the queued-answer protocol occurred.
330
+
331
+ Translates ``CommandSessionProtocolError`` from the adapter framework.
332
+ Indicates the service's responses did not follow the expected protocol
333
+ for queued operations.
334
+ """
335
+
336
+
337
+ class SyncFallbackWaitExhaustedError(AiLoggerClientError):
338
+ """
339
+ A command overran the service's sync cap and its queued job did not finish
340
+ within the client's configured wait.
341
+
342
+ When a command exceeds the service's synchronous time cap the service
343
+ answers with an acknowledgement naming a job instead of the result, and the
344
+ job finishes on its own. The client follows that job until it ends. When the
345
+ wait set by ``client.sync_fallback_wait_seconds`` runs out first, this error
346
+ is raised. It is not ``CompletionTimeoutError`` (the wait of an always-queued
347
+ command): the command was dispatched exactly once and its job may still
348
+ complete, so the caller can read the outcome later through ``job_id``.
349
+
350
+ Attributes:
351
+ job_id: The identity of the still-unfinished job.
352
+ """
353
+
354
+ def __init__(self, message: str, job_id: str) -> None:
355
+ super().__init__(message)
356
+ self.job_id = job_id
357
+
358
+
359
+ class DialogueFileTransferError(AiLoggerClientError):
360
+ """
361
+ File transfer between client and service encountered an error.
362
+
363
+ Translates the entire ``TransferClientError`` family from the adapter
364
+ framework as a single kind, covering all seven members: session-not-found,
365
+ resume-conflict, checksum-mismatch, compression, too-large,
366
+ range-not-satisfiable, and expired. File transfer between this client and
367
+ service is wanted only for importing and exporting dialogues; that purpose
368
+ is not yet designed, and the method that would carry a file raises
369
+ NotImplementedError. This kind exists to make the mapping exhaustive over
370
+ the framework hierarchy today. When import and export are taken up, this
371
+ one kind is widened into individual members rather than a second family
372
+ being invented.
373
+ """
374
+
375
+
376
+ _TRANSLATIONS: types.MappingProxyType[type[ClientError], type[AiLoggerClientError]] = (
377
+ types.MappingProxyType(
378
+ {
379
+ # The adapter classifies connect-phase failures as siblings of
380
+ # ClientConnectionError, not as its subclasses, so each is listed.
381
+ ClientConnectionError: ServiceUnreachableError,
382
+ ClientConnectionRefusedError: ServiceUnreachableError,
383
+ ClientConnectTimeoutError: ServiceUnreachableError,
384
+ ClientDnsResolutionError: ServiceUnreachableError,
385
+ ClientRequestError: ServiceRefusalError,
386
+ MethodNotFoundError: UnknownCommandError,
387
+ RequiredParameterMissingError: MissingArgumentError,
388
+ InvalidParameterTypeError: WrongArgumentTypeError,
389
+ InvalidParameterValueError: InadmissibleArgumentValueError,
390
+ SchemaGeneratorError: RequestRejectedError,
391
+ SchemaValidationError: SchemaDisagreementError,
392
+ CommandSessionClientError: QueuedCompletionError,
393
+ CommandSessionTimeoutError: CompletionTimeoutError,
394
+ CommandSessionFailedError: CompletionFailedError,
395
+ CommandSessionResultError: ResultUnavailableError,
396
+ CommandSessionProtocolError: QueuedProtocolError,
397
+ TransferClientError: DialogueFileTransferError,
398
+ }
399
+ )
400
+ )
401
+
402
+
403
+ def translate_framework_failure(failure: ClientError) -> AiLoggerClientError:
404
+ """
405
+ Translate a framework failure to a corresponding client error.
406
+
407
+ Walks the MRO of the failure's type to find the most-specific matching
408
+ member in the translation table. If found, returns an instance of the
409
+ corresponding client error kind. If no MRO member is in the table, returns
410
+ an instance of AiLoggerClientError (the fallback).
411
+
412
+ Args:
413
+ failure: A ClientError raised by the framework client.
414
+
415
+ Returns:
416
+ An instance of AiLoggerClientError or one of its subclasses.
417
+ """
418
+ in_band = _in_band_refusal(failure)
419
+ if in_band is not None:
420
+ return in_band
421
+ for mro_type in type(failure).__mro__:
422
+ if mro_type in _TRANSLATIONS:
423
+ return _TRANSLATIONS[mro_type]()
424
+ return AiLoggerClientError()
425
+
426
+
427
+ def _in_band_refusal(failure: ClientError) -> ServiceRefusalError | None:
428
+ """
429
+ The service's refusal of a request it declined to queue, when the
430
+ framework reported it as an in-band JSON-RPC error that carries the
431
+ service's stable domain code.
432
+
433
+ An always-queued command is validated when the request is accepted for
434
+ queueing, before any job exists, and the framework answers a refusal there
435
+ as a JSON-RPC error rather than as a command answer. When that error's
436
+ data carries the stable code of the service's closed vocabulary, it is the
437
+ same refusal a direct command answers with, and arrives as the same kind:
438
+ ``ServiceRefusalError`` with that code. An in-band error carrying no such
439
+ code is not recognised here and keeps the generic translation.
440
+ """
441
+ if not isinstance(failure, ClientRemoteResponseError):
442
+ return None
443
+ details = failure.details
444
+ data = details.get("data") if isinstance(details, Mapping) else None
445
+ code = data.get("code") if isinstance(data, Mapping) else None
446
+ if not isinstance(code, str) or not code.strip():
447
+ return None
448
+ return ServiceRefusalError(
449
+ code=code, statement=str(failure), **_refusal_fields(data)
450
+ )
451
+
452
+
453
+ def _refusal_fields(payload: Mapping[str, Any]) -> dict[str, Any]:
454
+ """
455
+ The ``violation``, ``expected`` and ``got`` of a refusal, copied from the
456
+ ``details`` of its payload; an absent datum reads as ``None``. Nothing is
457
+ recomputed here: the service states them.
458
+ """
459
+ details = payload.get("details")
460
+ source: Mapping[str, Any] = details if isinstance(details, Mapping) else {}
461
+ marker = source.get("violation")
462
+ return {
463
+ "violation": marker if isinstance(marker, str) else None,
464
+ "expected": source.get("expected"),
465
+ "got": source.get("got"),
466
+ }
467
+
468
+
469
+ @contextlib.contextmanager
470
+ def translated_failures() -> Iterator[None]:
471
+ """
472
+ Context manager that translates framework failures to client errors.
473
+
474
+ Wraps code that calls the privately held framework client. Catches
475
+ ClientError and only ClientError (never Exception or BaseException),
476
+ translates it through translate_framework_failure, and raises the
477
+ translated error with the original as its cause. Allows other exceptions
478
+ to propagate unchanged.
479
+
480
+ Usage:
481
+ with translated_failures():
482
+ result = await framework_client.execute_command(...)
483
+ """
484
+ try:
485
+ yield
486
+ except ClientError as exc:
487
+ raise translate_framework_failure(exc) from exc
488
+
489
+
490
+ def refusal_from_answer(
491
+ answer: dict[str, Any] | None,
492
+ command: str,
493
+ ) -> ServiceRefusalError | None:
494
+ """
495
+ Recognise a service refusal in an answer dict and produce the error.
496
+
497
+ The only place in this client where an answer is examined for a refusal.
498
+ Reads the refusal from the structural shapes the service's answers
499
+ actually carry via two call paths. Returns None when the answer is not a
500
+ refusal (normal result, empty result, or ordinary data).
501
+
502
+ TWO ROUTES TO A REFUSAL, two shapes:
503
+
504
+ 1. FULL ENVELOPE (legacy/fallback path): answer has top-level keys
505
+ success=False, data (dict), and error (str). Framework wrapping intact.
506
+ Referenced at: mcp_proxy_adapter.client.jsonrpc_client.transport,
507
+ _extract_result (line 632).
508
+
509
+ 2. BARE PAYLOAD (default WS path): answer has no success key and no error
510
+ key at top level. Framework's execute_command_unified (command_api.py
511
+ lines 330-336) unwraps the full envelope by taking answer.get("data"),
512
+ leaving only the service's CommandErrorResult payload dict. Referenced
513
+ at: mcp_proxy_adapter.client.jsonrpc_client.command_api.py:330-336,
514
+ where if isinstance(raw_result, dict) and "data" in raw_result:
515
+ result_value = raw_result.get("data").
516
+
517
+ In both cases, the refusal is recognised by the presence of framework_code
518
+ in the dict (service's CommandErrorResult payload always carries this;
519
+ ailogger/api/command_result.py:142, 145). The stable domain code is read
520
+ from the code position in the payload. The framework's numeric code is
521
+ never used as the domain code.
522
+
523
+ In the full envelope, the human-readable statement is at the top level
524
+ (answer["error"]). In the bare payload, the statement is lost (it was only
525
+ in the envelope's error field, not in the payload dict that gets unwrapped),
526
+ so statement is None on the default path.
527
+
528
+ Args:
529
+ answer: An answer dict from the service. May be None.
530
+ command: The name of the command the answer is for.
531
+
532
+ Returns:
533
+ A ServiceRefusalError with code, statement, and command set, when the
534
+ answer carries the service's refusal shape. None when it does not.
535
+ """
536
+ if answer is None:
537
+ return None
538
+
539
+ if not isinstance(answer, dict):
540
+ return None
541
+
542
+ # Check for both shapes:
543
+ # - Full envelope: top-level success=False with data and error
544
+ # - Bare payload: no success key, just payload with framework_code
545
+
546
+ is_full_envelope: bool = answer.get("success") is False
547
+ is_bare_payload: bool = "success" not in answer
548
+
549
+ if not (is_full_envelope or is_bare_payload):
550
+ return None
551
+
552
+ # Extract the refusal payload dict
553
+ if is_full_envelope:
554
+ data = answer.get("data")
555
+ else:
556
+ # Bare payload case: the answer dict itself is the payload
557
+ data = answer
558
+
559
+ if not isinstance(data, dict):
560
+ return None
561
+
562
+ # Validate refusal structure: must have framework_code to be a real refusal
563
+ # (ordinary data would not have this framework marker)
564
+ if "framework_code" not in data:
565
+ return None
566
+
567
+ # Read the stable domain code from payload position
568
+ code: str | None = None
569
+ code_value = data.get("code")
570
+ if isinstance(code_value, str):
571
+ code = code_value
572
+ elif code_value is not None:
573
+ # Non-string codes (e.g., from framework) are not the domain code
574
+ code = None
575
+
576
+ # Extract the human-readable statement
577
+ # Only present in full envelope (full answer["error"]), not in bare payload
578
+ statement: str | None = None
579
+ if is_full_envelope:
580
+ error_value = answer.get("error")
581
+ if isinstance(error_value, str):
582
+ statement = error_value
583
+
584
+ # Construct and return the refusal
585
+ return ServiceRefusalError(
586
+ code=code, statement=statement, command=command, **_refusal_fields(data)
587
+ )
588
+
589
+
590
+ __all__ = [
591
+ "AiLoggerClientError",
592
+ "CompletionFailedError",
593
+ "CompletionTimeoutError",
594
+ "DialogueFileTransferError",
595
+ "InadmissibleArgumentValueError",
596
+ "MissingArgumentError",
597
+ "QueuedCompletionError",
598
+ "QueuedProtocolError",
599
+ "RequestRejectedError",
600
+ "ResultUnavailableError",
601
+ "SchemaDisagreementError",
602
+ "ServiceRefusalError",
603
+ "ServiceUnreachableError",
604
+ "SyncFallbackWaitExhaustedError",
605
+ "UnknownCommandError",
606
+ "WrongArgumentTypeError",
607
+ "refusal_from_answer",
608
+ "translate_framework_failure",
609
+ "translated_failures",
610
+ ]