@openlucaskaka/kagent 0.1.7

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 (117) hide show
  1. package/README.md +353 -0
  2. package/npm/bin/kagent-serve.js +6 -0
  3. package/npm/bin/kagent.js +6 -0
  4. package/npm/lib/App.js +524 -0
  5. package/npm/lib/app-state.js +224 -0
  6. package/npm/lib/approval-choice.js +25 -0
  7. package/npm/lib/commands.js +59 -0
  8. package/npm/lib/editor.js +188 -0
  9. package/npm/lib/ink-runner.js +41 -0
  10. package/npm/lib/kagent-home.js +39 -0
  11. package/npm/lib/launcher.js +221 -0
  12. package/npm/lib/protocol.js +33 -0
  13. package/npm/lib/provider-setup.js +139 -0
  14. package/npm/lib/python-runner.js +892 -0
  15. package/npm/lib/runtime-client.js +390 -0
  16. package/npm/lib/terminal-input.js +127 -0
  17. package/npm/lib/terminal-text.js +19 -0
  18. package/npm/lib/terminal-width.js +14 -0
  19. package/npm/lib/transcript.js +227 -0
  20. package/npm/lib/ui-components.js +247 -0
  21. package/npm/lib/update-manager.js +334 -0
  22. package/package.json +39 -0
  23. package/pyproject.toml +55 -0
  24. package/src/kagent/__init__.py +90 -0
  25. package/src/kagent/cli/__init__.py +5 -0
  26. package/src/kagent/cli/__main__.py +6 -0
  27. package/src/kagent/cli/commands.py +112 -0
  28. package/src/kagent/cli/conversation.py +127 -0
  29. package/src/kagent/cli/interactive.py +841 -0
  30. package/src/kagent/cli/main.py +685 -0
  31. package/src/kagent/cli/memory.py +460 -0
  32. package/src/kagent/cli/pending_approval.py +169 -0
  33. package/src/kagent/cli/provider.py +27 -0
  34. package/src/kagent/cli/session_commands.py +401 -0
  35. package/src/kagent/cli/stdio_runtime.py +784 -0
  36. package/src/kagent/cli/trace.py +67 -0
  37. package/src/kagent/cli/ui.py +931 -0
  38. package/src/kagent/core/__init__.py +0 -0
  39. package/src/kagent/core/agent.py +296 -0
  40. package/src/kagent/core/faults.py +11 -0
  41. package/src/kagent/core/invariants.py +47 -0
  42. package/src/kagent/core/normalization.py +81 -0
  43. package/src/kagent/core/planning.py +48 -0
  44. package/src/kagent/core/state.py +73 -0
  45. package/src/kagent/core/summary.py +64 -0
  46. package/src/kagent/core/tools.py +196 -0
  47. package/src/kagent/core/trace.py +57 -0
  48. package/src/kagent/eval/__init__.py +11 -0
  49. package/src/kagent/eval/cases.py +229 -0
  50. package/src/kagent/eval/evaluator.py +216 -0
  51. package/src/kagent/integrations/__init__.py +3 -0
  52. package/src/kagent/integrations/audit.py +95 -0
  53. package/src/kagent/integrations/backends.py +131 -0
  54. package/src/kagent/integrations/memory.py +301 -0
  55. package/src/kagent/ops/__init__.py +0 -0
  56. package/src/kagent/ops/batch.py +156 -0
  57. package/src/kagent/ops/doctor.py +255 -0
  58. package/src/kagent/ops/metrics.py +214 -0
  59. package/src/kagent/ops/release_evidence.py +877 -0
  60. package/src/kagent/ops/release_manifest.py +142 -0
  61. package/src/kagent/ops/trace_replay.py +285 -0
  62. package/src/kagent/providers/__init__.py +7 -0
  63. package/src/kagent/providers/embeddings.py +187 -0
  64. package/src/kagent/providers/llm.py +770 -0
  65. package/src/kagent/py.typed +1 -0
  66. package/src/kagent/runtime/__init__.py +28 -0
  67. package/src/kagent/runtime/action_graph.py +543 -0
  68. package/src/kagent/runtime/agent.py +2089 -0
  69. package/src/kagent/runtime/approval.py +64 -0
  70. package/src/kagent/runtime/cancellation.py +64 -0
  71. package/src/kagent/runtime/checkpoint_state.py +146 -0
  72. package/src/kagent/runtime/checkpoint_storage.py +270 -0
  73. package/src/kagent/runtime/context.py +65 -0
  74. package/src/kagent/runtime/file_transaction.py +195 -0
  75. package/src/kagent/runtime/hooks.py +74 -0
  76. package/src/kagent/runtime/metadata.py +116 -0
  77. package/src/kagent/runtime/patch_checkpoints.py +385 -0
  78. package/src/kagent/runtime/policy.py +50 -0
  79. package/src/kagent/runtime/presentation.py +205 -0
  80. package/src/kagent/runtime/redaction.py +130 -0
  81. package/src/kagent/runtime/sandbox.py +331 -0
  82. package/src/kagent/runtime/skills.py +66 -0
  83. package/src/kagent/runtime/steering.py +56 -0
  84. package/src/kagent/runtime/steps.py +255 -0
  85. package/src/kagent/runtime/task_state.py +40 -0
  86. package/src/kagent/runtime/tools.py +3532 -0
  87. package/src/kagent/runtime/types.py +240 -0
  88. package/src/kagent/runtime/workspace.py +597 -0
  89. package/src/kagent/service/__init__.py +26 -0
  90. package/src/kagent/service/__main__.py +6 -0
  91. package/src/kagent/service/active_runs.py +178 -0
  92. package/src/kagent/service/cli.py +737 -0
  93. package/src/kagent/service/contract.py +2571 -0
  94. package/src/kagent/service/errors.py +71 -0
  95. package/src/kagent/service/idempotency.py +584 -0
  96. package/src/kagent/service/router.py +884 -0
  97. package/src/kagent/service/run.py +150 -0
  98. package/src/kagent/service/runtime.py +1915 -0
  99. package/src/kagent/service/runtime_approval.py +20 -0
  100. package/src/kagent/service/runtime_cancel.py +192 -0
  101. package/src/kagent/service/runtime_lifecycle.py +229 -0
  102. package/src/kagent/service/runtime_metadata.py +21 -0
  103. package/src/kagent/service/runtime_policy.py +115 -0
  104. package/src/kagent/service/runtime_recovery.py +573 -0
  105. package/src/kagent/service/runtime_resume.py +382 -0
  106. package/src/kagent/service/runtime_resume_claim.py +116 -0
  107. package/src/kagent/service/runtime_run.py +350 -0
  108. package/src/kagent/service/runtime_status.py +2007 -0
  109. package/src/kagent/service/safety.py +139 -0
  110. package/src/kagent/service/server.py +114 -0
  111. package/src/kagent/service/status.py +233 -0
  112. package/src/kagent/service/trace_store.py +322 -0
  113. package/src/kagent/service/transport.py +24 -0
  114. package/src/kagent/utils/__init__.py +0 -0
  115. package/src/kagent/utils/config_validation.py +21 -0
  116. package/src/kagent/utils/json_output.py +23 -0
  117. package/src/kagent/utils/paths.py +473 -0
@@ -0,0 +1,382 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from concurrent.futures import TimeoutError
5
+ from datetime import datetime, timezone
6
+ from typing import Any, Dict, Tuple
7
+ from uuid import uuid4
8
+
9
+ from kagent.integrations.audit import KafkaRestProgressEventSink
10
+ from kagent.providers.llm import FakeLLMProvider
11
+ from kagent.runtime import (
12
+ RuntimeCancellationToken,
13
+ build_resumable_plan,
14
+ run_runtime_agent,
15
+ )
16
+ from kagent.runtime.policy import RuntimePolicy
17
+ from kagent.service import errors as service_errors
18
+ from kagent.service.active_runs import ActiveRunRegistry, ExecutionSlotLease
19
+ from kagent.service.errors import failure_payload
20
+ from kagent.service.run import run_with_timeout
21
+ from kagent.service.runtime import ServiceConfig
22
+ from kagent.service.runtime_approval import (
23
+ validate_approved_action_ids,
24
+ )
25
+ from kagent.service.runtime_lifecycle import (
26
+ persist_cancelled_runtime_trace,
27
+ persist_failed_runtime_trace,
28
+ persist_runtime_worker_result,
29
+ persisted_runtime_cancellation_probe,
30
+ running_runtime_trace,
31
+ )
32
+ from kagent.service.runtime_resume_claim import (
33
+ RuntimeResumeClaimConflict,
34
+ claim_runtime_resume,
35
+ complete_runtime_resume_claim,
36
+ release_runtime_resume_claim,
37
+ )
38
+ from kagent.service.runtime_status import is_runtime_trace
39
+ from kagent.service.trace_store import (
40
+ load_trace_by_run_id,
41
+ persist_trace,
42
+ )
43
+ from kagent.utils.json_output import json_ready
44
+
45
+ _TRACE_READ_ERRORS = (OSError, ValueError)
46
+
47
+
48
+ def execute_runtime_resume_request(
49
+ body: bytes,
50
+ service_config: ServiceConfig,
51
+ auth_subject: str = "",
52
+ *,
53
+ request_auth_is_admin: bool = False,
54
+ active_run_registry: ActiveRunRegistry | None = None,
55
+ execution_slot_lease: ExecutionSlotLease | None = None,
56
+ ) -> Tuple[int, Dict[str, Any]]:
57
+ try:
58
+ payload = json.loads(body.decode("utf-8"))
59
+ except (UnicodeDecodeError, json.JSONDecodeError) as exc:
60
+ return 400, failure_payload(service_errors.INVALID_JSON, f"invalid JSON: {exc}")
61
+ if not isinstance(payload, dict):
62
+ return 400, failure_payload(
63
+ service_errors.INVALID_REQUEST_BODY,
64
+ "request body must be a JSON object",
65
+ )
66
+ if not service_config.trace_dir:
67
+ return 400, failure_payload(
68
+ service_errors.INVALID_AGENT_CONFIG,
69
+ "trace_dir is required for runtime resume",
70
+ )
71
+ run_id = str(payload.get("run_id", ""))
72
+ if not run_id.strip():
73
+ return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, "run_id is required")
74
+ max_iterations = payload.get("max_iterations", 1)
75
+ if (
76
+ not isinstance(max_iterations, int)
77
+ or isinstance(max_iterations, bool)
78
+ or max_iterations < 1
79
+ ):
80
+ return 400, failure_payload(
81
+ service_errors.INVALID_REQUEST_BODY,
82
+ "max_iterations must be an integer greater than or equal to 1",
83
+ )
84
+ if max_iterations > service_config.runtime_max_iterations:
85
+ return 400, failure_payload(
86
+ service_errors.INVALID_REQUEST_BODY,
87
+ "max_iterations exceeds runtime_max_iterations",
88
+ )
89
+ approved_action_ids_payload = payload.get("approved_action_ids", [])
90
+ approved_action_ids, approval_error = validate_approved_action_ids(
91
+ approved_action_ids_payload
92
+ )
93
+ if approval_error:
94
+ return 400, failure_payload(
95
+ service_errors.INVALID_REQUEST_BODY,
96
+ approval_error,
97
+ )
98
+
99
+ try:
100
+ previous_trace = load_trace_by_run_id(run_id, service_config.trace_dir)
101
+ except _TRACE_READ_ERRORS:
102
+ return 500, failure_payload(
103
+ service_errors.TRACE_READ_FAILED,
104
+ "runtime run trace could not be read",
105
+ )
106
+ if previous_trace is None or not is_runtime_trace(previous_trace):
107
+ return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
108
+ previous_auth_subject = str(previous_trace.get("auth_subject", ""))
109
+ owner_auth_subject = previous_auth_subject or auth_subject
110
+ if (
111
+ auth_subject
112
+ and not request_auth_is_admin
113
+ and owner_auth_subject != auth_subject
114
+ ):
115
+ return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
116
+ previous_status = str(previous_trace.get("status", ""))
117
+ if previous_status in {"resuming", "resumed"}:
118
+ return 409, failure_payload(
119
+ service_errors.INVALID_REQUEST_BODY,
120
+ "runtime run approval is already being resumed",
121
+ )
122
+ if previous_status != "requires_approval" or not isinstance(
123
+ previous_trace.get("pending_approval"),
124
+ dict,
125
+ ):
126
+ return 400, failure_payload(
127
+ service_errors.INVALID_REQUEST_BODY,
128
+ "runtime run is not waiting for approval",
129
+ )
130
+ pending_approval = previous_trace["pending_approval"]
131
+ pending_action_id = str(pending_approval.get("id", ""))
132
+ if not pending_action_id.strip():
133
+ return 400, failure_payload(
134
+ service_errors.INVALID_REQUEST_BODY,
135
+ "runtime run trace is missing pending approval action id",
136
+ )
137
+ if pending_action_id not in approved_action_ids:
138
+ return 400, failure_payload(
139
+ service_errors.INVALID_REQUEST_BODY,
140
+ "approved_action_ids must include the pending approval action id",
141
+ )
142
+ if any(action_id != pending_action_id for action_id in approved_action_ids):
143
+ return 400, failure_payload(
144
+ service_errors.INVALID_REQUEST_BODY,
145
+ "approved_action_ids must contain only the pending approval action id",
146
+ )
147
+ goal = str(previous_trace.get("goal", ""))
148
+ plan = previous_trace.get("plan")
149
+ if not goal.strip() or not isinstance(plan, dict):
150
+ return 400, failure_payload(
151
+ service_errors.INVALID_REQUEST_BODY,
152
+ "runtime run trace is missing resumable plan state",
153
+ )
154
+ resumable_plan = build_resumable_plan(plan, pending_approval)
155
+ if resumable_plan is None:
156
+ return 400, failure_payload(
157
+ service_errors.INVALID_REQUEST_BODY,
158
+ "runtime run trace is missing pending approval action plan",
159
+ )
160
+
161
+ registry = active_run_registry or ActiveRunRegistry()
162
+ resumed_run_id = str(uuid4())
163
+ claim_id = str(uuid4())
164
+ try:
165
+ claim_runtime_resume(
166
+ trace_dir=service_config.trace_dir,
167
+ run_id=run_id,
168
+ pending_action_id=pending_action_id,
169
+ claim_id=claim_id,
170
+ resumed_run_id=resumed_run_id,
171
+ claimed_by_auth_subject=auth_subject,
172
+ runtime_instance_id=registry.instance_id,
173
+ )
174
+ except RuntimeResumeClaimConflict as exc:
175
+ return 409, failure_payload(service_errors.INVALID_REQUEST_BODY, str(exc))
176
+ except (OSError, ValueError) as exc:
177
+ return 500, failure_payload(
178
+ service_errors.TRACE_PERSISTENCE_FAILED,
179
+ f"could not claim runtime approval: {exc}",
180
+ )
181
+
182
+ cancellation_token = RuntimeCancellationToken(
183
+ external_cancellation_probe=lambda: persisted_runtime_cancellation_probe(
184
+ run_id=resumed_run_id,
185
+ trace_dir=service_config.trace_dir,
186
+ )
187
+ )
188
+ approved_at = _utc_timestamp()
189
+ initial_trace = running_runtime_trace(
190
+ run_id=resumed_run_id,
191
+ goal=goal,
192
+ max_iterations=max_iterations,
193
+ auth_subject=owner_auth_subject,
194
+ resumed_from_run_id=run_id,
195
+ runtime_instance_id=registry.instance_id,
196
+ )
197
+ initial_trace["approved_at"] = approved_at
198
+ if auth_subject:
199
+ initial_trace["resumed_by_auth_subject"] = auth_subject
200
+ initial_trace["approved_by_auth_subject"] = auth_subject
201
+ if isinstance(previous_trace.get("metadata"), dict):
202
+ initial_trace["metadata"] = dict(previous_trace["metadata"])
203
+ if isinstance(previous_trace.get("tags"), list):
204
+ initial_trace["tags"] = list(previous_trace["tags"])
205
+
206
+ trace_path = ""
207
+ release_worker_slot = None
208
+ try:
209
+ trace_path = persist_trace(initial_trace, service_config.trace_dir)
210
+ allowed_tools = service_config.runtime_allowed_tools_for_subject(owner_auth_subject)
211
+ policy = (
212
+ RuntimePolicy(allowed_tools=set(allowed_tools))
213
+ if allowed_tools is not None
214
+ else RuntimePolicy()
215
+ )
216
+ release_worker_slot = (
217
+ execution_slot_lease.transfer()
218
+ if execution_slot_lease is not None
219
+ else None
220
+ )
221
+ registry.register(
222
+ resumed_run_id,
223
+ owner_auth_subject,
224
+ cancellation_token,
225
+ release=release_worker_slot,
226
+ )
227
+ except (OSError, ValueError) as exc:
228
+ if release_worker_slot is not None:
229
+ release_worker_slot()
230
+ try:
231
+ release_runtime_resume_claim(
232
+ trace_dir=service_config.trace_dir,
233
+ run_id=run_id,
234
+ claim_id=claim_id,
235
+ )
236
+ except (OSError, ValueError):
237
+ pass
238
+ return 500, failure_payload(
239
+ service_errors.TRACE_PERSISTENCE_FAILED,
240
+ f"could not initialize resumed runtime run: {exc}",
241
+ )
242
+ except Exception:
243
+ if release_worker_slot is not None:
244
+ release_worker_slot()
245
+ try:
246
+ release_runtime_resume_claim(
247
+ trace_dir=service_config.trace_dir,
248
+ run_id=run_id,
249
+ claim_id=claim_id,
250
+ )
251
+ except (OSError, ValueError):
252
+ pass
253
+ return 500, failure_payload(service_errors.AGENT_RUN_FAILED, "agent run failed")
254
+
255
+ def execute_runtime_worker() -> Dict[str, Any]:
256
+ try:
257
+ result = run_runtime_agent(
258
+ goal,
259
+ provider=FakeLLMProvider(json.dumps(resumable_plan, sort_keys=True)),
260
+ run_id=resumed_run_id,
261
+ cancellation_token=cancellation_token,
262
+ policy=policy,
263
+ max_iterations=max_iterations,
264
+ event_sink=_runtime_event_sink(service_config),
265
+ approved_action_ids=set(approved_action_ids),
266
+ runtime_workspace_dir=service_config.runtime_workspace_dir,
267
+ redis_url=service_config.redis_url,
268
+ milvus_url=service_config.milvus_url,
269
+ embedding_base_url=service_config.embedding_base_url,
270
+ embedding_api_key=service_config.embedding_api_key,
271
+ embedding_model=service_config.embedding_model,
272
+ embedding_timeout_seconds=service_config.embedding_timeout_seconds,
273
+ embedding_max_retries=service_config.embedding_max_retries,
274
+ embedding_retry_backoff_seconds=(
275
+ service_config.embedding_retry_backoff_seconds
276
+ ),
277
+ external_backend_timeout_seconds=(
278
+ service_config.external_backend_timeout_seconds
279
+ ),
280
+ )
281
+ result["run_id"] = resumed_run_id
282
+ if owner_auth_subject:
283
+ result["auth_subject"] = owner_auth_subject
284
+ if auth_subject:
285
+ result["resumed_by_auth_subject"] = auth_subject
286
+ result["approved_by_auth_subject"] = auth_subject
287
+ result["approved_at"] = approved_at
288
+ if isinstance(previous_trace.get("metadata"), dict):
289
+ result["metadata"] = dict(previous_trace["metadata"])
290
+ if isinstance(previous_trace.get("tags"), list):
291
+ result["tags"] = list(previous_trace["tags"])
292
+ result["resumed_from_run_id"] = run_id
293
+ result = persist_runtime_worker_result(
294
+ run_id=resumed_run_id,
295
+ trace_dir=service_config.trace_dir,
296
+ result=result,
297
+ persist_trace_fn=persist_trace,
298
+ )
299
+ return result
300
+ except Exception as exc:
301
+ try:
302
+ persist_failed_runtime_trace(
303
+ run_id=resumed_run_id,
304
+ trace_dir=service_config.trace_dir,
305
+ error_code=service_errors.AGENT_RUN_FAILED,
306
+ error=f"agent run failed: {exc}",
307
+ )
308
+ except (OSError, ValueError):
309
+ pass
310
+ raise
311
+
312
+ def cancel_timed_out_worker() -> None:
313
+ active_run = registry.mark_timed_out(
314
+ resumed_run_id,
315
+ reason="runtime run timed out",
316
+ )
317
+ if active_run is None:
318
+ return
319
+ try:
320
+ persist_cancelled_runtime_trace(
321
+ run_id=resumed_run_id,
322
+ trace_dir=service_config.trace_dir,
323
+ active_run=active_run,
324
+ error_code=service_errors.AGENT_RUN_TIMEOUT,
325
+ error="agent run timed out",
326
+ )
327
+ except (OSError, ValueError):
328
+ pass
329
+
330
+ def complete_resumed_worker() -> None:
331
+ try:
332
+ complete_runtime_resume_claim(
333
+ trace_dir=service_config.trace_dir,
334
+ run_id=run_id,
335
+ claim_id=claim_id,
336
+ resumed_run_id=resumed_run_id,
337
+ )
338
+ except (OSError, ValueError):
339
+ pass
340
+ finally:
341
+ registry.complete(resumed_run_id)
342
+
343
+ try:
344
+ result = run_with_timeout(
345
+ execute_runtime_worker,
346
+ timeout_seconds=service_config.run_timeout_seconds,
347
+ on_timeout=cancel_timed_out_worker,
348
+ on_complete=complete_resumed_worker,
349
+ )
350
+ except TimeoutError:
351
+ timeout_payload = failure_payload(
352
+ service_errors.AGENT_RUN_TIMEOUT,
353
+ "agent run timed out",
354
+ )
355
+ timeout_payload["run_id"] = resumed_run_id
356
+ timeout_payload["resumed_from_run_id"] = run_id
357
+ if trace_path:
358
+ timeout_payload["trace_path"] = trace_path
359
+ return 504, timeout_payload
360
+ except Exception:
361
+ failure = failure_payload(service_errors.AGENT_RUN_FAILED, "agent run failed")
362
+ failure["run_id"] = resumed_run_id
363
+ failure["resumed_from_run_id"] = run_id
364
+ if trace_path:
365
+ failure["trace_path"] = trace_path
366
+ return 500, failure
367
+ return 200, json_ready(result)
368
+
369
+
370
+ def _runtime_event_sink(config: ServiceConfig):
371
+ if not config.kafka_audit_url:
372
+ return None
373
+ return KafkaRestProgressEventSink(
374
+ url=config.kafka_audit_url,
375
+ topic=config.kafka_audit_topic,
376
+ timeout_seconds=config.external_backend_timeout_seconds,
377
+ fail_closed=True,
378
+ )
379
+
380
+
381
+ def _utc_timestamp() -> str:
382
+ return datetime.now(timezone.utc).isoformat()
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from datetime import datetime, timezone
5
+ from pathlib import Path
6
+ from typing import Any, Dict
7
+
8
+ from kagent.service.safety import safe_trace_file_stem
9
+ from kagent.service.trace_store import (
10
+ load_trace_by_run_id,
11
+ persist_trace,
12
+ runtime_trace_lock,
13
+ )
14
+
15
+
16
+ class RuntimeResumeClaimConflict(Exception):
17
+ pass
18
+
19
+
20
+ def claim_runtime_resume(
21
+ *,
22
+ trace_dir: str,
23
+ run_id: str,
24
+ pending_action_id: str,
25
+ claim_id: str,
26
+ resumed_run_id: str,
27
+ claimed_by_auth_subject: str = "",
28
+ runtime_instance_id: str = "",
29
+ ) -> Dict[str, Any]:
30
+ lock_path = _resume_lock_path(trace_dir, run_id)
31
+ try:
32
+ lock_fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
33
+ except FileExistsError as exc:
34
+ raise RuntimeResumeClaimConflict("runtime run approval is already being resumed") from exc
35
+ try:
36
+ with os.fdopen(lock_fd, "w", encoding="utf-8") as lock_file:
37
+ lock_file.write(claim_id)
38
+ lock_file.flush()
39
+ os.fsync(lock_file.fileno())
40
+ with runtime_trace_lock(run_id, trace_dir):
41
+ trace = load_trace_by_run_id(run_id, trace_dir)
42
+ if trace is None:
43
+ raise RuntimeResumeClaimConflict("runtime run trace no longer exists")
44
+ pending_approval = trace.get("pending_approval")
45
+ if trace.get("status") != "requires_approval" or not isinstance(
46
+ pending_approval,
47
+ dict,
48
+ ):
49
+ raise RuntimeResumeClaimConflict("runtime run is not waiting for approval")
50
+ if str(pending_approval.get("id", "")) != pending_action_id:
51
+ raise RuntimeResumeClaimConflict("runtime pending approval changed")
52
+ trace["status"] = "resuming"
53
+ trace["resume_claim_id"] = claim_id
54
+ trace["resume_claimed_at"] = _utc_timestamp()
55
+ trace["resumed_to_run_id"] = resumed_run_id
56
+ if runtime_instance_id:
57
+ trace["resume_runtime_instance_id"] = runtime_instance_id
58
+ if claimed_by_auth_subject:
59
+ trace["resumed_by_auth_subject"] = claimed_by_auth_subject
60
+ trace["approved_by_auth_subject"] = claimed_by_auth_subject
61
+ trace["trace_path"] = persist_trace(trace, trace_dir)
62
+ return trace
63
+ finally:
64
+ lock_path.unlink(missing_ok=True)
65
+
66
+
67
+ def release_runtime_resume_claim(
68
+ *,
69
+ trace_dir: str,
70
+ run_id: str,
71
+ claim_id: str,
72
+ ) -> None:
73
+ with runtime_trace_lock(run_id, trace_dir):
74
+ trace = load_trace_by_run_id(run_id, trace_dir)
75
+ if trace is None or trace.get("resume_claim_id") != claim_id:
76
+ return
77
+ if trace.get("status") != "resuming":
78
+ return
79
+ trace["status"] = "requires_approval"
80
+ trace.pop("resume_claim_id", None)
81
+ trace.pop("resume_claimed_at", None)
82
+ trace.pop("resumed_to_run_id", None)
83
+ trace.pop("resumed_by_auth_subject", None)
84
+ trace.pop("approved_by_auth_subject", None)
85
+ trace.pop("resume_runtime_instance_id", None)
86
+ persist_trace(trace, trace_dir)
87
+
88
+
89
+ def complete_runtime_resume_claim(
90
+ *,
91
+ trace_dir: str,
92
+ run_id: str,
93
+ claim_id: str,
94
+ resumed_run_id: str,
95
+ ) -> None:
96
+ with runtime_trace_lock(run_id, trace_dir):
97
+ trace = load_trace_by_run_id(run_id, trace_dir)
98
+ if trace is None or trace.get("resume_claim_id") != claim_id:
99
+ return
100
+ if trace.get("status") != "resuming":
101
+ return
102
+ completed_at = _utc_timestamp()
103
+ trace["status"] = "resumed"
104
+ trace["completed_at"] = completed_at
105
+ trace["resumed_at"] = completed_at
106
+ trace["resumed_to_run_id"] = resumed_run_id
107
+ trace.pop("pending_approval", None)
108
+ persist_trace(trace, trace_dir)
109
+
110
+
111
+ def _resume_lock_path(trace_dir: str, run_id: str) -> Path:
112
+ return Path(trace_dir) / f".{safe_trace_file_stem(run_id)}.resume.lock"
113
+
114
+
115
+ def _utc_timestamp() -> str:
116
+ return datetime.now(timezone.utc).isoformat()