@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,20 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def validate_approved_action_ids(value: Any) -> tuple[list[str], str]:
|
|
7
|
+
if not isinstance(value, list) or any(not isinstance(item, str) for item in value):
|
|
8
|
+
return [], "approved_action_ids must be an array of strings"
|
|
9
|
+
seen = set()
|
|
10
|
+
approved_action_ids = []
|
|
11
|
+
for action_id in value:
|
|
12
|
+
if not action_id.strip():
|
|
13
|
+
return [], "approved_action_ids must contain non-empty strings"
|
|
14
|
+
if action_id != action_id.strip():
|
|
15
|
+
return [], "approved_action_ids must not contain surrounding whitespace"
|
|
16
|
+
if action_id in seen:
|
|
17
|
+
return [], "approved_action_ids must not contain duplicate action ids"
|
|
18
|
+
seen.add(action_id)
|
|
19
|
+
approved_action_ids.append(action_id)
|
|
20
|
+
return approved_action_ids, ""
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Dict, Tuple
|
|
6
|
+
|
|
7
|
+
from kagent.service import errors as service_errors
|
|
8
|
+
from kagent.service.active_runs import ActiveRunRegistry, ActiveRunSnapshot
|
|
9
|
+
from kagent.service.errors import failure_payload
|
|
10
|
+
from kagent.service.runtime import ServiceConfig
|
|
11
|
+
from kagent.service.runtime_lifecycle import persist_cancelled_runtime_trace
|
|
12
|
+
from kagent.service.runtime_status import (
|
|
13
|
+
is_runtime_trace,
|
|
14
|
+
runtime_status_summary,
|
|
15
|
+
)
|
|
16
|
+
from kagent.service.trace_store import (
|
|
17
|
+
load_trace_by_run_id,
|
|
18
|
+
)
|
|
19
|
+
from kagent.utils.json_output import json_ready
|
|
20
|
+
|
|
21
|
+
_TRACE_READ_ERRORS = (OSError, ValueError)
|
|
22
|
+
_TERMINAL_RUNTIME_STATUSES = {"cancelled", "done", "failed", "resumed"}
|
|
23
|
+
MAX_CANCEL_REASON_CHARS = 500
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def execute_runtime_cancel_request(
|
|
27
|
+
run_id: str,
|
|
28
|
+
body: bytes,
|
|
29
|
+
service_config: ServiceConfig,
|
|
30
|
+
auth_subject: str = "",
|
|
31
|
+
*,
|
|
32
|
+
request_auth_is_admin: bool = False,
|
|
33
|
+
active_run_registry: ActiveRunRegistry | None = None,
|
|
34
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
35
|
+
if not service_config.trace_dir:
|
|
36
|
+
return 400, failure_payload(
|
|
37
|
+
service_errors.INVALID_AGENT_CONFIG,
|
|
38
|
+
"trace_dir is required for runtime cancel",
|
|
39
|
+
)
|
|
40
|
+
if not run_id.strip():
|
|
41
|
+
return 400, failure_payload(service_errors.INVALID_REQUEST_BODY, "run_id is required")
|
|
42
|
+
try:
|
|
43
|
+
payload = json.loads(body.decode("utf-8") or "{}")
|
|
44
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
45
|
+
return 400, failure_payload(service_errors.INVALID_JSON, f"invalid JSON: {exc}")
|
|
46
|
+
if not isinstance(payload, dict):
|
|
47
|
+
return 400, failure_payload(
|
|
48
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
49
|
+
"request body must be a JSON object",
|
|
50
|
+
)
|
|
51
|
+
if set(payload) - {"reason"}:
|
|
52
|
+
return 400, failure_payload(
|
|
53
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
54
|
+
"cancel request only accepts an optional reason field",
|
|
55
|
+
)
|
|
56
|
+
reason = payload.get("reason", "")
|
|
57
|
+
if not isinstance(reason, str):
|
|
58
|
+
return 400, failure_payload(
|
|
59
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
60
|
+
"reason must be a string",
|
|
61
|
+
)
|
|
62
|
+
reason = reason.strip()
|
|
63
|
+
if len(reason) > MAX_CANCEL_REASON_CHARS:
|
|
64
|
+
return 400, failure_payload(
|
|
65
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
66
|
+
f"reason must be at most {MAX_CANCEL_REASON_CHARS} characters",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
trace = load_trace_by_run_id(run_id, service_config.trace_dir)
|
|
71
|
+
except _TRACE_READ_ERRORS:
|
|
72
|
+
return 500, failure_payload(
|
|
73
|
+
service_errors.TRACE_READ_FAILED,
|
|
74
|
+
"runtime run trace could not be read",
|
|
75
|
+
)
|
|
76
|
+
if trace is None or not is_runtime_trace(trace):
|
|
77
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
78
|
+
owner_auth_subject = str(trace.get("auth_subject", "")) or auth_subject
|
|
79
|
+
if auth_subject and not request_auth_is_admin and owner_auth_subject != auth_subject:
|
|
80
|
+
return 404, failure_payload(service_errors.NOT_FOUND, "runtime run trace not found")
|
|
81
|
+
if active_run_registry is not None:
|
|
82
|
+
active_run = active_run_registry.request_cancel(
|
|
83
|
+
run_id,
|
|
84
|
+
requested_by_auth_subject=auth_subject,
|
|
85
|
+
request_auth_is_admin=request_auth_is_admin,
|
|
86
|
+
reason=reason,
|
|
87
|
+
)
|
|
88
|
+
if active_run is not None:
|
|
89
|
+
if active_run.state != "cancelled":
|
|
90
|
+
return 409, failure_payload(
|
|
91
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
92
|
+
"runtime run is already terminal",
|
|
93
|
+
)
|
|
94
|
+
try:
|
|
95
|
+
cancelled_trace = persist_cancelled_runtime_trace(
|
|
96
|
+
run_id=run_id,
|
|
97
|
+
trace_dir=service_config.trace_dir,
|
|
98
|
+
active_run=active_run,
|
|
99
|
+
cancelled_by_auth_subject=auth_subject,
|
|
100
|
+
)
|
|
101
|
+
except (OSError, ValueError) as exc:
|
|
102
|
+
return 500, failure_payload(
|
|
103
|
+
service_errors.TRACE_PERSISTENCE_FAILED,
|
|
104
|
+
f"could not persist trace: {exc}",
|
|
105
|
+
)
|
|
106
|
+
persisted_status = str(cancelled_trace.get("status", ""))
|
|
107
|
+
if persisted_status == "resuming":
|
|
108
|
+
return 409, failure_payload(
|
|
109
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
110
|
+
"runtime run approval is being resumed",
|
|
111
|
+
)
|
|
112
|
+
if persisted_status != "cancelled":
|
|
113
|
+
return 409, failure_payload(
|
|
114
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
115
|
+
"runtime run is already terminal",
|
|
116
|
+
)
|
|
117
|
+
return 200, json_ready(
|
|
118
|
+
runtime_status_summary(
|
|
119
|
+
cancelled_trace,
|
|
120
|
+
service_config.trace_dir,
|
|
121
|
+
run_id,
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
try:
|
|
125
|
+
refreshed_trace = load_trace_by_run_id(run_id, service_config.trace_dir)
|
|
126
|
+
except _TRACE_READ_ERRORS:
|
|
127
|
+
return 500, failure_payload(
|
|
128
|
+
service_errors.TRACE_READ_FAILED,
|
|
129
|
+
"runtime run trace could not be read",
|
|
130
|
+
)
|
|
131
|
+
if refreshed_trace is None or not is_runtime_trace(refreshed_trace):
|
|
132
|
+
return 404, failure_payload(
|
|
133
|
+
service_errors.NOT_FOUND,
|
|
134
|
+
"runtime run trace not found",
|
|
135
|
+
)
|
|
136
|
+
trace = refreshed_trace
|
|
137
|
+
owner_auth_subject = str(trace.get("auth_subject", "")) or auth_subject
|
|
138
|
+
if auth_subject and not request_auth_is_admin and owner_auth_subject != auth_subject:
|
|
139
|
+
return 404, failure_payload(
|
|
140
|
+
service_errors.NOT_FOUND,
|
|
141
|
+
"runtime run trace not found",
|
|
142
|
+
)
|
|
143
|
+
trace_status = str(trace.get("status", ""))
|
|
144
|
+
if trace_status in _TERMINAL_RUNTIME_STATUSES:
|
|
145
|
+
return 409, failure_payload(
|
|
146
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
147
|
+
"runtime run is already terminal",
|
|
148
|
+
)
|
|
149
|
+
if trace_status == "resuming":
|
|
150
|
+
return 409, failure_payload(
|
|
151
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
152
|
+
"runtime run approval is being resumed",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
cancelled_at = _utc_timestamp()
|
|
156
|
+
try:
|
|
157
|
+
trace = persist_cancelled_runtime_trace(
|
|
158
|
+
run_id=run_id,
|
|
159
|
+
trace_dir=service_config.trace_dir,
|
|
160
|
+
active_run=ActiveRunSnapshot(
|
|
161
|
+
run_id=run_id,
|
|
162
|
+
owner_auth_subject=owner_auth_subject,
|
|
163
|
+
state="cancelled",
|
|
164
|
+
started_at=str(trace.get("started_at", "")) or cancelled_at,
|
|
165
|
+
cancel_reason=reason,
|
|
166
|
+
cancelled_at=cancelled_at,
|
|
167
|
+
),
|
|
168
|
+
cancelled_by_auth_subject=auth_subject,
|
|
169
|
+
)
|
|
170
|
+
except (OSError, ValueError) as exc:
|
|
171
|
+
return 500, failure_payload(
|
|
172
|
+
service_errors.TRACE_PERSISTENCE_FAILED,
|
|
173
|
+
f"could not persist trace: {exc}",
|
|
174
|
+
)
|
|
175
|
+
persisted_status = str(trace.get("status", ""))
|
|
176
|
+
if persisted_status == "resuming":
|
|
177
|
+
return 409, failure_payload(
|
|
178
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
179
|
+
"runtime run approval is being resumed",
|
|
180
|
+
)
|
|
181
|
+
if persisted_status != "cancelled":
|
|
182
|
+
return 409, failure_payload(
|
|
183
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
184
|
+
"runtime run is already terminal",
|
|
185
|
+
)
|
|
186
|
+
return 200, json_ready(
|
|
187
|
+
runtime_status_summary(trace, service_config.trace_dir, run_id)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _utc_timestamp() -> str:
|
|
192
|
+
return datetime.now(timezone.utc).isoformat()
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
from typing import Any, Callable, Dict
|
|
6
|
+
|
|
7
|
+
from kagent.runtime import RUNTIME_TRACE_TYPE
|
|
8
|
+
from kagent.service.active_runs import ActiveRunSnapshot
|
|
9
|
+
from kagent.service.trace_store import (
|
|
10
|
+
load_trace_by_run_id,
|
|
11
|
+
persist_trace,
|
|
12
|
+
runtime_trace_lock,
|
|
13
|
+
trace_path_for_run_id,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_TERMINAL_RUNTIME_STATUSES = {"cancelled", "done", "failed", "resumed"}
|
|
17
|
+
_CANCELLATION_PROTECTED_STATUSES = _TERMINAL_RUNTIME_STATUSES | {"resuming"}
|
|
18
|
+
TracePersistFunction = Callable[[Dict[str, Any], str], str]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def running_runtime_trace(
|
|
22
|
+
*,
|
|
23
|
+
run_id: str,
|
|
24
|
+
goal: str,
|
|
25
|
+
max_iterations: int,
|
|
26
|
+
auth_subject: str = "",
|
|
27
|
+
resumed_from_run_id: str = "",
|
|
28
|
+
runtime_instance_id: str = "",
|
|
29
|
+
) -> Dict[str, Any]:
|
|
30
|
+
started_at = _utc_timestamp()
|
|
31
|
+
trace: Dict[str, Any] = {
|
|
32
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
33
|
+
"run_id": run_id,
|
|
34
|
+
"status": "running",
|
|
35
|
+
"goal": goal,
|
|
36
|
+
"started_at": started_at,
|
|
37
|
+
"max_iterations": str(max_iterations),
|
|
38
|
+
"iteration_count": "0",
|
|
39
|
+
"iteration_budget_remaining": str(max_iterations),
|
|
40
|
+
"events": [],
|
|
41
|
+
"observations": [],
|
|
42
|
+
"plans": [],
|
|
43
|
+
"plan": {"actions": []},
|
|
44
|
+
}
|
|
45
|
+
if auth_subject:
|
|
46
|
+
trace["auth_subject"] = auth_subject
|
|
47
|
+
if resumed_from_run_id:
|
|
48
|
+
trace["resumed_from_run_id"] = resumed_from_run_id
|
|
49
|
+
if runtime_instance_id:
|
|
50
|
+
trace["runtime_instance_id"] = runtime_instance_id
|
|
51
|
+
return trace
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def persist_cancelled_runtime_trace(
|
|
55
|
+
*,
|
|
56
|
+
run_id: str,
|
|
57
|
+
trace_dir: str,
|
|
58
|
+
active_run: ActiveRunSnapshot,
|
|
59
|
+
cancelled_by_auth_subject: str = "",
|
|
60
|
+
error_code: str = "run_cancelled",
|
|
61
|
+
error: str = "runtime run cancelled",
|
|
62
|
+
) -> Dict[str, Any]:
|
|
63
|
+
with runtime_trace_lock(run_id, trace_dir):
|
|
64
|
+
trace = load_trace_by_run_id(run_id, trace_dir) or {
|
|
65
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
66
|
+
"run_id": run_id,
|
|
67
|
+
"status": "running",
|
|
68
|
+
"started_at": active_run.started_at,
|
|
69
|
+
"events": [],
|
|
70
|
+
}
|
|
71
|
+
current_status = str(trace.get("status", ""))
|
|
72
|
+
timeout_upgrade = (
|
|
73
|
+
active_run.state == "timed_out" and current_status == "cancelled"
|
|
74
|
+
)
|
|
75
|
+
if current_status in _CANCELLATION_PROTECTED_STATUSES and not timeout_upgrade:
|
|
76
|
+
return _with_trace_path(trace, run_id, trace_dir)
|
|
77
|
+
cancelled_at = active_run.cancelled_at or _utc_timestamp()
|
|
78
|
+
trace["status"] = "cancelled"
|
|
79
|
+
trace["completed_at"] = cancelled_at
|
|
80
|
+
trace["cancelled_at"] = cancelled_at
|
|
81
|
+
trace["error_code"] = error_code
|
|
82
|
+
trace["error"] = error
|
|
83
|
+
if cancelled_by_auth_subject:
|
|
84
|
+
trace["cancelled_by_auth_subject"] = cancelled_by_auth_subject
|
|
85
|
+
if active_run.cancel_reason:
|
|
86
|
+
trace["cancel_reason"] = active_run.cancel_reason
|
|
87
|
+
trace.pop("pending_approval", None)
|
|
88
|
+
_append_cancel_event(trace, cancelled_at, active_run.cancel_reason)
|
|
89
|
+
_refresh_duration_seconds(trace)
|
|
90
|
+
trace["trace_path"] = persist_trace(trace, trace_dir)
|
|
91
|
+
return trace
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def persist_failed_runtime_trace(
|
|
95
|
+
*,
|
|
96
|
+
run_id: str,
|
|
97
|
+
trace_dir: str,
|
|
98
|
+
error_code: str,
|
|
99
|
+
error: str,
|
|
100
|
+
) -> Dict[str, Any]:
|
|
101
|
+
with runtime_trace_lock(run_id, trace_dir):
|
|
102
|
+
trace = load_trace_by_run_id(run_id, trace_dir) or {
|
|
103
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
104
|
+
"run_id": run_id,
|
|
105
|
+
"status": "running",
|
|
106
|
+
"started_at": _utc_timestamp(),
|
|
107
|
+
"events": [],
|
|
108
|
+
}
|
|
109
|
+
if str(trace.get("status", "")) in _TERMINAL_RUNTIME_STATUSES:
|
|
110
|
+
return _with_trace_path(trace, run_id, trace_dir)
|
|
111
|
+
completed_at = _utc_timestamp()
|
|
112
|
+
trace["status"] = "failed"
|
|
113
|
+
trace["completed_at"] = completed_at
|
|
114
|
+
trace["error_code"] = error_code
|
|
115
|
+
trace["error"] = error
|
|
116
|
+
trace.pop("pending_approval", None)
|
|
117
|
+
_append_failure_event(trace, completed_at, error_code, error)
|
|
118
|
+
_refresh_duration_seconds(trace)
|
|
119
|
+
trace["trace_path"] = persist_trace(trace, trace_dir)
|
|
120
|
+
return trace
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def persist_runtime_worker_result(
|
|
124
|
+
*,
|
|
125
|
+
run_id: str,
|
|
126
|
+
trace_dir: str,
|
|
127
|
+
result: Dict[str, Any],
|
|
128
|
+
persist_trace_fn: TracePersistFunction,
|
|
129
|
+
) -> Dict[str, Any]:
|
|
130
|
+
"""Persist a worker result unless another replica already committed a terminal state."""
|
|
131
|
+
|
|
132
|
+
with runtime_trace_lock(run_id, trace_dir):
|
|
133
|
+
try:
|
|
134
|
+
current = load_trace_by_run_id(run_id, trace_dir)
|
|
135
|
+
except ValueError:
|
|
136
|
+
current = None
|
|
137
|
+
if current is not None and str(current.get("status", "")) in (
|
|
138
|
+
_TERMINAL_RUNTIME_STATUSES
|
|
139
|
+
):
|
|
140
|
+
return _with_trace_path(current, run_id, trace_dir)
|
|
141
|
+
result["trace_path"] = persist_trace_fn(result, trace_dir)
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def persisted_runtime_cancellation_probe(
|
|
146
|
+
*,
|
|
147
|
+
run_id: str,
|
|
148
|
+
trace_dir: str,
|
|
149
|
+
) -> Dict[str, str] | None:
|
|
150
|
+
trace = load_trace_by_run_id(run_id, trace_dir)
|
|
151
|
+
if trace is None or str(trace.get("status", "")) != "cancelled":
|
|
152
|
+
return None
|
|
153
|
+
return {
|
|
154
|
+
"reason": str(trace.get("cancel_reason", "")),
|
|
155
|
+
"cancelled_at": str(trace.get("cancelled_at", "")),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _with_trace_path(
|
|
160
|
+
trace: Dict[str, Any],
|
|
161
|
+
run_id: str,
|
|
162
|
+
trace_dir: str,
|
|
163
|
+
) -> Dict[str, Any]:
|
|
164
|
+
trace["trace_path"] = str(trace_path_for_run_id(run_id, trace_dir))
|
|
165
|
+
return trace
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _append_cancel_event(trace: Dict[str, Any], cancelled_at: str, reason: str) -> None:
|
|
169
|
+
events = trace.get("events")
|
|
170
|
+
if not isinstance(events, list):
|
|
171
|
+
events = []
|
|
172
|
+
trace["events"] = events
|
|
173
|
+
if any(
|
|
174
|
+
isinstance(event, dict)
|
|
175
|
+
and event.get("node") == "control"
|
|
176
|
+
and event.get("status") == "cancelled"
|
|
177
|
+
for event in events
|
|
178
|
+
):
|
|
179
|
+
return
|
|
180
|
+
event: Dict[str, Any] = {
|
|
181
|
+
"node": "control",
|
|
182
|
+
"status": "cancelled",
|
|
183
|
+
"started_at": cancelled_at,
|
|
184
|
+
"completed_at": cancelled_at,
|
|
185
|
+
"duration_seconds": "0.0000",
|
|
186
|
+
}
|
|
187
|
+
if reason:
|
|
188
|
+
event["reason"] = reason
|
|
189
|
+
events.append(event)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _append_failure_event(
|
|
193
|
+
trace: Dict[str, Any],
|
|
194
|
+
completed_at: str,
|
|
195
|
+
error_code: str,
|
|
196
|
+
error: str,
|
|
197
|
+
) -> None:
|
|
198
|
+
events = trace.get("events")
|
|
199
|
+
if not isinstance(events, list):
|
|
200
|
+
events = []
|
|
201
|
+
trace["events"] = events
|
|
202
|
+
events.append(
|
|
203
|
+
{
|
|
204
|
+
"node": "runtime",
|
|
205
|
+
"status": "failed",
|
|
206
|
+
"started_at": completed_at,
|
|
207
|
+
"completed_at": completed_at,
|
|
208
|
+
"duration_seconds": "0.0000",
|
|
209
|
+
"error_code": error_code,
|
|
210
|
+
"error": error,
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _refresh_duration_seconds(trace: Dict[str, Any]) -> None:
|
|
216
|
+
started_at = trace.get("started_at")
|
|
217
|
+
if not isinstance(started_at, str) or not started_at.strip():
|
|
218
|
+
return
|
|
219
|
+
try:
|
|
220
|
+
started = datetime.fromisoformat(started_at)
|
|
221
|
+
except ValueError:
|
|
222
|
+
return
|
|
223
|
+
if started.tzinfo is None:
|
|
224
|
+
started = started.replace(tzinfo=timezone.utc)
|
|
225
|
+
trace["duration_seconds"] = f"{max(0.0, time.time() - started.timestamp()):.4f}"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _utc_timestamp() -> str:
|
|
229
|
+
return datetime.now(timezone.utc).isoformat()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from kagent.runtime.metadata import (
|
|
4
|
+
MAX_RUNTIME_METADATA_ENTRIES,
|
|
5
|
+
MAX_RUNTIME_METADATA_KEY_CHARS,
|
|
6
|
+
MAX_RUNTIME_METADATA_VALUE_CHARS,
|
|
7
|
+
MAX_RUNTIME_TAG_CHARS,
|
|
8
|
+
MAX_RUNTIME_TAGS,
|
|
9
|
+
validate_runtime_metadata,
|
|
10
|
+
validate_runtime_tags,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"MAX_RUNTIME_METADATA_ENTRIES",
|
|
15
|
+
"MAX_RUNTIME_METADATA_KEY_CHARS",
|
|
16
|
+
"MAX_RUNTIME_METADATA_VALUE_CHARS",
|
|
17
|
+
"MAX_RUNTIME_TAG_CHARS",
|
|
18
|
+
"MAX_RUNTIME_TAGS",
|
|
19
|
+
"validate_runtime_metadata",
|
|
20
|
+
"validate_runtime_tags",
|
|
21
|
+
]
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from hashlib import sha256
|
|
5
|
+
from typing import Any, Dict, Tuple
|
|
6
|
+
|
|
7
|
+
from kagent.runtime import RUNTIME_TRACE_TYPE
|
|
8
|
+
from kagent.runtime.policy import RuntimePolicy
|
|
9
|
+
from kagent.runtime.tools import default_runtime_tools
|
|
10
|
+
from kagent.service.runtime import ServiceConfig
|
|
11
|
+
from kagent.utils.json_output import json_ready
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def execute_runtime_policy_request(
|
|
15
|
+
service_config: ServiceConfig,
|
|
16
|
+
*,
|
|
17
|
+
request_auth_subject: str = "",
|
|
18
|
+
request_auth_is_admin: bool = False,
|
|
19
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
20
|
+
default_allowed_tools = _sorted_tuple(RuntimePolicy().allowed_tools)
|
|
21
|
+
global_allowed_tools = _sorted_tuple(service_config.runtime_allowed_tools)
|
|
22
|
+
subject_allowed_tools = _subject_allowed_tools(
|
|
23
|
+
service_config,
|
|
24
|
+
request_auth_subject=request_auth_subject,
|
|
25
|
+
request_auth_is_admin=request_auth_is_admin,
|
|
26
|
+
)
|
|
27
|
+
effective_allowed_tools, effective_policy_source = _effective_policy(
|
|
28
|
+
service_config,
|
|
29
|
+
request_auth_subject=request_auth_subject,
|
|
30
|
+
default_allowed_tools=default_allowed_tools,
|
|
31
|
+
global_allowed_tools=global_allowed_tools,
|
|
32
|
+
)
|
|
33
|
+
effective_tool_policy = _effective_tool_policy(effective_allowed_tools)
|
|
34
|
+
return 200, json_ready(
|
|
35
|
+
{
|
|
36
|
+
"trace_type": RUNTIME_TRACE_TYPE,
|
|
37
|
+
"auth_subject": request_auth_subject,
|
|
38
|
+
"is_admin": str(request_auth_is_admin).lower(),
|
|
39
|
+
"default_allowed_tools": list(default_allowed_tools),
|
|
40
|
+
"global_allowed_tools": list(global_allowed_tools),
|
|
41
|
+
"subject_policy_count": str(
|
|
42
|
+
len(service_config.runtime_allowed_tools_by_subject)
|
|
43
|
+
),
|
|
44
|
+
"subject_allowed_tools": subject_allowed_tools,
|
|
45
|
+
"effective_allowed_tools": list(effective_allowed_tools),
|
|
46
|
+
"effective_policy_source": effective_policy_source,
|
|
47
|
+
"effective_tool_policy": effective_tool_policy,
|
|
48
|
+
"effective_tool_policy_sha256": _policy_sha256(
|
|
49
|
+
effective_tool_policy
|
|
50
|
+
),
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _subject_allowed_tools(
|
|
56
|
+
service_config: ServiceConfig,
|
|
57
|
+
*,
|
|
58
|
+
request_auth_subject: str,
|
|
59
|
+
request_auth_is_admin: bool,
|
|
60
|
+
) -> Dict[str, list[str]]:
|
|
61
|
+
subjects = service_config.runtime_allowed_tools_by_subject
|
|
62
|
+
if request_auth_is_admin:
|
|
63
|
+
return {
|
|
64
|
+
subject: list(_sorted_tuple(tools))
|
|
65
|
+
for subject, tools in sorted(subjects.items())
|
|
66
|
+
}
|
|
67
|
+
if request_auth_subject in subjects:
|
|
68
|
+
return {
|
|
69
|
+
request_auth_subject: list(
|
|
70
|
+
_sorted_tuple(subjects[request_auth_subject])
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _effective_policy(
|
|
77
|
+
service_config: ServiceConfig,
|
|
78
|
+
*,
|
|
79
|
+
request_auth_subject: str,
|
|
80
|
+
default_allowed_tools: tuple[str, ...],
|
|
81
|
+
global_allowed_tools: tuple[str, ...],
|
|
82
|
+
) -> tuple[tuple[str, ...], str]:
|
|
83
|
+
subject_allowed_tools = service_config.runtime_allowed_tools_by_subject.get(
|
|
84
|
+
request_auth_subject,
|
|
85
|
+
)
|
|
86
|
+
if subject_allowed_tools is not None:
|
|
87
|
+
return _sorted_tuple(subject_allowed_tools), "subject"
|
|
88
|
+
if global_allowed_tools:
|
|
89
|
+
return global_allowed_tools, "global"
|
|
90
|
+
return default_allowed_tools, "default"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _sorted_tuple(values: Any) -> tuple[str, ...]:
|
|
94
|
+
if not values:
|
|
95
|
+
return ()
|
|
96
|
+
return tuple(sorted(str(value) for value in values if str(value).strip()))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _effective_tool_policy(
|
|
100
|
+
effective_allowed_tools: tuple[str, ...],
|
|
101
|
+
) -> list[dict[str, str]]:
|
|
102
|
+
allowed_tools = set(effective_allowed_tools)
|
|
103
|
+
return [
|
|
104
|
+
{
|
|
105
|
+
"name": name,
|
|
106
|
+
"allowed": str(name in allowed_tools).lower(),
|
|
107
|
+
"approval_required": str(name not in allowed_tools).lower(),
|
|
108
|
+
}
|
|
109
|
+
for name in sorted(default_runtime_tools())
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _policy_sha256(policy: list[dict[str, str]]) -> str:
|
|
114
|
+
payload = json.dumps(policy, sort_keys=True, separators=(",", ":"))
|
|
115
|
+
return sha256(payload.encode("utf-8")).hexdigest()
|