@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,841 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import queue
|
|
6
|
+
import shlex
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from kagent.cli.commands import (
|
|
13
|
+
is_runtime_interactive_command,
|
|
14
|
+
runtime_interactive_command_suggestions,
|
|
15
|
+
runtime_interactive_command_usage,
|
|
16
|
+
runtime_interactive_completion_words,
|
|
17
|
+
)
|
|
18
|
+
from kagent.cli.conversation import (
|
|
19
|
+
RUNTIME_MEMORY_MAX_TURNS,
|
|
20
|
+
compact_runtime_conversation_memory,
|
|
21
|
+
remember_runtime_turn,
|
|
22
|
+
runtime_goal_with_memory,
|
|
23
|
+
)
|
|
24
|
+
from kagent.cli.memory import (
|
|
25
|
+
RuntimeSessionMemory,
|
|
26
|
+
clear_runtime_history,
|
|
27
|
+
default_runtime_history_path,
|
|
28
|
+
load_runtime_session_memory,
|
|
29
|
+
runtime_prompt_history,
|
|
30
|
+
save_runtime_session_memory,
|
|
31
|
+
)
|
|
32
|
+
from kagent.cli.trace import (
|
|
33
|
+
persist_runtime_cli_trace_or_raise,
|
|
34
|
+
save_runtime_trace_snapshot_or_raise,
|
|
35
|
+
)
|
|
36
|
+
from kagent.cli.ui import (
|
|
37
|
+
approval_prompt,
|
|
38
|
+
format_runtime_interactive_doctor,
|
|
39
|
+
format_runtime_interactive_status,
|
|
40
|
+
format_runtime_interactive_summary,
|
|
41
|
+
format_runtime_interactive_tools,
|
|
42
|
+
format_runtime_notice,
|
|
43
|
+
format_runtime_pending_approval_detail,
|
|
44
|
+
format_runtime_progress_event,
|
|
45
|
+
format_runtime_provider_config,
|
|
46
|
+
format_runtime_session_memory,
|
|
47
|
+
runtime_interactive_help,
|
|
48
|
+
runtime_prompt,
|
|
49
|
+
runtime_prompt_reset,
|
|
50
|
+
runtime_ready_message,
|
|
51
|
+
runtime_ui_color_enabled,
|
|
52
|
+
runtime_user_message_block,
|
|
53
|
+
)
|
|
54
|
+
from kagent.utils.json_output import format_and_write_json, json_ready
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_runtime_interactive(
|
|
58
|
+
*,
|
|
59
|
+
provider: Any,
|
|
60
|
+
run_runtime_agent: Any,
|
|
61
|
+
max_iterations: int,
|
|
62
|
+
fail_on_agent_failure: bool,
|
|
63
|
+
full_trace_output: bool = False,
|
|
64
|
+
metadata: dict[str, str] | None = None,
|
|
65
|
+
tags: list[str] | None = None,
|
|
66
|
+
trace_dir: str = "",
|
|
67
|
+
persist_trace: Any = None,
|
|
68
|
+
session_memory_path: str = "",
|
|
69
|
+
) -> None:
|
|
70
|
+
interactive_tty = sys.stdin.isatty()
|
|
71
|
+
prompt_stream = sys.__stderr__ or sys.stderr
|
|
72
|
+
full_json_mode = full_trace_output
|
|
73
|
+
session_memory = load_runtime_session_memory(
|
|
74
|
+
session_memory_path,
|
|
75
|
+
max_turns=RUNTIME_MEMORY_MAX_TURNS,
|
|
76
|
+
)
|
|
77
|
+
last_payload: Any = None
|
|
78
|
+
line_reader: Any = None
|
|
79
|
+
state_lock = threading.RLock()
|
|
80
|
+
if interactive_tty:
|
|
81
|
+
line_reader = _runtime_interactive_line_reader(prompt_stream)
|
|
82
|
+
print(runtime_ready_message(color=runtime_ui_color_enabled()), file=prompt_stream)
|
|
83
|
+
|
|
84
|
+
def run_goal_once(
|
|
85
|
+
goal: str,
|
|
86
|
+
approval_reader: Any = None,
|
|
87
|
+
full_json_override: bool | None = None,
|
|
88
|
+
) -> None:
|
|
89
|
+
nonlocal last_payload
|
|
90
|
+
with state_lock:
|
|
91
|
+
runtime_goal = runtime_goal_with_memory(goal, session_memory)
|
|
92
|
+
run_full_json_mode = (
|
|
93
|
+
full_json_mode if full_json_override is None else full_json_override
|
|
94
|
+
)
|
|
95
|
+
progress_sink = _runtime_interactive_progress_sink(
|
|
96
|
+
enabled=interactive_tty and not run_full_json_mode,
|
|
97
|
+
)
|
|
98
|
+
try:
|
|
99
|
+
payload = json_ready(
|
|
100
|
+
run_runtime_agent(
|
|
101
|
+
runtime_goal,
|
|
102
|
+
provider=provider,
|
|
103
|
+
max_iterations=max_iterations,
|
|
104
|
+
metadata=metadata,
|
|
105
|
+
tags=tags,
|
|
106
|
+
event_sink=progress_sink,
|
|
107
|
+
stream_answers=interactive_tty and not run_full_json_mode,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
finally:
|
|
111
|
+
_close_runtime_progress_sink(progress_sink)
|
|
112
|
+
if trace_dir and persist_trace is not None:
|
|
113
|
+
persist_runtime_cli_trace_or_raise(payload, trace_dir, persist_trace)
|
|
114
|
+
_print_runtime_interactive_payload(
|
|
115
|
+
payload,
|
|
116
|
+
full_json=run_full_json_mode or not interactive_tty,
|
|
117
|
+
)
|
|
118
|
+
if payload.get("status") == "requires_approval" and interactive_tty:
|
|
119
|
+
payload = _maybe_run_approved_runtime_action(
|
|
120
|
+
payload=payload,
|
|
121
|
+
goal=runtime_goal,
|
|
122
|
+
run_runtime_agent=run_runtime_agent,
|
|
123
|
+
metadata=metadata,
|
|
124
|
+
tags=tags,
|
|
125
|
+
progress_enabled=not run_full_json_mode,
|
|
126
|
+
response_reader=approval_reader,
|
|
127
|
+
)
|
|
128
|
+
if payload is not None:
|
|
129
|
+
if trace_dir and persist_trace is not None:
|
|
130
|
+
persist_runtime_cli_trace_or_raise(payload, trace_dir, persist_trace)
|
|
131
|
+
_print_runtime_interactive_payload(
|
|
132
|
+
payload,
|
|
133
|
+
full_json=run_full_json_mode,
|
|
134
|
+
)
|
|
135
|
+
with state_lock:
|
|
136
|
+
last_payload = payload
|
|
137
|
+
remember_runtime_turn(session_memory, goal, payload)
|
|
138
|
+
save_runtime_session_memory(session_memory_path, session_memory)
|
|
139
|
+
if (
|
|
140
|
+
fail_on_agent_failure
|
|
141
|
+
and isinstance(payload, dict)
|
|
142
|
+
and payload.get("status") == "failed"
|
|
143
|
+
):
|
|
144
|
+
raise SystemExit(1)
|
|
145
|
+
|
|
146
|
+
if interactive_tty:
|
|
147
|
+
work_queue: queue.Queue[tuple[str, bool] | None] = queue.Queue()
|
|
148
|
+
approval_broker = _RuntimeInteractiveApprovalBroker()
|
|
149
|
+
worker_errors: list[BaseException] = []
|
|
150
|
+
|
|
151
|
+
def worker() -> None:
|
|
152
|
+
while True:
|
|
153
|
+
queued_item = work_queue.get()
|
|
154
|
+
try:
|
|
155
|
+
if queued_item is None:
|
|
156
|
+
return
|
|
157
|
+
queued_goal, queued_full_json_mode = queued_item
|
|
158
|
+
run_goal_once(
|
|
159
|
+
queued_goal,
|
|
160
|
+
approval_reader=approval_broker.read,
|
|
161
|
+
full_json_override=queued_full_json_mode,
|
|
162
|
+
)
|
|
163
|
+
except BaseException as exc:
|
|
164
|
+
worker_errors.append(exc)
|
|
165
|
+
finally:
|
|
166
|
+
work_queue.task_done()
|
|
167
|
+
|
|
168
|
+
worker_thread = threading.Thread(target=worker, daemon=True)
|
|
169
|
+
worker_thread.start()
|
|
170
|
+
|
|
171
|
+
def shutdown_worker() -> None:
|
|
172
|
+
work_queue.join()
|
|
173
|
+
work_queue.put(None)
|
|
174
|
+
worker_thread.join()
|
|
175
|
+
if worker_errors:
|
|
176
|
+
raise worker_errors[0]
|
|
177
|
+
|
|
178
|
+
while True:
|
|
179
|
+
try:
|
|
180
|
+
line = line_reader.read(color=runtime_ui_color_enabled())
|
|
181
|
+
except EOFError:
|
|
182
|
+
shutdown_worker()
|
|
183
|
+
return
|
|
184
|
+
goal = line.strip()
|
|
185
|
+
if not goal:
|
|
186
|
+
if line_reader.should_erase_empty_line():
|
|
187
|
+
_erase_empty_runtime_prompt_line()
|
|
188
|
+
continue
|
|
189
|
+
if approval_broker.submit_if_waiting(goal):
|
|
190
|
+
continue
|
|
191
|
+
if _is_runtime_approval_reply(goal):
|
|
192
|
+
if approval_broker.submit_when_waiting(goal, timeout=0.15):
|
|
193
|
+
continue
|
|
194
|
+
if goal.lower() in {"exit", "quit", ":q"}:
|
|
195
|
+
shutdown_worker()
|
|
196
|
+
return
|
|
197
|
+
if goal.startswith("/"):
|
|
198
|
+
work_queue.join()
|
|
199
|
+
if not is_runtime_interactive_command(goal):
|
|
200
|
+
_print_unknown_runtime_interactive_command(goal)
|
|
201
|
+
continue
|
|
202
|
+
with state_lock:
|
|
203
|
+
handled, full_json_mode = _handle_runtime_interactive_command(
|
|
204
|
+
goal,
|
|
205
|
+
full_json_mode,
|
|
206
|
+
session_memory,
|
|
207
|
+
last_payload,
|
|
208
|
+
session_memory_path=session_memory_path,
|
|
209
|
+
trace_dir=trace_dir,
|
|
210
|
+
provider=provider,
|
|
211
|
+
line_reader=line_reader,
|
|
212
|
+
)
|
|
213
|
+
if handled:
|
|
214
|
+
continue
|
|
215
|
+
_print_invalid_runtime_interactive_command(goal)
|
|
216
|
+
continue
|
|
217
|
+
_replace_runtime_prompt_with_user_message(goal)
|
|
218
|
+
with state_lock:
|
|
219
|
+
queued_full_json_mode = full_json_mode
|
|
220
|
+
work_queue.put((goal, queued_full_json_mode))
|
|
221
|
+
|
|
222
|
+
while True:
|
|
223
|
+
try:
|
|
224
|
+
line = sys.stdin.readline()
|
|
225
|
+
except EOFError:
|
|
226
|
+
return
|
|
227
|
+
if line == "":
|
|
228
|
+
return
|
|
229
|
+
goal = line.strip()
|
|
230
|
+
if not goal:
|
|
231
|
+
continue
|
|
232
|
+
if goal.lower() in {"exit", "quit", ":q"}:
|
|
233
|
+
return
|
|
234
|
+
run_goal_once(goal)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _enable_interactive_line_editing() -> None:
|
|
238
|
+
try:
|
|
239
|
+
import readline # noqa: F401
|
|
240
|
+
except ImportError:
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class _RuntimeLineReader:
|
|
245
|
+
def read(self, *, color: bool) -> str:
|
|
246
|
+
raise NotImplementedError
|
|
247
|
+
|
|
248
|
+
def should_erase_empty_line(self) -> bool:
|
|
249
|
+
return True
|
|
250
|
+
|
|
251
|
+
def uses_prompt_status(self) -> bool:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
def clear_history(self) -> None:
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
def line_editor_name(self) -> str:
|
|
258
|
+
return "input"
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class _InputLineReader(_RuntimeLineReader):
|
|
262
|
+
def read(self, *, color: bool) -> str:
|
|
263
|
+
try:
|
|
264
|
+
return input(runtime_prompt(color=color))
|
|
265
|
+
finally:
|
|
266
|
+
reset = runtime_prompt_reset(color=color)
|
|
267
|
+
if reset:
|
|
268
|
+
sys.stdout.write(reset)
|
|
269
|
+
sys.stdout.flush()
|
|
270
|
+
|
|
271
|
+
def line_editor_name(self) -> str:
|
|
272
|
+
return "readline/input"
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class _PromptToolkitLineReader(_RuntimeLineReader):
|
|
276
|
+
def __init__(self, session: Any):
|
|
277
|
+
self._session = session
|
|
278
|
+
|
|
279
|
+
def read(self, *, color: bool) -> str:
|
|
280
|
+
message = _runtime_prompt_fragments(color=color)
|
|
281
|
+
try:
|
|
282
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
283
|
+
except ImportError:
|
|
284
|
+
return self._session.prompt(
|
|
285
|
+
message,
|
|
286
|
+
wrap_lines=True,
|
|
287
|
+
multiline=False,
|
|
288
|
+
prompt_continuation=_runtime_prompt_continuation,
|
|
289
|
+
reserve_space_for_menu=0,
|
|
290
|
+
refresh_interval=0.12,
|
|
291
|
+
)
|
|
292
|
+
with patch_stdout(raw=True):
|
|
293
|
+
return self._session.prompt(
|
|
294
|
+
message,
|
|
295
|
+
wrap_lines=True,
|
|
296
|
+
multiline=False,
|
|
297
|
+
prompt_continuation=_runtime_prompt_continuation,
|
|
298
|
+
reserve_space_for_menu=0,
|
|
299
|
+
refresh_interval=0.12,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
def clear_history(self) -> None:
|
|
303
|
+
history = getattr(self._session, "history", None)
|
|
304
|
+
loaded_strings = getattr(history, "_loaded_strings", None)
|
|
305
|
+
if isinstance(loaded_strings, list):
|
|
306
|
+
loaded_strings.clear()
|
|
307
|
+
|
|
308
|
+
def should_erase_empty_line(self) -> bool:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
def uses_prompt_status(self) -> bool:
|
|
312
|
+
return False
|
|
313
|
+
|
|
314
|
+
def line_editor_name(self) -> str:
|
|
315
|
+
return "prompt_toolkit"
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _runtime_prompt_fragments(*, color: bool) -> Any:
|
|
319
|
+
if not color:
|
|
320
|
+
return "› "
|
|
321
|
+
return [
|
|
322
|
+
("class:input-bar.prompt", "› "),
|
|
323
|
+
]
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def _runtime_prompt_continuation(
|
|
327
|
+
_width: int,
|
|
328
|
+
_line_number: int,
|
|
329
|
+
_is_soft_wrap: bool,
|
|
330
|
+
) -> list[tuple[str, str]]:
|
|
331
|
+
return [("class:input-bar.continuation", " ")]
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _runtime_interactive_line_reader(
|
|
335
|
+
prompt_stream: Any,
|
|
336
|
+
) -> _RuntimeLineReader:
|
|
337
|
+
prompt_toolkit_session = _prompt_toolkit_session_for_tty(prompt_stream)
|
|
338
|
+
if prompt_toolkit_session is not None:
|
|
339
|
+
return _PromptToolkitLineReader(prompt_toolkit_session)
|
|
340
|
+
_enable_interactive_line_editing()
|
|
341
|
+
return _InputLineReader()
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _prompt_toolkit_session_for_tty(prompt_stream: Any) -> Any:
|
|
345
|
+
if sys.stdin is not getattr(sys, "__stdin__", None):
|
|
346
|
+
return None
|
|
347
|
+
if not _stream_is_tty(sys.stdin):
|
|
348
|
+
return None
|
|
349
|
+
if not _stream_is_tty(prompt_stream):
|
|
350
|
+
return None
|
|
351
|
+
try:
|
|
352
|
+
from prompt_toolkit import PromptSession
|
|
353
|
+
from prompt_toolkit.completion import WordCompleter
|
|
354
|
+
from prompt_toolkit.styles import Style
|
|
355
|
+
except ImportError:
|
|
356
|
+
return None
|
|
357
|
+
completer = WordCompleter(
|
|
358
|
+
runtime_interactive_completion_words(),
|
|
359
|
+
ignore_case=True,
|
|
360
|
+
sentence=True,
|
|
361
|
+
)
|
|
362
|
+
return PromptSession(
|
|
363
|
+
complete_while_typing=True,
|
|
364
|
+
completer=completer,
|
|
365
|
+
enable_history_search=True,
|
|
366
|
+
history=runtime_prompt_history(default_runtime_history_path()),
|
|
367
|
+
style=Style.from_dict(
|
|
368
|
+
{
|
|
369
|
+
"": "#ffffff",
|
|
370
|
+
"input-bar": "#ffffff",
|
|
371
|
+
"input-bar.prompt": "ansicyan bold",
|
|
372
|
+
"input-bar.continuation": "#6b7280",
|
|
373
|
+
}
|
|
374
|
+
),
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _stream_is_tty(stream: Any) -> bool:
|
|
379
|
+
isatty = getattr(stream, "isatty", None)
|
|
380
|
+
return bool(callable(isatty) and isatty())
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _print_runtime_interactive_payload(payload: Any, *, full_json: bool) -> None:
|
|
384
|
+
if full_json:
|
|
385
|
+
print(format_and_write_json(payload, ""))
|
|
386
|
+
return
|
|
387
|
+
if sys.stdin.isatty():
|
|
388
|
+
print()
|
|
389
|
+
print(
|
|
390
|
+
format_runtime_interactive_summary(
|
|
391
|
+
payload,
|
|
392
|
+
color=runtime_ui_color_enabled(),
|
|
393
|
+
)
|
|
394
|
+
)
|
|
395
|
+
if sys.stdin.isatty():
|
|
396
|
+
print()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _runtime_interactive_progress_sink(
|
|
400
|
+
*,
|
|
401
|
+
enabled: bool,
|
|
402
|
+
) -> Any:
|
|
403
|
+
if not enabled:
|
|
404
|
+
return None
|
|
405
|
+
return _RuntimeInteractiveProgress()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _close_runtime_progress_sink(progress_sink: Any) -> None:
|
|
409
|
+
close = getattr(progress_sink, "close", None)
|
|
410
|
+
if callable(close):
|
|
411
|
+
close()
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
class _RuntimeInteractiveApprovalBroker:
|
|
415
|
+
def __init__(self) -> None:
|
|
416
|
+
self._condition = threading.Condition()
|
|
417
|
+
self._responses: queue.Queue[str] = queue.Queue()
|
|
418
|
+
self._waiting = False
|
|
419
|
+
|
|
420
|
+
def read(self, prompt: str) -> str:
|
|
421
|
+
with self._condition:
|
|
422
|
+
self._waiting = True
|
|
423
|
+
self._condition.notify_all()
|
|
424
|
+
try:
|
|
425
|
+
sys.stdout.write(prompt)
|
|
426
|
+
sys.stdout.flush()
|
|
427
|
+
return self._responses.get()
|
|
428
|
+
finally:
|
|
429
|
+
with self._condition:
|
|
430
|
+
self._waiting = False
|
|
431
|
+
self._condition.notify_all()
|
|
432
|
+
|
|
433
|
+
def submit_if_waiting(self, line: str) -> bool:
|
|
434
|
+
with self._condition:
|
|
435
|
+
if not self._waiting:
|
|
436
|
+
return False
|
|
437
|
+
self._responses.put(line)
|
|
438
|
+
return True
|
|
439
|
+
|
|
440
|
+
def submit_when_waiting(self, line: str, *, timeout: float) -> bool:
|
|
441
|
+
deadline = time.monotonic() + timeout
|
|
442
|
+
with self._condition:
|
|
443
|
+
while not self._waiting:
|
|
444
|
+
remaining = deadline - time.monotonic()
|
|
445
|
+
if remaining <= 0:
|
|
446
|
+
return False
|
|
447
|
+
self._condition.wait(timeout=remaining)
|
|
448
|
+
self._responses.put(line)
|
|
449
|
+
return True
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _is_runtime_approval_reply(goal: str) -> bool:
|
|
453
|
+
return goal.strip().lower() in {
|
|
454
|
+
"y",
|
|
455
|
+
"yes",
|
|
456
|
+
"approve",
|
|
457
|
+
"n",
|
|
458
|
+
"no",
|
|
459
|
+
"d",
|
|
460
|
+
"detail",
|
|
461
|
+
"details",
|
|
462
|
+
"view",
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _erase_empty_runtime_prompt_line() -> None:
|
|
467
|
+
sys.stdout.write("\x1b[1A\r\x1b[2K")
|
|
468
|
+
sys.stdout.flush()
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _replace_runtime_prompt_with_user_message(goal: str) -> None:
|
|
472
|
+
if not runtime_ui_color_enabled():
|
|
473
|
+
return
|
|
474
|
+
sys.stdout.write("\x1b[1A\r\x1b[2K")
|
|
475
|
+
sys.stdout.write(
|
|
476
|
+
runtime_user_message_block(
|
|
477
|
+
goal,
|
|
478
|
+
color=True,
|
|
479
|
+
)
|
|
480
|
+
)
|
|
481
|
+
sys.stdout.write("\n\n")
|
|
482
|
+
sys.stdout.flush()
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
class _RuntimeInteractiveProgress:
|
|
486
|
+
_FRAMES = ("⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏")
|
|
487
|
+
|
|
488
|
+
def __init__(self) -> None:
|
|
489
|
+
self._message = ""
|
|
490
|
+
self._frame_index = 0
|
|
491
|
+
self._last_width = 0
|
|
492
|
+
self._started = False
|
|
493
|
+
self._closed = False
|
|
494
|
+
self._active = False
|
|
495
|
+
self._streaming_answer = False
|
|
496
|
+
self._lock = threading.Lock()
|
|
497
|
+
self._thread: threading.Thread | None = None
|
|
498
|
+
|
|
499
|
+
def __call__(self, event: Any) -> None:
|
|
500
|
+
if isinstance(event, dict):
|
|
501
|
+
event_type = str(event.get("type", "")).strip()
|
|
502
|
+
if event_type == "answer_started":
|
|
503
|
+
self._start_answer_stream()
|
|
504
|
+
return
|
|
505
|
+
if event_type == "answer_delta":
|
|
506
|
+
self._write_answer_delta(str(event.get("delta", "")))
|
|
507
|
+
return
|
|
508
|
+
if event_type == "answer_completed":
|
|
509
|
+
self._finish_answer_stream()
|
|
510
|
+
return
|
|
511
|
+
message = format_runtime_progress_event(
|
|
512
|
+
event,
|
|
513
|
+
color=runtime_ui_color_enabled(),
|
|
514
|
+
)
|
|
515
|
+
if not message or not isinstance(event, dict):
|
|
516
|
+
return
|
|
517
|
+
event_type = str(event.get("type", "")).strip()
|
|
518
|
+
if event_type in {"planner_started", "planner_completed", "tool_started"}:
|
|
519
|
+
self._start_or_update(message)
|
|
520
|
+
return
|
|
521
|
+
self._finish_active(clear=True)
|
|
522
|
+
self._write_line(message)
|
|
523
|
+
|
|
524
|
+
def close(self) -> None:
|
|
525
|
+
with self._lock:
|
|
526
|
+
self._closed = True
|
|
527
|
+
self._finish_active(clear=True)
|
|
528
|
+
|
|
529
|
+
def _start_or_update(self, message: str) -> None:
|
|
530
|
+
with self._lock:
|
|
531
|
+
if self._closed:
|
|
532
|
+
return
|
|
533
|
+
if self._message == message and self._active:
|
|
534
|
+
return
|
|
535
|
+
self._message = message
|
|
536
|
+
if not self._started:
|
|
537
|
+
sys.stdout.write("\n")
|
|
538
|
+
self._started = True
|
|
539
|
+
if not _stream_is_tty(sys.stdout):
|
|
540
|
+
sys.stdout.write(f" {message}\n")
|
|
541
|
+
sys.stdout.flush()
|
|
542
|
+
self._active = False
|
|
543
|
+
self._last_width = 0
|
|
544
|
+
return
|
|
545
|
+
self._active = True
|
|
546
|
+
self._render_locked()
|
|
547
|
+
if self._thread is None or not self._thread.is_alive():
|
|
548
|
+
self._thread = threading.Thread(target=self._spin, daemon=True)
|
|
549
|
+
self._thread.start()
|
|
550
|
+
|
|
551
|
+
def _finish_active(self, *, clear: bool) -> None:
|
|
552
|
+
thread: threading.Thread | None
|
|
553
|
+
with self._lock:
|
|
554
|
+
self._active = False
|
|
555
|
+
thread = self._thread
|
|
556
|
+
if thread is not None and thread.is_alive():
|
|
557
|
+
thread.join(timeout=0.25)
|
|
558
|
+
with self._lock:
|
|
559
|
+
if clear:
|
|
560
|
+
self._clear_locked()
|
|
561
|
+
|
|
562
|
+
def _write_line(self, message: str) -> None:
|
|
563
|
+
with self._lock:
|
|
564
|
+
if not self._started:
|
|
565
|
+
sys.stdout.write("\n")
|
|
566
|
+
self._started = True
|
|
567
|
+
sys.stdout.write(f"{message}\n")
|
|
568
|
+
sys.stdout.flush()
|
|
569
|
+
self._last_width = 0
|
|
570
|
+
|
|
571
|
+
def _start_answer_stream(self) -> None:
|
|
572
|
+
self._finish_active(clear=True)
|
|
573
|
+
with self._lock:
|
|
574
|
+
if not self._started:
|
|
575
|
+
sys.stdout.write("\n")
|
|
576
|
+
self._started = True
|
|
577
|
+
if not self._streaming_answer:
|
|
578
|
+
sys.stdout.write("Answer\n ")
|
|
579
|
+
self._streaming_answer = True
|
|
580
|
+
self._last_width = 0
|
|
581
|
+
sys.stdout.flush()
|
|
582
|
+
|
|
583
|
+
def _write_answer_delta(self, delta: str) -> None:
|
|
584
|
+
if not delta:
|
|
585
|
+
return
|
|
586
|
+
with self._lock:
|
|
587
|
+
if not self._streaming_answer:
|
|
588
|
+
if not self._started:
|
|
589
|
+
sys.stdout.write("\n")
|
|
590
|
+
self._started = True
|
|
591
|
+
sys.stdout.write("Answer\n ")
|
|
592
|
+
self._streaming_answer = True
|
|
593
|
+
sys.stdout.write(delta)
|
|
594
|
+
sys.stdout.flush()
|
|
595
|
+
|
|
596
|
+
def _finish_answer_stream(self) -> None:
|
|
597
|
+
with self._lock:
|
|
598
|
+
if self._streaming_answer:
|
|
599
|
+
sys.stdout.write("\n")
|
|
600
|
+
sys.stdout.flush()
|
|
601
|
+
self._streaming_answer = False
|
|
602
|
+
self._last_width = 0
|
|
603
|
+
|
|
604
|
+
def _spin(self) -> None:
|
|
605
|
+
while True:
|
|
606
|
+
time.sleep(0.12)
|
|
607
|
+
with self._lock:
|
|
608
|
+
if self._closed or not self._active:
|
|
609
|
+
return
|
|
610
|
+
self._frame_index += 1
|
|
611
|
+
self._render_locked()
|
|
612
|
+
|
|
613
|
+
def _render_locked(self) -> None:
|
|
614
|
+
frame = self._FRAMES[self._frame_index % len(self._FRAMES)]
|
|
615
|
+
line = f" {frame} {self._message}"
|
|
616
|
+
padding = " " * max(0, self._last_width - len(line))
|
|
617
|
+
sys.stdout.write(f"\r{line}{padding}")
|
|
618
|
+
sys.stdout.flush()
|
|
619
|
+
self._last_width = len(line)
|
|
620
|
+
|
|
621
|
+
def _clear_locked(self) -> None:
|
|
622
|
+
if self._last_width:
|
|
623
|
+
sys.stdout.write("\r" + (" " * self._last_width) + "\r")
|
|
624
|
+
sys.stdout.flush()
|
|
625
|
+
self._last_width = 0
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
def _handle_runtime_interactive_command(
|
|
629
|
+
command: str,
|
|
630
|
+
full_json_mode: bool,
|
|
631
|
+
session_memory: RuntimeSessionMemory,
|
|
632
|
+
last_payload: Any,
|
|
633
|
+
*,
|
|
634
|
+
session_memory_path: str = "",
|
|
635
|
+
trace_dir: str = "",
|
|
636
|
+
provider: Any = None,
|
|
637
|
+
line_reader: Any = None,
|
|
638
|
+
) -> tuple[bool, bool]:
|
|
639
|
+
normalized = command.strip().lower()
|
|
640
|
+
if normalized in {"/json", "/full", "/debug"}:
|
|
641
|
+
print(format_runtime_notice("Output mode", "full JSON traces"))
|
|
642
|
+
return True, True
|
|
643
|
+
if normalized in {"/compact", "/summary"}:
|
|
644
|
+
print(format_runtime_notice("Output mode", "compact transcript"))
|
|
645
|
+
return True, False
|
|
646
|
+
if normalized in {"/help", "/?"}:
|
|
647
|
+
print(runtime_interactive_help())
|
|
648
|
+
return True, full_json_mode
|
|
649
|
+
if normalized in {"/pwd", "/cwd"}:
|
|
650
|
+
print(format_runtime_notice("Working directory", os.getcwd()))
|
|
651
|
+
return True, full_json_mode
|
|
652
|
+
if normalized == "/cd" or normalized.startswith("/cd "):
|
|
653
|
+
_change_runtime_interactive_directory(command)
|
|
654
|
+
return True, full_json_mode
|
|
655
|
+
if normalized in {"/status", "/stat"}:
|
|
656
|
+
print(
|
|
657
|
+
format_runtime_interactive_status(
|
|
658
|
+
cwd=os.getcwd(),
|
|
659
|
+
full_json_mode=full_json_mode,
|
|
660
|
+
session_memory=session_memory,
|
|
661
|
+
last_payload=last_payload,
|
|
662
|
+
trace_dir=trace_dir,
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
return True, full_json_mode
|
|
666
|
+
if normalized in {"/doctor", "/diagnostics"}:
|
|
667
|
+
line_editor = ""
|
|
668
|
+
if line_reader is not None:
|
|
669
|
+
line_editor_name = getattr(line_reader, "line_editor_name", None)
|
|
670
|
+
if callable(line_editor_name):
|
|
671
|
+
line_editor = str(line_editor_name())
|
|
672
|
+
print(
|
|
673
|
+
format_runtime_interactive_doctor(
|
|
674
|
+
cwd=os.getcwd(),
|
|
675
|
+
provider=provider,
|
|
676
|
+
session_memory_path=session_memory_path,
|
|
677
|
+
history_path=default_runtime_history_path(),
|
|
678
|
+
trace_dir=trace_dir,
|
|
679
|
+
line_editor=line_editor,
|
|
680
|
+
)
|
|
681
|
+
)
|
|
682
|
+
return True, full_json_mode
|
|
683
|
+
if normalized in {"/config", "/provider"}:
|
|
684
|
+
print(format_runtime_provider_config(provider))
|
|
685
|
+
return True, full_json_mode
|
|
686
|
+
if normalized in {"/tools", "/actions"}:
|
|
687
|
+
from kagent.runtime.tools import registered_runtime_tool_metadata
|
|
688
|
+
|
|
689
|
+
print(format_runtime_interactive_tools(registered_runtime_tool_metadata()))
|
|
690
|
+
return True, full_json_mode
|
|
691
|
+
if normalized in {"/memory", "/mem"}:
|
|
692
|
+
print(format_runtime_session_memory(session_memory))
|
|
693
|
+
return True, full_json_mode
|
|
694
|
+
if normalized in {"/compact-memory", "/compress-memory"}:
|
|
695
|
+
compacted_now = compact_runtime_conversation_memory(session_memory)
|
|
696
|
+
save_runtime_session_memory(session_memory_path, session_memory)
|
|
697
|
+
detail = (
|
|
698
|
+
f"{compacted_now} turn{'s' if compacted_now != 1 else ''} compacted"
|
|
699
|
+
if compacted_now
|
|
700
|
+
else "already compact"
|
|
701
|
+
)
|
|
702
|
+
print(format_runtime_notice("Memory compacted", detail))
|
|
703
|
+
return True, full_json_mode
|
|
704
|
+
if normalized in {"/last", "/last-run"}:
|
|
705
|
+
if last_payload is None:
|
|
706
|
+
print(format_runtime_notice("Last run", "no previous run"))
|
|
707
|
+
else:
|
|
708
|
+
_print_runtime_interactive_payload(last_payload, full_json=False)
|
|
709
|
+
return True, full_json_mode
|
|
710
|
+
if normalized in {"/trace", "/last-json"}:
|
|
711
|
+
if last_payload is None:
|
|
712
|
+
print(format_runtime_notice("Last run", "no previous run"))
|
|
713
|
+
else:
|
|
714
|
+
_print_runtime_interactive_payload(last_payload, full_json=True)
|
|
715
|
+
return True, full_json_mode
|
|
716
|
+
if normalized == "/save-trace" or normalized.startswith("/save-trace "):
|
|
717
|
+
_save_last_runtime_trace(command, last_payload)
|
|
718
|
+
return True, full_json_mode
|
|
719
|
+
if normalized == "/export-trace" or normalized.startswith("/export-trace "):
|
|
720
|
+
_save_last_runtime_trace(command, last_payload)
|
|
721
|
+
return True, full_json_mode
|
|
722
|
+
if normalized in {"/clear", "/clear-memory"}:
|
|
723
|
+
session_memory.clear()
|
|
724
|
+
save_runtime_session_memory(session_memory_path, session_memory)
|
|
725
|
+
print(format_runtime_notice("Memory", "cleared"))
|
|
726
|
+
return True, full_json_mode
|
|
727
|
+
if normalized in {"/reset", "/reset-session"}:
|
|
728
|
+
session_memory.clear()
|
|
729
|
+
save_runtime_session_memory(session_memory_path, session_memory)
|
|
730
|
+
clear_runtime_history(default_runtime_history_path())
|
|
731
|
+
if line_reader is not None:
|
|
732
|
+
clear_history = getattr(line_reader, "clear_history", None)
|
|
733
|
+
if callable(clear_history):
|
|
734
|
+
clear_history()
|
|
735
|
+
print(format_runtime_notice("Reset", "memory and prompt history cleared"))
|
|
736
|
+
return True, full_json_mode
|
|
737
|
+
return False, full_json_mode
|
|
738
|
+
|
|
739
|
+
|
|
740
|
+
def _print_unknown_runtime_interactive_command(command: str) -> None:
|
|
741
|
+
suggestions = runtime_interactive_command_suggestions(command)
|
|
742
|
+
detail = "try /help"
|
|
743
|
+
if suggestions:
|
|
744
|
+
detail = "try " + ", ".join(suggestions)
|
|
745
|
+
print(format_runtime_notice("Unknown command", detail))
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _print_invalid_runtime_interactive_command(command: str) -> None:
|
|
749
|
+
usage = runtime_interactive_command_usage(command)
|
|
750
|
+
detail = f"usage: {usage}" if usage else "try /help"
|
|
751
|
+
print(format_runtime_notice("Invalid command", detail))
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
def _save_last_runtime_trace(command: str, last_payload: Any) -> None:
|
|
755
|
+
if last_payload is None:
|
|
756
|
+
print(format_runtime_notice("Last run", "no previous run"))
|
|
757
|
+
return
|
|
758
|
+
try:
|
|
759
|
+
parts = shlex.split(command.strip())
|
|
760
|
+
except ValueError as exc:
|
|
761
|
+
print(format_runtime_notice("Save trace failed", str(exc)))
|
|
762
|
+
return
|
|
763
|
+
if len(parts) < 2 or not parts[1].strip():
|
|
764
|
+
print(format_runtime_notice("Save trace", "path required: /save-trace PATH"))
|
|
765
|
+
return
|
|
766
|
+
try:
|
|
767
|
+
saved_path = save_runtime_trace_snapshot_or_raise(
|
|
768
|
+
last_payload,
|
|
769
|
+
parts[1].strip(),
|
|
770
|
+
)
|
|
771
|
+
except (OSError, ValueError) as exc:
|
|
772
|
+
print(format_runtime_notice("Save trace failed", str(exc)))
|
|
773
|
+
return
|
|
774
|
+
print(format_runtime_notice("Trace saved", saved_path))
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
def _change_runtime_interactive_directory(command: str) -> None:
|
|
778
|
+
raw_path = command.strip()[3:].strip()
|
|
779
|
+
target = os.path.expanduser(raw_path or "~")
|
|
780
|
+
if not os.path.isabs(target):
|
|
781
|
+
target = os.path.abspath(target)
|
|
782
|
+
if not os.path.isdir(target):
|
|
783
|
+
print(format_runtime_notice("Directory not found", target))
|
|
784
|
+
return
|
|
785
|
+
os.chdir(target)
|
|
786
|
+
print(format_runtime_notice("Working directory", os.getcwd()))
|
|
787
|
+
|
|
788
|
+
|
|
789
|
+
def _maybe_run_approved_runtime_action(
|
|
790
|
+
*,
|
|
791
|
+
payload: Any,
|
|
792
|
+
goal: str,
|
|
793
|
+
run_runtime_agent: Any,
|
|
794
|
+
metadata: dict[str, str] | None = None,
|
|
795
|
+
tags: list[str] | None = None,
|
|
796
|
+
progress_enabled: bool = True,
|
|
797
|
+
response_reader: Any = None,
|
|
798
|
+
) -> Any:
|
|
799
|
+
pending = payload.get("pending_approval") if isinstance(payload, dict) else None
|
|
800
|
+
if not isinstance(pending, dict):
|
|
801
|
+
return None
|
|
802
|
+
action_id = str(pending.get("id", "")).strip()
|
|
803
|
+
tool = str(pending.get("tool", "")).strip()
|
|
804
|
+
if not action_id or not tool:
|
|
805
|
+
return None
|
|
806
|
+
while True:
|
|
807
|
+
prompt = approval_prompt(action_id, tool, color=runtime_ui_color_enabled())
|
|
808
|
+
if response_reader is None:
|
|
809
|
+
answer = input(prompt).strip().lower()
|
|
810
|
+
else:
|
|
811
|
+
answer = str(response_reader(prompt)).strip().lower()
|
|
812
|
+
if answer in {"d", "detail", "details", "view"}:
|
|
813
|
+
print(format_runtime_pending_approval_detail(pending))
|
|
814
|
+
continue
|
|
815
|
+
break
|
|
816
|
+
if answer not in {"y", "yes", "approve"}:
|
|
817
|
+
print(format_runtime_notice("Approval skipped", "action not approved"))
|
|
818
|
+
return None
|
|
819
|
+
progress_sink = _runtime_interactive_progress_sink(enabled=progress_enabled)
|
|
820
|
+
try:
|
|
821
|
+
return json_ready(
|
|
822
|
+
run_runtime_agent(
|
|
823
|
+
goal,
|
|
824
|
+
provider=_InlineRuntimePlanProvider({"actions": [pending]}),
|
|
825
|
+
max_iterations=1,
|
|
826
|
+
approved_action_ids={action_id},
|
|
827
|
+
metadata=metadata,
|
|
828
|
+
tags=tags,
|
|
829
|
+
event_sink=progress_sink,
|
|
830
|
+
)
|
|
831
|
+
)
|
|
832
|
+
finally:
|
|
833
|
+
_close_runtime_progress_sink(progress_sink)
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
class _InlineRuntimePlanProvider:
|
|
837
|
+
def __init__(self, plan: dict) -> None:
|
|
838
|
+
self.plan = plan
|
|
839
|
+
|
|
840
|
+
def complete(self, _system: str, _user: str) -> str:
|
|
841
|
+
return json.dumps(self.plan, ensure_ascii=False, sort_keys=True)
|