agent-devkit 0.3.0 → 0.3.2

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.
@@ -0,0 +1,351 @@
1
+ """Embedded mini-brain runtime backed by an on-demand GGUF model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import urllib.error
11
+ import urllib.request
12
+ from pathlib import Path
13
+ from typing import Any
14
+
15
+ from cli.aikit.app_home import app_path, ensure_app_home
16
+ from cli.aikit.identity import identity_system_prompt
17
+ from cli.aikit.runtime_paths import ROOT
18
+
19
+
20
+ EMBEDDED_BACKEND_ID = "embedded-mini-brain"
21
+ EMBEDDED_MODEL_ID = "Qwen/Qwen2.5-0.5B-Instruct"
22
+ EMBEDDED_MODEL_NAME = "qwen2.5-0.5b-instruct"
23
+ EMBEDDED_MODEL_SOURCE = "https://huggingface.co/Qwen/Qwen2.5-0.5B-Instruct-GGUF/resolve/main/qwen2.5-0.5b-instruct-q2_k.gguf"
24
+ EMBEDDED_MODEL_SIZE_BYTES = 415182688
25
+ EMBEDDED_MODEL_PATH = app_path("models", EMBEDDED_MODEL_NAME)
26
+ EMBEDDED_MANIFEST_PATH = ROOT / "models" / EMBEDDED_MODEL_NAME / "manifest.json"
27
+ EMBEDDED_MODEL_FILE = EMBEDDED_MODEL_PATH / "qwen2.5-0.5b-instruct-q2_k.gguf"
28
+ EMBEDDED_MODEL_SHA256 = "9ee36184e616dfc76df4f5dd66f908dbde6979524ae36e6cefb67f532f798cb8"
29
+ EMBEDDED_RUNTIME = "llama-cpp-python"
30
+ EMBEDDED_RUNTIME_REQUIREMENT = "llama-cpp-python>=0.3.9"
31
+ EMBEDDED_MAX_RESPONSE_CHARS = 2000
32
+ DEFAULT_MAX_TOKENS = 220
33
+ DEFAULT_CONTEXT_TOKENS = 2048
34
+ SMOKE_RESPONSE_ENV = "AGENT_DEVKIT_EMBEDDED_SMOKE_RESPONSE"
35
+ SOURCE_ENV = "AGENT_DEVKIT_EMBEDDED_MODEL_SOURCE"
36
+ SKIP_DEP_INSTALL_ENV = "AGENT_DEVKIT_EMBEDDED_SKIP_DEP_INSTALL"
37
+
38
+ _LLAMA_CACHE: Any | None = None
39
+
40
+
41
+ def embedded_mini_brain_status() -> dict[str, Any]:
42
+ manifest_exists = EMBEDDED_MANIFEST_PATH.exists()
43
+ model_exists = EMBEDDED_MODEL_FILE.exists()
44
+ model_sha256 = sha256_file(EMBEDDED_MODEL_FILE) if model_exists else None
45
+ smoke_mode = bool(os.environ.get(SMOKE_RESPONSE_ENV))
46
+ model_file_valid = smoke_mode or (model_sha256 == EMBEDDED_MODEL_SHA256 if model_exists else False)
47
+ dependency = llama_cpp_dependency_status()
48
+ available = model_file_valid and dependency["status"] == "ok"
49
+ if available:
50
+ status = "ok"
51
+ elif not model_exists:
52
+ status = "not-installed"
53
+ elif not model_file_valid:
54
+ status = "invalid-model"
55
+ elif dependency["status"] != "ok":
56
+ status = "dependency-missing"
57
+ else:
58
+ status = "missing"
59
+ return {
60
+ "kind": "embedded-mini-brain",
61
+ "id": EMBEDDED_BACKEND_ID,
62
+ "status": status,
63
+ "available": available,
64
+ "configured": model_file_valid,
65
+ "provider": EMBEDDED_BACKEND_ID,
66
+ "runtime": EMBEDDED_RUNTIME,
67
+ "runtime_requirement": EMBEDDED_RUNTIME_REQUIREMENT,
68
+ "model": EMBEDDED_MODEL_ID,
69
+ "hf_model": EMBEDDED_MODEL_ID,
70
+ "model_name": EMBEDDED_MODEL_NAME,
71
+ "model_path": str(EMBEDDED_MODEL_PATH),
72
+ "model_file": str(EMBEDDED_MODEL_FILE),
73
+ "model_file_present": model_exists,
74
+ "model_file_valid": model_file_valid,
75
+ "model_file_sha256": model_sha256,
76
+ "smoke_mode": smoke_mode,
77
+ "model_size_bytes": EMBEDDED_MODEL_SIZE_BYTES,
78
+ "download_url": model_source(),
79
+ "sha256": EMBEDDED_MODEL_SHA256,
80
+ "manifest_path": str(EMBEDDED_MANIFEST_PATH),
81
+ "manifest_present": manifest_exists,
82
+ "dependency": dependency,
83
+ "auth": "none",
84
+ "stored_secret": False,
85
+ "install_command": "agent setup mini-brain --yes",
86
+ "message": (
87
+ "Embedded Qwen2.5 mini-brain is available for real local inference."
88
+ if available
89
+ else "Embedded mini-brain model is not installed or llama_cpp runtime is missing."
90
+ ),
91
+ }
92
+
93
+
94
+ def invoke_embedded_mini_brain(prompt: str, *, public_name: str = "Agent DevKit") -> str:
95
+ status = embedded_mini_brain_status()
96
+ if not status["available"]:
97
+ raise EmbeddedMiniBrainError(status["message"])
98
+ smoke_response = os.environ.get(SMOKE_RESPONSE_ENV)
99
+ if smoke_response:
100
+ return f"{public_name}: {smoke_response}"[:EMBEDDED_MAX_RESPONSE_CHARS]
101
+ llama = load_llama()
102
+ payload = llama.create_chat_completion(
103
+ messages=[
104
+ {
105
+ "role": "system",
106
+ "content": embedded_system_prompt(public_name),
107
+ },
108
+ {
109
+ "role": "user",
110
+ "content": prompt,
111
+ },
112
+ ],
113
+ max_tokens=int(os.environ.get("AGENT_DEVKIT_EMBEDDED_MAX_TOKENS", str(DEFAULT_MAX_TOKENS))),
114
+ temperature=float(os.environ.get("AGENT_DEVKIT_EMBEDDED_TEMPERATURE", "0.2")),
115
+ top_p=float(os.environ.get("AGENT_DEVKIT_EMBEDDED_TOP_P", "0.9")),
116
+ repeat_penalty=float(os.environ.get("AGENT_DEVKIT_EMBEDDED_REPEAT_PENALTY", "1.08")),
117
+ )
118
+ try:
119
+ content = str(payload["choices"][0]["message"]["content"]).strip()
120
+ except (KeyError, IndexError, TypeError) as exc:
121
+ raise EmbeddedMiniBrainError("Embedded mini-brain returned an unexpected response shape.") from exc
122
+ if not content:
123
+ raise EmbeddedMiniBrainError("Embedded mini-brain returned an empty response.")
124
+ return content[:EMBEDDED_MAX_RESPONSE_CHARS]
125
+
126
+
127
+ def embedded_backend_doctor() -> dict[str, Any]:
128
+ status = embedded_mini_brain_status()
129
+ return {
130
+ "id": EMBEDDED_BACKEND_ID,
131
+ "display_name": "Embedded mini-brain",
132
+ "kind": "embedded-local",
133
+ "status": status["status"],
134
+ "configured": status["configured"],
135
+ "model": EMBEDDED_MODEL_ID,
136
+ "model_file": status["model_file"],
137
+ "runtime": EMBEDDED_RUNTIME,
138
+ "auth_status": "none",
139
+ "message": status["message"],
140
+ }
141
+
142
+
143
+ def embedded_backend_config() -> dict[str, Any]:
144
+ return {
145
+ "kind": "embedded-local",
146
+ "auth": "none",
147
+ "model": EMBEDDED_MODEL_ID,
148
+ "runtime": EMBEDDED_RUNTIME,
149
+ "model_file": str(EMBEDDED_MODEL_FILE),
150
+ }
151
+
152
+
153
+ def setup_embedded_mini_brain(*, dry_run: bool = False, yes: bool = False) -> dict[str, Any]:
154
+ before = embedded_mini_brain_status()
155
+ plan = embedded_install_plan()
156
+ if dry_run or not yes:
157
+ needs_confirmation = not dry_run and not yes
158
+ return {
159
+ "kind": "embedded-mini-brain-install",
160
+ "status": "planned" if dry_run else "needs-confirmation",
161
+ "ok": bool(dry_run),
162
+ "exit_code": 2 if needs_confirmation else 0,
163
+ "dry_run": dry_run,
164
+ "yes": yes,
165
+ "before": before,
166
+ "after": before,
167
+ "plan": plan,
168
+ "message": "Use --yes to download the embedded mini-brain model and install its local runtime.",
169
+ }
170
+
171
+ ensure_app_home()
172
+ EMBEDDED_MODEL_PATH.mkdir(parents=True, exist_ok=True)
173
+ download_result = ensure_model_file()
174
+ dependency_result = ensure_llama_cpp_dependency()
175
+ after = embedded_mini_brain_status()
176
+ ok = after.get("available") is True
177
+ return {
178
+ "kind": "embedded-mini-brain-install",
179
+ "status": "ok" if ok else "failed",
180
+ "ok": ok,
181
+ "exit_code": 0 if ok else 1,
182
+ "dry_run": False,
183
+ "yes": True,
184
+ "before": before,
185
+ "after": after,
186
+ "plan": plan,
187
+ "download": download_result,
188
+ "dependency_install": dependency_result,
189
+ }
190
+
191
+
192
+ def embedded_install_plan() -> dict[str, Any]:
193
+ return {
194
+ "provider": EMBEDDED_BACKEND_ID,
195
+ "model": EMBEDDED_MODEL_ID,
196
+ "model_name": EMBEDDED_MODEL_NAME,
197
+ "download_url": model_source(),
198
+ "size_bytes": EMBEDDED_MODEL_SIZE_BYTES,
199
+ "sha256": EMBEDDED_MODEL_SHA256,
200
+ "destination": str(EMBEDDED_MODEL_FILE),
201
+ "runtime_requirement": EMBEDDED_RUNTIME_REQUIREMENT,
202
+ "writes": [
203
+ str(EMBEDDED_MODEL_FILE),
204
+ str(app_path("python")),
205
+ ],
206
+ }
207
+
208
+
209
+ def embedded_system_prompt(public_name: str) -> str:
210
+ return "\n".join(
211
+ [
212
+ identity_system_prompt(name=public_name),
213
+ "Voce e o mini cerebro local embarcado do Agent DevKit.",
214
+ "Responda em portugues claro quando o usuario escrever em portugues.",
215
+ "Voce pode conversar, orientar onboarding/setup, explicar capacidades e preparar tarefas simples.",
216
+ "Nao finja ser Claude, Codex, OpenAI ou Ollama.",
217
+ "Nao aprove escrita externa, operacoes destrutivas, decisoes finais de seguranca ou revisoes finais.",
218
+ "Quando a tarefa exigir alto julgamento, diga que pode acionar Claude, Codex, Ollama ou APIs se configurados.",
219
+ ]
220
+ )
221
+
222
+
223
+ def load_llama() -> Any:
224
+ global _LLAMA_CACHE
225
+ if _LLAMA_CACHE is not None:
226
+ return _LLAMA_CACHE
227
+ try:
228
+ from llama_cpp import Llama # type: ignore
229
+ except ImportError as exc:
230
+ raise EmbeddedMiniBrainError("llama-cpp-python is required for embedded mini-brain inference.") from exc
231
+ if not EMBEDDED_MODEL_FILE.exists():
232
+ raise EmbeddedMiniBrainError(f"Embedded model file not found: {EMBEDDED_MODEL_FILE}")
233
+ if sha256_file(EMBEDDED_MODEL_FILE) != EMBEDDED_MODEL_SHA256:
234
+ raise EmbeddedMiniBrainError(f"Embedded model file failed SHA-256 validation: {EMBEDDED_MODEL_FILE}")
235
+ _LLAMA_CACHE = Llama(
236
+ model_path=str(EMBEDDED_MODEL_FILE),
237
+ n_ctx=int(os.environ.get("AGENT_DEVKIT_EMBEDDED_N_CTX", str(DEFAULT_CONTEXT_TOKENS))),
238
+ n_threads=int(os.environ.get("AGENT_DEVKIT_EMBEDDED_THREADS", str(max(1, min(4, os.cpu_count() or 1))))),
239
+ verbose=os.environ.get("AGENT_DEVKIT_EMBEDDED_VERBOSE") == "1",
240
+ )
241
+ return _LLAMA_CACHE
242
+
243
+
244
+ def llama_cpp_dependency_status() -> dict[str, Any]:
245
+ if os.environ.get(SMOKE_RESPONSE_ENV):
246
+ return {
247
+ "status": "ok",
248
+ "module": "llama_cpp",
249
+ "package": "llama-cpp-python",
250
+ "mode": "smoke",
251
+ }
252
+ try:
253
+ import llama_cpp # type: ignore
254
+ except ImportError:
255
+ return {
256
+ "status": "missing",
257
+ "module": "llama_cpp",
258
+ "package": "llama-cpp-python",
259
+ }
260
+ return {
261
+ "status": "ok",
262
+ "module": "llama_cpp",
263
+ "package": "llama-cpp-python",
264
+ "version": getattr(llama_cpp, "__version__", None),
265
+ }
266
+
267
+
268
+ def ensure_model_file() -> dict[str, Any]:
269
+ if os.environ.get(SMOKE_RESPONSE_ENV):
270
+ return {
271
+ "status": "skipped",
272
+ "ok": True,
273
+ "model_file": str(EMBEDDED_MODEL_FILE),
274
+ "reason": "smoke-mode",
275
+ }
276
+ if EMBEDDED_MODEL_FILE.exists() and sha256_file(EMBEDDED_MODEL_FILE) == EMBEDDED_MODEL_SHA256:
277
+ return {
278
+ "status": "already-installed",
279
+ "ok": True,
280
+ "model_file": str(EMBEDDED_MODEL_FILE),
281
+ "sha256": EMBEDDED_MODEL_SHA256,
282
+ }
283
+ partial = EMBEDDED_MODEL_FILE.with_suffix(EMBEDDED_MODEL_FILE.suffix + ".part")
284
+ source = model_source()
285
+ try:
286
+ if Path(source).expanduser().exists():
287
+ shutil.copyfile(Path(source).expanduser(), partial)
288
+ else:
289
+ with urllib.request.urlopen(source, timeout=120) as response, partial.open("wb") as target:
290
+ shutil.copyfileobj(response, target)
291
+ except (OSError, urllib.error.URLError) as exc:
292
+ return {
293
+ "status": "failed",
294
+ "ok": False,
295
+ "model_file": str(EMBEDDED_MODEL_FILE),
296
+ "source": source,
297
+ "message": str(exc),
298
+ }
299
+ actual_sha = sha256_file(partial)
300
+ if actual_sha != EMBEDDED_MODEL_SHA256:
301
+ return {
302
+ "status": "failed",
303
+ "ok": False,
304
+ "model_file": str(EMBEDDED_MODEL_FILE),
305
+ "source": source,
306
+ "sha256": actual_sha,
307
+ "expected_sha256": EMBEDDED_MODEL_SHA256,
308
+ "message": "Downloaded embedded model failed SHA-256 validation.",
309
+ }
310
+ partial.replace(EMBEDDED_MODEL_FILE)
311
+ return {
312
+ "status": "downloaded",
313
+ "ok": True,
314
+ "model_file": str(EMBEDDED_MODEL_FILE),
315
+ "source": source,
316
+ "sha256": EMBEDDED_MODEL_SHA256,
317
+ }
318
+
319
+
320
+ def ensure_llama_cpp_dependency() -> dict[str, Any]:
321
+ current = llama_cpp_dependency_status()
322
+ if current.get("status") == "ok":
323
+ return {"status": "already-installed", "ok": True, "dependency": current}
324
+ if os.environ.get(SKIP_DEP_INSTALL_ENV) == "1":
325
+ return {"status": "skipped", "ok": True, "dependency": current, "reason": "disabled-by-env"}
326
+ command = [sys.executable, "-m", "pip", "install", EMBEDDED_RUNTIME_REQUIREMENT]
327
+ process = subprocess.run(command, check=False, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=900)
328
+ return {
329
+ "status": "installed" if process.returncode == 0 else "failed",
330
+ "ok": process.returncode == 0,
331
+ "command": command,
332
+ "exit_code": process.returncode,
333
+ "stdout": process.stdout[-4000:],
334
+ "stderr": process.stderr[-4000:],
335
+ }
336
+
337
+
338
+ def model_source() -> str:
339
+ return os.environ.get(SOURCE_ENV) or EMBEDDED_MODEL_SOURCE
340
+
341
+
342
+ def sha256_file(path: Path) -> str:
343
+ hash_obj = hashlib.sha256()
344
+ with path.open("rb") as file:
345
+ for chunk in iter(lambda: file.read(1024 * 1024), b""):
346
+ hash_obj.update(chunk)
347
+ return hash_obj.hexdigest()
348
+
349
+
350
+ class EmbeddedMiniBrainError(RuntimeError):
351
+ """Raised when embedded local inference cannot run."""
@@ -115,6 +115,8 @@ def print_human(result: dict[str, Any]) -> None:
115
115
  print_aliases(result)
116
116
  elif kind == "alias":
117
117
  print_alias(result)
118
+ elif kind == "alias-path":
119
+ print_alias_path(result)
118
120
  elif kind == "sessions":
119
121
  print_sessions(result)
120
122
  elif kind == "session":
@@ -630,6 +632,9 @@ def print_architecture(result: dict[str, Any]) -> None:
630
632
  def print_agent_response(result: dict[str, Any]) -> None:
631
633
  if result.get("status") == "ok":
632
634
  print(result.get("response", ""))
635
+ alias = result.get("alias") if isinstance(result.get("alias"), dict) else None
636
+ if alias:
637
+ print_alias_path_hint(alias.get("path_status") if isinstance(alias.get("path_status"), dict) else None)
633
638
  return
634
639
  print(result.get("message") or result.get("response") or "Agent execution did not complete.")
635
640
  question = result.get("next_question") or ((result.get("setup_wizard") or {}).get("next_question") if isinstance(result.get("setup_wizard"), dict) else None)
@@ -815,6 +820,12 @@ def print_personality(result: dict[str, Any]) -> None:
815
820
  print("Setup questions:")
816
821
  for question in result["questions"]:
817
822
  print(f"- {question}")
823
+ alias = result.get("alias") if isinstance(result.get("alias"), dict) else None
824
+ if alias:
825
+ print(f"Alias: {alias.get('name') or alias.get('suggested_name') or '-'} ({alias.get('status')})")
826
+ if alias.get("path"):
827
+ print(f"Alias path: {alias['path']}")
828
+ print_alias_path_hint(alias.get("path_status") if isinstance(alias.get("path_status"), dict) else None)
818
829
 
819
830
 
820
831
  def print_aliases(result: dict[str, Any]) -> None:
@@ -824,6 +835,7 @@ def print_aliases(result: dict[str, Any]) -> None:
824
835
  return
825
836
  for item in result["items"]:
826
837
  print(f"- {item['name']}: {item['path']}")
838
+ print_alias_path_hint(item.get("path_status") if isinstance(item.get("path_status"), dict) else None, indent=" ")
827
839
 
828
840
 
829
841
  def print_alias(result: dict[str, Any]) -> None:
@@ -835,6 +847,26 @@ def print_alias(result: dict[str, Any]) -> None:
835
847
  for path in result["removed_paths"]:
836
848
  print(f"- {path}")
837
849
  print(f"Config: {result['config_path']}")
850
+ print_alias_path_hint(result.get("path_status") if isinstance(result.get("path_status"), dict) else None)
851
+
852
+
853
+ def print_alias_path(result: dict[str, Any]) -> None:
854
+ print(f"Alias PATH: {result.get('status')}")
855
+ print(f"Bin: {result.get('bin_dir') or '-'}")
856
+ if result.get("profile"):
857
+ print(f"Profile: {result['profile']}")
858
+ if result.get("message"):
859
+ print(result["message"])
860
+
861
+
862
+ def print_alias_path_hint(path_status: dict[str, Any] | None, *, indent: str = "") -> None:
863
+ if not path_status or not path_status.get("setup_required"):
864
+ return
865
+ setup = path_status.get("setup") if isinstance(path_status.get("setup"), dict) else {}
866
+ print(f"{indent}PATH: {path_status.get('bin_dir')} is not active in this shell.")
867
+ command = setup.get("command")
868
+ if command:
869
+ print(f"{indent}Run: {command}")
838
870
 
839
871
 
840
872
  def print_sessions(result: dict[str, Any]) -> None:
@@ -2,19 +2,45 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  import sys
6
7
  from typing import Any
7
8
 
8
9
  from cli.aikit.core.requests import AgentPromptRequest
9
10
  from cli.aikit.core.runtime import run_agent_prompt
11
+ from cli.aikit.aliases import setup_alias_path
10
12
  from cli.aikit.llm import BACKENDS, configure_backend
11
- from cli.aikit.mini_brain import DEFAULT_OLLAMA_MODEL, setup_mini_brain
13
+ from cli.aikit.mini_brain import DEFAULT_OLLAMA_MODEL
12
14
  from cli.aikit.ollama import ollama_status
13
15
  from cli.aikit.onboarding import onboarding_status
14
16
  from cli.aikit.personality import load_personality, update_personality
15
17
  from cli.aikit.runtime_paths import ROOT
16
18
  from cli.aikit.wizard_state import WizardStateError, answer_wizard, cancel_wizard, show_wizard
17
19
 
20
+ ONBOARDING_MODE_OPTIONS = [
21
+ {
22
+ "id": "minimal",
23
+ "number": "1",
24
+ "label": "minimo",
25
+ "description": "identidade, mini-cerebro local embarcado e memoria",
26
+ "recommended": True,
27
+ },
28
+ {
29
+ "id": "complete",
30
+ "number": "2",
31
+ "label": "completo",
32
+ "description": "minimo + toolchain, sources, notificacoes, knowledge e memorias",
33
+ "recommended": False,
34
+ },
35
+ {
36
+ "id": "skip",
37
+ "number": "3",
38
+ "label": "pular",
39
+ "description": "",
40
+ "recommended": False,
41
+ },
42
+ ]
43
+
18
44
 
19
45
  def maybe_run_interactive_wizard(result: dict[str, Any]) -> dict[str, Any]:
20
46
  if not sys.stdin.isatty() or not sys.stdout.isatty():
@@ -104,11 +130,9 @@ def run_interactive_onboarding(result: dict[str, Any]) -> dict[str, Any]:
104
130
  print("\nOllama nao foi encontrado.")
105
131
  if command:
106
132
  print(f"Instalacao sugerida: {command}")
107
- print("Depois de instalar, rode `agent setup mini-brain --yes` para baixar o Qwen3-0.6B.")
108
- elif ask_yes_no(f"Deseja habilitar o mini cerebro local com {DEFAULT_OLLAMA_MODEL}?", default=False):
109
- set_default = ask_yes_no("Usar este mini cerebro como backend LLM padrao?", default=False)
110
- setup = setup_mini_brain(yes=True, set_default=set_default)
111
- print(setup.get("message") or f"Mini cerebro: {setup.get('status')}")
133
+ print("O mini cerebro embarcado ja funciona; instale Ollama apenas se quiser workers locais adicionais.")
134
+ elif ask_yes_no(f"Deseja instalar o modelo Ollama opcional {DEFAULT_OLLAMA_MODEL} para workers locais?", default=False):
135
+ print("Rode: agent local-llm install " + DEFAULT_OLLAMA_MODEL + " --yes")
112
136
 
113
137
  fresh = onboarding_status(ROOT)
114
138
  toolchain = fresh.get("toolchain") if isinstance(fresh.get("toolchain"), dict) else {}
@@ -128,18 +152,148 @@ def run_interactive_onboarding(result: dict[str, Any]) -> dict[str, Any]:
128
152
 
129
153
 
130
154
  def choose_onboarding_mode() -> str:
155
+ selected = choose_onboarding_mode_with_arrows()
156
+ if selected:
157
+ return selected
158
+ print("\nModos de onboarding:")
159
+ for option in ONBOARDING_MODE_OPTIONS:
160
+ print(format_onboarding_option(option, selected=False, include_selector=False))
161
+ answer = ask_text("Escolha o modo:").strip().lower()
162
+ return parse_onboarding_mode_answer(answer)
163
+
164
+
165
+ def choose_onboarding_mode_with_arrows() -> str | None:
166
+ if not sys.stdin.isatty() or not sys.stdout.isatty() or os.environ.get("TERM") == "dumb":
167
+ return None
168
+ try:
169
+ return read_onboarding_mode_selection()
170
+ except KeyboardInterrupt:
171
+ print()
172
+ return "skip"
173
+ except (OSError, ValueError):
174
+ return None
175
+
176
+
177
+ def read_onboarding_mode_selection() -> str:
178
+ selected_index = 0
179
+ typed_answer = ""
131
180
  print("\nModos de onboarding:")
132
- print("1. minimo: identidade, coordenador LLM, mini-cerebro local e memoria")
133
- print("2. completo: minimo + toolchain, sources, notificacoes, knowledge e memorias")
134
- print("3. pular")
135
- answer = ask_text("Escolha o modo", default="minimo").strip().lower()
181
+ render_onboarding_options(selected_index)
182
+ print_onboarding_prompt(typed_answer)
183
+ while True:
184
+ key = read_key()
185
+ if key in {"\x03", "\x04"}:
186
+ raise KeyboardInterrupt
187
+ if key in {"\r", "\n"}:
188
+ if typed_answer:
189
+ parsed = parse_onboarding_mode_answer(typed_answer.strip().lower(), default="")
190
+ if parsed:
191
+ return parsed
192
+ typed_answer = ""
193
+ rerender_onboarding_options(selected_index, typed_answer)
194
+ continue
195
+ return str(ONBOARDING_MODE_OPTIONS[selected_index]["id"])
196
+ if key in {"\x1b[A", "k"}:
197
+ typed_answer = ""
198
+ selected_index = (selected_index - 1) % len(ONBOARDING_MODE_OPTIONS)
199
+ rerender_onboarding_options(selected_index, typed_answer)
200
+ continue
201
+ if key in {"\x1b[B", "j"}:
202
+ typed_answer = ""
203
+ selected_index = (selected_index + 1) % len(ONBOARDING_MODE_OPTIONS)
204
+ rerender_onboarding_options(selected_index, typed_answer)
205
+ continue
206
+ if key in {"\x7f", "\b"}:
207
+ typed_answer = typed_answer[:-1]
208
+ rerender_onboarding_options(selected_index, typed_answer)
209
+ continue
210
+ parsed = parse_onboarding_mode_answer(key.strip().lower(), default="")
211
+ if parsed:
212
+ print()
213
+ return parsed
214
+ if key.isprintable():
215
+ typed_answer += key
216
+ rerender_onboarding_options(selected_index, typed_answer)
217
+
218
+
219
+ def render_onboarding_options(selected_index: int) -> None:
220
+ for index, option in enumerate(ONBOARDING_MODE_OPTIONS):
221
+ print(format_onboarding_option(option, selected=index == selected_index, include_selector=True))
222
+
223
+
224
+ def rerender_onboarding_options(selected_index: int, typed_answer: str) -> None:
225
+ lines_to_move = len(ONBOARDING_MODE_OPTIONS) + 1
226
+ sys.stdout.write(f"\x1b[{lines_to_move}A")
227
+ for index, option in enumerate(ONBOARDING_MODE_OPTIONS):
228
+ sys.stdout.write("\x1b[2K")
229
+ sys.stdout.write(format_onboarding_option(option, selected=index == selected_index, include_selector=True) + "\n")
230
+ sys.stdout.write("\x1b[2K")
231
+ sys.stdout.write(onboarding_prompt_line(typed_answer) + "\n")
232
+ sys.stdout.flush()
233
+
234
+
235
+ def format_onboarding_option(option: dict[str, Any], *, selected: bool, include_selector: bool) -> str:
236
+ selector = "> " if selected else " "
237
+ prefix = selector if include_selector else ""
238
+ description = f": {option['description']}" if option.get("description") else ""
239
+ recommended = " (Recomendado)" if option.get("recommended") else ""
240
+ return f"{prefix}{option['number']}. {option['label']}{description}{recommended}"
241
+
242
+
243
+ def parse_onboarding_mode_answer(answer: str, *, default: str = "minimal") -> str:
244
+ if not answer:
245
+ return default
136
246
  if answer in {"1", "minimo", "mínimo", "minimal"}:
137
247
  return "minimal"
138
248
  if answer in {"2", "completo", "complete", "full"}:
139
249
  return "complete"
140
250
  if answer in {"3", "pular", "skip", "cancelar", "cancel"}:
141
251
  return "skip"
142
- return "minimal"
252
+ return default
253
+
254
+
255
+ def print_onboarding_prompt(typed_answer: str) -> None:
256
+ print(onboarding_prompt_line(typed_answer))
257
+
258
+
259
+ def onboarding_prompt_line(typed_answer: str) -> str:
260
+ suffix = f" {typed_answer}" if typed_answer else ""
261
+ return f"Escolha o modo:{suffix}"
262
+
263
+
264
+ def read_key() -> str:
265
+ if os.name == "nt":
266
+ import msvcrt
267
+
268
+ char = msvcrt.getwch()
269
+ if char in {"\x00", "\xe0"}:
270
+ code = msvcrt.getwch()
271
+ if code == "H":
272
+ return "\x1b[A"
273
+ if code == "P":
274
+ return "\x1b[B"
275
+ return char
276
+
277
+ import termios
278
+ import tty
279
+ import select
280
+
281
+ fd = sys.stdin.fileno()
282
+ old_settings = termios.tcgetattr(fd)
283
+ try:
284
+ tty.setraw(fd)
285
+ char = sys.stdin.read(1)
286
+ if char == "\x1b":
287
+ sequence = ""
288
+ for _ in range(2):
289
+ ready, _, _ = select.select([sys.stdin], [], [], 0.01)
290
+ if not ready:
291
+ break
292
+ sequence += sys.stdin.read(1)
293
+ char += sequence
294
+ return char
295
+ finally:
296
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
143
297
 
144
298
 
145
299
  def configure_personality_interactively(agent: dict[str, Any]) -> None:
@@ -151,17 +305,31 @@ def configure_personality_interactively(agent: dict[str, Any]) -> None:
151
305
  language = ask_text("Idioma padrao das respostas?", default=str(agent.get("language") or "pt-BR"))
152
306
  tone = ask_text("Tom das respostas?", default=current_tone)
153
307
  detail_level = ask_text("Nivel de detalhe?", default=current_detail)
154
- update_personality(
308
+ payload = update_personality(
155
309
  agent_name=agent_name,
156
310
  user_name=user_name,
157
311
  language=language,
158
312
  tone=tone,
159
313
  detail_level=detail_level,
160
314
  )
315
+ alias = payload.get("alias") if isinstance(payload.get("alias"), dict) else None
316
+ if not alias:
317
+ return
318
+ if alias.get("status") == "added":
319
+ print(f"Comando local criado: {alias.get('name')}")
320
+ elif alias.get("message"):
321
+ print(f"Alias nao configurado automaticamente: {alias['message']}")
322
+ path_status = alias.get("path_status") if isinstance(alias.get("path_status"), dict) else {}
323
+ if path_status.get("setup_required"):
324
+ bin_dir = path_status.get("bin_dir")
325
+ print(f"O comando foi criado em {bin_dir}, mas essa pasta ainda nao esta no PATH.")
326
+ if ask_yes_no("Deseja habilitar aliases do Agent DevKit no shell para proximas sessoes?", default=True):
327
+ setup = setup_alias_path(yes=True)
328
+ print(setup.get("message") or "PATH de aliases atualizado.")
161
329
 
162
330
 
163
331
  def configure_llm_interactively() -> None:
164
- print("\nNenhum backend LLM coordenador utilizavel foi detectado.")
332
+ print("\nNenhum backend LLM coordenador externo utilizavel foi detectado.")
165
333
  print("Opcoes: claude-code, codex-cli, ollama, openai, anthropic, openrouter, pular")
166
334
  choice = ask_text("Qual backend deseja configurar primeiro?", default="pular").strip().lower()
167
335
  if choice in {"", "pular", "skip", "cancelar", "cancel"}: