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,110 @@
1
+ """
2
+ File transfer methods for dialogue import and export.
3
+
4
+ This module carries the single mixin class that names the two methods a caller
5
+ would use to import a dialogue from a file and to export a dialogue to a file.
6
+ Import and export are not designed yet: neither method does transfer work when
7
+ called. Each is fully declared and fully annotated, and each raises
8
+ NotImplementedError with a message that states plainly that import and export
9
+ are not designed yet and that transfer will be built on the framework client's
10
+ own transfer surface (upload_file, resume_upload, download_file,
11
+ resume_download, execute_command_with_file) when they are.
12
+
13
+ The framework client held privately by the client object carries the complete
14
+ chunked transfer surface. This client wraps that framework client and exposes
15
+ only the dialogue vocabulary of the ai-logger service. But the dialogue surface
16
+ has no command that carries a file - no message carries one and no read returns
17
+ one. Transfer between this client and this service is wanted for exactly one
18
+ purpose: importing a dialogue from a file and exporting a dialogue to a file.
19
+ That purpose is not designed yet, and no step of the plan owns its design.
20
+
21
+ A consumer that reaches for dialogue import or export should meet a definite
22
+ answer rather than a missing attribute error, and the eventual work should have
23
+ a named place already established for it.
24
+
25
+ NETWORK LAW. This module imports no aiohttp, httpx, requests, fastapi, uvicorn
26
+ or standalone websocket client; opens no socket and no file; assemble no
27
+ address, connection, session or security context by hand; and builds no request
28
+ envelope. This module adds no transport of its own: the privately held framework
29
+ client carries all transfer, and nothing here touches it. Every declaration in
30
+ this file is fully annotated for the pipeline's named strict type check, and the
31
+ module satisfies its named lint check.
32
+
33
+ BOUNDARIES. This module declares no typed method for any command of the service
34
+ surface. It does not add a generic call, a completion default, a non-blocking
35
+ path or any job-observing call. It defines no error vocabulary and does not
36
+ fold this refusal into any error type: NotImplementedError is the language's
37
+ own. Import and export are not designed here and remain unimplemented: they
38
+ will be built on the framework client's transfer surface (upload_file,
39
+ download_file, execute_command_with_file, etc.) when design work completes.
40
+ """
41
+
42
+ from __future__ import annotations
43
+
44
+ __all__ = ["FileTransferMixin"]
45
+
46
+
47
+ class FileTransferMixin:
48
+ """Mixin providing methods for dialogue file transfer.
49
+
50
+ Declares the two methods by which a caller would import a dialogue from a
51
+ file and export a dialogue to a file. Import and export are not designed
52
+ yet: each method raises NotImplementedError when called, with a message
53
+ stating plainly that import and export are not designed yet and that
54
+ transfer will be built on the framework client's own transfer surface when
55
+ they are.
56
+
57
+ This mixin does not compose with InvocationMixin and does not touch the
58
+ framework client's transfer surface. When import and export are eventually
59
+ designed, they will be built on that surface rather than on anything in
60
+ this mixin.
61
+ """
62
+
63
+ async def import_dialogue(self, source_path: str) -> None:
64
+ """Import a dialogue from a file.
65
+
66
+ Dialogue import is not designed yet. When called, this method raises
67
+ NotImplementedError. Transfer will be built on the framework client's
68
+ own transfer surface (upload_file, resume_upload,
69
+ execute_command_with_file) when import design completes.
70
+
71
+ Args:
72
+ source_path: Path to the file containing the dialogue to import.
73
+
74
+ Raises:
75
+ NotImplementedError: Dialogue import is not designed yet. Transfer
76
+ will be built on the framework client's transfer surface when
77
+ designed.
78
+ """
79
+ raise NotImplementedError(
80
+ "Dialogue import is not designed yet. Transfer will be built on "
81
+ "the framework client's transfer surface when designed."
82
+ )
83
+
84
+ async def export_dialogue(self, destination_path: str) -> None:
85
+ """Export a dialogue to a file.
86
+
87
+ Dialogue export is not designed yet. When called, this method raises
88
+ NotImplementedError. Transfer will be built on the framework client's
89
+ own transfer surface (download_file, resume_download,
90
+ execute_command_with_file) when export design completes.
91
+
92
+ Args:
93
+ destination_path: Path where the exported dialogue file will be
94
+ written.
95
+
96
+ Raises:
97
+ NotImplementedError: Dialogue export is not designed yet. Transfer
98
+ will be built on the framework client's transfer surface when
99
+ designed.
100
+ """
101
+ raise NotImplementedError(
102
+ "Dialogue export is not designed yet. Transfer will be built on "
103
+ "the framework client's transfer surface when designed."
104
+ )
105
+
106
+
107
+ # TODO(G-012/T-002/A-010): dialogue import/export deliberately deferred -- no
108
+ # step of the plan owns their design yet; build on the framework client's
109
+ # transfer surface (upload_file/download_file/execute_command_with_file) when
110
+ # designed.
@@ -0,0 +1,550 @@
1
+ """
2
+ Invocation entry point for the ai-logger client.
3
+
4
+ This module declares the single seam through which every caller-facing method of
5
+ the client reaches the service. It carries no typed method of any command, only:
6
+
7
+ - A private invocation entry point that takes a command name and a mapping of
8
+ parameters, carries them unchanged to the framework client's own invocation
9
+ surface, and returns what that surface returns.
10
+
11
+ - A public generic call that takes a command name and a mapping of parameters
12
+ and reaches any command of the service, including a command the service
13
+ publishes after this client was released, so a growing surface stays usable
14
+ from an already installed client.
15
+
16
+ The private entry point is a naming, not machinery: request and answer
17
+ serialisation, connection handling, retries, timeouts, OpenAPI schema fetching,
18
+ and schema validation are the framework client's own work and are reached
19
+ through the held instance. No request envelope, no JSON-RPC payload, no address,
20
+ no header set, and no transport concept in any signature, argument or return
21
+ type appears here.
22
+
23
+ NETWORK LAW. This module imports nothing from the adapter framework's
24
+ implementation; it does not reach the framework client except through the single
25
+ private route (_client attribute) and calls only the methods it publishes.
26
+ Connection handling, retries, timeouts, and all other transport concerns remain
27
+ the framework's responsibility.
28
+
29
+ COMPOSITION. This module defines a mixin class, intended to be composed into
30
+ the AiLoggerClient so the typed methods added by the plan's later steps reach
31
+ the service through the single invocation entry point here.
32
+
33
+ JOB OBSERVATION. When a command is executed with wait_for_result=False and the
34
+ service queues the command, the client returns a job identity envelope. The
35
+ caller may then use the job observation methods (get_job_status, get_job_messages,
36
+ cancel_command) to track the job's progress. These methods are not typed commands
37
+ of the service; they are observation surfaces over the framework client's own
38
+ job-tracking API and are not counted in the client-surface-agreement comparison.
39
+
40
+ A-007 SECTION: Guarded Seam and Refusal Recognition
41
+
42
+ Every route through which this module reaches the privately held framework
43
+ client is wrapped in the reusable translation guard (translated_failures) from
44
+ G-012/T-003/A-002. The guard catches framework ClientError, translates it to
45
+ this client's vocabulary via translate_framework_failure, and raises it with the
46
+ original as its cause. Plain programming errors (ValueError, RuntimeError) and
47
+ the NotImplementedError from undesigned file-transfer methods propagate
48
+ unchanged.
49
+
50
+ The answer-borne refusal recogniser (refusal_from_answer) is applied exactly
51
+ once: in _invoke, on the completed answer (the default wait_for_result=True
52
+ path), immediately before the return, so a refusal carried intact over a working
53
+ transport becomes the same service-refusal kind as one that arrives as a raised
54
+ failure. The non-blocking queued envelope and the job-observing calls' answers
55
+ are not passed through the recogniser. Both routes (raised and answer-borne)
56
+ end in the same ServiceRefusalError kind.
57
+ """
58
+
59
+ from __future__ import annotations
60
+
61
+ import asyncio
62
+ import time
63
+ from collections.abc import Mapping
64
+ from typing import Any, Final, Protocol
65
+
66
+ from .config import SYNC_FALLBACK_WAIT_SECONDS
67
+ from .errors import (
68
+ CompletionFailedError,
69
+ ResultUnavailableError,
70
+ SyncFallbackWaitExhaustedError,
71
+ refusal_from_answer,
72
+ translated_failures,
73
+ )
74
+
75
+ __all__ = ["InvocationMixin", "is_sync_timeout_fallback"]
76
+
77
+ #: How often the client asks the queue for the status of a followed job, in
78
+ #: seconds.
79
+ _FALLBACK_POLL_SECONDS: Final[float] = 1.0
80
+
81
+ #: Job-status values that end a job without a usable result.
82
+ _FALLBACK_FAILED_STATUSES: Final[frozenset[str]] = frozenset(
83
+ {"failed", "error", "timeout", "stopped", "cancelled"}
84
+ )
85
+
86
+ #: Job-status values that end a job with its result.
87
+ _FALLBACK_DONE_STATUSES: Final[frozenset[str]] = frozenset({"completed"})
88
+
89
+
90
+ def is_sync_timeout_fallback(answer: object) -> bool:
91
+ """
92
+ Whether ``answer`` is the service's sync-timeout queue fallback: the
93
+ acknowledgement of a command that overran the synchronous cap, carrying a job
94
+ identity and ``queued_after_timeout`` true instead of the command's result.
95
+ """
96
+ if not isinstance(answer, Mapping):
97
+ return False
98
+ job_id = answer.get("job_id")
99
+ return (
100
+ answer.get("queued_after_timeout") is True
101
+ and isinstance(job_id, str)
102
+ and bool(job_id.strip())
103
+ )
104
+
105
+
106
+ def _job_answer(outcome: object) -> dict[str, Any] | None:
107
+ """
108
+ The command's own answer inside a finished job's ``result``: the envelope the
109
+ job's command returned (``result.result``), or the result itself when it is
110
+ already an envelope. ``None`` when the job carries none.
111
+ """
112
+ if not isinstance(outcome, Mapping):
113
+ return None
114
+ inner = outcome.get("result")
115
+ if isinstance(inner, Mapping):
116
+ return dict(inner)
117
+ if "success" in outcome:
118
+ return dict(outcome)
119
+ return None
120
+
121
+
122
+ class _FrameworkClientProtocol(Protocol):
123
+ """Protocol for the type of the held framework client instance.
124
+
125
+ Defines the minimal interface this mixin expects from the held _client
126
+ attribute, allowing strict type checking without importing the concrete
127
+ JsonRpcClient class.
128
+ """
129
+
130
+ async def execute_command_unified(
131
+ self,
132
+ command: str,
133
+ params: dict[str, Any] | None = None,
134
+ *,
135
+ expect_queue: bool | None = None,
136
+ auto_poll: bool = True,
137
+ timeout: float | None = None,
138
+ ) -> Any:
139
+ """Execute a command, transparently handling queued or immediate responses.
140
+
141
+ When the service answers by queueing, this method waits for the job to
142
+ reach a terminal state via the framework's WebSocket facilities and
143
+ returns the finished answer. When the service answers directly, it
144
+ returns the answer immediately. To the caller, both paths are
145
+ indistinguishable: the return value in both cases is the finished
146
+ command result.
147
+
148
+ Args:
149
+ command: Name of the command to execute.
150
+ params: Optional mapping of command parameters.
151
+ auto_poll: When True (default), wait for queued jobs to completion.
152
+ timeout: Maximum seconds to wait for a queued job. Defaults to
153
+ the framework's configured timeout.
154
+
155
+ Returns:
156
+ A dictionary with "mode" and "result" keys. The "mode" field
157
+ indicates whether the response was "immediate" or "queued"; the
158
+ "result" field holds the finished command result.
159
+ """
160
+ ...
161
+
162
+ async def get_job_status(
163
+ self,
164
+ job_id: str,
165
+ *,
166
+ sync_from_queue: bool = False,
167
+ ) -> Any:
168
+ """Return the status snapshot for a job.
169
+
170
+ Args:
171
+ job_id: The job identifier.
172
+ sync_from_queue: When True, fetch status from the server queue
173
+ and merge it with locally recorded events.
174
+
175
+ Returns:
176
+ A job status snapshot with normalized fields.
177
+ """
178
+ ...
179
+
180
+ async def get_job_messages(
181
+ self,
182
+ job_id: str,
183
+ *,
184
+ limit: int | None = None,
185
+ ) -> list[dict[str, Any]]:
186
+ """Return the progress messages recorded for a job.
187
+
188
+ Args:
189
+ job_id: The job identifier.
190
+ limit: Maximum number of recent messages to return.
191
+
192
+ Returns:
193
+ A list of message dictionaries (oldest first).
194
+ """
195
+ ...
196
+
197
+ async def queue_get_job_status(self, job_id: str) -> dict[str, Any]:
198
+ """Return the raw ``queue_get_job_status`` answer of the service.
199
+
200
+ Args:
201
+ job_id: The job identifier.
202
+
203
+ Returns:
204
+ The queue's answer: ``{"success": ..., "data": {"status": ...,
205
+ "result": ...}}``.
206
+ """
207
+ ...
208
+
209
+ async def cancel_command(
210
+ self,
211
+ job_id: str,
212
+ ) -> dict[str, Any]:
213
+ """Cancel a queued command execution.
214
+
215
+ Args:
216
+ job_id: The job identifier.
217
+
218
+ Returns:
219
+ The result of the cancellation operation.
220
+ """
221
+ ...
222
+
223
+
224
+ class InvocationMixin:
225
+ """Mixin providing the single invocation seam for the ai-logger client.
226
+
227
+ Declares the private invocation entry point through which all typed methods
228
+ reach the service, and the public generic call for commands not yet named.
229
+
230
+ The held _client attribute (an instance of the adapter framework's
231
+ JsonRpcClient) is accessed only through the private invocation entry point,
232
+ so the service is reached through exactly one place in the client codebase.
233
+ """
234
+
235
+ # _client is declared here for type annotation purposes only.
236
+ # At runtime, _client is set by AiLoggerClient.__init__ to an instance
237
+ # of mcp_proxy_adapter.client.jsonrpc_client.JsonRpcClient.
238
+ _client: _FrameworkClientProtocol
239
+
240
+ # How long a command that overran the service's sync cap is followed to the
241
+ # end of its queued job; AiLoggerClient.__init__ replaces it with the
242
+ # configured ``client.sync_fallback_wait_seconds``.
243
+ _sync_fallback_wait_seconds: float = SYNC_FALLBACK_WAIT_SECONDS
244
+
245
+ async def _invoke(
246
+ self,
247
+ command: str,
248
+ params: dict[str, Any] | None = None,
249
+ *,
250
+ wait_for_result: bool = True,
251
+ ) -> Any:
252
+ """Private invocation entry point.
253
+
254
+ Forwards the command name and parameters to the held framework
255
+ client's execute_command_unified method with ``expect_queue=False``.
256
+ Every ai-logger command is declared direct, so this selects the
257
+ adapter's HTTP JSON-RPC path and avoids opening a WebSocket command
258
+ session before the request. The caller receives the direct command
259
+ result without any job identity or status.
260
+
261
+ The ``wait_for_result`` argument remains part of the public client
262
+ surface for compatibility, but direct commands do not queue and return
263
+ their result on either path.
264
+
265
+ This is the single seam through which every typed method of the
266
+ client reaches the service.
267
+
268
+ Every call to the framework client is wrapped in the reusable
269
+ translation guard (A-007): a ClientError from the framework is
270
+ translated to this client's vocabulary and raised with the original
271
+ as its cause. Plain programming errors propagate unchanged.
272
+
273
+ SYNC-TIMEOUT FALLBACK. A command that overruns the service's sync cap
274
+ is answered with the acknowledgement of a detached job (``job_id``,
275
+ ``queued_after_timeout`` true) instead of its result. The client then
276
+ follows that job with ``queue_get_job_status`` until it ends, within
277
+ ``client.sync_fallback_wait_seconds``, and continues with the answer
278
+ the job finished with, so the caller sees what an inline call returns.
279
+ The command is dispatched once. When the wait runs out,
280
+ ``SyncFallbackWaitExhaustedError`` (with the ``job_id``) is raised.
281
+
282
+ The answer-borne refusal recogniser is applied exactly once, on the
283
+ completed answer (the default wait_for_result=True path), immediately
284
+ before the return (A-007): a refusal carried in the answer dict is
285
+ recognised and converted to ServiceRefusalError, matching the path
286
+ where a refusal is raised as a translated framework failure.
287
+
288
+ Args:
289
+ command: The name of the command to execute.
290
+ params: An optional mapping of command parameters. If None, passed
291
+ as None to the framework client, which treats it as an empty
292
+ mapping.
293
+ wait_for_result: Retained for the public client API. ai-logger
294
+ commands are direct, so it does not change their result.
295
+
296
+ Returns:
297
+ The direct result returned by the service.
298
+
299
+ Raises:
300
+ ServiceRefusalError: When the service or transport indicates refusal.
301
+ ServiceUnreachableError: When the service cannot be reached.
302
+ Any error subclassing AiLoggerClientError: From other failures of
303
+ the service or transport, translated from framework ClientError.
304
+ RuntimeError or ValueError: Propagated unchanged if raised by the
305
+ client itself or the caller's code.
306
+ """
307
+ result = await self._invoke_unconverted(command, params, wait_for_result)
308
+
309
+ # A command that overran the service's sync cap is answered with the
310
+ # acknowledgement of a detached job instead of its result: follow that
311
+ # job (the command is not dispatched again) and carry on with the
312
+ # command's own answer exactly as if it had been given inline.
313
+ if is_sync_timeout_fallback(result):
314
+ result = await self._follow_sync_fallback(result, command)
315
+
316
+ # Apply the refusal recogniser exactly once, on the completed answer.
317
+ # Both the raised route (framework ClientRequestError translated to
318
+ # ServiceRefusalError) and the answer-borne route (refusal dict
319
+ # carrying framework_code) end in the same kind.
320
+ refusal = refusal_from_answer(result, command)
321
+ if refusal is not None:
322
+ raise refusal
323
+
324
+ return result
325
+
326
+ async def _follow_sync_fallback(
327
+ self, acknowledgement: Mapping[str, Any], command: str
328
+ ) -> Any:
329
+ """The command's own answer, taken from the job a sync timeout queued.
330
+
331
+ The service answered ``command`` with the sync-timeout acknowledgement:
332
+ the command was dispatched once and runs on as a job. Poll that job
333
+ through ``queue_get_job_status`` until it ends and return the answer it
334
+ finished with - the command's ``{success, data}`` envelope, from
335
+ ``data.result.result`` of the status answer. A refusing envelope is
336
+ returned as it is, so the caller's refusal recogniser turns it into the
337
+ same ``ServiceRefusalError`` the inline answer would have raised.
338
+
339
+ Raises:
340
+ SyncFallbackWaitExhaustedError: When the job does not end within
341
+ ``client.sync_fallback_wait_seconds``; carries the job id.
342
+ CompletionFailedError: When the job ends failed without an answer.
343
+ ResultUnavailableError: When the job ends without an answer.
344
+ """
345
+ job_id = str(acknowledgement["job_id"]).strip()
346
+ limit = self._sync_fallback_wait_seconds
347
+ deadline = time.monotonic() + limit
348
+ while True:
349
+ with translated_failures():
350
+ raw = await self._client.queue_get_job_status(job_id)
351
+ data = raw.get("data") if isinstance(raw, Mapping) else None
352
+ status = ""
353
+ if isinstance(data, Mapping):
354
+ status = str(data.get("status") or "").lower()
355
+ if status in _FALLBACK_DONE_STATUSES or status in _FALLBACK_FAILED_STATUSES:
356
+ assert isinstance(data, Mapping)
357
+ answer = _job_answer(data.get("result"))
358
+ if answer is not None:
359
+ return answer
360
+ if status in _FALLBACK_FAILED_STATUSES:
361
+ raise CompletionFailedError(
362
+ f"{command} job {job_id} ended {status!r} without an answer"
363
+ )
364
+ raise ResultUnavailableError(
365
+ f"{command} job {job_id} ended {status!r} without an answer"
366
+ )
367
+ remaining = deadline - time.monotonic()
368
+ if remaining <= 0:
369
+ raise SyncFallbackWaitExhaustedError(
370
+ f"{command} exceeded the service's sync timeout and its job "
371
+ f"{job_id} did not finish within {limit:g} seconds "
372
+ "(client.sync_fallback_wait_seconds)",
373
+ job_id,
374
+ )
375
+ await asyncio.sleep(min(_FALLBACK_POLL_SECONDS, remaining))
376
+
377
+ async def _invoke_unconverted(
378
+ self,
379
+ command: str,
380
+ params: dict[str, Any] | None,
381
+ wait_for_result: bool,
382
+ ) -> Any:
383
+ """The answer of one command as the service gave it, refusals unconverted.
384
+
385
+ Everything ``_invoke`` does except recognising an answer-borne refusal:
386
+ the request goes out through the translation guard (a framework failure
387
+ arrives as a typed error of this client) and the command's answer comes
388
+ back as it was, a refusal answer included. Used by the observation
389
+ methods that classify an answer rather than act on it.
390
+ """
391
+ with translated_failures():
392
+ response: dict[str, Any] = await self._client.execute_command_unified(
393
+ command,
394
+ params,
395
+ expect_queue=False,
396
+ auto_poll=wait_for_result,
397
+ )
398
+
399
+ # The direct execution path returns the command answer in ``result``.
400
+ return response.get("result")
401
+
402
+ async def call(
403
+ self,
404
+ command: str,
405
+ params: dict[str, Any] | None = None,
406
+ *,
407
+ wait_for_result: bool = True,
408
+ ) -> Any:
409
+ """Execute any command the service publishes.
410
+
411
+ A generic invocation interface for commands not yet named in this
412
+ client. Reaches the service through the private invocation entry
413
+ point. The command name and parameters are carried unchanged.
414
+
415
+ This is an escape hatch, allowing an already-installed client to
416
+ reach commands published after the client was released, without
417
+ waiting for a new client release. The client-surface-agreement check
418
+ owned by the plan counts this method on neither side of its comparison,
419
+ as it is not a typed method of any command and does not commit the
420
+ client to any specific command name or contract.
421
+
422
+ Args:
423
+ command: The name of the command to execute. The service need not
424
+ have published a typed method for this command; any command
425
+ the service publishes can be reached.
426
+ params: An optional mapping of command parameters.
427
+ wait_for_result: When True (default), wait for queued commands to
428
+ completion and return the finished result. When False, return
429
+ the job identity if queued, or the direct result if immediate.
430
+
431
+ Returns:
432
+ When wait_for_result is True: the result returned by the service.
433
+
434
+ When wait_for_result is False: the job identity envelope if the
435
+ command was queued, or the direct result if immediate.
436
+ """
437
+ return await self._invoke(command, params, wait_for_result=wait_for_result)
438
+
439
+ async def get_job_status(
440
+ self,
441
+ job_id: str,
442
+ *,
443
+ sync_from_queue: bool = False,
444
+ ) -> Any:
445
+ """Get the status of a queued command.
446
+
447
+ Delegates to the framework client's job-status observation surface to
448
+ return the current snapshot of a job's execution state. This method
449
+ is not a command of the service; it is an observation surface over
450
+ the framework's own job-tracking API and is not counted in the
451
+ client-surface-agreement comparison.
452
+
453
+ The call to the framework client is wrapped in the reusable
454
+ translation guard (A-007): a ClientError from the framework is
455
+ translated to this client's vocabulary and raised with the original
456
+ as its cause.
457
+
458
+ Args:
459
+ job_id: The job identifier, typically returned by a non-blocking
460
+ command execution (when wait_for_result=False).
461
+ sync_from_queue: When True, fetch the latest status from the
462
+ server queue and merge it with locally recorded events. When
463
+ False (default), use only events recorded locally.
464
+
465
+ Returns:
466
+ A job status snapshot with normalized fields from the framework
467
+ client's tracking API.
468
+
469
+ Raises:
470
+ Any error subclassing AiLoggerClientError: From failures of
471
+ the transport, translated from framework ClientError.
472
+ RuntimeError or ValueError: Propagated unchanged if raised by the
473
+ client itself.
474
+ """
475
+ with translated_failures():
476
+ return await self._client.get_job_status(
477
+ job_id, sync_from_queue=sync_from_queue
478
+ )
479
+
480
+ async def get_job_messages(
481
+ self,
482
+ job_id: str,
483
+ *,
484
+ limit: int | None = None,
485
+ ) -> list[dict[str, Any]]:
486
+ """Get progress messages for a queued command.
487
+
488
+ Delegates to the framework client's job-message observation surface to
489
+ return the bounded recent history of progress messages emitted by a
490
+ job. This method is not a command of the service; it is an observation
491
+ surface over the framework's own job-tracking API and is not counted
492
+ in the client-surface-agreement comparison.
493
+
494
+ The call to the framework client is wrapped in the reusable
495
+ translation guard (A-007): a ClientError from the framework is
496
+ translated to this client's vocabulary and raised with the original
497
+ as its cause.
498
+
499
+ Args:
500
+ job_id: The job identifier, typically returned by a non-blocking
501
+ command execution (when wait_for_result=False).
502
+ limit: Maximum number of recent messages to return. If None, the
503
+ framework client determines the default limit.
504
+
505
+ Returns:
506
+ A list of message dictionaries in chronological order (oldest
507
+ first), each containing the fields published by the service or
508
+ framework for that message.
509
+
510
+ Raises:
511
+ Any error subclassing AiLoggerClientError: From failures of
512
+ the transport, translated from framework ClientError.
513
+ RuntimeError or ValueError: Propagated unchanged if raised by the
514
+ client itself.
515
+ """
516
+ with translated_failures():
517
+ return await self._client.get_job_messages(job_id, limit=limit)
518
+
519
+ async def cancel_command(
520
+ self,
521
+ job_id: str,
522
+ ) -> dict[str, Any]:
523
+ """Cancel a queued command execution.
524
+
525
+ Delegates to the framework client's cancellation surface to stop and
526
+ remove a queued job. This method is not a command of the service; it
527
+ is an observation surface over the framework's own job-tracking API
528
+ and is not counted in the client-surface-agreement comparison.
529
+
530
+ The call to the framework client is wrapped in the reusable
531
+ translation guard (A-007): a ClientError from the framework is
532
+ translated to this client's vocabulary and raised with the original
533
+ as its cause.
534
+
535
+ Args:
536
+ job_id: The job identifier, typically returned by a non-blocking
537
+ command execution (when wait_for_result=False).
538
+
539
+ Returns:
540
+ The result dictionary from the framework client's cancellation
541
+ operation.
542
+
543
+ Raises:
544
+ Any error subclassing AiLoggerClientError: From failures of
545
+ the transport, translated from framework ClientError.
546
+ RuntimeError or ValueError: Propagated unchanged if raised by the
547
+ client itself.
548
+ """
549
+ with translated_failures():
550
+ return await self._client.cancel_command(job_id)