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.
- ailogger_client/__init__.py +96 -0
- ailogger_client/client.py +220 -0
- ailogger_client/config.py +386 -0
- ailogger_client/errors.py +610 -0
- ailogger_client/file_transfer.py +110 -0
- ailogger_client/invocation.py +550 -0
- ailogger_client/message_methods.py +332 -0
- ailogger_client/observation_methods.py +195 -0
- ailogger_client/py.typed +0 -0
- ailogger_client/queued_methods.py +247 -0
- ailogger_client/read_methods.py +585 -0
- ailogger_client/retrieval_methods.py +314 -0
- ailogger_client/service_methods.py +148 -0
- ailogger_client/session_methods.py +236 -0
- ailogger_client_dev-0.1.51.dist-info/METADATA +108 -0
- ailogger_client_dev-0.1.51.dist-info/RECORD +18 -0
- ailogger_client_dev-0.1.51.dist-info/WHEEL +5 -0
- ailogger_client_dev-0.1.51.dist-info/top_level.txt +1 -0
|
@@ -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
|
+
]
|