agent-devkit 0.1.0 → 0.1.6

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.
Files changed (93) hide show
  1. package/README.md +42 -2
  2. package/package.json +1 -1
  3. package/runtime/README.md +119 -5
  4. package/runtime/agents/README.md +8 -0
  5. package/runtime/agents/execution-reviewer/AGENTS.md +8 -0
  6. package/runtime/agents/execution-reviewer/README.md +13 -0
  7. package/runtime/agents/execution-reviewer/agent.yaml +25 -0
  8. package/runtime/agents/execution-reviewer/capabilities/review-agent-result/capability.yaml +15 -0
  9. package/runtime/agents/execution-reviewer/capabilities/review-agent-result/decision-rules.md +6 -0
  10. package/runtime/agents/execution-reviewer/capabilities/review-agent-result/workflow.md +7 -0
  11. package/runtime/agents/execution-reviewer/capabilities/review-final-output/capability.yaml +15 -0
  12. package/runtime/agents/execution-reviewer/capabilities/review-final-output/decision-rules.md +6 -0
  13. package/runtime/agents/execution-reviewer/capabilities/review-final-output/workflow.md +7 -0
  14. package/runtime/agents/execution-reviewer/capabilities/review-plan/capability.yaml +15 -0
  15. package/runtime/agents/execution-reviewer/capabilities/review-plan/decision-rules.md +6 -0
  16. package/runtime/agents/execution-reviewer/capabilities/review-plan/workflow.md +7 -0
  17. package/runtime/agents/execution-reviewer/infra/README.md +4 -0
  18. package/runtime/agents/execution-reviewer/knowledge/context.md +6 -0
  19. package/runtime/agents/execution-reviewer/knowledge/system.md +18 -0
  20. package/runtime/agents/execution-reviewer/templates/README.md +4 -0
  21. package/runtime/agents/local-llm-operator/AGENTS.md +8 -0
  22. package/runtime/agents/local-llm-operator/README.md +12 -0
  23. package/runtime/agents/local-llm-operator/agent.yaml +26 -0
  24. package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/capability.yaml +15 -0
  25. package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/decision-rules.md +6 -0
  26. package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/workflow.md +7 -0
  27. package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/capability.yaml +15 -0
  28. package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/decision-rules.md +5 -0
  29. package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/workflow.md +6 -0
  30. package/runtime/agents/local-llm-operator/capabilities/select-local-worker/capability.yaml +15 -0
  31. package/runtime/agents/local-llm-operator/capabilities/select-local-worker/decision-rules.md +6 -0
  32. package/runtime/agents/local-llm-operator/capabilities/select-local-worker/workflow.md +7 -0
  33. package/runtime/agents/local-llm-operator/infra/README.md +4 -0
  34. package/runtime/agents/local-llm-operator/knowledge/context.md +6 -0
  35. package/runtime/agents/local-llm-operator/knowledge/system.md +18 -0
  36. package/runtime/agents/local-llm-operator/templates/README.md +4 -0
  37. package/runtime/agents/provider-configurator/AGENTS.md +8 -0
  38. package/runtime/agents/provider-configurator/README.md +5 -0
  39. package/runtime/agents/provider-configurator/agent.yaml +25 -0
  40. package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/capability.yaml +15 -0
  41. package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/decision-rules.md +6 -0
  42. package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/workflow.md +7 -0
  43. package/runtime/agents/provider-configurator/capabilities/configure-provider-source/capability.yaml +15 -0
  44. package/runtime/agents/provider-configurator/capabilities/configure-provider-source/decision-rules.md +6 -0
  45. package/runtime/agents/provider-configurator/capabilities/configure-provider-source/workflow.md +8 -0
  46. package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/capability.yaml +15 -0
  47. package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/decision-rules.md +6 -0
  48. package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/workflow.md +7 -0
  49. package/runtime/agents/provider-configurator/infra/README.md +5 -0
  50. package/runtime/agents/provider-configurator/knowledge/context.md +8 -0
  51. package/runtime/agents/provider-configurator/knowledge/system.md +19 -0
  52. package/runtime/agents/provider-configurator/templates/README.md +4 -0
  53. package/runtime/agents/task-orchestrator/AGENTS.md +8 -0
  54. package/runtime/agents/task-orchestrator/README.md +7 -0
  55. package/runtime/agents/task-orchestrator/agent.yaml +31 -0
  56. package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/capability.yaml +15 -0
  57. package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/decision-rules.md +6 -0
  58. package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/workflow.md +7 -0
  59. package/runtime/agents/task-orchestrator/capabilities/plan-task/capability.yaml +15 -0
  60. package/runtime/agents/task-orchestrator/capabilities/plan-task/decision-rules.md +6 -0
  61. package/runtime/agents/task-orchestrator/capabilities/plan-task/workflow.md +8 -0
  62. package/runtime/agents/task-orchestrator/capabilities/select-specialists/capability.yaml +15 -0
  63. package/runtime/agents/task-orchestrator/capabilities/select-specialists/decision-rules.md +6 -0
  64. package/runtime/agents/task-orchestrator/capabilities/select-specialists/workflow.md +7 -0
  65. package/runtime/agents/task-orchestrator/infra/README.md +5 -0
  66. package/runtime/agents/task-orchestrator/knowledge/context.md +9 -0
  67. package/runtime/agents/task-orchestrator/knowledge/system.md +20 -0
  68. package/runtime/agents/task-orchestrator/templates/README.md +4 -0
  69. package/runtime/cli/README.md +111 -10
  70. package/runtime/cli/aikit/__init__.py +1 -1
  71. package/runtime/cli/aikit/agent_executor.py +66 -0
  72. package/runtime/cli/aikit/agent_registry.py +80 -0
  73. package/runtime/cli/aikit/app_home.py +87 -2
  74. package/runtime/cli/aikit/audit.py +44 -0
  75. package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
  76. package/runtime/cli/aikit/control_router.py +298 -0
  77. package/runtime/cli/aikit/decision_store.py +175 -0
  78. package/runtime/cli/aikit/diagnostics.py +2 -1
  79. package/runtime/cli/aikit/execution_reviewer.py +174 -0
  80. package/runtime/cli/aikit/fallback.py +48 -2
  81. package/runtime/cli/aikit/install.py +4 -3
  82. package/runtime/cli/aikit/llm.py +9 -0
  83. package/runtime/cli/aikit/local_llm_operator.py +112 -0
  84. package/runtime/cli/aikit/lock.py +4 -1
  85. package/runtime/cli/aikit/main.py +604 -21
  86. package/runtime/cli/aikit/model_router.py +79 -0
  87. package/runtime/cli/aikit/ollama.py +237 -0
  88. package/runtime/cli/aikit/orchestrator.py +535 -0
  89. package/runtime/cli/aikit/provider_wizard.py +19 -0
  90. package/runtime/cli/aikit/review_gate.py +40 -0
  91. package/runtime/cli/aikit/sessions.py +37 -0
  92. package/runtime/cli/aikit/sources.py +6 -2
  93. package/runtime/cli/aikit/wizard_state.py +503 -0
@@ -0,0 +1,66 @@
1
+ """Execution helpers for multi-agent plans."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Callable
6
+
7
+
8
+ RunCapability = Callable[[dict[str, Any], str, list[str]], dict[str, Any]]
9
+ LoadAgent = Callable[[str], dict[str, Any]]
10
+
11
+
12
+ def execute_plan_tasks(
13
+ plan: dict[str, Any],
14
+ *,
15
+ load_agent: LoadAgent,
16
+ run_capability: RunCapability,
17
+ ) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
18
+ executed: list[dict[str, Any]] = []
19
+ blocked: list[dict[str, Any]] = []
20
+ for task in plan.get("specialist_tasks") or []:
21
+ if not task.get("primary"):
22
+ continue
23
+ if not task.get("executable"):
24
+ blocked.append(blocked_task(task, "Capability has no executable runner."))
25
+ continue
26
+ if str(task.get("write_policy") or "") not in {"read_only", "dry_run"}:
27
+ blocked.append(blocked_task(task, "Capability requires explicit confirmation for external write effects."))
28
+ continue
29
+ try:
30
+ agent = load_agent(str(task["agent_id"]))
31
+ result = run_capability(agent, str(task["capability_id"]), list(task.get("args") or []))
32
+ except Exception as exc: # pragma: no cover - converted to user-facing payload.
33
+ blocked.append(blocked_task(task, str(exc)))
34
+ continue
35
+ item = public_task_result(task, result)
36
+ if result.get("ok"):
37
+ executed.append(item)
38
+ else:
39
+ blocked.append(item)
40
+ return executed, blocked
41
+
42
+
43
+ def public_task_result(task: dict[str, Any], result: dict[str, Any]) -> dict[str, Any]:
44
+ return {
45
+ "id": task.get("id"),
46
+ "agent_id": task.get("agent_id"),
47
+ "capability_id": task.get("capability_id"),
48
+ "status": result.get("status"),
49
+ "ok": bool(result.get("ok")),
50
+ "provider": task.get("provider"),
51
+ "write_policy": task.get("write_policy"),
52
+ "result": result,
53
+ }
54
+
55
+
56
+ def blocked_task(task: dict[str, Any], reason: str) -> dict[str, Any]:
57
+ return {
58
+ "id": task.get("id"),
59
+ "agent_id": task.get("agent_id"),
60
+ "capability_id": task.get("capability_id"),
61
+ "status": "blocked",
62
+ "ok": False,
63
+ "provider": task.get("provider"),
64
+ "write_policy": task.get("write_policy"),
65
+ "reason": reason,
66
+ }
@@ -0,0 +1,80 @@
1
+ """Agent and capability registry helpers for orchestration."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import yaml
9
+
10
+
11
+ def load_agent_registry(root: Path) -> dict[str, Any]:
12
+ agents_dir = root / "agents"
13
+ agents: dict[str, dict[str, Any]] = {}
14
+ capabilities: dict[str, dict[str, Any]] = {}
15
+ if not agents_dir.exists():
16
+ return {"agents": agents, "capabilities": capabilities}
17
+ for agent_path in sorted(path for path in agents_dir.iterdir() if path.is_dir()):
18
+ manifest_path = agent_path / "agent.yaml"
19
+ if not manifest_path.exists():
20
+ continue
21
+ agent = read_yaml(manifest_path)
22
+ agent_id = str(agent.get("id") or agent_path.name)
23
+ agent["id"] = agent_id
24
+ agent["path"] = str(agent_path.relative_to(root))
25
+ agent["capabilities_index"] = {}
26
+ for capability_path in sorted((agent_path / "capabilities").glob("*/capability.yaml")):
27
+ capability = read_yaml(capability_path)
28
+ capability_id = str(capability.get("id") or capability_path.parent.name)
29
+ short_id = capability_id.rsplit(".", 1)[-1]
30
+ public_capability = {
31
+ "id": capability_id,
32
+ "short_id": short_id,
33
+ "agent_id": agent_id,
34
+ "name": capability.get("name") or short_id,
35
+ "purpose": compact(str(capability.get("purpose") or "")),
36
+ "write_policy": capability.get("write_policy"),
37
+ "inputs": capability.get("inputs") or {},
38
+ "outputs": capability.get("outputs") or {},
39
+ "integration": capability.get("integration") or {},
40
+ "requires": capability.get("requires") or {},
41
+ "has_runner": bool(((capability.get("entrypoint") or {}).get("runner"))),
42
+ "path": str(capability_path.parent.relative_to(root)),
43
+ }
44
+ agent["capabilities_index"][short_id] = public_capability
45
+ capabilities[f"{agent_id}/{short_id}"] = public_capability
46
+ agents[agent_id] = public_agent(agent)
47
+ return {"agents": agents, "capabilities": capabilities}
48
+
49
+
50
+ def public_agent(agent: dict[str, Any]) -> dict[str, Any]:
51
+ surface = agent.get("agent_surface") if isinstance(agent.get("agent_surface"), dict) else {}
52
+ return {
53
+ "id": agent.get("id"),
54
+ "name": agent.get("name") or agent.get("id"),
55
+ "kind": agent.get("kind"),
56
+ "status": agent.get("status"),
57
+ "purpose": compact(str(agent.get("purpose") or "")),
58
+ "depends_on_agents": list(agent.get("depends_on_agents") or []),
59
+ "orchestrated_agents": list(surface.get("orchestrated_agents") or []),
60
+ "capabilities": list(agent.get("capabilities") or []),
61
+ "capabilities_index": dict(agent.get("capabilities_index") or {}),
62
+ "write_policy": agent.get("write_policy"),
63
+ "path": agent.get("path"),
64
+ }
65
+
66
+
67
+ def find_capability(registry: dict[str, Any], agent_id: str, capability_id: str) -> dict[str, Any] | None:
68
+ return (registry.get("capabilities") or {}).get(f"{agent_id}/{capability_id}")
69
+
70
+
71
+ def read_yaml(path: Path) -> dict[str, Any]:
72
+ data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
73
+ return data if isinstance(data, dict) else {}
74
+
75
+
76
+ def compact(value: str, *, limit: int = 240) -> str:
77
+ text = " ".join(value.split())
78
+ if len(text) <= limit:
79
+ return text
80
+ return text[: limit - 3].rstrip() + "..."
@@ -3,12 +3,16 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import os
6
+ import shutil
6
7
  from pathlib import Path
8
+ from typing import Any
7
9
 
8
10
 
11
+ CANONICAL_APP_HOME_ENV = "AGENT_DEVKIT_HOME"
9
12
  APP_HOME_ENV = "AI_DEVKIT_CONFIG_HOME"
10
13
  LEGACY_APP_HOME_ENV = "AIKIT_CONFIG_HOME"
11
- DEFAULT_APP_HOME_NAME = ".ai-devkit"
14
+ DEFAULT_APP_HOME_NAME = ".agent-devkit"
15
+ LEGACY_DEFAULT_APP_HOME_NAME = ".ai-devkit"
12
16
 
13
17
  APP_DIRS = (
14
18
  "bin",
@@ -27,12 +31,93 @@ APP_DIRS = (
27
31
 
28
32
  def app_home() -> Path:
29
33
  """Return the configured Agent DevKit home directory."""
30
- raw = os.environ.get(APP_HOME_ENV) or os.environ.get(LEGACY_APP_HOME_ENV)
34
+ raw = os.environ.get(CANONICAL_APP_HOME_ENV) or os.environ.get(APP_HOME_ENV) or os.environ.get(LEGACY_APP_HOME_ENV)
31
35
  if raw:
32
36
  return Path(raw).expanduser().resolve()
37
+ canonical = canonical_default_app_home()
38
+ legacy = legacy_default_app_home()
39
+ if canonical.exists():
40
+ return canonical
41
+ if legacy.exists():
42
+ return legacy
43
+ return canonical
44
+
45
+
46
+ def canonical_default_app_home() -> Path:
33
47
  return (Path.home() / DEFAULT_APP_HOME_NAME).resolve()
34
48
 
35
49
 
50
+ def legacy_default_app_home() -> Path:
51
+ return (Path.home() / LEGACY_DEFAULT_APP_HOME_NAME).resolve()
52
+
53
+
54
+ def app_home_status() -> dict[str, Any]:
55
+ explicit_env = active_home_env()
56
+ canonical = canonical_default_app_home()
57
+ legacy = legacy_default_app_home()
58
+ home = app_home()
59
+ status = "canonical"
60
+ if explicit_env:
61
+ status = "env"
62
+ elif home == legacy:
63
+ status = "legacy-default-detected"
64
+ elif not canonical.exists() and not legacy.exists():
65
+ status = "canonical-default"
66
+ return {
67
+ "kind": "app-home-status",
68
+ "status": status,
69
+ "home": str(home),
70
+ "canonical_home": str(canonical),
71
+ "legacy_home": str(legacy),
72
+ "active_env": explicit_env,
73
+ "canonical_exists": canonical.exists(),
74
+ "legacy_exists": legacy.exists(),
75
+ "migration_available": not explicit_env and legacy.exists() and not canonical.exists(),
76
+ "migration_command": "agent config migrate-home",
77
+ }
78
+
79
+
80
+ def active_home_env() -> dict[str, str] | None:
81
+ for name in (CANONICAL_APP_HOME_ENV, APP_HOME_ENV, LEGACY_APP_HOME_ENV):
82
+ value = os.environ.get(name)
83
+ if value:
84
+ return {"name": name, "value": str(Path(value).expanduser().resolve())}
85
+ return None
86
+
87
+
88
+ def migrate_default_home(*, dry_run: bool = False) -> dict[str, Any]:
89
+ source = legacy_default_app_home()
90
+ destination = canonical_default_app_home()
91
+ payload: dict[str, Any] = {
92
+ "kind": "home-migration",
93
+ "source": str(source),
94
+ "destination": str(destination),
95
+ "dry_run": dry_run,
96
+ "executed": False,
97
+ }
98
+ if destination.exists():
99
+ return {**payload, "status": "not-needed", "message": "Canonical Agent DevKit home already exists."}
100
+ if not source.exists():
101
+ return {**payload, "status": "not-needed", "message": "Legacy AI DevKit home was not found."}
102
+ if dry_run:
103
+ return {**payload, "status": "planned", "message": "Legacy home would be migrated to canonical Agent DevKit home."}
104
+ destination.parent.mkdir(parents=True, exist_ok=True)
105
+ try:
106
+ source.rename(destination)
107
+ method = "rename"
108
+ except OSError:
109
+ shutil.copytree(source, destination)
110
+ shutil.rmtree(source)
111
+ method = "copytree"
112
+ return {
113
+ **payload,
114
+ "status": "migrated",
115
+ "executed": True,
116
+ "method": method,
117
+ "message": "Legacy home migrated to canonical Agent DevKit home.",
118
+ }
119
+
120
+
36
121
  def app_path(*parts: str) -> Path:
37
122
  return app_home().joinpath(*parts)
38
123
 
@@ -60,6 +60,7 @@ def record_audit(
60
60
  "prompt_normalized": normalize_prompt(prompt) if prompt else None,
61
61
  "session": extract_session(safe_result),
62
62
  "route": safe_result.get("route"),
63
+ "orchestration": extract_orchestration(safe_result),
63
64
  "agent": extract_agent(safe_result, args),
64
65
  "providers": extract_providers(safe_result),
65
66
  "sources": extract_sources(safe_result),
@@ -282,6 +283,43 @@ def extract_external_actions(result: dict[str, Any]) -> list[dict[str, Any]]:
282
283
  return actions
283
284
 
284
285
 
286
+ def extract_orchestration(result: dict[str, Any]) -> dict[str, Any] | None:
287
+ plan = result.get("execution_plan")
288
+ if not isinstance(plan, dict):
289
+ return None
290
+ return {
291
+ "kind": plan.get("kind"),
292
+ "status": plan.get("status"),
293
+ "coordinator_agent": (plan.get("coordinator_agent") or {}).get("id") if isinstance(plan.get("coordinator_agent"), dict) else None,
294
+ "domain_agent": (plan.get("domain_agent") or {}).get("id") if isinstance(plan.get("domain_agent"), dict) else None,
295
+ "specialist_tasks": [
296
+ {
297
+ "agent_id": task.get("agent_id"),
298
+ "capability_id": task.get("capability_id"),
299
+ "status": task.get("status"),
300
+ }
301
+ for task in plan.get("specialist_tasks") or []
302
+ if isinstance(task, dict)
303
+ ],
304
+ "configuration_tasks": [
305
+ {
306
+ "agent_id": task.get("agent_id"),
307
+ "provider": task.get("provider"),
308
+ "status": task.get("status"),
309
+ }
310
+ for task in plan.get("configuration_tasks") or []
311
+ if isinstance(task, dict)
312
+ ],
313
+ "review_task": {
314
+ "agent_id": (plan.get("review_task") or {}).get("agent_id"),
315
+ "status": (plan.get("review_task") or {}).get("status"),
316
+ }
317
+ if isinstance(plan.get("review_task"), dict)
318
+ else None,
319
+ "trace": result.get("orchestration_trace") or plan.get("trace") or [],
320
+ }
321
+
322
+
285
323
  def summarize_result(result: dict[str, Any]) -> dict[str, Any]:
286
324
  return {
287
325
  "kind": result.get("kind"),
@@ -326,6 +364,12 @@ def render_audit_md(entry: dict[str, Any]) -> str:
326
364
  json.dumps(entry.get("providers") or {}, ensure_ascii=False, indent=2, sort_keys=True),
327
365
  "```",
328
366
  "",
367
+ "## Orchestration",
368
+ "",
369
+ "```json",
370
+ json.dumps(entry.get("orchestration") or {}, ensure_ascii=False, indent=2, sort_keys=True),
371
+ "```",
372
+ "",
329
373
  "## Permissions",
330
374
  "",
331
375
  "```json",
@@ -0,0 +1,291 @@
1
+ """Global configuration orchestration for missing providers and sources."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from cli.aikit.decision_store import is_disabled
10
+ from cli.aikit.providers import ProviderRegistryError, load_providers, provider_or_error
11
+
12
+
13
+ PROJECT_PATTERN = re.compile(r"(?i)\bprojeto\s+([A-Za-z0-9._ -]{2,80}?)(?:\s+no\b|\s+na\b|\s+do\b|\s+da\b|$)")
14
+
15
+
16
+ def provider_setup_wizard(
17
+ root: Path,
18
+ provider_id: str,
19
+ *,
20
+ prompt: str | None = None,
21
+ route: dict[str, Any] | None = None,
22
+ agent_id: str | None = None,
23
+ capability_id: str | None = None,
24
+ reason: str | None = None,
25
+ ) -> dict[str, Any]:
26
+ provider = load_provider(root, provider_id)
27
+ provider_name = str(provider.get("name") or provider_id)
28
+ intent = str((route or {}).get("intent") or capability_id or "provider-configuration")
29
+ suggested_project = infer_project(prompt or "")
30
+ source_context = bool(route and route.get("intent"))
31
+ suggested_source_id = suggest_source_id(provider_id, suggested_project)
32
+ disabled = is_disabled("tools", provider_id) or is_disabled("integrations", provider_id)
33
+ questions = build_questions(provider, suggested_project=suggested_project, suggested_source_id=suggested_source_id, include_source=source_context)
34
+ next_question = disabled_notice(provider_id, provider_name) if disabled else opt_in_question(provider_id, provider_name)
35
+ return {
36
+ "kind": "provider-setup-wizard",
37
+ "status": "denied-by-user" if disabled else "waiting-for-user",
38
+ "provider": provider_id,
39
+ "provider_name": provider_name,
40
+ "provider_kind": provider.get("kind"),
41
+ "intent": intent,
42
+ "agent_id": agent_id or (route or {}).get("agent_id"),
43
+ "capability_id": capability_id or (route or {}).get("capability_id"),
44
+ "reason": reason,
45
+ "resume_prompt": prompt,
46
+ "suggested_source_id": suggested_source_id if source_context else None,
47
+ "suggested_config": suggested_config(provider_id, suggested_project),
48
+ "next_question": next_question,
49
+ "questions": questions,
50
+ "credential_options": credential_options(provider),
51
+ "config_fields": public_config_fields(provider),
52
+ "auth_methods": public_auth_methods(provider),
53
+ "fallbacks": list(provider.get("fallbacks", []) or []),
54
+ "stores_secret": False,
55
+ "owner_agent": "provider-configurator",
56
+ "message": (
57
+ f"{provider_name} esta desativado. O agente seguira sem essa ferramenta ate voce reativar."
58
+ if disabled
59
+ else f"Para continuar, o agente precisa configurar ou autorizar {provider_name}."
60
+ ),
61
+ }
62
+
63
+
64
+ def provider_wizard_from_requirement(
65
+ root: Path,
66
+ provider_id: str,
67
+ *,
68
+ agent_id: str | None = None,
69
+ capability_id: str | None = None,
70
+ reason: str | None = None,
71
+ ) -> dict[str, Any]:
72
+ return provider_setup_wizard(
73
+ root,
74
+ provider_id,
75
+ agent_id=agent_id,
76
+ capability_id=capability_id,
77
+ reason=reason or "Capability provider requirement is not configured.",
78
+ )
79
+
80
+
81
+ def load_provider(root: Path, provider_id: str) -> dict[str, Any]:
82
+ try:
83
+ return provider_or_error(load_providers(root), provider_id)
84
+ except ProviderRegistryError:
85
+ return {
86
+ "id": provider_id,
87
+ "name": provider_id,
88
+ "kind": "unknown",
89
+ "config_fields": [],
90
+ "auth_methods": [],
91
+ "fallbacks": [],
92
+ }
93
+
94
+
95
+ def build_questions(
96
+ provider: dict[str, Any],
97
+ *,
98
+ suggested_project: str | None,
99
+ suggested_source_id: str,
100
+ include_source: bool,
101
+ ) -> list[dict[str, Any]]:
102
+ questions: list[dict[str, Any]] = []
103
+ for field in provider.get("config_fields", []) or []:
104
+ if not isinstance(field, dict):
105
+ continue
106
+ name = str(field.get("name") or "").strip()
107
+ if not name:
108
+ continue
109
+ suggested_value = suggested_value_for_field(provider, name, suggested_project)
110
+ questions.append(
111
+ {
112
+ "id": field_question_id(provider["id"], name),
113
+ "type": "text",
114
+ "text": question_text(provider, name, suggested_value=suggested_value),
115
+ "config_key": name,
116
+ "env": name,
117
+ "required": bool(field.get("required")),
118
+ "secret": bool(field.get("secret")),
119
+ "default": field.get("default"),
120
+ "suggested_value": suggested_value,
121
+ }
122
+ )
123
+ auth_methods = public_auth_methods(provider)
124
+ if auth_methods:
125
+ questions.append(
126
+ {
127
+ "id": f"{provider_slug(str(provider['id']))}_auth",
128
+ "type": "select",
129
+ "text": "Como deseja autenticar?",
130
+ "options": [method["id"] for method in auth_methods] + ["env", "file", "skip"],
131
+ "auth_methods": auth_methods,
132
+ }
133
+ )
134
+ if include_source:
135
+ questions.append(
136
+ {
137
+ "id": "source_id",
138
+ "type": "text",
139
+ "text": "Qual nome local deseja dar para esta fonte?",
140
+ "suggested_value": suggested_source_id,
141
+ }
142
+ )
143
+ questions.append(
144
+ {
145
+ "id": "default_for_intent",
146
+ "type": "confirm",
147
+ "text": "Posso usar esta fonte como padrao para este tipo de pedido?",
148
+ "default": True,
149
+ }
150
+ )
151
+ if not questions:
152
+ questions.append(
153
+ {
154
+ "id": f"{provider_slug(str(provider['id']))}_confirm",
155
+ "type": "confirm",
156
+ "text": f"Posso tentar usar {provider.get('name') or provider['id']} com a cadeia de credenciais nativa?",
157
+ "default": False,
158
+ }
159
+ )
160
+ return questions
161
+
162
+
163
+ def opt_in_question(provider_id: str, provider_name: str) -> dict[str, Any]:
164
+ return {
165
+ "id": f"{provider_slug(provider_id)}_opt_in",
166
+ "type": "confirm",
167
+ "text": f"Posso configurar ou autorizar {provider_name} para atender este pedido?",
168
+ "default": False,
169
+ }
170
+
171
+
172
+ def disabled_notice(provider_id: str, provider_name: str) -> dict[str, Any]:
173
+ return {
174
+ "id": f"{provider_slug(provider_id)}_disabled",
175
+ "type": "notice",
176
+ "text": f"{provider_name} esta desativado por decisao do usuario. Reative para usar esta integracao.",
177
+ "default": False,
178
+ }
179
+
180
+
181
+ def public_config_fields(provider: dict[str, Any]) -> list[dict[str, Any]]:
182
+ fields = []
183
+ for field in provider.get("config_fields", []) or []:
184
+ if not isinstance(field, dict):
185
+ continue
186
+ fields.append(
187
+ {
188
+ "name": field.get("name"),
189
+ "required": bool(field.get("required")),
190
+ "secret": bool(field.get("secret")),
191
+ "default": field.get("default"),
192
+ }
193
+ )
194
+ return fields
195
+
196
+
197
+ def public_auth_methods(provider: dict[str, Any]) -> list[dict[str, Any]]:
198
+ methods = []
199
+ for method in provider.get("auth_methods", []) or []:
200
+ if not isinstance(method, dict):
201
+ continue
202
+ methods.append(
203
+ {
204
+ "id": method.get("id"),
205
+ "label": method.get("label") or method.get("id"),
206
+ "native": bool(method.get("native")),
207
+ "config_fields": list(method.get("config_fields", []) or []),
208
+ "secret_fields": list(method.get("secret_fields", []) or []),
209
+ }
210
+ )
211
+ return methods
212
+
213
+
214
+ def credential_options(provider: dict[str, Any]) -> list[dict[str, Any]]:
215
+ options = []
216
+ for method in public_auth_methods(provider):
217
+ for secret_field in method.get("secret_fields") or []:
218
+ options.append(
219
+ {
220
+ "id": f"env:{secret_field}",
221
+ "label": f"Variavel de ambiente {secret_field}",
222
+ "env": secret_field,
223
+ "auth_method": method.get("id"),
224
+ "stores_secret": False,
225
+ }
226
+ )
227
+ options.extend(
228
+ [
229
+ {"id": "file", "label": "Arquivo local de credencial", "stores_secret": False},
230
+ {"id": "native", "label": "Cadeia nativa ou CLI ja autenticada", "stores_secret": False},
231
+ {"id": "skip", "label": "Ignorar esta ferramenta", "stores_secret": False},
232
+ ]
233
+ )
234
+ seen: set[str] = set()
235
+ unique = []
236
+ for option in options:
237
+ option_id = str(option.get("id"))
238
+ if option_id not in seen:
239
+ seen.add(option_id)
240
+ unique.append(option)
241
+ return unique
242
+
243
+
244
+ def suggested_config(provider_id: str, suggested_project: str | None) -> dict[str, Any]:
245
+ config: dict[str, Any] = {}
246
+ if provider_id == "azure-devops" and suggested_project:
247
+ config["project"] = suggested_project
248
+ config["AZURE_DEVOPS_PROJECT"] = suggested_project
249
+ return config
250
+
251
+
252
+ def suggested_value_for_field(provider: dict[str, Any], name: str, suggested_project: str | None) -> str | None:
253
+ if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_PROJECT" and suggested_project:
254
+ return suggested_project
255
+ return None
256
+
257
+
258
+ def question_text(provider: dict[str, Any], name: str, *, suggested_value: str | None) -> str:
259
+ if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_ORG":
260
+ return "Qual e a organizacao do Azure DevOps?"
261
+ if provider.get("id") == "azure-devops" and name == "AZURE_DEVOPS_PROJECT":
262
+ if suggested_value:
263
+ return f'O projeto e "{suggested_value}"? Se nao, informe o nome correto.'
264
+ return "Qual e o nome do projeto no Azure DevOps?"
265
+ label = name.replace("_", " ").lower()
266
+ return f"Informe {label} para {provider.get('name') or provider.get('id')}."
267
+
268
+
269
+ def field_question_id(provider_id: str, name: str) -> str:
270
+ return f"{provider_slug(provider_id)}_{provider_slug(name)}"
271
+
272
+
273
+ def infer_project(prompt: str) -> str | None:
274
+ match = PROJECT_PATTERN.search(prompt)
275
+ if not match:
276
+ return None
277
+ project = " ".join(match.group(1).strip(" .,_-").split())
278
+ return project or None
279
+
280
+
281
+ def suggest_source_id(provider: str, project: str | None) -> str:
282
+ parts = [provider]
283
+ if project:
284
+ parts.append(project)
285
+ raw = "-".join(parts)
286
+ normalized = re.sub(r"[^a-z0-9._-]+", "-", raw.lower()).strip("-")
287
+ return normalized or provider_slug(provider)
288
+
289
+
290
+ def provider_slug(provider: str) -> str:
291
+ return re.sub(r"[^a-z0-9]+", "_", provider.lower()).strip("_") or "provider"