kaos-cli 0.6.0__tar.gz → 0.7.0__tar.gz

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 (77) hide show
  1. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/PKG-INFO +1 -1
  2. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/__init__.py +1 -1
  3. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/__init__.py +79 -10
  4. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/deploy.py +24 -2
  5. kaos_cli-0.7.0/kaos_cli/agent/invoke.py +300 -0
  6. kaos_cli-0.7.0/kaos_cli/agent/memory.py +91 -0
  7. kaos_cli-0.7.0/kaos_cli/agent/tools.py +55 -0
  8. kaos_cli-0.7.0/kaos_cli/auth/__init__.py +79 -0
  9. kaos_cli-0.7.0/kaos_cli/auth/consent.py +193 -0
  10. kaos_cli-0.7.0/kaos_cli/auth/grant.py +155 -0
  11. kaos_cli-0.7.0/kaos_cli/auth/login.py +61 -0
  12. kaos_cli-0.7.0/kaos_cli/cluster_http.py +83 -0
  13. kaos_cli-0.7.0/kaos_cli/config.py +141 -0
  14. kaos_cli-0.7.0/kaos_cli/config_cli.py +56 -0
  15. kaos_cli-0.7.0/kaos_cli/install/__init__.py +540 -0
  16. kaos_cli-0.7.0/kaos_cli/install/auth.py +573 -0
  17. kaos_cli-0.7.0/kaos_cli/install/infra.py +268 -0
  18. kaos_cli-0.7.0/kaos_cli/install/observability.py +292 -0
  19. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/main.py +8 -0
  20. kaos_cli-0.7.0/kaos_cli/memory/__init__.py +399 -0
  21. kaos_cli-0.7.0/kaos_cli/memorystore/__init__.py +62 -0
  22. kaos_cli-0.7.0/kaos_cli/memorystore/create.py +72 -0
  23. kaos_cli-0.7.0/kaos_cli/samples/data/7-memory-agent.yaml +133 -0
  24. kaos_cli-0.7.0/kaos_cli/samples/data/8-authorization-walkthrough.yaml +78 -0
  25. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/system/__init__.py +253 -144
  26. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/system/create_rbac.py +3 -16
  27. kaos_cli-0.7.0/kaos_cli/system/status.py +55 -0
  28. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/utils/__init__.py +27 -1
  29. kaos_cli-0.7.0/kaos_cli/utils/port_forward.py +66 -0
  30. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/PKG-INFO +1 -1
  31. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/SOURCES.txt +24 -2
  32. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/pyproject.toml +1 -1
  33. kaos_cli-0.7.0/tests/test_agent_invoke.py +128 -0
  34. kaos_cli-0.7.0/tests/test_agent_tools.py +72 -0
  35. kaos_cli-0.7.0/tests/test_auth_cli.py +273 -0
  36. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/tests/test_cli_integration.py +954 -47
  37. kaos_cli-0.7.0/tests/test_config_cli.py +39 -0
  38. kaos_cli-0.7.0/tests/test_install_surface.py +97 -0
  39. kaos_cli-0.7.0/tests/test_memory_cli.py +345 -0
  40. kaos_cli-0.6.0/kaos_cli/agent/invoke.py +0 -128
  41. kaos_cli-0.6.0/kaos_cli/agent/memory.py +0 -119
  42. kaos_cli-0.6.0/kaos_cli/install.py +0 -1512
  43. kaos_cli-0.6.0/kaos_cli/samples/data/7-memory-agent.yaml +0 -97
  44. kaos_cli-0.6.0/kaos_cli/system/status.py +0 -82
  45. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/README.md +0 -0
  46. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/a2a.py +0 -0
  47. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/build.py +0 -0
  48. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/init.py +0 -0
  49. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/run.py +0 -0
  50. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/agent/status.py +0 -0
  51. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/__init__.py +0 -0
  52. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/build.py +0 -0
  53. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/crud.py +0 -0
  54. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/deploy.py +0 -0
  55. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/init.py +0 -0
  56. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/invoke.py +0 -0
  57. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/mcp/run.py +0 -0
  58. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/modelapi/__init__.py +0 -0
  59. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/modelapi/deploy.py +0 -0
  60. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/modelapi/invoke.py +0 -0
  61. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/proxy.py +0 -0
  62. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/__init__.py +0 -0
  63. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/1-simple-echo-agent.yaml +0 -0
  64. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/2-multi-agent-mcp.yaml +0 -0
  65. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/3-hierarchical-agents.yaml +0 -0
  66. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/4-dev-ollama-proxy-agent.yaml +0 -0
  67. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/5-proxy-external-api.yaml +0 -0
  68. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/samples/data/6-autonomous-monitor.yaml +0 -0
  69. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/system/install.py +0 -0
  70. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/system/runtimes.py +0 -0
  71. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/ui.py +0 -0
  72. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli/utils/crud.py +0 -0
  73. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/dependency_links.txt +0 -0
  74. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/entry_points.txt +0 -0
  75. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/requires.txt +0 -0
  76. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/kaos_cli.egg-info/top_level.txt +0 -0
  77. {kaos_cli-0.6.0 → kaos_cli-0.7.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaos-cli
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: CLI for KAOS (K8s Agent Orchestration System)
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -5,4 +5,4 @@ try:
5
5
 
6
6
  __version__ = version("kaos-cli")
7
7
  except Exception:
8
- __version__ = "0.5.1.dev0"
8
+ __version__ = "0.6.1.dev0"
@@ -12,6 +12,7 @@ from kaos_cli.agent.deploy import deploy_agent
12
12
  from kaos_cli.agent.invoke import invoke_command
13
13
  from kaos_cli.agent.status import status_command
14
14
  from kaos_cli.agent.memory import memory_command
15
+ from kaos_cli.agent.tools import tools_command
15
16
  from kaos_cli.agent.init import init_command
16
17
  from kaos_cli.agent.build import build_command
17
18
  from kaos_cli.agent.run import run_command
@@ -46,7 +47,9 @@ def build_agent(
46
47
  "agent:agent",
47
48
  help="Module:object target (default: agent:agent). No .py extension.",
48
49
  ),
49
- image: str = typer.Option(..., "--image", "-i", help="Image name with tag (e.g. my-agent:latest)."),
50
+ image: str = typer.Option(
51
+ ..., "--image", "-i", help="Image name with tag (e.g. my-agent:latest)."
52
+ ),
50
53
  directory: str = typer.Option(".", "--dir", "-d", help="Source directory."),
51
54
  kind_load: bool = typer.Option(
52
55
  False, "--kind-load", help="Load image to KIND cluster."
@@ -93,7 +96,9 @@ def run_agent(
93
96
  ),
94
97
  host: str = typer.Option("0.0.0.0", "--host", help="Bind host."),
95
98
  port: int = typer.Option(8000, "--port", "-p", help="Bind port."),
96
- reload: bool = typer.Option(False, "--reload", "-r", help="Auto-reload on changes."),
99
+ reload: bool = typer.Option(
100
+ False, "--reload", "-r", help="Auto-reload on changes."
101
+ ),
97
102
  ) -> None:
98
103
  """Run a Pydantic AI agent server locally."""
99
104
  run_command(target=target, host=host, port=port, reload=reload)
@@ -244,13 +249,28 @@ def deploy_agent_cmd(
244
249
  None, "--auto-interval", help="Seconds between autonomous iterations."
245
250
  ),
246
251
  task_max_iterations: int = typer.Option(
247
- None, "--task-max-iterations", help="Max iterations for A2A async tasks (0=unlimited)."
252
+ None,
253
+ "--task-max-iterations",
254
+ help="Max iterations for A2A async tasks (0=unlimited).",
248
255
  ),
249
256
  task_max_runtime: int = typer.Option(
250
- None, "--task-max-runtime", help="Max runtime in seconds for A2A async tasks (0=unlimited)."
257
+ None,
258
+ "--task-max-runtime",
259
+ help="Max runtime in seconds for A2A async tasks (0=unlimited).",
251
260
  ),
252
261
  task_max_tool_calls: int = typer.Option(
253
- None, "--task-max-tool-calls", help="Max cumulative tool calls for A2A async tasks (0=unlimited)."
262
+ None,
263
+ "--task-max-tool-calls",
264
+ help="Max cumulative tool calls for A2A async tasks (0=unlimited).",
265
+ ),
266
+ memory_store: str = typer.Option(
267
+ None, "--memory-store", help="MemoryStore reference."
268
+ ),
269
+ memory_max_read_scope: str = typer.Option(
270
+ None, "--memory-max-read-scope", help="Maximum memory read scope."
271
+ ),
272
+ memory_tools: str = typer.Option(
273
+ None, "--memory-tools", help="Memory tools to expose: read, write, or all."
254
274
  ),
255
275
  dry_run: bool = typer.Option(
256
276
  False, "--dry-run", help="Print YAML instead of deploying."
@@ -273,6 +293,17 @@ def deploy_agent_cmd(
273
293
  typer.echo("Error: --build requires --image to be set", err=True)
274
294
  sys.exit(1)
275
295
 
296
+ if not memory_store and any((memory_max_read_scope, memory_tools)):
297
+ typer.echo(
298
+ "Error: memory read scope and tools flags require --memory-store",
299
+ err=True,
300
+ )
301
+ raise typer.Exit(1)
302
+ if memory_tools not in (None, "read", "write", "all"):
303
+ raise typer.BadParameter(
304
+ "must be 'read', 'write', or 'all'", param_hint="--memory-tools"
305
+ )
306
+
276
307
  # Run build if requested
277
308
  if build is not None:
278
309
  build_target = build if build else "agent:agent"
@@ -308,6 +339,9 @@ def deploy_agent_cmd(
308
339
  task_max_iterations=task_max_iterations,
309
340
  task_max_runtime=task_max_runtime,
310
341
  task_max_tool_calls=task_max_tool_calls,
342
+ memory_store=memory_store,
343
+ memory_max_read_scope=memory_max_read_scope,
344
+ memory_tools=memory_tools,
311
345
  wait=wait,
312
346
  wait_timeout=wait_timeout,
313
347
  dry_run=dry_run,
@@ -318,7 +352,7 @@ def deploy_agent_cmd(
318
352
  def invoke_agent(
319
353
  name: str = typer.Argument(..., help="Name of the Agent."),
320
354
  message: str = typer.Option(
321
- ..., "--message", "-m", help="Message to send to the agent."
355
+ "Hello", "--message", "-m", help="Message to send to the agent."
322
356
  ),
323
357
  namespace: str = typer.Option(
324
358
  None,
@@ -338,10 +372,26 @@ def invoke_agent(
338
372
  "-s",
339
373
  help="Stream the response.",
340
374
  ),
375
+ user: str | None = typer.Option(
376
+ None,
377
+ "--user",
378
+ help="Invoke through the gateway with this user's cached token.",
379
+ ),
380
+ session: str = typer.Option(
381
+ None,
382
+ "--session",
383
+ help="Conversation session ID.",
384
+ ),
341
385
  ) -> None:
342
- """Send a message to an Agent via port-forward."""
386
+ """Send a message to an Agent directly or through the configured gateway."""
343
387
  invoke_command(
344
- name=name, namespace=namespace, message=message, port=port, stream=stream
388
+ name=name,
389
+ namespace=namespace,
390
+ message=message,
391
+ port=port,
392
+ stream=stream,
393
+ user=user,
394
+ session=session,
345
395
  )
346
396
 
347
397
 
@@ -367,7 +417,7 @@ def status_agent(
367
417
  ),
368
418
  ) -> None:
369
419
  """Get agent status and capabilities via agent card.
370
-
420
+
371
421
  Examples:
372
422
  kaos agent status my-agent
373
423
  kaos agent status my-agent --json
@@ -404,7 +454,7 @@ def memory_agent(
404
454
  ),
405
455
  ) -> None:
406
456
  """Get agent memory events.
407
-
457
+
408
458
  Examples:
409
459
  kaos agent memory my-agent
410
460
  kaos agent memory my-agent --json
@@ -417,3 +467,22 @@ def memory_agent(
417
467
  port=port,
418
468
  output_json=output_json,
419
469
  )
470
+
471
+
472
+ @app.command(name="tools")
473
+ def tools_agent(
474
+ name: str = typer.Argument(..., help="Name of the Agent."),
475
+ namespace: str = typer.Option(
476
+ None,
477
+ "--namespace",
478
+ "-n",
479
+ help="Namespace of the Agent.",
480
+ ),
481
+ output_json: bool = typer.Option(
482
+ False,
483
+ "--json",
484
+ help="Output in JSON format.",
485
+ ),
486
+ ) -> None:
487
+ """Show the tool definitions presented to the model."""
488
+ tools_command(name=name, namespace=namespace, output_json=output_json)
@@ -56,6 +56,9 @@ def deploy_agent(
56
56
  task_max_iterations: int | None = None,
57
57
  task_max_runtime: int | None = None,
58
58
  task_max_tool_calls: int | None = None,
59
+ memory_store: str | None = None,
60
+ memory_max_read_scope: str | None = None,
61
+ memory_tools: str | None = None,
59
62
  wait: bool = False,
60
63
  wait_timeout: int = DEFAULT_WAIT_TIMEOUT,
61
64
  dry_run: bool = False,
@@ -68,8 +71,19 @@ def deploy_agent(
68
71
  )
69
72
 
70
73
  # Add config section if description, instructions, telemetry, autonomous, or task budgets provided
71
- has_task_budgets = task_max_iterations is not None or task_max_runtime is not None or task_max_tool_calls is not None
72
- has_config = description or instructions or otel_endpoint or autonomous or has_task_budgets
74
+ has_task_budgets = (
75
+ task_max_iterations is not None
76
+ or task_max_runtime is not None
77
+ or task_max_tool_calls is not None
78
+ )
79
+ has_config = (
80
+ description
81
+ or instructions
82
+ or otel_endpoint
83
+ or autonomous
84
+ or has_task_budgets
85
+ or memory_store
86
+ )
73
87
  if has_config:
74
88
  yaml_content += " config:\n"
75
89
  if description:
@@ -98,6 +112,14 @@ def deploy_agent(
98
112
  yaml_content += f" maxRuntimeSeconds: {task_max_runtime}\n"
99
113
  if task_max_tool_calls is not None:
100
114
  yaml_content += f" maxToolCalls: {task_max_tool_calls}\n"
115
+ if memory_store:
116
+ yaml_content += " memory:\n"
117
+ yaml_content += " type: remote\n"
118
+ yaml_content += f" memoryStore: {memory_store}\n"
119
+ if memory_max_read_scope:
120
+ yaml_content += f" maxReadScope: {memory_max_read_scope}\n"
121
+ if memory_tools:
122
+ yaml_content += f" tools: {memory_tools}\n"
101
123
 
102
124
  # Add MCP servers if provided
103
125
  if mcp_servers:
@@ -0,0 +1,300 @@
1
+ """KAOS Agent invoke command."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import time
7
+ import signal
8
+ import typer
9
+
10
+ from kaos_cli.cluster_http import local_service_url
11
+ from kaos_cli.config import load_config, session_token
12
+ from kaos_cli.utils import current_context_namespace
13
+ from kaos_cli.auth.consent import (
14
+ active_service_alias,
15
+ reauth_url,
16
+ service_alias,
17
+ service_id_from_reauth_url,
18
+ )
19
+
20
+
21
+ REASON_TEXT = {
22
+ "platform_grant_missing": "not granted",
23
+ "user_grant_required": "user not in a granted group",
24
+ "access-control unreachable": "access-control unavailable (failing closed)",
25
+ "access_control_unreachable": "access-control unavailable (failing closed)",
26
+ "missing token": "no valid identity",
27
+ "missing_token": "no valid identity",
28
+ }
29
+
30
+
31
+ def plain_access_reason(reason: str) -> str:
32
+ """Translate gateway reason codes into concise user-facing text."""
33
+ normalized = reason.strip().lower()
34
+ if normalized in REASON_TEXT:
35
+ return REASON_TEXT[normalized]
36
+ if "access-control" in normalized and "unreachable" in normalized:
37
+ return REASON_TEXT["access-control unreachable"]
38
+ if "missing" in normalized and "token" in normalized:
39
+ return REASON_TEXT["missing token"]
40
+ return reason.replace("_", " ") or "request permitted"
41
+
42
+
43
+ def _response_content(response) -> str:
44
+ try:
45
+ data = response.json()
46
+ choices = data.get("choices", [])
47
+ if choices:
48
+ return choices[0].get("message", {}).get("content", "")
49
+ return json.dumps(data, indent=2)
50
+ except (ValueError, AttributeError):
51
+ return response.text
52
+
53
+
54
+ def _access_control_ready(namespace: str) -> bool:
55
+ result = subprocess.run(
56
+ [
57
+ "kubectl",
58
+ "get",
59
+ "deployment/kaos-pdp",
60
+ "-n",
61
+ namespace,
62
+ "-o",
63
+ "jsonpath={.status.readyReplicas}/{.spec.replicas}",
64
+ ],
65
+ capture_output=True,
66
+ text=True,
67
+ )
68
+ if result.returncode != 0:
69
+ return False
70
+ ready, _, desired = result.stdout.partition("/")
71
+ return bool(ready) and ready == desired and desired != "0"
72
+
73
+
74
+ def _invoke_gateway(
75
+ name: str, namespace: str | None, message: str, user: str | None, session: str | None = None
76
+ ) -> None:
77
+ """Invoke an agent through the configured gateway and print its verdict."""
78
+ import httpx
79
+
80
+ config = load_config()
81
+ address = config["gateway"].get("address", "").rstrip("/")
82
+ namespace = namespace or config.get("namespace") or "default"
83
+ if not address:
84
+ typer.echo("✗ denied — no gateway address configured")
85
+ return
86
+ token = session_token(config, user) if user else None
87
+ headers = {"Authorization": f"Bearer {token}"} if token else {}
88
+ if session:
89
+ headers["X-Session-ID"] = session
90
+ try:
91
+ with local_service_url(address) as local_address:
92
+ response = httpx.post(
93
+ f"{local_address}/{namespace}/agent/{name}/v1/chat/completions",
94
+ headers=headers,
95
+ json={"messages": [{"role": "user", "content": message}], "stream": False},
96
+ timeout=120.0,
97
+ )
98
+ except (httpx.HTTPError, RuntimeError) as exc:
99
+ typer.echo(f"Error: {exc}", err=True)
100
+ return
101
+ content = _response_content(response)
102
+ approval_url = reauth_url(response)
103
+ if approval_url:
104
+ service_id = service_id_from_reauth_url(approval_url)
105
+ try:
106
+ service = service_alias(config, service_id or "")
107
+ except (httpx.HTTPError, RuntimeError, ValueError):
108
+ service = "service"
109
+ typer.echo(
110
+ f"✗ needs approval — run: kaos auth connect {service} --user {user}"
111
+ )
112
+ return
113
+ if content:
114
+ typer.echo(content)
115
+ reason = response.headers.get("x-kaos-access-reason", "")
116
+ allowed = response.status_code < 400
117
+ if not reason:
118
+ if response.status_code in (401, 403) and not token:
119
+ reason = "missing token"
120
+ elif response.status_code in (401, 403) and not _access_control_ready(namespace):
121
+ reason = "access-control unreachable"
122
+ elif response.status_code in (401, 403) and token:
123
+ reason = "user_grant_required"
124
+ elif response.status_code >= 500:
125
+ reason = "access-control unreachable"
126
+ else:
127
+ reason = "request permitted" if allowed else f"HTTP {response.status_code}"
128
+ if allowed and user:
129
+ try:
130
+ service = active_service_alias(config, user, message)
131
+ except (httpx.HTTPError, RuntimeError, ValueError):
132
+ service = None
133
+ if service:
134
+ typer.echo(f"✓ allowed — acting as {user} on {service}")
135
+ return
136
+ mark = "✓ allowed" if allowed else "✗ denied"
137
+ typer.echo(f"{mark} — {plain_access_reason(reason)}")
138
+
139
+
140
+ def _is_autonomous(name: str, namespace: str | None) -> bool:
141
+ cmd = [
142
+ "kubectl",
143
+ "get",
144
+ "agent",
145
+ name,
146
+ "-o",
147
+ "jsonpath={.spec.config.autonomous.goal}",
148
+ ]
149
+ if namespace:
150
+ cmd.extend(["-n", namespace])
151
+ result = subprocess.run(cmd, capture_output=True, text=True)
152
+ return result.returncode == 0 and bool(result.stdout.strip())
153
+
154
+
155
+ def invoke_command(
156
+ name: str,
157
+ namespace: str | None,
158
+ message: str,
159
+ port: int,
160
+ stream: bool,
161
+ user: str | None = None,
162
+ session: str | None = None,
163
+ ) -> None:
164
+ """Send a message to an Agent via port-forward."""
165
+ import httpx
166
+
167
+ config = load_config()
168
+ direct_namespace = namespace or current_context_namespace()
169
+ if user or (
170
+ config["gateway"].get("through_gateway")
171
+ and not _is_autonomous(name, direct_namespace)
172
+ ):
173
+ _invoke_gateway(name, namespace, message, user, session)
174
+ return
175
+ namespace = direct_namespace
176
+
177
+ # Find the service for this Agent
178
+ cmd = [
179
+ "kubectl",
180
+ "get",
181
+ "svc",
182
+ f"agent-{name}",
183
+ "-o",
184
+ "jsonpath={.spec.ports[0].port}",
185
+ ]
186
+ if namespace:
187
+ cmd.extend(["-n", namespace])
188
+ result = subprocess.run(cmd, capture_output=True, text=True)
189
+
190
+ if result.returncode != 0:
191
+ typer.echo(f"Error: Agent '{name}' not found", err=True)
192
+ sys.exit(1)
193
+
194
+ svc_port = result.stdout.strip() or "8000"
195
+
196
+ typer.echo(f"Port-forwarding to agent-{name}:{svc_port}...")
197
+
198
+ # Start port-forward in background
199
+ pf_cmd = ["kubectl", "port-forward", f"svc/agent-{name}", f"{port}:{svc_port}"]
200
+ if namespace:
201
+ pf_cmd.extend(["-n", namespace])
202
+ pf_process = subprocess.Popen(
203
+ pf_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
204
+ )
205
+
206
+ def cleanup():
207
+ pf_process.terminate()
208
+ pf_process.wait()
209
+
210
+ signal.signal(signal.SIGINT, lambda s, f: (cleanup(), sys.exit(0)))
211
+
212
+ try:
213
+ last_error = "port-forward did not become ready"
214
+ for _ in range(90):
215
+ if pf_process.poll() is not None:
216
+ stderr = pf_process.stderr.read().decode() if pf_process.stderr else ""
217
+ typer.echo(f"Error: Port-forward failed: {stderr}", err=True)
218
+ sys.exit(1)
219
+ try:
220
+ health = httpx.get(f"http://localhost:{port}/health", timeout=2.0)
221
+ if health.status_code == 200:
222
+ break
223
+ last_error = f"HTTP {health.status_code}"
224
+ except httpx.HTTPError as exc:
225
+ last_error = str(exc)
226
+ time.sleep(1)
227
+ else:
228
+ typer.echo(f"Error: Could not connect to Agent: {last_error}", err=True)
229
+ sys.exit(1)
230
+
231
+ typer.echo(f"Sending message: {message}")
232
+ headers = {"X-Session-ID": session} if session else None
233
+
234
+ try:
235
+ if stream:
236
+ # Streaming response
237
+ with httpx.stream(
238
+ "POST",
239
+ f"http://localhost:{port}/v1/chat/completions",
240
+ json={
241
+ "messages": [{"role": "user", "content": message}],
242
+ "stream": True,
243
+ },
244
+ headers=headers,
245
+ timeout=120.0,
246
+ ) as response:
247
+ typer.echo("\n📤 Response:")
248
+ for line in response.iter_lines():
249
+ if line.startswith("data: "):
250
+ data = line[6:]
251
+ if data != "[DONE]":
252
+ try:
253
+ chunk = json.loads(data)
254
+ if "choices" in chunk and chunk["choices"]:
255
+ delta = chunk["choices"][0].get("delta", {})
256
+ # Handle reasoning content (displayed before regular content)
257
+ if "content" in delta:
258
+ content = delta["content"]
259
+ if content.startswith("{"):
260
+ content = json.loads(content)
261
+ typer.echo(f'Reasoning step {content["step"]}/{content["max_steps"]} | Action: {content["action"]} | Target: {content["target"]}')
262
+ else:
263
+ typer.echo(content, nl=False)
264
+ except json.JSONDecodeError:
265
+ pass
266
+ typer.echo("") # Final newline
267
+ else:
268
+ # Non-streaming
269
+ response = httpx.post(
270
+ f"http://localhost:{port}/v1/chat/completions",
271
+ json={
272
+ "messages": [{"role": "user", "content": message}],
273
+ "stream": False,
274
+ },
275
+ headers=headers,
276
+ timeout=120.0,
277
+ )
278
+
279
+ if response.status_code == 200:
280
+ result = response.json()
281
+ if "choices" in result and result["choices"]:
282
+ content = (
283
+ result["choices"][0].get("message", {}).get("content", "")
284
+ )
285
+ typer.echo("\n📤 Response:")
286
+ typer.echo(content)
287
+ else:
288
+ typer.echo(json.dumps(result, indent=2))
289
+ typer.echo("✓ allowed — request permitted")
290
+ else:
291
+ typer.echo(
292
+ f"Error: HTTP {response.status_code}: {response.text}", err=True
293
+ )
294
+ except httpx.ConnectError:
295
+ typer.echo("Error: Could not connect to Agent", err=True)
296
+ sys.exit(1)
297
+ except Exception as e:
298
+ typer.echo(f"Error: {e}", err=True)
299
+ finally:
300
+ cleanup()
@@ -0,0 +1,91 @@
1
+ """KAOS Agent memory command - get agent memory events."""
2
+
3
+ import json
4
+ import subprocess
5
+ import sys
6
+ import typer
7
+
8
+ from kaos_cli.utils.port_forward import PortForwardError, port_forward
9
+
10
+
11
+ def memory_command(
12
+ name: str,
13
+ namespace: str | None,
14
+ session_id: str | None,
15
+ port: int,
16
+ output_json: bool,
17
+ ) -> None:
18
+ """Get agent memory events via port-forward."""
19
+ import httpx
20
+
21
+ # Find the service for this Agent
22
+ cmd = [
23
+ "kubectl",
24
+ "get",
25
+ "svc",
26
+ f"agent-{name}",
27
+ "-o",
28
+ "jsonpath={.spec.ports[0].port}",
29
+ ]
30
+ if namespace:
31
+ cmd.extend(["-n", namespace])
32
+ result = subprocess.run(cmd, capture_output=True, text=True)
33
+
34
+ if result.returncode != 0:
35
+ typer.echo(f"Error: Agent '{name}' not found", err=True)
36
+ sys.exit(1)
37
+
38
+ svc_port = result.stdout.strip() or "8000"
39
+
40
+ try:
41
+ with port_forward(
42
+ f"svc/agent-{name}",
43
+ int(svc_port),
44
+ namespace,
45
+ "/health",
46
+ local_port=port,
47
+ ) as base_url:
48
+ url = f"{base_url}/memory/events"
49
+ if session_id:
50
+ url += f"?session_id={session_id}"
51
+
52
+ try:
53
+ response = httpx.get(url, timeout=10.0)
54
+ response.raise_for_status()
55
+ memory = response.json()
56
+ except httpx.HTTPError as exc:
57
+ typer.echo(f"Error: Failed to get memory events: {exc}", err=True)
58
+ raise typer.Exit(1)
59
+
60
+ if output_json:
61
+ typer.echo(json.dumps(memory, indent=2))
62
+ else:
63
+ events = memory.get("events", [])
64
+ typer.echo(f"Memory Events: {len(events)} total")
65
+
66
+ event_types: dict[str, int] = {}
67
+ for event in events:
68
+ etype = event.get("event_type", "unknown")
69
+ event_types[etype] = event_types.get(etype, 0) + 1
70
+
71
+ if event_types:
72
+ typer.echo("Event Types:")
73
+ for etype, count in sorted(event_types.items()):
74
+ typer.echo(f" - {etype}: {count}")
75
+
76
+ if events:
77
+ typer.echo("\nAll Events:")
78
+ for event in events:
79
+ etype = event.get("event_type", "unknown")
80
+ content = event.get("content", "")
81
+ if isinstance(content, dict):
82
+ content = json.dumps(content)
83
+ typer.echo(f" [{etype}] {content}")
84
+ except PortForwardError as exc:
85
+ typer.echo(f"Error: Port-forward failed: {exc}", err=True)
86
+ raise typer.Exit(1)
87
+ except typer.Exit:
88
+ raise
89
+ except Exception as e:
90
+ typer.echo(f"Error: {e}", err=True)
91
+ raise typer.Exit(1)
@@ -0,0 +1,55 @@
1
+ """Inspect the tool definitions exposed by a deployed Agent."""
2
+
3
+ import json
4
+ import subprocess
5
+
6
+ import httpx
7
+ import typer
8
+
9
+ from kaos_cli.utils.port_forward import PortForwardError, port_forward
10
+
11
+
12
+ def tools_command(name: str, namespace: str | None, output_json: bool) -> None:
13
+ """Fetch and print an Agent's model-facing tool definitions."""
14
+ cmd = [
15
+ "kubectl",
16
+ "get",
17
+ "svc",
18
+ f"agent-{name}",
19
+ "-o",
20
+ "jsonpath={.spec.ports[0].port}",
21
+ ]
22
+ if namespace:
23
+ cmd.extend(["-n", namespace])
24
+ result = subprocess.run(cmd, capture_output=True, text=True)
25
+ if result.returncode != 0:
26
+ typer.echo(f"Error: Agent '{name}' not found", err=True)
27
+ raise typer.Exit(1)
28
+
29
+ try:
30
+ with port_forward(
31
+ f"svc/agent-{name}",
32
+ int(result.stdout.strip() or "8000"),
33
+ namespace,
34
+ "/health",
35
+ ) as base_url:
36
+ response = httpx.get(f"{base_url}/tools", timeout=30.0)
37
+ response.raise_for_status()
38
+ data = response.json()
39
+ except (PortForwardError, httpx.HTTPError, json.JSONDecodeError) as exc:
40
+ typer.echo(f"Error: Could not list Agent tools: {exc}", err=True)
41
+ raise typer.Exit(1)
42
+
43
+ if output_json:
44
+ typer.echo(json.dumps(data, indent=2))
45
+ return
46
+
47
+ tools = data.get("tools", [])
48
+ typer.echo(f"Tools for {data.get('agent', name)}: {len(tools)}")
49
+ for tool in tools:
50
+ typer.echo(f"\n{tool.get('name', '')}")
51
+ if tool.get("description"):
52
+ typer.echo(f" {tool['description']}")
53
+ schema = json.dumps(tool.get("parameters_json_schema", {}), indent=2)
54
+ typer.echo(" Schema:")
55
+ typer.echo("\n".join(f" {line}" for line in schema.splitlines()))