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.
- package/README.md +42 -2
- package/package.json +1 -1
- package/runtime/README.md +47 -2
- package/runtime/cli/README.md +37 -1
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
- package/runtime/cli/aikit/decision_store.py +158 -0
- package/runtime/cli/aikit/fallback.py +48 -2
- package/runtime/cli/aikit/llm.py +9 -0
- package/runtime/cli/aikit/main.py +317 -9
- package/runtime/cli/aikit/model_router.py +70 -0
- package/runtime/cli/aikit/ollama.py +237 -0
- package/runtime/cli/aikit/provider_wizard.py +19 -0
- package/runtime/cli/aikit/review_gate.py +40 -0
|
@@ -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 {
|
package/runtime/cli/aikit/llm.py
CHANGED
|
@@ -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
|
|
579
|
+
if args.action in {"set-default", "default"}:
|
|
533
580
|
if not args.backend:
|
|
534
|
-
raise DevKitError("llm
|
|
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
|
-
"
|
|
1350
|
+
"setup_wizard": wizard,
|
|
1351
|
+
"next_question": wizard.get("next_question"),
|
|
1352
|
+
"message": wizard.get("message"),
|
|
1145
1353
|
"next_steps": [
|
|
1146
|
-
"
|
|
1147
|
-
"
|
|
1148
|
-
"
|
|
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
|
-
|
|
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."
|