subactor-shell 0.2.2__py3-none-any.whl
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.
- subactor_shell/__init__.py +3 -0
- subactor_shell/__main__.py +4 -0
- subactor_shell/acp_agent.py +363 -0
- subactor_shell/app.py +482 -0
- subactor_shell/artifacts.py +198 -0
- subactor_shell/catalog.py +416 -0
- subactor_shell/chat.py +517 -0
- subactor_shell/compiler.py +191 -0
- subactor_shell/config.py +374 -0
- subactor_shell/connectors.py +503 -0
- subactor_shell/context_builder.py +153 -0
- subactor_shell/control.py +141 -0
- subactor_shell/control_env.py +93 -0
- subactor_shell/intent_ir.py +187 -0
- subactor_shell/models.py +78 -0
- subactor_shell/operations.py +287 -0
- subactor_shell/orchestration.py +324 -0
- subactor_shell/policy.py +38 -0
- subactor_shell/providers/__init__.py +63 -0
- subactor_shell/providers/anthropic.py +101 -0
- subactor_shell/providers/base.py +81 -0
- subactor_shell/providers/mock.py +32 -0
- subactor_shell/providers/openai_compat.py +303 -0
- subactor_shell/providers/subactor_control.py +191 -0
- subactor_shell/redaction.py +62 -0
- subactor_shell/repl.py +480 -0
- subactor_shell/routing.py +334 -0
- subactor_shell/secret_refs.py +82 -0
- subactor_shell/store.py +857 -0
- subactor_shell/terminal.py +79 -0
- subactor_shell/token_budget.py +46 -0
- subactor_shell/vault.py +170 -0
- subactor_shell-0.2.2.dist-info/METADATA +449 -0
- subactor_shell-0.2.2.dist-info/RECORD +38 -0
- subactor_shell-0.2.2.dist-info/WHEEL +5 -0
- subactor_shell-0.2.2.dist-info/entry_points.txt +2 -0
- subactor_shell-0.2.2.dist-info/licenses/LICENSE +13 -0
- subactor_shell-0.2.2.dist-info/top_level.txt +1 -0
subactor_shell/repl.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import getpass
|
|
5
|
+
import json
|
|
6
|
+
import shlex
|
|
7
|
+
import signal
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from prompt_toolkit import PromptSession
|
|
12
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
|
|
17
|
+
from .chat import ChatError, ChatService
|
|
18
|
+
from .control import ControlError, SubactorControlClient
|
|
19
|
+
from .models import Session
|
|
20
|
+
from .orchestration import OrchestrationError
|
|
21
|
+
from .terminal import terminal_hyperlinks_enabled, ticket_link_lines
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
HELP = """[bold]Rozmowa[/bold]
|
|
25
|
+
/new [nazwa] nowa sesja
|
|
26
|
+
/sessions lista sesji
|
|
27
|
+
/resume ID wznowienie sesji
|
|
28
|
+
/provider NAZWA zmiana profilu providera
|
|
29
|
+
/model MODEL zmiana modelu w sesji
|
|
30
|
+
/attach PLIK dołącz plik do następnej wiadomości
|
|
31
|
+
/info aktywna sesja
|
|
32
|
+
|
|
33
|
+
[bold]Dane[/bold]
|
|
34
|
+
/data set NAZWA WARTOŚĆ zapisz jawne dane tekstowe
|
|
35
|
+
/data put NAZWA PLIK zapisz plik jako artefakt
|
|
36
|
+
/data list lista danych
|
|
37
|
+
/data del NAZWA usuń dane
|
|
38
|
+
W wiadomości użyj: {{data:NAZWA}}
|
|
39
|
+
|
|
40
|
+
[bold]Sekrety[/bold]
|
|
41
|
+
/vault bind ALIAS REF zapisz wyłącznie referencję vault://, env:// lub file://
|
|
42
|
+
/vault put ALIAS VAULT_REF wczytaj wartość bez echa, zapisz do KV v2 i utwórz binding
|
|
43
|
+
/vault grant ALIAS jednorazowo zezwól na {{secret:ALIAS}}
|
|
44
|
+
/vault list lista aliasów i referencji (bez wartości)
|
|
45
|
+
/vault unbind ALIAS usuń binding
|
|
46
|
+
/vault wrap ALIAS [TTL] utwórz jednorazowy wrapping token Vault
|
|
47
|
+
|
|
48
|
+
[bold]Orkiestracja i tokeny[/bold]
|
|
49
|
+
/plans lista planów sesji
|
|
50
|
+
/plan ID pokaż plan JSON
|
|
51
|
+
/apply ID zastosuj plan; zmiany wymagają EXECUTE
|
|
52
|
+
/receipts lista receipts sesji
|
|
53
|
+
/receipt ID pokaż receipt JSON
|
|
54
|
+
/route ostatnia decyzja routera
|
|
55
|
+
/metrics tokeny, koszt i udział fast path
|
|
56
|
+
/catalog lokalny katalog intentów
|
|
57
|
+
/connectors allowlista connectorów
|
|
58
|
+
|
|
59
|
+
[bold]Subactor Control[/bold]
|
|
60
|
+
/status wywołaj cli.status
|
|
61
|
+
/control tools sprawdź zamkniętą granicę MCP
|
|
62
|
+
/control call TOOL JSON wywołaj cli.status/plan/execute
|
|
63
|
+
|
|
64
|
+
[bold]Pozostałe[/bold]
|
|
65
|
+
/export PLIK eksport rozmowy do JSON (bez rozwiniętych sekretów)
|
|
66
|
+
/help ta pomoc
|
|
67
|
+
/q | /quit | /exit wyjście; działa też q, quit, exit i Ctrl-C
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
EXIT_COMMANDS = frozenset({"/q", "/quit", "/exit", "q", "quit", "exit"})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_exit_command(value: str) -> bool:
|
|
75
|
+
return value.strip().lower() in EXIT_COMMANDS
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class ShellRepl:
|
|
79
|
+
def __init__(self, chat: ChatService, session: Session, console: Console | None = None):
|
|
80
|
+
self.chat = chat
|
|
81
|
+
self.session = session
|
|
82
|
+
self.console = console or Console()
|
|
83
|
+
self.prompt = PromptSession()
|
|
84
|
+
self.pending_attachments: list[Path] = []
|
|
85
|
+
self.control = SubactorControlClient(chat.config.control, chat.resolver)
|
|
86
|
+
|
|
87
|
+
async def run(self) -> None:
|
|
88
|
+
self.console.print(
|
|
89
|
+
f"[bold]Subactor Shell[/bold] — sesja [cyan]{self.session.id}[/cyan], "
|
|
90
|
+
f"provider [green]{self.session.provider}[/green], model [green]{self.session.model}[/green]"
|
|
91
|
+
)
|
|
92
|
+
self.console.print("Wpisz /help, aby zobaczyć komendy. Sekrety podawaj jako {{secret:ALIAS}}.")
|
|
93
|
+
while True:
|
|
94
|
+
try:
|
|
95
|
+
with patch_stdout(raw=True):
|
|
96
|
+
line = await self.prompt.prompt_async(self._prompt_text())
|
|
97
|
+
except EOFError:
|
|
98
|
+
self.console.print()
|
|
99
|
+
return
|
|
100
|
+
except KeyboardInterrupt:
|
|
101
|
+
self.console.print()
|
|
102
|
+
return
|
|
103
|
+
line = line.strip()
|
|
104
|
+
if not line:
|
|
105
|
+
continue
|
|
106
|
+
if is_exit_command(line):
|
|
107
|
+
return
|
|
108
|
+
try:
|
|
109
|
+
if line.startswith("/"):
|
|
110
|
+
keep_running = await self._command(line)
|
|
111
|
+
if not keep_running:
|
|
112
|
+
return
|
|
113
|
+
else:
|
|
114
|
+
await self._send(line)
|
|
115
|
+
except (ChatError, ControlError, OrchestrationError, ValueError, KeyError, OSError, json.JSONDecodeError) as exc:
|
|
116
|
+
self.console.print(f"[red]Błąd:[/red] {exc}")
|
|
117
|
+
|
|
118
|
+
def _prompt_text(self) -> str:
|
|
119
|
+
marker = f" +{len(self.pending_attachments)} plik" if self.pending_attachments else ""
|
|
120
|
+
return f"subactor:{self.session.id[:8]}{marker}> "
|
|
121
|
+
|
|
122
|
+
async def _command(self, line: str) -> bool:
|
|
123
|
+
try:
|
|
124
|
+
parts = shlex.split(line)
|
|
125
|
+
except ValueError as exc:
|
|
126
|
+
raise ValueError(f"Nieprawidłowe cudzysłowy: {exc}") from exc
|
|
127
|
+
command = parts[0].lower()
|
|
128
|
+
args = parts[1:]
|
|
129
|
+
if is_exit_command(command):
|
|
130
|
+
return False
|
|
131
|
+
if command == "/help":
|
|
132
|
+
self.console.print(HELP)
|
|
133
|
+
return True
|
|
134
|
+
if command == "/new":
|
|
135
|
+
name = " ".join(args) or "Nowa rozmowa"
|
|
136
|
+
self.session = self.chat.new_session(name=name)
|
|
137
|
+
self.pending_attachments.clear()
|
|
138
|
+
self.console.print(f"Nowa sesja: [cyan]{self.session.id}[/cyan]")
|
|
139
|
+
return True
|
|
140
|
+
if command == "/sessions":
|
|
141
|
+
self._print_sessions()
|
|
142
|
+
return True
|
|
143
|
+
if command == "/resume":
|
|
144
|
+
self._require(args, 1, "/resume ID")
|
|
145
|
+
self.session = self._resolve_session(args[0])
|
|
146
|
+
self.pending_attachments.clear()
|
|
147
|
+
self.console.print(f"Wznowiono [cyan]{self.session.id}[/cyan]")
|
|
148
|
+
return True
|
|
149
|
+
if command == "/provider":
|
|
150
|
+
self._require(args, 1, "/provider NAZWA")
|
|
151
|
+
profile = self.chat.config.provider(args[0])
|
|
152
|
+
self.session = self.chat.store.update_session(
|
|
153
|
+
self.session.id, provider=profile.name, model=profile.model
|
|
154
|
+
)
|
|
155
|
+
self.console.print(
|
|
156
|
+
f"Provider: [green]{self.session.provider}[/green], model: [green]{self.session.model}[/green]"
|
|
157
|
+
)
|
|
158
|
+
return True
|
|
159
|
+
if command == "/model":
|
|
160
|
+
self._require(args, 1, "/model MODEL")
|
|
161
|
+
self.session = self.chat.store.update_session(self.session.id, model=args[0])
|
|
162
|
+
self.console.print(f"Model: [green]{self.session.model}[/green]")
|
|
163
|
+
return True
|
|
164
|
+
if command == "/attach":
|
|
165
|
+
self._require(args, 1, "/attach PLIK")
|
|
166
|
+
path = Path(args[0]).expanduser()
|
|
167
|
+
if not path.is_file():
|
|
168
|
+
raise ValueError(f"Brak pliku: {path}")
|
|
169
|
+
self.pending_attachments.append(path)
|
|
170
|
+
self.console.print(f"Do następnej wiadomości: {path}")
|
|
171
|
+
return True
|
|
172
|
+
if command == "/info":
|
|
173
|
+
self._print_info()
|
|
174
|
+
return True
|
|
175
|
+
if command == "/data":
|
|
176
|
+
self._handle_data(args)
|
|
177
|
+
return True
|
|
178
|
+
if command == "/vault":
|
|
179
|
+
self._handle_vault(args)
|
|
180
|
+
return True
|
|
181
|
+
if command == "/plans":
|
|
182
|
+
self._print_plans()
|
|
183
|
+
return True
|
|
184
|
+
if command == "/plan":
|
|
185
|
+
self._require(args, 1, "/plan ID")
|
|
186
|
+
payload = self.chat.store.get_execution_plan(args[0])
|
|
187
|
+
if not payload:
|
|
188
|
+
raise ValueError(f"Nie ma planu {args[0]}")
|
|
189
|
+
self._print_json(payload)
|
|
190
|
+
return True
|
|
191
|
+
if command == "/apply":
|
|
192
|
+
self._require(args, 1, "/apply ID")
|
|
193
|
+
payload = self.chat.store.get_execution_plan(args[0])
|
|
194
|
+
if not payload:
|
|
195
|
+
raise ValueError(f"Nie ma planu {args[0]}")
|
|
196
|
+
confirmation = ""
|
|
197
|
+
if str(payload.get("effect", "read")) != "read":
|
|
198
|
+
with patch_stdout(raw=True):
|
|
199
|
+
confirmation = await self.prompt.prompt_async(
|
|
200
|
+
"Plan może zmienić system. Wpisz dokładnie EXECUTE: "
|
|
201
|
+
)
|
|
202
|
+
if confirmation != "EXECUTE":
|
|
203
|
+
raise OrchestrationError("Anulowano apply")
|
|
204
|
+
receipt = await self.chat.orchestration.apply_plan(
|
|
205
|
+
args[0], confirmation=confirmation
|
|
206
|
+
)
|
|
207
|
+
self.console.print(self.chat.orchestration.format_receipt(receipt), markup=False)
|
|
208
|
+
self.chat.context_builder.update_state(
|
|
209
|
+
self.session.id,
|
|
210
|
+
user_text=f"apply {args[0]}",
|
|
211
|
+
receipt_id=receipt.id,
|
|
212
|
+
)
|
|
213
|
+
return True
|
|
214
|
+
if command == "/receipts":
|
|
215
|
+
self._print_receipts()
|
|
216
|
+
return True
|
|
217
|
+
if command == "/receipt":
|
|
218
|
+
self._require(args, 1, "/receipt ID")
|
|
219
|
+
payload = self.chat.store.get_execution_receipt(args[0])
|
|
220
|
+
if not payload:
|
|
221
|
+
raise ValueError(f"Nie ma receiptu {args[0]}")
|
|
222
|
+
self._print_json(payload)
|
|
223
|
+
return True
|
|
224
|
+
if command == "/route":
|
|
225
|
+
payload = self.chat.store.last_routing_decision(self.session.id)
|
|
226
|
+
self._print_json(payload or {})
|
|
227
|
+
return True
|
|
228
|
+
if command == "/metrics":
|
|
229
|
+
self._print_json(self.chat.store.usage_summary(self.session.id))
|
|
230
|
+
return True
|
|
231
|
+
if command == "/catalog":
|
|
232
|
+
table = Table("Intent", "Execution", "Risk", "Źródło")
|
|
233
|
+
for item in self.chat.orchestration.catalog.list():
|
|
234
|
+
table.add_row(
|
|
235
|
+
item.id, str(item.execution.get("kind", "chat")), item.risk, item.source
|
|
236
|
+
)
|
|
237
|
+
self.console.print(table)
|
|
238
|
+
return True
|
|
239
|
+
if command == "/connectors":
|
|
240
|
+
table = Table("Nazwa", "Kind", "Effect", "Operations")
|
|
241
|
+
for item in self.chat.orchestration.registry.list():
|
|
242
|
+
table.add_row(
|
|
243
|
+
item.name, item.kind, item.effect, ", ".join(item.allowed_operations)
|
|
244
|
+
)
|
|
245
|
+
self.console.print(table)
|
|
246
|
+
return True
|
|
247
|
+
if command == "/status":
|
|
248
|
+
await self._send("pokaż status subactora")
|
|
249
|
+
return True
|
|
250
|
+
if command == "/control":
|
|
251
|
+
await self._handle_control(args)
|
|
252
|
+
return True
|
|
253
|
+
if command == "/export":
|
|
254
|
+
self._require(args, 1, "/export PLIK")
|
|
255
|
+
target = Path(args[0]).expanduser()
|
|
256
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
257
|
+
payload = self.chat.store.export_session(self.session.id)
|
|
258
|
+
target.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
259
|
+
self.console.print(f"Zapisano: {target}")
|
|
260
|
+
return True
|
|
261
|
+
raise ValueError(f"Nieznana komenda: {command}. Użyj /help")
|
|
262
|
+
|
|
263
|
+
async def _send(self, text: str) -> None:
|
|
264
|
+
cancel_event = asyncio.Event()
|
|
265
|
+
loop = asyncio.get_running_loop()
|
|
266
|
+
installed_handler = False
|
|
267
|
+
previous_sigint = signal.getsignal(signal.SIGINT)
|
|
268
|
+
try:
|
|
269
|
+
try:
|
|
270
|
+
loop.add_signal_handler(signal.SIGINT, cancel_event.set)
|
|
271
|
+
installed_handler = True
|
|
272
|
+
except (NotImplementedError, RuntimeError):
|
|
273
|
+
pass
|
|
274
|
+
self.console.print("[bold cyan]agent>[/bold cyan] ", end="")
|
|
275
|
+
answer_parts: list[str] = []
|
|
276
|
+
async for chunk in self.chat.stream_message(
|
|
277
|
+
self.session.id,
|
|
278
|
+
text,
|
|
279
|
+
attachment_paths=list(self.pending_attachments),
|
|
280
|
+
cancel_event=cancel_event,
|
|
281
|
+
):
|
|
282
|
+
answer_parts.append(chunk)
|
|
283
|
+
self.console.print(Text(chunk), end="", soft_wrap=True)
|
|
284
|
+
self.console.print()
|
|
285
|
+
profile = self.chat.config.provider(self.session.provider)
|
|
286
|
+
if profile.kind == "subactor_control":
|
|
287
|
+
lines = ticket_link_lines(
|
|
288
|
+
"".join(answer_parts),
|
|
289
|
+
profile.base_url,
|
|
290
|
+
hyperlinks=terminal_hyperlinks_enabled(is_terminal=self.console.is_terminal),
|
|
291
|
+
)
|
|
292
|
+
if lines:
|
|
293
|
+
self.console.print(" [dim][tickety][/dim]")
|
|
294
|
+
for line in lines:
|
|
295
|
+
self.console.print(line)
|
|
296
|
+
if cancel_event.is_set():
|
|
297
|
+
self.console.print("[yellow]Anulowano.[/yellow]")
|
|
298
|
+
if bool(self.chat.config.orchestration.get("show_route", False)):
|
|
299
|
+
route = self.chat.store.last_routing_decision(self.session.id)
|
|
300
|
+
if route:
|
|
301
|
+
self.console.print(
|
|
302
|
+
f"[dim]route={route['route']} intent={route['intent_id']} "
|
|
303
|
+
f"confidence={route['confidence']:.3f}[/dim]"
|
|
304
|
+
)
|
|
305
|
+
self.pending_attachments.clear()
|
|
306
|
+
except KeyboardInterrupt:
|
|
307
|
+
cancel_event.set()
|
|
308
|
+
self.console.print("\n[yellow]Anulowano.[/yellow]")
|
|
309
|
+
finally:
|
|
310
|
+
if installed_handler:
|
|
311
|
+
loop.remove_signal_handler(signal.SIGINT)
|
|
312
|
+
signal.signal(signal.SIGINT, previous_sigint)
|
|
313
|
+
|
|
314
|
+
def _handle_data(self, args: list[str]) -> None:
|
|
315
|
+
self._require(args, 1, "/data set|put|list|del ...")
|
|
316
|
+
action = args[0].lower()
|
|
317
|
+
if action == "set":
|
|
318
|
+
self._require(args, 3, "/data set NAZWA WARTOŚĆ")
|
|
319
|
+
self.chat.set_data_text(args[1], " ".join(args[2:]))
|
|
320
|
+
self.console.print(f"Zapisano dane [cyan]{args[1]}[/cyan].")
|
|
321
|
+
elif action == "put":
|
|
322
|
+
self._require(args, 3, "/data put NAZWA PLIK")
|
|
323
|
+
artifact = self.chat.set_data_file(args[1], Path(args[2]), self.session.id)
|
|
324
|
+
self.console.print(f"Zapisano [cyan]{args[1]}[/cyan] jako sha256:{artifact.id}")
|
|
325
|
+
elif action == "list":
|
|
326
|
+
table = Table("Nazwa", "Typ", "Wartość/ID")
|
|
327
|
+
for name, kind, value in self.chat.store.list_data():
|
|
328
|
+
shown = value if kind == "artifact" else f"{len(value)} znaków"
|
|
329
|
+
table.add_row(name, kind, shown)
|
|
330
|
+
self.console.print(table)
|
|
331
|
+
elif action == "del":
|
|
332
|
+
self._require(args, 2, "/data del NAZWA")
|
|
333
|
+
removed = self.chat.store.delete_data(args[1])
|
|
334
|
+
self.console.print("Usunięto." if removed else "Nie znaleziono.")
|
|
335
|
+
else:
|
|
336
|
+
raise ValueError("Użyj /data set|put|list|del")
|
|
337
|
+
|
|
338
|
+
def _handle_vault(self, args: list[str]) -> None:
|
|
339
|
+
self._require(args, 1, "/vault bind|put|grant|list|unbind|wrap ...")
|
|
340
|
+
action = args[0].lower()
|
|
341
|
+
if action == "bind":
|
|
342
|
+
self._require(args, 3, "/vault bind ALIAS REF")
|
|
343
|
+
self.chat.bind_secret(args[1], args[2])
|
|
344
|
+
self.console.print(f"Binding [cyan]{args[1]}[/cyan] zapisany; wartość nie została odczytana.")
|
|
345
|
+
elif action == "put":
|
|
346
|
+
self._require(args, 3, "/vault put ALIAS vault://MOUNT/SCIEZKA#POLE")
|
|
347
|
+
reference = args[2]
|
|
348
|
+
if not reference.startswith("vault://"):
|
|
349
|
+
raise ValueError("/vault put wymaga referencji vault://")
|
|
350
|
+
value = getpass.getpass("Wartość sekretu (bez echa): ")
|
|
351
|
+
if not value:
|
|
352
|
+
raise ValueError("Pusta wartość sekretu")
|
|
353
|
+
self.chat.resolver.vault.write_field(reference, value)
|
|
354
|
+
self.chat.bind_secret(args[1], reference)
|
|
355
|
+
self.console.print(f"Zapisano sekret i binding [cyan]{args[1]}[/cyan].")
|
|
356
|
+
elif action == "grant":
|
|
357
|
+
self._require(args, 2, "/vault grant ALIAS")
|
|
358
|
+
self.chat.grant_secret(args[1])
|
|
359
|
+
self.console.print(
|
|
360
|
+
f"Jednorazowy grant dla [cyan]{args[1]}[/cyan]. Zostanie zużyty przez następną wiadomość."
|
|
361
|
+
)
|
|
362
|
+
elif action == "list":
|
|
363
|
+
table = Table("Alias", "Referencja", "Grant w pamięci")
|
|
364
|
+
granted = set(self.chat.grants.list())
|
|
365
|
+
for alias, reference in self.chat.store.list_secret_bindings():
|
|
366
|
+
table.add_row(alias, reference, "tak" if alias in granted else "nie")
|
|
367
|
+
self.console.print(table)
|
|
368
|
+
elif action == "unbind":
|
|
369
|
+
self._require(args, 2, "/vault unbind ALIAS")
|
|
370
|
+
removed = self.chat.store.unbind_secret(args[1])
|
|
371
|
+
self.console.print("Usunięto binding." if removed else "Nie znaleziono bindingu.")
|
|
372
|
+
elif action == "wrap":
|
|
373
|
+
self._require(args, 2, "/vault wrap ALIAS [TTL]")
|
|
374
|
+
reference = self.chat.store.get_secret_binding(args[1])
|
|
375
|
+
if not reference:
|
|
376
|
+
raise ValueError(f"Brak bindingu {args[1]}")
|
|
377
|
+
if not reference.startswith("vault://"):
|
|
378
|
+
raise ValueError("Response wrapping działa tylko dla vault://")
|
|
379
|
+
token = self.chat.resolver.vault.wrap_read(reference, args[2] if len(args) > 2 else "5m")
|
|
380
|
+
self.console.print("[yellow]Wrapping token (jednorazowy; obejmuje całą odpowiedź ścieżki KV):[/yellow]")
|
|
381
|
+
self.console.print(Text(token))
|
|
382
|
+
else:
|
|
383
|
+
raise ValueError("Użyj /vault bind|put|grant|list|unbind|wrap")
|
|
384
|
+
|
|
385
|
+
async def _handle_control(self, args: list[str]) -> None:
|
|
386
|
+
self._require(args, 1, "/control tools|call ...")
|
|
387
|
+
action = args[0].lower()
|
|
388
|
+
if action == "tools":
|
|
389
|
+
tools = await asyncio.to_thread(self.control.list_tools, strict=True)
|
|
390
|
+
table = Table("Narzędzie", "Opis")
|
|
391
|
+
for item in tools:
|
|
392
|
+
table.add_row(str(item.get("name", "")), str(item.get("description", "")))
|
|
393
|
+
self.console.print(table)
|
|
394
|
+
return
|
|
395
|
+
if action == "call":
|
|
396
|
+
self._require(args, 3, "/control call TOOL JSON")
|
|
397
|
+
name = args[1]
|
|
398
|
+
arguments = json.loads(" ".join(args[2:]))
|
|
399
|
+
if not isinstance(arguments, dict):
|
|
400
|
+
raise ValueError("Argumenty narzędzia muszą być obiektem JSON")
|
|
401
|
+
allow_execute = False
|
|
402
|
+
if name == "cli.execute":
|
|
403
|
+
with patch_stdout(raw=True):
|
|
404
|
+
confirmation = await self.prompt.prompt_async(
|
|
405
|
+
"cli.execute może zmienić system. Wpisz dokładnie EXECUTE: "
|
|
406
|
+
)
|
|
407
|
+
allow_execute = confirmation == "EXECUTE"
|
|
408
|
+
if not allow_execute:
|
|
409
|
+
raise ControlError("Anulowano cli.execute")
|
|
410
|
+
result = await asyncio.to_thread(
|
|
411
|
+
self.control.call_tool,
|
|
412
|
+
name,
|
|
413
|
+
arguments,
|
|
414
|
+
allow_execute=allow_execute,
|
|
415
|
+
)
|
|
416
|
+
self._print_json(result)
|
|
417
|
+
return
|
|
418
|
+
raise ValueError("Użyj /control tools|call")
|
|
419
|
+
|
|
420
|
+
def _print_plans(self) -> None:
|
|
421
|
+
table = Table("ID", "Intent", "Effect", "Status", "Utworzono")
|
|
422
|
+
for item in self.chat.store.list_execution_plans(self.session.id):
|
|
423
|
+
table.add_row(
|
|
424
|
+
str(item.get("id", "")),
|
|
425
|
+
str(item.get("intent_id", "")),
|
|
426
|
+
str(item.get("effect", "")),
|
|
427
|
+
str(item.get("status", "")),
|
|
428
|
+
str(item.get("created_at", "")),
|
|
429
|
+
)
|
|
430
|
+
self.console.print(table)
|
|
431
|
+
|
|
432
|
+
def _print_receipts(self) -> None:
|
|
433
|
+
table = Table("ID", "Plan", "OK", "Utworzono")
|
|
434
|
+
for item in self.chat.store.list_execution_receipts(self.session.id):
|
|
435
|
+
table.add_row(
|
|
436
|
+
str(item.get("id", "")),
|
|
437
|
+
str(item.get("plan_id", "")),
|
|
438
|
+
str(bool(item.get("ok"))),
|
|
439
|
+
str(item.get("created_at", "")),
|
|
440
|
+
)
|
|
441
|
+
self.console.print(table)
|
|
442
|
+
|
|
443
|
+
def _resolve_session(self, value: str) -> Session:
|
|
444
|
+
exact = self.chat.store.get_session(value)
|
|
445
|
+
if exact:
|
|
446
|
+
return exact
|
|
447
|
+
matches = [item for item in self.chat.store.list_sessions() if item.id.startswith(value)]
|
|
448
|
+
if len(matches) == 1:
|
|
449
|
+
return matches[0]
|
|
450
|
+
if not matches:
|
|
451
|
+
raise ValueError(f"Nie ma sesji pasującej do {value}")
|
|
452
|
+
raise ValueError(f"Prefiks {value} jest niejednoznaczny")
|
|
453
|
+
|
|
454
|
+
def _print_sessions(self) -> None:
|
|
455
|
+
table = Table("ID", "Nazwa", "Provider", "Model", "Aktualizacja")
|
|
456
|
+
for item in self.chat.store.list_sessions():
|
|
457
|
+
table.add_row(item.id, item.name, item.provider, item.model, item.updated_at)
|
|
458
|
+
self.console.print(table)
|
|
459
|
+
|
|
460
|
+
def _print_info(self) -> None:
|
|
461
|
+
self.console.print(
|
|
462
|
+
{
|
|
463
|
+
"id": self.session.id,
|
|
464
|
+
"name": self.session.name,
|
|
465
|
+
"provider": self.session.provider,
|
|
466
|
+
"model": self.session.model,
|
|
467
|
+
"pending_attachments": [str(path) for path in self.pending_attachments],
|
|
468
|
+
"working_state": self.chat.store.get_session_state(self.session.id),
|
|
469
|
+
"last_route": self.chat.store.last_routing_decision(self.session.id),
|
|
470
|
+
"usage": self.chat.store.usage_summary(self.session.id),
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def _print_json(self, payload: Any) -> None:
|
|
475
|
+
self.console.print_json(json.dumps(payload, ensure_ascii=False, default=str))
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def _require(args: list[str], count: int, usage: str) -> None:
|
|
479
|
+
if len(args) < count:
|
|
480
|
+
raise ValueError(f"Użycie: {usage}")
|