@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.
- package/README.md +353 -0
- package/npm/bin/kagent-serve.js +6 -0
- package/npm/bin/kagent.js +6 -0
- package/npm/lib/App.js +524 -0
- package/npm/lib/app-state.js +224 -0
- package/npm/lib/approval-choice.js +25 -0
- package/npm/lib/commands.js +59 -0
- package/npm/lib/editor.js +188 -0
- package/npm/lib/ink-runner.js +41 -0
- package/npm/lib/kagent-home.js +39 -0
- package/npm/lib/launcher.js +221 -0
- package/npm/lib/protocol.js +33 -0
- package/npm/lib/provider-setup.js +139 -0
- package/npm/lib/python-runner.js +892 -0
- package/npm/lib/runtime-client.js +390 -0
- package/npm/lib/terminal-input.js +127 -0
- package/npm/lib/terminal-text.js +19 -0
- package/npm/lib/terminal-width.js +14 -0
- package/npm/lib/transcript.js +227 -0
- package/npm/lib/ui-components.js +247 -0
- package/npm/lib/update-manager.js +334 -0
- package/package.json +39 -0
- package/pyproject.toml +55 -0
- package/src/kagent/__init__.py +90 -0
- package/src/kagent/cli/__init__.py +5 -0
- package/src/kagent/cli/__main__.py +6 -0
- package/src/kagent/cli/commands.py +112 -0
- package/src/kagent/cli/conversation.py +127 -0
- package/src/kagent/cli/interactive.py +841 -0
- package/src/kagent/cli/main.py +685 -0
- package/src/kagent/cli/memory.py +460 -0
- package/src/kagent/cli/pending_approval.py +169 -0
- package/src/kagent/cli/provider.py +27 -0
- package/src/kagent/cli/session_commands.py +401 -0
- package/src/kagent/cli/stdio_runtime.py +784 -0
- package/src/kagent/cli/trace.py +67 -0
- package/src/kagent/cli/ui.py +931 -0
- package/src/kagent/core/__init__.py +0 -0
- package/src/kagent/core/agent.py +296 -0
- package/src/kagent/core/faults.py +11 -0
- package/src/kagent/core/invariants.py +47 -0
- package/src/kagent/core/normalization.py +81 -0
- package/src/kagent/core/planning.py +48 -0
- package/src/kagent/core/state.py +73 -0
- package/src/kagent/core/summary.py +64 -0
- package/src/kagent/core/tools.py +196 -0
- package/src/kagent/core/trace.py +57 -0
- package/src/kagent/eval/__init__.py +11 -0
- package/src/kagent/eval/cases.py +229 -0
- package/src/kagent/eval/evaluator.py +216 -0
- package/src/kagent/integrations/__init__.py +3 -0
- package/src/kagent/integrations/audit.py +95 -0
- package/src/kagent/integrations/backends.py +131 -0
- package/src/kagent/integrations/memory.py +301 -0
- package/src/kagent/ops/__init__.py +0 -0
- package/src/kagent/ops/batch.py +156 -0
- package/src/kagent/ops/doctor.py +255 -0
- package/src/kagent/ops/metrics.py +214 -0
- package/src/kagent/ops/release_evidence.py +877 -0
- package/src/kagent/ops/release_manifest.py +142 -0
- package/src/kagent/ops/trace_replay.py +285 -0
- package/src/kagent/providers/__init__.py +7 -0
- package/src/kagent/providers/embeddings.py +187 -0
- package/src/kagent/providers/llm.py +770 -0
- package/src/kagent/py.typed +1 -0
- package/src/kagent/runtime/__init__.py +28 -0
- package/src/kagent/runtime/action_graph.py +543 -0
- package/src/kagent/runtime/agent.py +2089 -0
- package/src/kagent/runtime/approval.py +64 -0
- package/src/kagent/runtime/cancellation.py +64 -0
- package/src/kagent/runtime/checkpoint_state.py +146 -0
- package/src/kagent/runtime/checkpoint_storage.py +270 -0
- package/src/kagent/runtime/context.py +65 -0
- package/src/kagent/runtime/file_transaction.py +195 -0
- package/src/kagent/runtime/hooks.py +74 -0
- package/src/kagent/runtime/metadata.py +116 -0
- package/src/kagent/runtime/patch_checkpoints.py +385 -0
- package/src/kagent/runtime/policy.py +50 -0
- package/src/kagent/runtime/presentation.py +205 -0
- package/src/kagent/runtime/redaction.py +130 -0
- package/src/kagent/runtime/sandbox.py +331 -0
- package/src/kagent/runtime/skills.py +66 -0
- package/src/kagent/runtime/steering.py +56 -0
- package/src/kagent/runtime/steps.py +255 -0
- package/src/kagent/runtime/task_state.py +40 -0
- package/src/kagent/runtime/tools.py +3532 -0
- package/src/kagent/runtime/types.py +240 -0
- package/src/kagent/runtime/workspace.py +597 -0
- package/src/kagent/service/__init__.py +26 -0
- package/src/kagent/service/__main__.py +6 -0
- package/src/kagent/service/active_runs.py +178 -0
- package/src/kagent/service/cli.py +737 -0
- package/src/kagent/service/contract.py +2571 -0
- package/src/kagent/service/errors.py +71 -0
- package/src/kagent/service/idempotency.py +584 -0
- package/src/kagent/service/router.py +884 -0
- package/src/kagent/service/run.py +150 -0
- package/src/kagent/service/runtime.py +1915 -0
- package/src/kagent/service/runtime_approval.py +20 -0
- package/src/kagent/service/runtime_cancel.py +192 -0
- package/src/kagent/service/runtime_lifecycle.py +229 -0
- package/src/kagent/service/runtime_metadata.py +21 -0
- package/src/kagent/service/runtime_policy.py +115 -0
- package/src/kagent/service/runtime_recovery.py +573 -0
- package/src/kagent/service/runtime_resume.py +382 -0
- package/src/kagent/service/runtime_resume_claim.py +116 -0
- package/src/kagent/service/runtime_run.py +350 -0
- package/src/kagent/service/runtime_status.py +2007 -0
- package/src/kagent/service/safety.py +139 -0
- package/src/kagent/service/server.py +114 -0
- package/src/kagent/service/status.py +233 -0
- package/src/kagent/service/trace_store.py +322 -0
- package/src/kagent/service/transport.py +24 -0
- package/src/kagent/utils/__init__.py +0 -0
- package/src/kagent/utils/config_validation.py +21 -0
- package/src/kagent/utils/json_output.py +23 -0
- 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()
|