local-coding-agent 1.1.0__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.
Files changed (101) hide show
  1. local_coding_agent/__init__.py +314 -0
  2. local_coding_agent/__main__.py +5 -0
  3. local_coding_agent/acp_server/__init__.py +38 -0
  4. local_coding_agent/acp_server/_codec.py +95 -0
  5. local_coding_agent/acp_server/_protocol.py +13 -0
  6. local_coding_agent/acp_server/_server.py +663 -0
  7. local_coding_agent/acp_server/_session.py +24 -0
  8. local_coding_agent/acp_server/_tools.py +126 -0
  9. local_coding_agent/ast_compactor.py +130 -0
  10. local_coding_agent/atomizer.py +77 -0
  11. local_coding_agent/benchmark/__init__.py +57 -0
  12. local_coding_agent/benchmark/_cases.py +307 -0
  13. local_coding_agent/benchmark/_judge.py +175 -0
  14. local_coding_agent/benchmark/_oracles.py +157 -0
  15. local_coding_agent/benchmark/_runner.py +259 -0
  16. local_coding_agent/benchmark/_stats.py +22 -0
  17. local_coding_agent/benchmark/_summary.py +143 -0
  18. local_coding_agent/benchmark_oracle_worker.py +169 -0
  19. local_coding_agent/calibration.py +154 -0
  20. local_coding_agent/capability.py +450 -0
  21. local_coding_agent/chat_compaction.py +276 -0
  22. local_coding_agent/cli/__init__.py +12 -0
  23. local_coding_agent/cli/_dispatch.py +377 -0
  24. local_coding_agent/cli/_handlers.py +320 -0
  25. local_coding_agent/cli/_handlers2.py +725 -0
  26. local_coding_agent/cli/_input.py +61 -0
  27. local_coding_agent/cli/_main.py +159 -0
  28. local_coding_agent/cli/_parser.py +327 -0
  29. local_coding_agent/context_manager.py +155 -0
  30. local_coding_agent/controller/__init__.py +18 -0
  31. local_coding_agent/controller/_constants.py +108 -0
  32. local_coding_agent/controller/_controller.py +1008 -0
  33. local_coding_agent/controller/_post_apply.py +216 -0
  34. local_coding_agent/delegator.py +180 -0
  35. local_coding_agent/desktop/__init__.py +8 -0
  36. local_coding_agent/desktop/app.py +100 -0
  37. local_coding_agent/desktop/assets/lucide.min.js +12 -0
  38. local_coding_agent/desktop/assets/tailwind.css +1 -0
  39. local_coding_agent/desktop/client_js.py +1271 -0
  40. local_coding_agent/desktop/components.py +555 -0
  41. local_coding_agent/desktop/server/__init__.py +37 -0
  42. local_coding_agent/desktop/server/_handlers.py +2178 -0
  43. local_coding_agent/desktop/server/_models.py +204 -0
  44. local_coding_agent/desktop/server/_server.py +347 -0
  45. local_coding_agent/desktop/server/_telemetry.py +87 -0
  46. local_coding_agent/desktop/styles.py +76 -0
  47. local_coding_agent/desktop/ui.py +53 -0
  48. local_coding_agent/doctor.py +413 -0
  49. local_coding_agent/hooks/__init__.py +19 -0
  50. local_coding_agent/hooks/_adapters.py +458 -0
  51. local_coding_agent/hooks/_bridge.py +346 -0
  52. local_coding_agent/hooks/_decision.py +26 -0
  53. local_coding_agent/lsp/__init__.py +60 -0
  54. local_coding_agent/lsp/_client.py +322 -0
  55. local_coding_agent/lsp/_fallback.py +297 -0
  56. local_coding_agent/lsp/_manager.py +173 -0
  57. local_coding_agent/lsp/_mock.py +200 -0
  58. local_coding_agent/lsp/_types.py +398 -0
  59. local_coding_agent/mcp_config.py +398 -0
  60. local_coding_agent/mcp_server.py +377 -0
  61. local_coding_agent/memory.py +140 -0
  62. local_coding_agent/mode_router.py +200 -0
  63. local_coding_agent/model_scanner.py +525 -0
  64. local_coding_agent/monitor.py +635 -0
  65. local_coding_agent/observation_policy.py +184 -0
  66. local_coding_agent/ollama_adapter.py +736 -0
  67. local_coding_agent/plan_mode.py +653 -0
  68. local_coding_agent/prescriptions.py +278 -0
  69. local_coding_agent/profiles.py +239 -0
  70. local_coding_agent/repository_tools.py +782 -0
  71. local_coding_agent/ripgrep.py +326 -0
  72. local_coding_agent/semantic_linter.py +122 -0
  73. local_coding_agent/service.py +405 -0
  74. local_coding_agent/session_events.py +684 -0
  75. local_coding_agent/session_query.py +578 -0
  76. local_coding_agent/skill_config.py +426 -0
  77. local_coding_agent/smoke.py +268 -0
  78. local_coding_agent/speculative_racing.py +85 -0
  79. local_coding_agent/spill.py +258 -0
  80. local_coding_agent/stats.py +255 -0
  81. local_coding_agent/stdio.py +132 -0
  82. local_coding_agent/subagent.py +516 -0
  83. local_coding_agent/task.py +90 -0
  84. local_coding_agent/task_store.py +142 -0
  85. local_coding_agent/tasks.py +240 -0
  86. local_coding_agent/terminal/__init__.py +35 -0
  87. local_coding_agent/terminal/_manager.py +156 -0
  88. local_coding_agent/terminal/_process.py +148 -0
  89. local_coding_agent/terminal/_session.py +452 -0
  90. local_coding_agent/terminal/_tools.py +233 -0
  91. local_coding_agent/terminal/_types.py +60 -0
  92. local_coding_agent/tool_call_parser.py +178 -0
  93. local_coding_agent/validators.py +563 -0
  94. local_coding_agent/vram_fit.py +240 -0
  95. local_coding_agent/worker_pool.py +516 -0
  96. local_coding_agent-1.1.0.dist-info/METADATA +202 -0
  97. local_coding_agent-1.1.0.dist-info/RECORD +101 -0
  98. local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
  99. local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
  100. local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
  101. local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,516 @@
1
+ """Bounded in-memory worker pool for proposal-only delegations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import json
7
+ import math
8
+ import threading
9
+ import time
10
+ import uuid
11
+ from collections import deque
12
+ from dataclasses import asdict, dataclass
13
+ from datetime import datetime, timezone
14
+ from typing import Any, Deque, Mapping
15
+
16
+ from .service import DelegationRequest
17
+ from .task_store import TaskRecord, TaskStore
18
+
19
+
20
+
21
+ _TERMINAL_STATES = frozenset({"completed", "failed", "cancelled", "interrupted"})
22
+
23
+
24
+ def _now_iso() -> str:
25
+ return datetime.now(timezone.utc).isoformat()
26
+
27
+
28
+ class ExecutionOverload(RuntimeError):
29
+ """Raised when the shared runtime admission capacity is full."""
30
+
31
+
32
+ class _ExecutionLease:
33
+ def __init__(self, gate: "SharedExecutionGate") -> None:
34
+ self._gate = gate
35
+ self._released = False
36
+ self._lock = threading.Lock()
37
+
38
+ def release(self) -> None:
39
+ with self._lock:
40
+ if self._released:
41
+ return
42
+ self._released = True
43
+ self._gate._release()
44
+
45
+
46
+ class SharedExecutionGate:
47
+ """Thread-safe admission and active-slot gate shared by all MCP paths."""
48
+
49
+ def __init__(self, max_workers: int, max_queue: int) -> None:
50
+ if max_workers <= 0:
51
+ raise ValueError("max_workers must be positive")
52
+ if max_queue < 0:
53
+ raise ValueError("max_queue must be non-negative")
54
+ self._slots = threading.Semaphore(max_workers)
55
+ self._capacity = max_workers + max_queue
56
+ self._condition = threading.Condition()
57
+ self._admitted = 0
58
+
59
+ def try_acquire(self) -> _ExecutionLease | None:
60
+ with self._condition:
61
+ if self._admitted >= self._capacity:
62
+ return None
63
+ self._admitted += 1
64
+ return _ExecutionLease(self)
65
+
66
+ def run(self, function, *args):
67
+ lease = self.try_acquire()
68
+ if lease is None:
69
+ raise ExecutionOverload("bounded execution queue is full")
70
+ try:
71
+ return self.run_reserved(lease, function, *args)
72
+ finally:
73
+ lease.release()
74
+
75
+ def run_reserved(self, lease: _ExecutionLease, function, *args):
76
+ """Run an already-admitted task while holding a shared active slot."""
77
+
78
+ self._slots.acquire()
79
+ try:
80
+ return function(*args)
81
+ finally:
82
+ self._slots.release()
83
+
84
+ def _release(self) -> None:
85
+ with self._condition:
86
+ self._admitted -= 1
87
+ self._condition.notify_all()
88
+
89
+
90
+ @dataclass
91
+ class _Job:
92
+ job_id: str
93
+ caller_id: str
94
+ request_key: tuple[str, str, str]
95
+ fingerprint: str
96
+ request: DelegationRequest
97
+ state: str = "queued"
98
+ created_at: str = ""
99
+ updated_at: str = ""
100
+ result: dict[str, Any] | None = None
101
+ cancel_event: threading.Event | None = None
102
+ execution_complete: threading.Event | None = None
103
+ completed: threading.Event | None = None
104
+ execution_lease: _ExecutionLease | None = None
105
+
106
+
107
+ class BoundedWorkerPool:
108
+ """Run service delegations with bounded queue and caller-scoped jobs.
109
+
110
+ This is deliberately an in-memory execution primitive. It provides bounded
111
+ overload behavior and cooperative cancellation for a future task adapter,
112
+ but it does not claim durable persistence or MCP Tasks conformance.
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ service: Any,
118
+ *,
119
+ max_workers: int = 1,
120
+ max_queue: int = 16,
121
+ max_completed_jobs: int = 256,
122
+ execution_gate: SharedExecutionGate | None = None,
123
+ task_store: TaskStore | None = None,
124
+ ) -> None:
125
+ if max_workers <= 0:
126
+ raise ValueError("max_workers must be positive")
127
+ if max_queue < 0:
128
+ raise ValueError("max_queue must be non-negative")
129
+ if max_completed_jobs <= 0:
130
+ raise ValueError("max_completed_jobs must be positive")
131
+ self._service = service
132
+ self._max_completed_jobs = max_completed_jobs
133
+ self._condition = threading.Condition()
134
+ self._queue: Deque[str] = deque()
135
+ self._jobs: dict[str, _Job] = {}
136
+ self._idempotency: dict[tuple[str, str, str], str] = {}
137
+ self._max_workers = max_workers
138
+ self._max_queue = max_queue
139
+ self._execution_gate = execution_gate
140
+ self._task_store = task_store
141
+ self._active_workers = 0
142
+ self._stopping = False
143
+ if self._task_store is not None:
144
+ for record in self._task_store.list(limit=self._max_completed_jobs):
145
+ completed_evt = threading.Event()
146
+ completed_evt.set()
147
+ recovered_result = copy.deepcopy(record.result)
148
+ if record.state == "interrupted":
149
+ # The durable store has no request envelope to safely
150
+ # replay after a process restart. Keep the explicit
151
+ # interrupted state, but expose a terminal controller
152
+ # result so pollers cannot wait forever in "working".
153
+ interruption = copy.deepcopy(record.error) or {
154
+ "kind": "process_interrupted",
155
+ "message": "task was interrupted by server restart",
156
+ }
157
+ recovered_result = dict(recovered_result or {})
158
+ recovered_result.update(
159
+ {
160
+ "status": "failed",
161
+ "error": interruption,
162
+ "applied": False,
163
+ }
164
+ )
165
+ job = _Job(
166
+ job_id=record.task_id,
167
+ caller_id=record.caller_id,
168
+ request_key=(record.caller_id, record.workspace_ref, record.request_id),
169
+ fingerprint=record.fingerprint,
170
+ request=None, # type: ignore[arg-type]
171
+ state=record.state,
172
+ created_at=record.created_at,
173
+ updated_at=record.updated_at,
174
+ result=recovered_result,
175
+ cancel_event=threading.Event(),
176
+ execution_complete=threading.Event(),
177
+ completed=completed_evt,
178
+ )
179
+ self._jobs[record.task_id] = job
180
+ self._idempotency[job.request_key] = job.job_id
181
+
182
+ self._workers = [
183
+ threading.Thread(
184
+ target=self._worker_loop,
185
+ name=f"local-agent-worker-{index}",
186
+ daemon=True,
187
+ )
188
+ for index in range(max_workers)
189
+ ]
190
+ for worker in self._workers:
191
+ worker.start()
192
+
193
+ def _persist_job(self, job: _Job) -> None:
194
+ if self._task_store is None:
195
+ return
196
+ try:
197
+ record = TaskRecord(
198
+ task_id=job.job_id,
199
+ caller_id=job.caller_id,
200
+ request_id=job.request.request_id if job.request else job.job_id,
201
+ workspace_ref=job.request.workspace_ref if job.request else "",
202
+ model_profile=job.request.model_profile if job.request else "",
203
+ state=job.state,
204
+ created_at=job.created_at,
205
+ updated_at=job.updated_at,
206
+ result=job.result,
207
+ error=job.result.get("error") if isinstance(job.result, dict) else None,
208
+ fingerprint=job.fingerprint,
209
+ )
210
+ self._task_store.save(record)
211
+ except Exception:
212
+ pass
213
+
214
+ def submit(self, caller_id: str, request: DelegationRequest) -> dict[str, Any]:
215
+ """Queue one proposal-only request or return a bounded policy failure."""
216
+
217
+ if not isinstance(caller_id, str) or not caller_id.strip():
218
+ return self._failure("invalid_caller", "caller_id must be a non-empty string")
219
+ if not isinstance(request, DelegationRequest):
220
+ return self._failure("invalid_request", "request must be a DelegationRequest")
221
+
222
+ request_key = (caller_id, request.workspace_ref, request.request_id)
223
+ fingerprint = self._fingerprint(request)
224
+ with self._condition:
225
+ existing_id = self._idempotency.get(request_key)
226
+ if existing_id is not None:
227
+ existing = self._jobs.get(existing_id)
228
+ if existing is not None:
229
+ if existing.fingerprint and existing.fingerprint != fingerprint:
230
+ return self._failure(
231
+ "idempotency_conflict",
232
+ "request_id was already used with a different request payload",
233
+ )
234
+ return self._snapshot(existing)
235
+ self._idempotency.pop(request_key, None)
236
+
237
+ if self._stopping:
238
+ return self._failure("pool_shutdown", "worker pool is shutting down")
239
+ if self._active_workers + len(self._queue) >= self._max_workers + self._max_queue:
240
+ return self._failure(
241
+ "queue_overload",
242
+ f"worker capacity is full at max_workers={self._max_workers}, "
243
+ f"max_queue={self._max_queue}",
244
+ )
245
+
246
+ execution_lease = self._execution_gate.try_acquire() if self._execution_gate is not None else None
247
+ if self._execution_gate is not None and execution_lease is None:
248
+ return self._failure(
249
+ "queue_overload",
250
+ "shared execution capacity is full",
251
+ )
252
+
253
+ job = _Job(
254
+ job_id=uuid.uuid4().hex,
255
+ caller_id=caller_id,
256
+ request_key=request_key,
257
+ fingerprint=fingerprint,
258
+ request=request,
259
+ created_at=_now_iso(),
260
+ updated_at=_now_iso(),
261
+ cancel_event=threading.Event(),
262
+ execution_complete=threading.Event(),
263
+ completed=threading.Event(),
264
+ execution_lease=execution_lease,
265
+ )
266
+ self._jobs[job.job_id] = job
267
+ self._idempotency[request_key] = job.job_id
268
+ self._queue.append(job.job_id)
269
+ self._persist_job(job)
270
+ self._condition.notify()
271
+ return self._snapshot(job)
272
+
273
+
274
+ def get(self, caller_id: str, job_id: str) -> dict[str, Any]:
275
+ """Return a caller-scoped job snapshot without waiting."""
276
+
277
+ with self._condition:
278
+ job = self._owned_job(caller_id, job_id)
279
+ if job is None:
280
+ return self._failure("unknown_job", "job is unknown or belongs to another caller")
281
+ return self._snapshot(job)
282
+
283
+ def status(self) -> dict[str, Any]:
284
+ """Return a snapshot of worker pool load, queue, and jobs for monitoring."""
285
+ with self._condition:
286
+ jobs_summary = [
287
+ {
288
+ "job_id": job.job_id,
289
+ "caller_id": job.caller_id,
290
+ "state": job.state,
291
+ "created_at": job.created_at,
292
+ "updated_at": job.updated_at,
293
+ "has_result": job.result is not None,
294
+ }
295
+ for job in self._jobs.values()
296
+ ]
297
+ return {
298
+ "active_workers": self._active_workers,
299
+ "max_workers": self._max_workers,
300
+ "queued_jobs": len(self._queue),
301
+ "max_queue": self._max_queue,
302
+ "stopping": self._stopping,
303
+ "total_jobs": len(self._jobs),
304
+ "jobs": jobs_summary,
305
+ }
306
+
307
+
308
+ def wait(self, caller_id: str, job_id: str, *, timeout: float = 5.0) -> dict[str, Any]:
309
+ """Wait at most ``timeout`` seconds, then return the current snapshot."""
310
+
311
+ if not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or timeout < 0:
312
+ return self._failure("invalid_timeout", "timeout must be a finite non-negative number")
313
+ with self._condition:
314
+ job = self._owned_job(caller_id, job_id)
315
+ if job is None:
316
+ return self._failure("unknown_job", "job is unknown or belongs to another caller")
317
+ completed = job.completed
318
+ assert completed is not None
319
+ completed.wait(timeout)
320
+ with self._condition:
321
+ return self._snapshot(job)
322
+
323
+ def cancel(self, caller_id: str, job_id: str) -> dict[str, Any]:
324
+ """Request cooperative cancellation for a queued or running job."""
325
+
326
+ with self._condition:
327
+ job = self._owned_job(caller_id, job_id)
328
+ if job is None:
329
+ return self._failure("unknown_job", "job is unknown or belongs to another caller")
330
+ assert job.cancel_event is not None
331
+ if job.state == "queued":
332
+ try:
333
+ self._queue.remove(job.job_id)
334
+ except ValueError:
335
+ # A worker may have claimed the job between the state check
336
+ # and removal. The worker owns the running transition.
337
+ if job.state != "queued":
338
+ return self._snapshot(job)
339
+ job.cancel_event.set()
340
+ job.state = "cancelled"
341
+ job.updated_at = _now_iso()
342
+ self._release_execution_lease(job)
343
+ self._complete_locked(job)
344
+ self._persist_job(job)
345
+ self._condition.notify_all()
346
+ return self._snapshot(job)
347
+ if job.state == "working":
348
+ job.cancel_event.set()
349
+ snapshot = self._snapshot(job)
350
+ snapshot["cancellation_requested"] = True
351
+ return snapshot
352
+ return self._snapshot(job)
353
+
354
+ def shutdown(self, *, wait: bool = True, timeout: float = 5.0) -> bool:
355
+ """Stop workers and cancel pending work with a bounded join."""
356
+
357
+ if not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or timeout < 0:
358
+ raise ValueError("timeout must be a finite non-negative number")
359
+ with self._condition:
360
+ self._stopping = True
361
+ while self._queue:
362
+ job_id = self._queue.popleft()
363
+ job = self._jobs.get(job_id)
364
+ if job is None or job.state != "queued":
365
+ continue
366
+ assert job.cancel_event is not None
367
+ job.cancel_event.set()
368
+ job.state = "cancelled"
369
+ self._release_execution_lease(job)
370
+ self._complete_locked(job)
371
+ self._persist_job(job)
372
+ for job in self._jobs.values():
373
+ if job.state == "working" and job.cancel_event is not None:
374
+ job.cancel_event.set()
375
+ self._condition.notify_all()
376
+
377
+ if not wait:
378
+ return not any(worker.is_alive() for worker in self._workers)
379
+ deadline = time.monotonic() + timeout
380
+ for worker in self._workers:
381
+ remaining = max(0.0, deadline - time.monotonic())
382
+ worker.join(remaining)
383
+ return not any(worker.is_alive() for worker in self._workers)
384
+
385
+ def _worker_loop(self) -> None:
386
+ while True:
387
+ with self._condition:
388
+ while not self._queue and not self._stopping:
389
+ self._condition.wait()
390
+ if not self._queue and self._stopping:
391
+ return
392
+ job_id = self._queue.popleft()
393
+ job = self._jobs.get(job_id)
394
+ if job is None or job.state == "cancelled":
395
+ if job is not None:
396
+ self._release_execution_lease(job)
397
+ continue
398
+ job.state = "working"
399
+ job.updated_at = _now_iso()
400
+ self._active_workers += 1
401
+ self._persist_job(job)
402
+
403
+ try:
404
+ assert job.cancel_event is not None
405
+ assert job.execution_complete is not None
406
+ if self._execution_gate is not None and job.execution_lease is not None:
407
+ def delegate_and_wait():
408
+ try:
409
+ result = self._service.delegate(
410
+ job.caller_id,
411
+ job.request,
412
+ cancel_event=job.cancel_event,
413
+ completion_event=job.execution_complete,
414
+ )
415
+ finally:
416
+ job.execution_complete.set()
417
+ return result
418
+
419
+ result = self._execution_gate.run_reserved(job.execution_lease, delegate_and_wait)
420
+ else:
421
+ try:
422
+ result = self._service.delegate(
423
+ job.caller_id,
424
+ job.request,
425
+ cancel_event=job.cancel_event,
426
+ completion_event=job.execution_complete,
427
+ )
428
+ finally:
429
+ job.execution_complete.set()
430
+ if not isinstance(result, Mapping):
431
+ raise TypeError("service returned a non-object result")
432
+ result = dict(result)
433
+ except Exception: # noqa: BLE001 - worker boundary must terminate the job.
434
+ result = self._failure("worker_error", "delegation worker failed")
435
+ finally:
436
+ if job.execution_complete is not None:
437
+ job.execution_complete.set()
438
+
439
+
440
+ assert job.execution_complete is not None
441
+ # A service may finish its public call before a cancellable model
442
+ # thread has actually terminated. Keep the worker slot occupied
443
+ # until the service reports that physical execution is complete.
444
+ job.execution_complete.wait()
445
+ self._release_execution_lease(job)
446
+
447
+ with self._condition:
448
+ self._active_workers -= 1
449
+ if job.cancel_event is not None and job.cancel_event.is_set():
450
+ job.state = "cancelled"
451
+ job.result = None
452
+ else:
453
+ job.result = copy.deepcopy(result)
454
+ job.state = "failed" if result.get("status") == "failed" else "completed"
455
+ job.updated_at = _now_iso()
456
+ self._complete_locked(job)
457
+ self._persist_job(job)
458
+ self._condition.notify_all()
459
+
460
+
461
+ def _complete_locked(self, job: _Job) -> None:
462
+ assert job.completed is not None
463
+ job.completed.set()
464
+ self._evict_completed_locked()
465
+
466
+ @staticmethod
467
+ def _release_execution_lease(job: _Job) -> None:
468
+ if job.execution_lease is not None:
469
+ job.execution_lease.release()
470
+ job.execution_lease = None
471
+
472
+ def _evict_completed_locked(self) -> None:
473
+ terminal_jobs = sum(job.state in _TERMINAL_STATES for job in self._jobs.values())
474
+ while terminal_jobs > self._max_completed_jobs:
475
+ candidate_id = next(
476
+ (job_id for job_id, job in self._jobs.items() if job.state in _TERMINAL_STATES),
477
+ None,
478
+ )
479
+ if candidate_id is None:
480
+ return
481
+ candidate = self._jobs.pop(candidate_id)
482
+ if self._idempotency.get(candidate.request_key) == candidate_id:
483
+ self._idempotency.pop(candidate.request_key, None)
484
+ terminal_jobs -= 1
485
+
486
+ def _owned_job(self, caller_id: str, job_id: str) -> _Job | None:
487
+ job = self._jobs.get(job_id)
488
+ if job is None or job.caller_id != caller_id:
489
+ return None
490
+ return job
491
+
492
+ @staticmethod
493
+ def _snapshot(job: _Job) -> dict[str, Any]:
494
+ snapshot: dict[str, Any] = {
495
+ "job_id": job.job_id,
496
+ "status": job.state,
497
+ "created_at": job.created_at,
498
+ "updated_at": job.updated_at,
499
+ }
500
+ if job.state in {"completed", "failed", "interrupted"} and job.result is not None:
501
+ snapshot["result"] = copy.deepcopy(job.result)
502
+ if job.state == "working" and job.cancel_event is not None and job.cancel_event.is_set():
503
+ snapshot["cancellation_requested"] = True
504
+ return snapshot
505
+
506
+ @staticmethod
507
+ def _fingerprint(request: DelegationRequest) -> str:
508
+ return json.dumps(asdict(request), ensure_ascii=False, sort_keys=True, separators=(",", ":"))
509
+
510
+ @staticmethod
511
+ def _failure(kind: str, message: str) -> dict[str, Any]:
512
+ return {
513
+ "status": "failed",
514
+ "error": {"kind": kind, "message": message},
515
+ "applied": False,
516
+ }