@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,685 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import contextlib
5
+ import getpass
6
+ import io
7
+ import sys
8
+ import warnings
9
+ from typing import Any
10
+
11
+ from kagent.cli.interactive import (
12
+ run_runtime_interactive as _run_runtime_interactive,
13
+ )
14
+ from kagent.cli.memory import default_runtime_session_memory_path
15
+ from kagent.cli.provider import RuntimeProviderConfigError, runtime_provider_config_message
16
+ from kagent.cli.trace import (
17
+ persist_runtime_cli_trace,
18
+ )
19
+ from kagent.cli.trace import (
20
+ persist_runtime_cli_trace_or_raise as _persist_runtime_cli_trace_or_raise,
21
+ )
22
+ from kagent.cli.ui import runtime_setup_message, runtime_ui_color_enabled
23
+ from kagent.runtime.metadata import (
24
+ validate_runtime_metadata,
25
+ validate_runtime_tags,
26
+ )
27
+ from kagent.utils.json_output import format_and_write_json, json_ready
28
+
29
+ DEFAULT_RUNTIME_MAX_ITERATIONS = 3
30
+
31
+
32
+ def main() -> None:
33
+ warnings.filterwarnings("ignore")
34
+
35
+ parser = argparse.ArgumentParser(description="Run the kagent.")
36
+ parser.add_argument(
37
+ "goal",
38
+ nargs="?",
39
+ help="Goal for the agent, for example: 'calculate 2 + 3'",
40
+ )
41
+ parser.add_argument("--max-steps", type=int, default=None)
42
+ parser.add_argument("--max-retries", type=int, default=None)
43
+ parser.add_argument(
44
+ "--runtime",
45
+ action="store_true",
46
+ help="Run the plan-act-observe runtime.",
47
+ )
48
+ parser.add_argument(
49
+ "--deterministic",
50
+ "--legacy-graph",
51
+ action="store_true",
52
+ help="Run the deterministic regression graph instead of the default runtime.",
53
+ )
54
+ parser.add_argument(
55
+ "--interactive",
56
+ action="store_true",
57
+ help="Start the kagent terminal agent that reads goals from stdin.",
58
+ )
59
+ parser.add_argument(
60
+ "--interactive-json",
61
+ action="store_true",
62
+ help="Print full JSON traces in interactive runtime sessions.",
63
+ )
64
+ parser.add_argument(
65
+ "--session-memory",
66
+ default="",
67
+ metavar="PATH",
68
+ help=(
69
+ "Persist interactive runtime session memory to PATH. "
70
+ "Only valid with --runtime --interactive."
71
+ ),
72
+ )
73
+ parser.add_argument(
74
+ "--max-iterations",
75
+ type=int,
76
+ default=None,
77
+ help="Maximum runtime planner iterations.",
78
+ )
79
+ parser.add_argument(
80
+ "--runtime-plan",
81
+ default="",
82
+ metavar="JSON",
83
+ help="Use an inline strict runtime plan JSON payload instead of an LLM provider.",
84
+ )
85
+ parser.add_argument(
86
+ "--tag",
87
+ action="append",
88
+ default=[],
89
+ metavar="TAG",
90
+ help="Attach a non-secret runtime tag; may be repeated.",
91
+ )
92
+ parser.add_argument(
93
+ "--metadata",
94
+ action="append",
95
+ default=[],
96
+ metavar="KEY=VALUE",
97
+ help="Attach non-secret runtime metadata; may be repeated.",
98
+ )
99
+ parser.add_argument(
100
+ "--trace-dir",
101
+ default="",
102
+ metavar="PATH",
103
+ help="Persist one-shot runtime traces to PATH using run_id-based filenames.",
104
+ )
105
+ parser.add_argument(
106
+ "--list-tools",
107
+ action="store_true",
108
+ help="Print the registered deterministic tool names as JSON.",
109
+ )
110
+ parser.add_argument(
111
+ "--verbose",
112
+ action="store_true",
113
+ help="Print richer metadata for discovery commands that support it.",
114
+ )
115
+ parser.add_argument(
116
+ "--list-faults",
117
+ action="store_true",
118
+ help="Print supported fault injection names as JSON.",
119
+ )
120
+ parser.add_argument(
121
+ "--graph",
122
+ action="store_true",
123
+ help="Print the graph topology as JSON.",
124
+ )
125
+ parser.add_argument(
126
+ "--version",
127
+ action="store_true",
128
+ help="Print the package version as JSON.",
129
+ )
130
+ parser.add_argument(
131
+ "--configure",
132
+ action="store_true",
133
+ help="Configure the local OpenAI-compatible runtime provider.",
134
+ )
135
+ parser.add_argument(
136
+ "--inject-wrong-answer",
137
+ action="append",
138
+ default=[],
139
+ metavar="STEP",
140
+ help="Force one wrong answer for STEP to demonstrate reflection and retry.",
141
+ )
142
+ parser.add_argument(
143
+ "--inject-fault",
144
+ action="append",
145
+ default=[],
146
+ metavar="STEP=FAULT",
147
+ help="Force a named fault for STEP, such as STEP=empty-answer.",
148
+ )
149
+ parser.add_argument(
150
+ "--summary",
151
+ action="store_true",
152
+ help="Print a compact run summary instead of the full trace.",
153
+ )
154
+ parser.add_argument(
155
+ "--plan",
156
+ action="store_true",
157
+ help="Print planner output and validation without executing the graph.",
158
+ )
159
+ parser.add_argument(
160
+ "--fail-on-agent-failure",
161
+ action="store_true",
162
+ help="Exit with code 1 when an executed agent run returns failed status.",
163
+ )
164
+ parser.add_argument(
165
+ "--output",
166
+ default="",
167
+ metavar="PATH",
168
+ help="Write the JSON payload to PATH as well as stdout.",
169
+ )
170
+ args = parser.parse_args()
171
+ _apply_default_cli_mode(args)
172
+
173
+ if args.max_steps is not None and args.max_steps < 1:
174
+ parser.error("--max-steps must be at least 1")
175
+ if args.max_retries is not None and args.max_retries < 0:
176
+ parser.error("--max-retries must be non-negative")
177
+ if args.max_iterations is not None and args.max_iterations < 1:
178
+ parser.error("--max-iterations must be at least 1")
179
+ if args.deterministic and args.runtime_plan:
180
+ parser.error("--deterministic cannot be combined with --runtime-plan")
181
+ if args.deterministic:
182
+ args.runtime = False
183
+ if args.deterministic and args.interactive:
184
+ parser.error("--deterministic cannot be combined with --interactive")
185
+ if args.runtime and args.plan:
186
+ parser.error("--plan is not supported with --runtime")
187
+ if args.interactive_json and not args.interactive:
188
+ parser.error("--interactive-json requires --interactive")
189
+ if args.configure and args.goal is not None:
190
+ parser.error("--configure cannot be combined with a goal")
191
+ if args.session_memory and not args.interactive:
192
+ parser.error("--session-memory requires --interactive")
193
+ if args.interactive and args.output:
194
+ parser.error("--output is not supported with --interactive")
195
+ if args.trace_dir and not args.runtime:
196
+ parser.error("--trace-dir requires --runtime")
197
+ if (args.tag or args.metadata) and not args.runtime:
198
+ parser.error("--tag and --metadata require --runtime")
199
+ runtime_metadata, runtime_tags = _runtime_labels_from_args(
200
+ args.metadata,
201
+ args.tag,
202
+ parser,
203
+ )
204
+
205
+ if args.goal is not None:
206
+ try:
207
+ fault_plan = _build_fault_plan(args.inject_wrong_answer, args.inject_fault)
208
+ except ValueError as exc:
209
+ parser.error(str(exc))
210
+ else:
211
+ fault_plan = {}
212
+
213
+ warning_sink = io.StringIO()
214
+ config_error = ""
215
+ result = {}
216
+ with contextlib.redirect_stderr(warning_sink), warnings.catch_warnings():
217
+ warnings.simplefilter("ignore")
218
+ from kagent import __version__
219
+ from kagent.core.agent import (
220
+ AgentConfig,
221
+ agent_topology,
222
+ preview_plan,
223
+ run_agent,
224
+ )
225
+ from kagent.core.faults import SUPPORTED_FAULTS
226
+ from kagent.core.summary import summarize_run
227
+ from kagent.core.tools import (
228
+ registered_tool_metadata,
229
+ registered_tool_names,
230
+ )
231
+ from kagent.providers.llm import (
232
+ DEFAULT_LLM_MODEL,
233
+ FakeLLMProvider,
234
+ LLMProviderConfig,
235
+ build_llm_provider,
236
+ default_provider_config_path,
237
+ save_provider_config,
238
+ )
239
+ from kagent.runtime import run_runtime_agent, runtime_topology
240
+ from kagent.runtime.tools import (
241
+ registered_runtime_tool_metadata,
242
+ )
243
+ from kagent.service.trace_store import persist_trace
244
+
245
+ if args.list_tools:
246
+ if args.runtime:
247
+ runtime_tools = registered_runtime_tool_metadata()
248
+ tools = (
249
+ runtime_tools
250
+ if args.verbose
251
+ else [tool["name"] for tool in runtime_tools]
252
+ )
253
+ else:
254
+ tools = registered_tool_metadata() if args.verbose else registered_tool_names()
255
+ _emit_json_payload({"tools": tools}, args.output, parser)
256
+ return
257
+
258
+ if args.list_faults:
259
+ _emit_json_payload({"faults": sorted(SUPPORTED_FAULTS)}, args.output, parser)
260
+ return
261
+
262
+ if args.graph:
263
+ topology = runtime_topology() if args.runtime else agent_topology()
264
+ _emit_json_payload(topology, args.output, parser)
265
+ return
266
+
267
+ if args.version:
268
+ _emit_json_payload({"version": __version__}, args.output, parser)
269
+ return
270
+
271
+ if args.configure:
272
+ _configure_runtime_provider_interactively(
273
+ LLMProviderConfig,
274
+ default_model=DEFAULT_LLM_MODEL,
275
+ default_config_path=default_provider_config_path,
276
+ save_config=save_provider_config,
277
+ )
278
+ return
279
+
280
+ if args.interactive:
281
+ try:
282
+ session_memory_path = _session_memory_path_from_args(
283
+ args,
284
+ interactive_tty=sys.stdin.isatty(),
285
+ )
286
+ provider = (
287
+ _runtime_provider_from_args(
288
+ args,
289
+ FakeLLMProvider,
290
+ build_llm_provider,
291
+ LLMProviderConfig,
292
+ interactive_setup=sys.stdin.isatty(),
293
+ default_model=DEFAULT_LLM_MODEL,
294
+ default_config_path=default_provider_config_path,
295
+ save_config=save_provider_config,
296
+ )
297
+ )
298
+ _run_runtime_interactive(
299
+ provider=provider,
300
+ run_runtime_agent=run_runtime_agent,
301
+ max_iterations=args.max_iterations or DEFAULT_RUNTIME_MAX_ITERATIONS,
302
+ fail_on_agent_failure=args.fail_on_agent_failure,
303
+ full_trace_output=args.interactive_json,
304
+ metadata=runtime_metadata,
305
+ tags=runtime_tags,
306
+ trace_dir=args.trace_dir,
307
+ persist_trace=persist_trace,
308
+ session_memory_path=session_memory_path,
309
+ )
310
+ return
311
+ except RuntimeProviderConfigError as exc:
312
+ _exit_runtime_provider_config_error(str(exc))
313
+ except ValueError as exc:
314
+ config_error = str(exc)
315
+ except OSError as exc:
316
+ config_error = f"could not use session memory: {exc}"
317
+ elif args.goal is None:
318
+ parser.error(
319
+ "goal is required unless --interactive, --list-tools, "
320
+ "--list-faults, --graph, or --version is used"
321
+ )
322
+
323
+ elif args.runtime:
324
+ try:
325
+ provider = (
326
+ _runtime_provider_from_args(
327
+ args,
328
+ FakeLLMProvider,
329
+ build_llm_provider,
330
+ LLMProviderConfig,
331
+ interactive_setup=sys.stdin.isatty(),
332
+ default_model=DEFAULT_LLM_MODEL,
333
+ default_config_path=default_provider_config_path,
334
+ save_config=save_provider_config,
335
+ )
336
+ )
337
+ result = run_runtime_agent(
338
+ args.goal,
339
+ provider=provider,
340
+ max_iterations=args.max_iterations or DEFAULT_RUNTIME_MAX_ITERATIONS,
341
+ metadata=runtime_metadata,
342
+ tags=runtime_tags,
343
+ )
344
+ if args.trace_dir:
345
+ _persist_runtime_cli_trace_or_raise(result, args.trace_dir, persist_trace)
346
+ except RuntimeProviderConfigError as exc:
347
+ _exit_runtime_provider_config_error(str(exc))
348
+ except ValueError as exc:
349
+ config_error = str(exc)
350
+ except OSError as exc:
351
+ config_error = f"could not persist --trace-dir trace: {exc}"
352
+ else:
353
+ try:
354
+ config = _config_from_args(AgentConfig, args.max_steps, args.max_retries)
355
+ except ValueError as exc:
356
+ config_error = str(exc)
357
+ else:
358
+ if args.plan:
359
+ result = preview_plan(args.goal, config=config)
360
+ else:
361
+ result = run_agent(
362
+ args.goal,
363
+ config=config,
364
+ fault_plan=fault_plan,
365
+ )
366
+ if args.summary and not args.plan:
367
+ result = summarize_run(result)
368
+ if config_error:
369
+ parser.error(config_error)
370
+ payload = json_ready(result)
371
+ _emit_json_payload(payload, args.output, parser)
372
+ if args.fail_on_agent_failure and not args.plan and payload.get("status") == "failed":
373
+ raise SystemExit(1)
374
+
375
+
376
+ def _runtime_provider_from_args(
377
+ args: argparse.Namespace,
378
+ FakeLLMProvider,
379
+ build_llm_provider,
380
+ LLMProviderConfig,
381
+ *,
382
+ interactive_setup: bool = False,
383
+ default_model: str = "qwen3.5-122b-a10b",
384
+ default_config_path=None,
385
+ save_config=None,
386
+ ):
387
+ from kagent.providers.llm import missing_provider_config_fields
388
+
389
+ if args.runtime_plan:
390
+ return FakeLLMProvider(args.runtime_plan)
391
+ config = LLMProviderConfig.from_sources()
392
+ missing = missing_provider_config_fields(config)
393
+ if missing:
394
+ if interactive_setup and default_config_path is not None and save_config is not None:
395
+ config = _configure_runtime_provider_interactively(
396
+ LLMProviderConfig,
397
+ default_model=default_model,
398
+ default_config_path=default_config_path,
399
+ save_config=save_config,
400
+ )
401
+ missing = missing_provider_config_fields(config)
402
+ if not missing:
403
+ return build_llm_provider(config)
404
+ raise RuntimeProviderConfigError(runtime_provider_config_message(missing))
405
+ return build_llm_provider(config)
406
+
407
+
408
+ def _configure_runtime_provider_interactively(
409
+ LLMProviderConfig,
410
+ *,
411
+ default_model: str,
412
+ default_config_path,
413
+ save_config,
414
+ input_fn=input,
415
+ secret_input_fn=getpass.getpass,
416
+ ) -> object:
417
+ prompt_stream = sys.__stderr__ or sys.stderr
418
+ config_path = default_config_path()
419
+ print(
420
+ runtime_setup_message(
421
+ config_path=config_path,
422
+ color=runtime_ui_color_enabled(),
423
+ ),
424
+ file=prompt_stream,
425
+ )
426
+ provider_option = _select_provider_for_setup(
427
+ default_model=default_model,
428
+ input_fn=input_fn,
429
+ prompt_stream=prompt_stream,
430
+ )
431
+ provider = provider_option["provider"]
432
+ default_base_url = str(provider_option["base_url"])
433
+ default_provider_model = str(provider_option["model"])
434
+ base_url_prompt = (
435
+ f"Base URL [{default_base_url}]: " if default_base_url else "Base URL: "
436
+ )
437
+ base_url = input_fn(base_url_prompt).strip() or default_base_url
438
+ model = (
439
+ input_fn(f"Model [{default_provider_model}]: ").strip()
440
+ or default_provider_model
441
+ )
442
+ api_key = secret_input_fn("API key: ").strip()
443
+ config = LLMProviderConfig(
444
+ provider=provider,
445
+ base_url=base_url,
446
+ api_key=api_key,
447
+ model=model,
448
+ )
449
+ from kagent.providers.llm import validate_provider_setup_config
450
+
451
+ validate_provider_setup_config(config)
452
+ saved_path = save_config(config)
453
+ print(f"kagent provider config saved to {saved_path}", file=prompt_stream)
454
+ return config
455
+
456
+
457
+ def _select_provider_for_setup(
458
+ *,
459
+ default_model: str,
460
+ input_fn,
461
+ prompt_stream,
462
+ ) -> dict[str, object]:
463
+ options = _provider_setup_options(default_model)
464
+ if _can_use_arrow_provider_menu(input_fn, prompt_stream):
465
+ return _select_provider_with_arrow_keys(options, prompt_stream)
466
+ print("Select provider:", file=prompt_stream)
467
+ for index, option in enumerate(options, start=1):
468
+ print(
469
+ f" {index}. {option['label']} ({option['provider'].value})",
470
+ file=prompt_stream,
471
+ )
472
+ answer = input_fn("Provider [1]: ").strip()
473
+ if not answer:
474
+ return options[0]
475
+ try:
476
+ selected_index = int(answer)
477
+ except ValueError as exc:
478
+ raise ValueError("provider selection must be a number") from exc
479
+ if selected_index < 1 or selected_index > len(options):
480
+ raise ValueError("provider selection is out of range")
481
+ return options[selected_index - 1]
482
+
483
+
484
+ def _provider_setup_options(default_model: str) -> list[dict[str, object]]:
485
+ from kagent.providers.llm import provider_setup_options
486
+
487
+ return provider_setup_options(default_model)
488
+
489
+
490
+ def _can_use_arrow_provider_menu(input_fn, prompt_stream) -> bool:
491
+ return (
492
+ input_fn is input
493
+ and sys.stdin.isatty()
494
+ and hasattr(prompt_stream, "isatty")
495
+ and prompt_stream.isatty()
496
+ )
497
+
498
+
499
+ def _select_provider_with_arrow_keys(
500
+ options: list[dict[str, object]],
501
+ prompt_stream,
502
+ ) -> dict[str, object]:
503
+ import termios
504
+ import tty
505
+
506
+ input_stream = sys.stdin
507
+ fd = input_stream.fileno()
508
+ old_settings = termios.tcgetattr(fd)
509
+ selected = 0
510
+ print("Select provider with Up/Down, Enter to confirm:", file=prompt_stream)
511
+
512
+ def render() -> None:
513
+ print(f"\x1b[{len(options)}A", end="", file=prompt_stream)
514
+ for index, option in enumerate(options):
515
+ marker = ">" if index == selected else " "
516
+ print(
517
+ f"\x1b[2K\r {marker} {option['label']} ({option['provider'].value})",
518
+ file=prompt_stream,
519
+ )
520
+ prompt_stream.flush()
521
+
522
+ try:
523
+ tty.setcbreak(fd)
524
+ print("\x1b[?25l", end="", file=prompt_stream)
525
+ for index, option in enumerate(options):
526
+ marker = ">" if index == selected else " "
527
+ print(
528
+ f" {marker} {option['label']} ({option['provider'].value})",
529
+ file=prompt_stream,
530
+ )
531
+ prompt_stream.flush()
532
+ while True:
533
+ char = input_stream.read(1)
534
+ if char in {"\r", "\n"}:
535
+ break
536
+ if char == "\x03":
537
+ raise KeyboardInterrupt
538
+ if char != "\x1b":
539
+ continue
540
+ sequence = input_stream.read(2)
541
+ if sequence == "[A":
542
+ selected = (selected - 1) % len(options)
543
+ render()
544
+ elif sequence == "[B":
545
+ selected = (selected + 1) % len(options)
546
+ render()
547
+ finally:
548
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
549
+ print("\x1b[?25h", end="", file=prompt_stream)
550
+ print(file=prompt_stream)
551
+ return options[selected]
552
+
553
+
554
+ def _exit_runtime_provider_config_error(message: str) -> None:
555
+ print(message, file=sys.__stderr__)
556
+ raise SystemExit(2)
557
+
558
+
559
+ def _apply_default_cli_mode(args: argparse.Namespace) -> None:
560
+ if getattr(args, "configure", False):
561
+ return
562
+ if _uses_deterministic_graph(args):
563
+ args.deterministic = True
564
+ return
565
+ if args.runtime_plan:
566
+ args.runtime = True
567
+ if args.interactive:
568
+ args.runtime = True
569
+ if args.goal is not None and not _is_introspection_command(args):
570
+ args.runtime = True
571
+ if args.goal is None and not _is_introspection_command(args):
572
+ args.runtime = True
573
+ args.interactive = True
574
+
575
+
576
+ def _uses_deterministic_graph(args: argparse.Namespace) -> bool:
577
+ return bool(
578
+ args.deterministic
579
+ or args.plan
580
+ or args.summary
581
+ or args.max_steps is not None
582
+ or args.max_retries is not None
583
+ or args.inject_wrong_answer
584
+ or args.inject_fault
585
+ )
586
+
587
+
588
+ def _is_introspection_command(args: argparse.Namespace) -> bool:
589
+ return bool(
590
+ args.list_tools
591
+ or args.list_faults
592
+ or args.graph
593
+ or args.version
594
+ or getattr(args, "configure", False)
595
+ )
596
+
597
+
598
+ def _session_memory_path_from_args(
599
+ args: argparse.Namespace,
600
+ *,
601
+ interactive_tty: bool,
602
+ ) -> str:
603
+ if args.session_memory:
604
+ return args.session_memory
605
+ if not interactive_tty:
606
+ return ""
607
+ return default_runtime_session_memory_path()
608
+
609
+
610
+ def _runtime_labels_from_args(
611
+ metadata_items: list[str],
612
+ tag_items: list[str],
613
+ parser: argparse.ArgumentParser,
614
+ ) -> tuple[dict[str, str], list[str]]:
615
+ raw_metadata = {}
616
+ for item in metadata_items:
617
+ if "=" not in item:
618
+ parser.error("--metadata must use KEY=VALUE")
619
+ key, value = item.split("=", 1)
620
+ raw_metadata[key] = value
621
+ metadata, metadata_error = validate_runtime_metadata(raw_metadata or None)
622
+ if metadata_error:
623
+ parser.error(metadata_error)
624
+ tags, tags_error = validate_runtime_tags(tag_items or None)
625
+ if tags_error:
626
+ parser.error(tags_error)
627
+ return metadata, tags
628
+
629
+
630
+ def _persist_runtime_cli_trace(
631
+ result: dict,
632
+ trace_dir: str,
633
+ persist_trace: Any,
634
+ ) -> None:
635
+ persist_runtime_cli_trace(result, trace_dir, persist_trace)
636
+
637
+
638
+ def _emit_json_payload(
639
+ payload: Any,
640
+ output_path: str,
641
+ parser: argparse.ArgumentParser,
642
+ ) -> None:
643
+ try:
644
+ json_payload = format_and_write_json(json_ready(payload), output_path)
645
+ except OSError as exc:
646
+ parser.error(f"could not write --output file: {exc}")
647
+ print(json_payload)
648
+
649
+
650
+ def _build_fault_plan(injected_steps: list, injected_faults: list) -> dict:
651
+ fault_plan = {}
652
+ for step in injected_steps:
653
+ _append_fault(fault_plan, step, "wrong-answer")
654
+ for item in injected_faults:
655
+ step, fault = _parse_fault(item)
656
+ _append_fault(fault_plan, step, fault)
657
+ return fault_plan
658
+
659
+
660
+ def _config_from_args(AgentConfig, max_steps, max_retries):
661
+ config = AgentConfig.from_env()
662
+ return AgentConfig(
663
+ max_steps=max_steps if max_steps is not None else config.max_steps,
664
+ max_retries=max_retries if max_retries is not None else config.max_retries,
665
+ )
666
+
667
+
668
+ def _append_fault(fault_plan: dict, step: str, fault: str) -> None:
669
+ from kagent.core.faults import validate_faults
670
+ from kagent.core.normalization import normalize_goal
671
+
672
+ validate_faults([fault])
673
+ normalized_step = normalize_goal(step)
674
+ fault_plan.setdefault(normalized_step, []).append(fault)
675
+
676
+
677
+ def _parse_fault(item: str) -> tuple:
678
+ if "=" not in item:
679
+ raise ValueError("--inject-fault must use STEP=FAULT")
680
+ step, fault = item.rsplit("=", 1)
681
+ return step, fault.strip()
682
+
683
+
684
+ if __name__ == "__main__":
685
+ main()