@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,350 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from concurrent.futures import TimeoutError
|
|
5
|
+
from typing import Any, Dict, Tuple
|
|
6
|
+
from uuid import uuid4
|
|
7
|
+
|
|
8
|
+
from kagent.integrations.audit import KafkaRestAuditHook, KafkaRestProgressEventSink
|
|
9
|
+
from kagent.providers.llm import (
|
|
10
|
+
FakeLLMProvider,
|
|
11
|
+
LLMProviderConfig,
|
|
12
|
+
OpenAICompatibleProvider,
|
|
13
|
+
SequentialFakeLLMProvider,
|
|
14
|
+
)
|
|
15
|
+
from kagent.runtime import RuntimeCancellationToken, run_runtime_agent
|
|
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_metadata import (
|
|
33
|
+
validate_runtime_metadata,
|
|
34
|
+
validate_runtime_tags,
|
|
35
|
+
)
|
|
36
|
+
from kagent.service.trace_store import persist_trace
|
|
37
|
+
from kagent.utils.json_output import json_ready
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def execute_runtime_run_request(
|
|
41
|
+
body: bytes,
|
|
42
|
+
_service_config: ServiceConfig,
|
|
43
|
+
auth_subject: str = "",
|
|
44
|
+
*,
|
|
45
|
+
active_run_registry: ActiveRunRegistry | None = None,
|
|
46
|
+
execution_slot_lease: ExecutionSlotLease | None = None,
|
|
47
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
48
|
+
try:
|
|
49
|
+
payload = json.loads(body.decode("utf-8"))
|
|
50
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
51
|
+
return 400, failure_payload(service_errors.INVALID_JSON, f"invalid JSON: {exc}")
|
|
52
|
+
if not isinstance(payload, dict):
|
|
53
|
+
return 400, failure_payload(
|
|
54
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
55
|
+
"request body must be a JSON object",
|
|
56
|
+
)
|
|
57
|
+
goal = str(payload.get("goal", ""))
|
|
58
|
+
if not goal.strip():
|
|
59
|
+
return 400, failure_payload(service_errors.MISSING_GOAL, "goal is required")
|
|
60
|
+
if len(goal) > _service_config.max_goal_chars:
|
|
61
|
+
return 413, failure_payload(
|
|
62
|
+
service_errors.GOAL_TOO_LARGE,
|
|
63
|
+
"goal exceeds max_goal_chars",
|
|
64
|
+
)
|
|
65
|
+
max_iterations = payload.get("max_iterations", 1)
|
|
66
|
+
if (
|
|
67
|
+
not isinstance(max_iterations, int)
|
|
68
|
+
or isinstance(max_iterations, bool)
|
|
69
|
+
or max_iterations < 1
|
|
70
|
+
):
|
|
71
|
+
return 400, failure_payload(
|
|
72
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
73
|
+
"max_iterations must be an integer greater than or equal to 1",
|
|
74
|
+
)
|
|
75
|
+
if max_iterations > _service_config.runtime_max_iterations:
|
|
76
|
+
return 400, failure_payload(
|
|
77
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
78
|
+
"max_iterations exceeds runtime_max_iterations",
|
|
79
|
+
)
|
|
80
|
+
approved_action_ids_payload = payload.get("approved_action_ids", [])
|
|
81
|
+
approved_action_ids, approval_error = validate_approved_action_ids(
|
|
82
|
+
approved_action_ids_payload
|
|
83
|
+
)
|
|
84
|
+
if approval_error:
|
|
85
|
+
return 400, failure_payload(
|
|
86
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
87
|
+
approval_error,
|
|
88
|
+
)
|
|
89
|
+
metadata, metadata_error = validate_runtime_metadata(payload.get("metadata"))
|
|
90
|
+
if metadata_error:
|
|
91
|
+
return 400, failure_payload(
|
|
92
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
93
|
+
metadata_error,
|
|
94
|
+
)
|
|
95
|
+
tags, tags_error = validate_runtime_tags(payload.get("tags"))
|
|
96
|
+
if tags_error:
|
|
97
|
+
return 400, failure_payload(
|
|
98
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
99
|
+
tags_error,
|
|
100
|
+
)
|
|
101
|
+
plan_payload = payload.get("plan")
|
|
102
|
+
plan_sequence_payload = payload.get("plan_sequence")
|
|
103
|
+
if plan_payload is not None and plan_sequence_payload is not None:
|
|
104
|
+
return 400, failure_payload(
|
|
105
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
106
|
+
"plan and plan_sequence are mutually exclusive",
|
|
107
|
+
)
|
|
108
|
+
if approved_action_ids and plan_payload is None and plan_sequence_payload is None:
|
|
109
|
+
return 400, failure_payload(
|
|
110
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
111
|
+
"approved_action_ids require plan or plan_sequence; use /runtime/resume "
|
|
112
|
+
"to approve live provider actions",
|
|
113
|
+
)
|
|
114
|
+
planned_action_ids = _planned_action_ids(plan_payload, plan_sequence_payload)
|
|
115
|
+
if approved_action_ids and planned_action_ids is not None:
|
|
116
|
+
missing_approved_ids = [
|
|
117
|
+
action_id
|
|
118
|
+
for action_id in approved_action_ids
|
|
119
|
+
if action_id not in planned_action_ids
|
|
120
|
+
]
|
|
121
|
+
if missing_approved_ids:
|
|
122
|
+
return 400, failure_payload(
|
|
123
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
124
|
+
"approved_action_ids must reference planned action ids: "
|
|
125
|
+
+ ", ".join(missing_approved_ids),
|
|
126
|
+
)
|
|
127
|
+
if plan_payload is not None:
|
|
128
|
+
if not isinstance(plan_payload, dict):
|
|
129
|
+
return 400, failure_payload(
|
|
130
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
131
|
+
"plan must be a JSON object",
|
|
132
|
+
)
|
|
133
|
+
provider = FakeLLMProvider(json.dumps(plan_payload, sort_keys=True))
|
|
134
|
+
elif plan_sequence_payload is not None:
|
|
135
|
+
if not isinstance(plan_sequence_payload, list) or not plan_sequence_payload:
|
|
136
|
+
return 400, failure_payload(
|
|
137
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
138
|
+
"plan_sequence must be a non-empty array of JSON objects",
|
|
139
|
+
)
|
|
140
|
+
if any(not isinstance(item, dict) for item in plan_sequence_payload):
|
|
141
|
+
return 400, failure_payload(
|
|
142
|
+
service_errors.INVALID_REQUEST_BODY,
|
|
143
|
+
"plan_sequence must be a non-empty array of JSON objects",
|
|
144
|
+
)
|
|
145
|
+
provider = SequentialFakeLLMProvider(
|
|
146
|
+
[json.dumps(item, sort_keys=True) for item in plan_sequence_payload]
|
|
147
|
+
)
|
|
148
|
+
else:
|
|
149
|
+
try:
|
|
150
|
+
provider = OpenAICompatibleProvider(LLMProviderConfig.from_sources())
|
|
151
|
+
except ValueError as exc:
|
|
152
|
+
return 400, failure_payload(service_errors.INVALID_AGENT_CONFIG, str(exc))
|
|
153
|
+
allowed_tools = _service_config.runtime_allowed_tools_for_subject(auth_subject)
|
|
154
|
+
policy = (
|
|
155
|
+
RuntimePolicy(allowed_tools=set(allowed_tools))
|
|
156
|
+
if allowed_tools is not None
|
|
157
|
+
else RuntimePolicy()
|
|
158
|
+
)
|
|
159
|
+
run_id = str(uuid4())
|
|
160
|
+
cancellation_token = RuntimeCancellationToken(
|
|
161
|
+
external_cancellation_probe=(
|
|
162
|
+
lambda: persisted_runtime_cancellation_probe(
|
|
163
|
+
run_id=run_id,
|
|
164
|
+
trace_dir=_service_config.trace_dir,
|
|
165
|
+
)
|
|
166
|
+
if _service_config.trace_dir
|
|
167
|
+
else None
|
|
168
|
+
)
|
|
169
|
+
)
|
|
170
|
+
registry = active_run_registry or ActiveRunRegistry()
|
|
171
|
+
trace_path = ""
|
|
172
|
+
if _service_config.trace_dir:
|
|
173
|
+
initial_trace = running_runtime_trace(
|
|
174
|
+
run_id=run_id,
|
|
175
|
+
goal=goal,
|
|
176
|
+
max_iterations=max_iterations,
|
|
177
|
+
auth_subject=auth_subject,
|
|
178
|
+
runtime_instance_id=registry.instance_id,
|
|
179
|
+
)
|
|
180
|
+
try:
|
|
181
|
+
trace_path = persist_trace(initial_trace, _service_config.trace_dir)
|
|
182
|
+
except OSError as exc:
|
|
183
|
+
return 500, failure_payload(
|
|
184
|
+
service_errors.TRACE_PERSISTENCE_FAILED,
|
|
185
|
+
f"could not persist trace: {exc}",
|
|
186
|
+
)
|
|
187
|
+
release_worker_slot = (
|
|
188
|
+
execution_slot_lease.transfer() if execution_slot_lease is not None else None
|
|
189
|
+
)
|
|
190
|
+
try:
|
|
191
|
+
registry.register(
|
|
192
|
+
run_id,
|
|
193
|
+
auth_subject,
|
|
194
|
+
cancellation_token,
|
|
195
|
+
release=release_worker_slot,
|
|
196
|
+
)
|
|
197
|
+
except Exception:
|
|
198
|
+
if release_worker_slot is not None:
|
|
199
|
+
release_worker_slot()
|
|
200
|
+
raise
|
|
201
|
+
|
|
202
|
+
def execute_runtime_worker() -> Dict[str, Any]:
|
|
203
|
+
try:
|
|
204
|
+
result = run_runtime_agent(
|
|
205
|
+
goal,
|
|
206
|
+
provider=provider,
|
|
207
|
+
run_id=run_id,
|
|
208
|
+
cancellation_token=cancellation_token,
|
|
209
|
+
policy=policy,
|
|
210
|
+
max_iterations=max_iterations,
|
|
211
|
+
approved_action_ids=set(approved_action_ids),
|
|
212
|
+
metadata=metadata,
|
|
213
|
+
tags=tags,
|
|
214
|
+
event_sink=_runtime_event_sink(_service_config),
|
|
215
|
+
hooks=_runtime_hooks(_service_config),
|
|
216
|
+
runtime_workspace_dir=_service_config.runtime_workspace_dir,
|
|
217
|
+
redis_url=_service_config.redis_url,
|
|
218
|
+
milvus_url=_service_config.milvus_url,
|
|
219
|
+
embedding_base_url=_service_config.embedding_base_url,
|
|
220
|
+
embedding_api_key=_service_config.embedding_api_key,
|
|
221
|
+
embedding_model=_service_config.embedding_model,
|
|
222
|
+
embedding_timeout_seconds=_service_config.embedding_timeout_seconds,
|
|
223
|
+
embedding_max_retries=_service_config.embedding_max_retries,
|
|
224
|
+
embedding_retry_backoff_seconds=(
|
|
225
|
+
_service_config.embedding_retry_backoff_seconds
|
|
226
|
+
),
|
|
227
|
+
external_backend_timeout_seconds=(
|
|
228
|
+
_service_config.external_backend_timeout_seconds
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
result["run_id"] = run_id
|
|
232
|
+
if auth_subject:
|
|
233
|
+
result["auth_subject"] = auth_subject
|
|
234
|
+
if _service_config.trace_dir:
|
|
235
|
+
result = persist_runtime_worker_result(
|
|
236
|
+
run_id=run_id,
|
|
237
|
+
trace_dir=_service_config.trace_dir,
|
|
238
|
+
result=result,
|
|
239
|
+
persist_trace_fn=persist_trace,
|
|
240
|
+
)
|
|
241
|
+
return result
|
|
242
|
+
except Exception as exc:
|
|
243
|
+
if _service_config.trace_dir:
|
|
244
|
+
try:
|
|
245
|
+
persist_failed_runtime_trace(
|
|
246
|
+
run_id=run_id,
|
|
247
|
+
trace_dir=_service_config.trace_dir,
|
|
248
|
+
error_code=service_errors.AGENT_RUN_FAILED,
|
|
249
|
+
error=f"agent run failed: {exc}",
|
|
250
|
+
)
|
|
251
|
+
except (OSError, ValueError):
|
|
252
|
+
pass
|
|
253
|
+
raise
|
|
254
|
+
|
|
255
|
+
def cancel_timed_out_worker() -> None:
|
|
256
|
+
active_run = registry.mark_timed_out(
|
|
257
|
+
run_id,
|
|
258
|
+
reason="runtime run timed out",
|
|
259
|
+
)
|
|
260
|
+
if active_run is None or not _service_config.trace_dir:
|
|
261
|
+
return
|
|
262
|
+
try:
|
|
263
|
+
persist_cancelled_runtime_trace(
|
|
264
|
+
run_id=run_id,
|
|
265
|
+
trace_dir=_service_config.trace_dir,
|
|
266
|
+
active_run=active_run,
|
|
267
|
+
error_code=service_errors.AGENT_RUN_TIMEOUT,
|
|
268
|
+
error="agent run timed out",
|
|
269
|
+
)
|
|
270
|
+
except (OSError, ValueError):
|
|
271
|
+
return
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
result = run_with_timeout(
|
|
275
|
+
execute_runtime_worker,
|
|
276
|
+
timeout_seconds=_service_config.run_timeout_seconds,
|
|
277
|
+
on_timeout=cancel_timed_out_worker,
|
|
278
|
+
on_complete=lambda: registry.complete(run_id),
|
|
279
|
+
)
|
|
280
|
+
except TimeoutError:
|
|
281
|
+
timeout_payload = failure_payload(
|
|
282
|
+
service_errors.AGENT_RUN_TIMEOUT,
|
|
283
|
+
"agent run timed out",
|
|
284
|
+
)
|
|
285
|
+
timeout_payload["run_id"] = run_id
|
|
286
|
+
if trace_path:
|
|
287
|
+
timeout_payload["trace_path"] = trace_path
|
|
288
|
+
return 504, timeout_payload
|
|
289
|
+
except Exception:
|
|
290
|
+
failure = failure_payload(service_errors.AGENT_RUN_FAILED, "agent run failed")
|
|
291
|
+
failure["run_id"] = run_id
|
|
292
|
+
if trace_path:
|
|
293
|
+
failure["trace_path"] = trace_path
|
|
294
|
+
return 500, failure
|
|
295
|
+
return 200, json_ready(result)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _runtime_hooks(config: ServiceConfig) -> list[Any]:
|
|
299
|
+
hooks: list[Any] = []
|
|
300
|
+
if config.kafka_audit_url:
|
|
301
|
+
hooks.append(
|
|
302
|
+
KafkaRestAuditHook(
|
|
303
|
+
url=config.kafka_audit_url,
|
|
304
|
+
topic=config.kafka_audit_topic,
|
|
305
|
+
timeout_seconds=config.external_backend_timeout_seconds,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
return hooks
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _runtime_event_sink(config: ServiceConfig):
|
|
312
|
+
if not config.kafka_audit_url:
|
|
313
|
+
return None
|
|
314
|
+
return KafkaRestProgressEventSink(
|
|
315
|
+
url=config.kafka_audit_url,
|
|
316
|
+
topic=config.kafka_audit_topic,
|
|
317
|
+
timeout_seconds=config.external_backend_timeout_seconds,
|
|
318
|
+
fail_closed=True,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _planned_action_ids(
|
|
323
|
+
plan_payload: Any,
|
|
324
|
+
plan_sequence_payload: Any,
|
|
325
|
+
) -> set[str] | None:
|
|
326
|
+
plan_payloads = []
|
|
327
|
+
if isinstance(plan_payload, dict):
|
|
328
|
+
plan_payloads.append(plan_payload)
|
|
329
|
+
if isinstance(plan_sequence_payload, list):
|
|
330
|
+
plan_payloads.extend(
|
|
331
|
+
item for item in plan_sequence_payload if isinstance(item, dict)
|
|
332
|
+
)
|
|
333
|
+
if not plan_payloads:
|
|
334
|
+
return None
|
|
335
|
+
action_ids = set()
|
|
336
|
+
inspected_actions = False
|
|
337
|
+
for item in plan_payloads:
|
|
338
|
+
actions = item.get("actions")
|
|
339
|
+
if not isinstance(actions, list):
|
|
340
|
+
continue
|
|
341
|
+
inspected_actions = True
|
|
342
|
+
for action in actions:
|
|
343
|
+
if not isinstance(action, dict):
|
|
344
|
+
continue
|
|
345
|
+
action_id = action.get("id")
|
|
346
|
+
if isinstance(action_id, str) and action_id.strip() == action_id:
|
|
347
|
+
action_ids.add(action_id)
|
|
348
|
+
if not inspected_actions:
|
|
349
|
+
return None
|
|
350
|
+
return action_ids
|