agent-devkit 0.1.0 → 0.1.5

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.
@@ -6,7 +6,7 @@ from pathlib import Path
6
6
  from typing import Any
7
7
 
8
8
  from cli.aikit.credentials import CredentialResolverError
9
- from cli.aikit.providers import ProviderRegistryError, provider_status_with_credentials
9
+ from cli.aikit.providers import ProviderRegistryError, load_providers, provider_status_with_credentials
10
10
 
11
11
 
12
12
  READY_PROVIDER_STATUSES = {"ok", "unknown"}
@@ -20,9 +20,11 @@ FALLBACKS = {
20
20
  }
21
21
 
22
22
 
23
- def evaluate_provider_requirements(root: Path, capability: dict[str, Any]) -> dict[str, Any]:
23
+ def evaluate_provider_requirements(root: Path, capability: dict[str, Any], args: list[str] | None = None) -> dict[str, Any]:
24
24
  """Evaluate `requires.providers` without returning credential values."""
25
25
  requirements = provider_requirements(capability)
26
+ if not requirements and not has_fixture_arg(args or []):
27
+ requirements = infer_provider_requirements(root, capability)
26
28
  providers = {
27
29
  "used": [],
28
30
  "missing": [],
@@ -75,6 +77,50 @@ def provider_requirements(capability: dict[str, Any]) -> list[dict[str, Any]]:
75
77
  return [item for item in providers if isinstance(item, dict)]
76
78
 
77
79
 
80
+ def infer_provider_requirements(root: Path, capability: dict[str, Any]) -> list[dict[str, Any]]:
81
+ capability_id = str(capability.get("id") or "")
82
+ if "." not in capability_id:
83
+ return []
84
+ agent_id, short_capability = capability_id.rsplit(".", 1)
85
+ candidates = {f"{agent_id}/{short_capability}", capability_id}
86
+ requirements: list[dict[str, Any]] = []
87
+ try:
88
+ providers = load_providers(root)
89
+ except ProviderRegistryError:
90
+ return []
91
+ for provider in providers:
92
+ capabilities = provider.get("capabilities") if isinstance(provider.get("capabilities"), dict) else {}
93
+ matched_mode = None
94
+ for mode in ("read", "write"):
95
+ declared = capabilities.get(mode) if isinstance(capabilities, dict) else []
96
+ if any(str(item) in candidates for item in (declared or [])):
97
+ matched_mode = mode
98
+ break
99
+ if not matched_mode:
100
+ continue
101
+ fallbacks = list(provider.get("fallbacks", []) or [])
102
+ requirements.append(
103
+ {
104
+ "id": provider.get("id"),
105
+ "mode": "required",
106
+ "fallback": fallbacks[0] if fallbacks else "blocked",
107
+ "purpose": f"inferred from provider registry for {agent_id}/{short_capability}",
108
+ "access": matched_mode,
109
+ "inferred": True,
110
+ }
111
+ )
112
+ return requirements
113
+
114
+
115
+ def has_fixture_arg(args: list[str]) -> bool:
116
+ for index, item in enumerate(args):
117
+ if item == "--fixture" and index + 1 < len(args):
118
+ return True
119
+ if item.startswith("--fixture="):
120
+ return True
121
+ return False
122
+
123
+
78
124
  def provider_detail(requirement: dict[str, Any], status_item: dict[str, Any]) -> dict[str, Any]:
79
125
  provider_id = str(requirement.get("id") or "")
80
126
  return {
@@ -159,6 +159,8 @@ def list_backends() -> dict[str, Any]:
159
159
  default_backend = config.get("llm", {}).get("default")
160
160
  configured = set(config.get("llm", {}).get("backends", {}))
161
161
  preference = llm_preference(config)
162
+ from cli.aikit.decision_store import get_decision
163
+
162
164
  return {
163
165
  "kind": "llm-backends",
164
166
  "config_path": str(config_path()),
@@ -179,6 +181,8 @@ def list_backends() -> dict[str, Any]:
179
181
  "default_model": backend.default_model,
180
182
  "command": backend.command,
181
183
  "notes": backend.notes,
184
+ "decision": get_decision("llms", backend.id),
185
+ "enabled": not bool((get_decision("llms", backend.id) or {}).get("state") in {"disabled_by_user", "denied_by_user"}),
182
186
  }
183
187
  for backend in BACKENDS.values()
184
188
  ],
@@ -445,6 +449,11 @@ def candidate_backend_ids(
445
449
  llm = config.get("llm") if isinstance(config.get("llm"), dict) else {}
446
450
  configured = llm.get("backends") if isinstance(llm.get("backends"), dict) else {}
447
451
  configured_ids = [backend_id for backend_id in configured if backend_id in BACKENDS]
452
+ if not configured_ids:
453
+ return []
454
+ from cli.aikit.decision_store import is_disabled
455
+
456
+ configured_ids = [backend_id for backend_id in configured_ids if not is_disabled("llms", backend_id)]
448
457
  if not configured_ids:
449
458
  return []
450
459
  preference = llm_preference(config)
@@ -16,6 +16,8 @@ from cli.aikit.aliases import add_alias, list_aliases, remove_alias, sync_aliase
16
16
  from cli.aikit import __version__
17
17
  from cli.aikit.audit import export_audit, list_audits, record_audit, show_audit
18
18
  from cli.aikit.calendar import calendar_list, calendar_summary, calendar_today, calendar_tomorrow, configure_calendar
19
+ from cli.aikit.configuration_orchestrator import provider_wizard_from_requirement
20
+ from cli.aikit.decision_store import list_decisions, reset_decisions, set_decision
19
21
  from cli.aikit.diagnostics import build_diagnostics
20
22
  from cli.aikit.fallback import evaluate_provider_requirements
21
23
  from cli.aikit.github_pr import pr_create_automation, pr_inspect, pr_list_review_requests, pr_review
@@ -31,8 +33,12 @@ from cli.aikit.llm import (
31
33
  set_llm_preference,
32
34
  )
33
35
  from cli.aikit.memory import memory_path_payload, napkin_context, record_usage, reset_memory, show_memory
36
+ from cli.aikit.model_router import build_model_plan
37
+ from cli.aikit.ollama import ollama_models, ollama_pull, ollama_status, ollama_update
34
38
  from cli.aikit.personality import load_personality, reset_personality, setup_personality, update_personality
35
39
  from cli.aikit.permissions import grant_permission, revoke_permission, show_permissions
40
+ from cli.aikit.provider_wizard import missing_source_wizard
41
+ from cli.aikit.review_gate import build_review_gate, mark_reviewed
36
42
  from cli.aikit.sessions import (
37
43
  build_contextual_prompt,
38
44
  get_or_create_session,
@@ -109,6 +115,12 @@ DETERMINISTIC_COMMANDS = (
109
115
  "pr",
110
116
  "permissions",
111
117
  "audit",
118
+ "config",
119
+ "tools",
120
+ "integrations",
121
+ "skills",
122
+ "decisions",
123
+ "ollama",
112
124
  "install",
113
125
  )
114
126
  LLM_COMMANDS = ("agent",)
@@ -306,13 +318,40 @@ def build_parser(prog: str | None = None) -> argparse.ArgumentParser:
306
318
  audit_parser.add_argument("--limit", type=int, default=20)
307
319
  audit_parser.add_argument("--format", default="md", choices=["md", "json"])
308
320
 
321
+ config_parser = subparsers.add_parser("config", help="inspect local Agent DevKit configuration")
322
+ config_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
323
+ config_parser.add_argument("action", nargs="?", default="show", choices=["show", "path"])
324
+
325
+ for command_name, help_text in (
326
+ ("tools", "manage enabled local tools"),
327
+ ("integrations", "manage provider integration decisions"),
328
+ ("skills", "manage local skill decisions"),
329
+ ):
330
+ control_parser = subparsers.add_parser(command_name, help=help_text)
331
+ control_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
332
+ control_parser.add_argument("action", nargs="?", default="list", choices=["list", "enable", "disable"])
333
+ control_parser.add_argument("item_id", nargs="?")
334
+
335
+ decisions_parser = subparsers.add_parser("decisions", help="inspect or reset local opt-in and opt-out decisions")
336
+ decisions_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
337
+ decisions_parser.add_argument("action", nargs="?", default="list", choices=["list", "forget", "reset"])
338
+ decisions_parser.add_argument("item_id", nargs="?")
339
+ decisions_parser.add_argument("--category", choices=["tools", "integrations", "skills", "llms"])
340
+
341
+ ollama_parser = subparsers.add_parser("ollama", help="inspect and manage local Ollama models")
342
+ ollama_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
343
+ ollama_parser.add_argument("action", nargs="?", default="status", choices=["status", "models", "pull", "update"])
344
+ ollama_parser.add_argument("model", nargs="?")
345
+ ollama_parser.add_argument("--yes", action="store_true", help="confirm Ollama model or update operation")
346
+ ollama_parser.add_argument("--dry-run", action="store_true", help="show Ollama operation without executing it")
347
+
309
348
  llm_parser = subparsers.add_parser("llm", help="manage LLM backends")
310
349
  llm_parser.add_argument("--json", action="store_true", default=argparse.SUPPRESS, help=argparse.SUPPRESS)
311
350
  llm_parser.add_argument(
312
351
  "action",
313
352
  nargs="?",
314
353
  default="list",
315
- choices=["list", "doctor", "configure", "set-default", "preference"],
354
+ choices=["list", "doctor", "configure", "set-default", "default", "enable", "disable", "preference"],
316
355
  )
317
356
  llm_parser.add_argument("backend", nargs="?")
318
357
  llm_parser.add_argument("preference_value", nargs="?")
@@ -425,6 +464,14 @@ def dispatch(args: argparse.Namespace) -> dict[str, Any] | None:
425
464
  return dispatch_permissions(args)
426
465
  if command == "audit":
427
466
  return dispatch_audit(args)
467
+ if command == "config":
468
+ return dispatch_config(args)
469
+ if command in {"tools", "integrations", "skills"}:
470
+ return dispatch_control_category(command, args)
471
+ if command == "decisions":
472
+ return dispatch_decisions(args)
473
+ if command == "ollama":
474
+ return dispatch_ollama(args)
428
475
  if command == "llm":
429
476
  return dispatch_llm(args)
430
477
  if command == "install":
@@ -529,10 +576,15 @@ def dispatch_llm(args: argparse.Namespace) -> dict[str, Any]:
529
576
  command=args.host_command,
530
577
  set_default=args.set_default,
531
578
  )
532
- if args.action == "set-default":
579
+ if args.action in {"set-default", "default"}:
533
580
  if not args.backend:
534
- raise DevKitError("llm set-default requires a backend")
581
+ raise DevKitError(f"llm {args.action} requires a backend")
535
582
  return set_default_backend(args.backend)
583
+ if args.action in {"enable", "disable"}:
584
+ if not args.backend:
585
+ raise DevKitError(f"llm {args.action} requires a backend")
586
+ state = "enabled" if args.action == "enable" else "disabled_by_user"
587
+ return set_decision("llms", args.backend, state, reason=f"llm {args.action} command")
536
588
  if args.action == "preference":
537
589
  if args.backend in {None, "show"}:
538
590
  return llm_preference()
@@ -551,6 +603,85 @@ def dispatch_llm(args: argparse.Namespace) -> dict[str, Any]:
551
603
  raise DevKitError(f"unsupported llm action: {args.action}")
552
604
 
553
605
 
606
+ def dispatch_config(args: argparse.Namespace) -> dict[str, Any]:
607
+ from cli.aikit.llm import config_path
608
+
609
+ if args.action == "path":
610
+ return {"kind": "config", "status": "ok", "path": str(config_path())}
611
+ if args.action == "show":
612
+ return {
613
+ "kind": "config",
614
+ "status": "ok",
615
+ "path": str(config_path()),
616
+ "decisions": list_decisions(),
617
+ "llm": llm_preference(),
618
+ "ollama": ollama_status(),
619
+ }
620
+ raise DevKitError(f"unsupported config action: {args.action}")
621
+
622
+
623
+ def dispatch_control_category(command: str, args: argparse.Namespace) -> dict[str, Any]:
624
+ try:
625
+ category = command
626
+ if args.action == "list":
627
+ if args.item_id:
628
+ raise DevKitError(f"{command} list does not accept an item id")
629
+ payload = list_decisions(category)
630
+ payload["kind"] = command
631
+ return payload
632
+ if args.action in {"enable", "disable"}:
633
+ if not args.item_id:
634
+ raise DevKitError(f"{command} {args.action} requires an item id")
635
+ state = "enabled" if args.action == "enable" else "disabled_by_user"
636
+ payload = set_decision(category, args.item_id, state, reason=f"{command} {args.action} command")
637
+ payload["kind"] = command[:-1] if command.endswith("s") else command
638
+ return payload
639
+ except ValueError as exc:
640
+ raise DevKitError(str(exc)) from exc
641
+ raise DevKitError(f"unsupported {command} action: {args.action}")
642
+
643
+
644
+ def dispatch_decisions(args: argparse.Namespace) -> dict[str, Any]:
645
+ try:
646
+ if args.action == "list":
647
+ if args.item_id:
648
+ raise DevKitError("decisions list does not accept an item id")
649
+ return list_decisions(args.category)
650
+ if args.action == "reset":
651
+ if args.item_id:
652
+ raise DevKitError("decisions reset does not accept an item id")
653
+ return reset_decisions(args.category)
654
+ if args.action == "forget":
655
+ if not args.item_id:
656
+ raise DevKitError("decisions forget requires an item id")
657
+ category = args.category or "tools"
658
+ return set_decision(category, args.item_id, "available", reason="decision forgotten")
659
+ except ValueError as exc:
660
+ raise DevKitError(str(exc)) from exc
661
+ raise DevKitError(f"unsupported decisions action: {args.action}")
662
+
663
+
664
+ def dispatch_ollama(args: argparse.Namespace) -> dict[str, Any]:
665
+ try:
666
+ if args.action == "status":
667
+ if args.model:
668
+ raise DevKitError("ollama status does not accept a model")
669
+ return ollama_status()
670
+ if args.action == "models":
671
+ if args.model:
672
+ raise DevKitError("ollama models does not accept a model")
673
+ return ollama_models()
674
+ if args.action == "pull":
675
+ return ollama_pull(args.model, yes=args.yes, dry_run=effective_dry_run(args))
676
+ if args.action == "update":
677
+ if args.model:
678
+ raise DevKitError("ollama update does not accept a model")
679
+ return ollama_update(yes=args.yes, dry_run=effective_dry_run(args))
680
+ except ValueError as exc:
681
+ raise DevKitError(str(exc)) from exc
682
+ raise DevKitError(f"unsupported ollama action: {args.action}")
683
+
684
+
554
685
  def dispatch_install(args: argparse.Namespace) -> dict[str, Any]:
555
686
  try:
556
687
  return install_runtime(
@@ -965,12 +1096,18 @@ def agent_requires_llm(args: argparse.Namespace) -> dict[str, Any]:
965
1096
  result = invoke_deterministic_route(prompt, route)
966
1097
  return finalize_agent_session(result, session, prompt, backend=args.llm)
967
1098
  contextual_prompt = build_contextual_prompt(str(session["id"]), prompt)
1099
+ model_plan = build_model_plan(prompt)
968
1100
  result = invoke_agent_prompt(
969
1101
  contextual_prompt,
970
1102
  args.llm,
971
1103
  public_name=name,
972
1104
  allow_fallback=not args.no_llm_fallback,
973
1105
  )
1106
+ review_gate = build_review_gate(prompt, model_plan=model_plan)
1107
+ if result.get("ok"):
1108
+ review_gate = mark_reviewed(review_gate, reviewer=str(result.get("llm_backend") or "coordinator"))
1109
+ result["model_plan"] = model_plan
1110
+ result["review_gate"] = review_gate
974
1111
  result["prompt_length"] = len(prompt)
975
1112
  result["session_context_applied"] = contextual_prompt != prompt
976
1113
  if result.get("response"):
@@ -981,6 +1118,11 @@ def agent_requires_llm(args: argparse.Namespace) -> dict[str, Any]:
981
1118
 
982
1119
  def dispatch_natural_operational_prompt(prompt: str) -> dict[str, Any] | None:
983
1120
  normalized = " ".join(prompt.lower().split())
1121
+ control_result = dispatch_natural_control_prompt(normalized)
1122
+ if control_result:
1123
+ control_result["prompt_received"] = True
1124
+ control_result["prompt_length"] = len(prompt)
1125
+ return control_result
984
1126
  if "agenda" in normalized:
985
1127
  if "amanha" in normalized or "amanhã" in normalized:
986
1128
  payload = calendar_tomorrow()
@@ -1028,9 +1170,69 @@ def dispatch_natural_operational_prompt(prompt: str) -> dict[str, Any] | None:
1028
1170
  return None
1029
1171
 
1030
1172
 
1173
+ def dispatch_natural_control_prompt(normalized_prompt: str) -> dict[str, Any] | None:
1174
+ if "decis" in normalized_prompt and any(marker in normalized_prompt for marker in ("mostre", "liste", "ver", "mostrar", "listar")):
1175
+ return {
1176
+ "kind": "agent",
1177
+ "status": "ok",
1178
+ "ok": True,
1179
+ "mode": "control-center-route",
1180
+ "requires_llm": False,
1181
+ "response": "Estas sao as decisoes locais registradas.",
1182
+ "result": list_decisions(),
1183
+ }
1184
+ control_targets = {
1185
+ "ollama": ("tools", "ollama"),
1186
+ "azure devops": ("tools", "azure-devops"),
1187
+ "azure": ("tools", "azure-devops"),
1188
+ "github": ("integrations", "github"),
1189
+ "figma": ("tools", "figma"),
1190
+ }
1191
+ action: str | None = None
1192
+ if any(marker in normalized_prompt for marker in ("desative", "desabilite", "disable", "bloqueie")):
1193
+ action = "disable"
1194
+ if any(marker in normalized_prompt for marker in ("reative", "ative", "habilite", "enable")):
1195
+ action = "enable"
1196
+ if not action:
1197
+ return None
1198
+ for marker, (category, item_id) in control_targets.items():
1199
+ if marker in normalized_prompt:
1200
+ state = "enabled" if action == "enable" else "disabled_by_user"
1201
+ result = set_decision(category, item_id, state, reason=f"natural prompt: {action}")
1202
+ return {
1203
+ "kind": "agent",
1204
+ "status": "ok",
1205
+ "ok": True,
1206
+ "mode": "control-center-route",
1207
+ "requires_llm": False,
1208
+ "response": f"{item_id} foi {'ativado' if state == 'enabled' else 'desativado'} para {category}.",
1209
+ "result": {
1210
+ "category": category,
1211
+ "id": item_id,
1212
+ "state": state,
1213
+ "decision": result.get("item"),
1214
+ },
1215
+ }
1216
+ if any(marker in normalized_prompt for marker in ("ferramentas", "tools")) and any(
1217
+ marker in normalized_prompt for marker in ("mostre", "liste", "ver", "mostrar", "listar")
1218
+ ):
1219
+ return {
1220
+ "kind": "agent",
1221
+ "status": "ok",
1222
+ "ok": True,
1223
+ "mode": "control-center-route",
1224
+ "requires_llm": False,
1225
+ "response": "Estas sao as ferramentas com decisoes locais registradas.",
1226
+ "result": list_decisions("tools"),
1227
+ }
1228
+ return None
1229
+
1230
+
1031
1231
  def build_agent_dry_run_plan(prompt: str, args: argparse.Namespace) -> dict[str, Any]:
1032
1232
  normalized = " ".join(prompt.lower().split())
1033
1233
  route = route_prompt(prompt)
1234
+ model_plan = build_model_plan(prompt, route=route)
1235
+ review_gate = build_review_gate(prompt, route=route, model_plan=model_plan)
1034
1236
  plan: dict[str, Any] = {
1035
1237
  "kind": "agent",
1036
1238
  "status": "planned",
@@ -1047,6 +1249,8 @@ def build_agent_dry_run_plan(prompt: str, args: argparse.Namespace) -> dict[str,
1047
1249
  "providers": {"used": [], "missing": [], "skipped": []},
1048
1250
  "commands": [],
1049
1251
  "permissions": [],
1252
+ "model_plan": model_plan,
1253
+ "review_gate": review_gate,
1050
1254
  "response": "Dry-run: nenhuma chamada LLM ou escrita externa foi executada.",
1051
1255
  }
1052
1256
  if "agenda" in normalized:
@@ -1131,21 +1335,25 @@ def invoke_deterministic_route(prompt: str, route: dict[str, Any]) -> dict[str,
1131
1335
  raise DevKitError(str(exc)) from exc
1132
1336
 
1133
1337
  if not source:
1338
+ wizard = missing_source_wizard(prompt, route, root=ROOT)
1134
1339
  return {
1135
1340
  "kind": "agent",
1136
1341
  "status": "needs-input",
1137
1342
  "ok": False,
1138
1343
  "requires_source": True,
1344
+ "provider": route.get("provider"),
1139
1345
  "source_provider": route.get("provider"),
1140
1346
  "prompt_received": True,
1141
1347
  "prompt_length": len(prompt),
1142
1348
  "route": route,
1143
1349
  "napkin": napkin_context(ROOT, agent_id=route.get("agent_id")),
1144
- "message": "agent identified the task, but no reusable source is configured for this provider.",
1350
+ "setup_wizard": wizard,
1351
+ "next_question": wizard.get("next_question"),
1352
+ "message": wizard.get("message"),
1145
1353
  "next_steps": [
1146
- "Configure a source with `agent source add azure-sustentacao --provider azure-devops --config project=<project> --env AZURE_DEVOPS_PAT=AZURE_DEVOPS_PAT --default-for card`.",
1147
- "Use `--config fixture=<path>` for local fixture-based tests or demos.",
1148
- "Rerun the same prompt after configuring the source.",
1354
+ "Responda a pergunta do wizard para autorizar ou negar a configuracao desta fonte.",
1355
+ "Se preferir teste local, configure uma source com fixture sem armazenar segredos.",
1356
+ "O prompt original sera retomado apos a fonte reutilizavel ser configurada.",
1149
1357
  ],
1150
1358
  "exit_code": 2,
1151
1359
  }
@@ -1155,6 +1363,10 @@ def invoke_deterministic_route(prompt: str, route: dict[str, Any]) -> dict[str,
1155
1363
  result = run_capability(agent, str(route["capability_id"]), capability_args, capture_output=True)
1156
1364
  response = result.get("stdout") or result.get("error") or ""
1157
1365
  record_usage(prompt, route=route, source_id=str(source["id"]))
1366
+ model_plan = build_model_plan(prompt, route=route)
1367
+ review_gate = build_review_gate(prompt, route=route, model_plan=model_plan)
1368
+ if result.get("ok"):
1369
+ review_gate = mark_reviewed(review_gate, reviewer="deterministic-coordinator")
1158
1370
  return {
1159
1371
  "kind": "agent",
1160
1372
  "status": result.get("status"),
@@ -1165,6 +1377,8 @@ def invoke_deterministic_route(prompt: str, route: dict[str, Any]) -> dict[str,
1165
1377
  "route": route,
1166
1378
  "source": public_source(source),
1167
1379
  "napkin": napkin_context(ROOT, agent_id=route.get("agent_id"), source_id=str(source["id"])),
1380
+ "model_plan": model_plan,
1381
+ "review_gate": review_gate,
1168
1382
  "response": response,
1169
1383
  "result": result,
1170
1384
  "exit_code": result.get("exit_code", 0 if result.get("ok") else 1),
@@ -1361,9 +1575,9 @@ def run_capability(
1361
1575
  next_steps=guardrail["next_steps"],
1362
1576
  exit_code=2,
1363
1577
  )
1364
- readiness = evaluate_provider_requirements(ROOT, data)
1578
+ readiness = evaluate_provider_requirements(ROOT, data, capability_args)
1365
1579
  if not readiness["ready"]:
1366
- return run_payload(
1580
+ payload = run_payload(
1367
1581
  status=readiness["status"],
1368
1582
  agent=summarize_agent(agent),
1369
1583
  capability=data.get("id", capability_id),
@@ -1377,6 +1591,17 @@ def run_capability(
1377
1591
  artifacts=readiness["artifacts"],
1378
1592
  exit_code=readiness.get("exit_code"),
1379
1593
  )
1594
+ wizard = setup_wizard_from_readiness(readiness, agent=summarize_agent(agent), capability_id=str(data.get("id", capability_id)))
1595
+ if wizard:
1596
+ payload["setup_wizard"] = wizard
1597
+ payload["next_question"] = wizard.get("next_question")
1598
+ payload["configuration_agent"] = wizard.get("owner_agent")
1599
+ payload["next_steps"] = [
1600
+ "Responda a pergunta do wizard para autorizar ou negar a configuracao deste provider.",
1601
+ "Informe uma referencia segura de credencial por variavel de ambiente, arquivo ou cadeia nativa quando solicitado.",
1602
+ "Reexecute ou retome a mesma capability depois que a configuracao estiver salva.",
1603
+ ]
1604
+ return payload
1380
1605
 
1381
1606
  runner_ref = (data.get("entrypoint", {}) or {}).get("runner")
1382
1607
  if not runner_ref:
@@ -1495,6 +1720,26 @@ def supports_runtime_source(agent_id: str, capability_id: str) -> bool:
1495
1720
  return agent_id == "azure-devops-orchestrator" and capability_id == "read-card"
1496
1721
 
1497
1722
 
1723
+ def setup_wizard_from_readiness(readiness: dict[str, Any], *, agent: dict[str, Any], capability_id: str) -> dict[str, Any] | None:
1724
+ providers = readiness.get("providers") if isinstance(readiness.get("providers"), dict) else {}
1725
+ missing = providers.get("missing") or []
1726
+ if not missing:
1727
+ return None
1728
+ provider_id = str(missing[0])
1729
+ details = providers.get("details") or []
1730
+ detail = next((item for item in details if isinstance(item, dict) and item.get("id") == provider_id), {})
1731
+ try:
1732
+ return provider_wizard_from_requirement(
1733
+ ROOT,
1734
+ provider_id,
1735
+ agent_id=str(agent.get("id") or ""),
1736
+ capability_id=capability_id,
1737
+ reason=str(detail.get("purpose") or "Provider is required but not configured."),
1738
+ )
1739
+ except Exception:
1740
+ return None
1741
+
1742
+
1498
1743
  def doctor(project: str | None = None, home: str | None = None, scope: str = "auto") -> dict[str, Any]:
1499
1744
  agents = list_agents()
1500
1745
  capabilities = list_all_capabilities()
@@ -1748,6 +1993,12 @@ def print_human(result: dict[str, Any]) -> None:
1748
1993
  print_permissions(result)
1749
1994
  elif kind in {"audit", "audit-entry", "audit-export"}:
1750
1995
  print_audit(result)
1996
+ elif kind == "config":
1997
+ print_config(result)
1998
+ elif kind in {"tools", "tool", "integrations", "integration", "skills", "skill", "decisions", "decision", "decisions-reset"}:
1999
+ print_control(result)
2000
+ elif kind in {"ollama-status", "ollama-models", "ollama-pull", "ollama-update"}:
2001
+ print_ollama(result)
1751
2002
  elif kind == "install":
1752
2003
  print_install(result)
1753
2004
  else:
@@ -1875,6 +2126,11 @@ def print_agent_response(result: dict[str, Any]) -> None:
1875
2126
  print(result.get("response", ""))
1876
2127
  return
1877
2128
  print(result.get("message") or result.get("response") or "Agent execution did not complete.")
2129
+ question = result.get("next_question") or ((result.get("setup_wizard") or {}).get("next_question") if isinstance(result.get("setup_wizard"), dict) else None)
2130
+ if isinstance(question, dict) and question.get("text"):
2131
+ print(f"\nPergunta: {question['text']}")
2132
+ if question.get("type") == "confirm":
2133
+ print("[s/N]")
1878
2134
  if result.get("llm_backend"):
1879
2135
  print(f"Requested backend: {result['llm_backend']}")
1880
2136
  if result.get("next_steps"):
@@ -2268,6 +2524,58 @@ def print_credential_backends(result: dict[str, Any]) -> None:
2268
2524
  print(f"- {item}")
2269
2525
 
2270
2526
 
2527
+ def print_config(result: dict[str, Any]) -> None:
2528
+ print(f"Config: {result.get('path')}")
2529
+ if result.get("llm"):
2530
+ print(f"Primary LLM: {(result['llm'] or {}).get('primary') or '-'}")
2531
+ if result.get("ollama"):
2532
+ print(f"Ollama: {(result['ollama'] or {}).get('status')}")
2533
+
2534
+
2535
+ def print_control(result: dict[str, Any]) -> None:
2536
+ kind = result.get("kind")
2537
+ if kind == "decisions-reset":
2538
+ print(f"Decisions reset: {result.get('category') or 'all'}")
2539
+ print(f"Path: {result.get('path')}")
2540
+ return
2541
+ if "items" in result:
2542
+ print(f"{kind}:")
2543
+ for item in result.get("items") or []:
2544
+ print(f"- {item.get('category')}:{item.get('id')} {item.get('state')}")
2545
+ if not result.get("items"):
2546
+ print("- none")
2547
+ return
2548
+ item = result.get("item") or result
2549
+ print(f"{item.get('category') or result.get('category')}:{item.get('id') or result.get('id')} {item.get('state') or result.get('state')}")
2550
+
2551
+
2552
+ def print_ollama(result: dict[str, Any]) -> None:
2553
+ kind = result.get("kind")
2554
+ if kind == "ollama-status":
2555
+ print(f"Ollama: {result.get('status')}")
2556
+ print(f"Binary: {result.get('binary') or '-'}")
2557
+ print(f"Version: {result.get('version') or '-'}")
2558
+ daemon = result.get("daemon") or {}
2559
+ print(f"Daemon: {daemon.get('status') or '-'}")
2560
+ print(f"Models: {result.get('model_count', 0)}")
2561
+ if result.get("install_plan"):
2562
+ print(f"Install: {(result['install_plan'] or {}).get('command')}")
2563
+ return
2564
+ if kind == "ollama-models":
2565
+ print(f"Ollama models: {result.get('status')}")
2566
+ for item in result.get("items") or []:
2567
+ print(f"- {item.get('name')} {item.get('size') or '-'}")
2568
+ if not result.get("items"):
2569
+ print("- none")
2570
+ return
2571
+ print(f"{kind}: {result.get('status')}")
2572
+ if result.get("command"):
2573
+ command = result["command"]
2574
+ print("Command: " + (" ".join(command) if isinstance(command, list) else str(command)))
2575
+ if result.get("message"):
2576
+ print(result["message"])
2577
+
2578
+
2271
2579
  def print_install(result: dict[str, Any]) -> None:
2272
2580
  print(f"AI DevKit install: {result['status']}")
2273
2581
  print(f"Scope: {result['scope']}")
@@ -0,0 +1,70 @@
1
+ """Task-to-model routing decisions for Agent DevKit."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from cli.aikit.llm import llm_preference, load_config
9
+ from cli.aikit.ollama import ollama_status
10
+
11
+
12
+ OPERATIONAL_PATTERN = re.compile(
13
+ r"(?i)\b(resum|sumari|classifi|extra(?:i|ir)|normaliz|compar|logs?|rascunho|agrupe|agrupar)\b"
14
+ )
15
+ HIGH_LEVEL_PATTERN = re.compile(
16
+ r"(?i)\b(arquitet|decid|aprovar|reprovar|especifica|requisit|implemente|codigo|c[oó]digo|documento|automac|deploy|seguran)\b"
17
+ )
18
+
19
+
20
+ def build_model_plan(prompt: str, *, route: dict[str, Any] | None = None) -> dict[str, Any]:
21
+ preference = llm_preference(load_config())
22
+ ollama = ollama_status()
23
+ local_available = ollama.get("status") == "ok"
24
+ operational = bool(OPERATIONAL_PATTERN.search(prompt))
25
+ high_level = bool(HIGH_LEVEL_PATTERN.search(prompt))
26
+ use_local = operational and local_available
27
+ return {
28
+ "kind": "model-plan",
29
+ "status": "planned",
30
+ "intent": route.get("intent") if route else "llm",
31
+ "primary_coordinators": coordinator_order(preference),
32
+ "local_llm_role": "operational-worker",
33
+ "local_llm_available": local_available,
34
+ "local_llm_provider": "ollama",
35
+ "local_llm_recommended": operational,
36
+ "local_llm_selected": use_local,
37
+ "delegation": {
38
+ "allowed": operational,
39
+ "selected": use_local,
40
+ "reason": local_reason(operational=operational, local_available=local_available, high_level=high_level),
41
+ "forbidden_actions": [
42
+ "final-review",
43
+ "external-write",
44
+ "permission-decision",
45
+ "architecture-decision",
46
+ "pr-approval",
47
+ ],
48
+ },
49
+ "fallback_order": preference.get("order") or [],
50
+ "route": route,
51
+ }
52
+
53
+
54
+ def coordinator_order(preference: dict[str, Any]) -> list[str]:
55
+ order = list(preference.get("order") or [])
56
+ preferred = [item for item in order if item in {"claude-code", "codex-cli"}]
57
+ for item in ("claude-code", "codex-cli"):
58
+ if item not in preferred:
59
+ preferred.append(item)
60
+ return preferred
61
+
62
+
63
+ def local_reason(*, operational: bool, local_available: bool, high_level: bool) -> str:
64
+ if operational and local_available and high_level:
65
+ return "Use Ollama for operational preprocessing; coordinator review remains mandatory."
66
+ if operational and local_available:
67
+ return "Task is operational and local Ollama is available."
68
+ if operational and not local_available:
69
+ return "Task is operational, but Ollama is not available; coordinator/API fallback should execute."
70
+ return "Task requires coordinator-level reasoning or review."