agent-devkit 0.1.5 → 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 (87) hide show
  1. package/README.md +2 -2
  2. package/package.json +1 -1
  3. package/runtime/README.md +74 -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 +76 -11
  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/control_router.py +298 -0
  76. package/runtime/cli/aikit/decision_store.py +17 -0
  77. package/runtime/cli/aikit/diagnostics.py +2 -1
  78. package/runtime/cli/aikit/execution_reviewer.py +174 -0
  79. package/runtime/cli/aikit/install.py +4 -3
  80. package/runtime/cli/aikit/local_llm_operator.py +112 -0
  81. package/runtime/cli/aikit/lock.py +4 -1
  82. package/runtime/cli/aikit/main.py +355 -80
  83. package/runtime/cli/aikit/model_router.py +13 -4
  84. package/runtime/cli/aikit/orchestrator.py +535 -0
  85. package/runtime/cli/aikit/sessions.py +37 -0
  86. package/runtime/cli/aikit/sources.py +6 -2
  87. package/runtime/cli/aikit/wizard_state.py +503 -0
@@ -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,298 @@
1
+ """Natural-language control routing for tools, integrations, skills and LLMs."""
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 forget_decision, list_decisions, reset_decisions, set_decision
10
+ from cli.aikit.llm import BACKENDS
11
+ from cli.aikit.providers import load_providers
12
+ from cli.aikit.toolchain import load_toolchain
13
+
14
+
15
+ CATEGORY_ALIASES: dict[str, tuple[str, ...]] = {
16
+ "tools": ("tool", "tools", "ferramenta", "ferramentas", "cli", "clis", "toolchain"),
17
+ "integrations": ("integration", "integrations", "integracao", "integracoes", "integração", "integrações", "provider", "providers"),
18
+ "skills": ("skill", "skills", "habilidade", "habilidades"),
19
+ "llms": ("llm", "llms", "modelo", "modelos", "backend", "backends"),
20
+ }
21
+ LIST_MARKERS = ("mostre", "mostrar", "liste", "listar", "ver", "exiba", "exibir")
22
+ ENABLE_MARKERS = ("reative", "ative", "habilite", "enable")
23
+ DISABLE_MARKERS = ("desative", "desabilite", "disable", "bloqueie", "bloquear")
24
+ FORGET_MARKERS = ("esqueca", "esqueça", "remova", "remover", "limpe", "limpar", "delete", "apague", "apagar")
25
+ DECISION_MARKERS = ("decisao", "decisão", "decisoes", "decisões", "decision", "decisions", "bloqueio")
26
+ TOKEN_PATTERN = re.compile(r"[a-z0-9._:-]+")
27
+ SKILL_ROW_PATTERN = re.compile(r"^\|\s*`([^`]+)`\s*\|")
28
+
29
+
30
+ def dispatch_natural_control_prompt(root: Path, normalized_prompt: str) -> dict[str, Any] | None:
31
+ action = detect_action(normalized_prompt)
32
+ category = detect_category(normalized_prompt)
33
+ if action == "list":
34
+ return list_payload(root, category=category)
35
+ if action == "reset" and category:
36
+ result = reset_decisions(category)
37
+ return control_payload(
38
+ status="ok",
39
+ response=f"Decisoes de {category} foram resetadas.",
40
+ result={"action": "reset", "category": category, "decision": result},
41
+ )
42
+ if action not in {"enable", "disable", "forget"}:
43
+ return None
44
+
45
+ index = build_control_index(root)
46
+ matches = resolve_target(index, normalized_prompt, category=category)
47
+ if not matches:
48
+ return needs_input_unknown(root, normalized_prompt, category=category)
49
+ if len(matches) > 1:
50
+ return needs_input_ambiguous(matches, normalized_prompt, category=category)
51
+
52
+ target = matches[0]
53
+ if action == "forget":
54
+ decision = forget_decision(str(target["category"]), str(target["id"]))
55
+ return control_payload(
56
+ status="ok",
57
+ response=f"Decisao local sobre {target['id']} removida.",
58
+ result={"action": "forget", "category": target["category"], "id": target["id"], "removed": decision.get("removed"), "decision": decision},
59
+ )
60
+
61
+ state = "enabled" if action == "enable" else "disabled_by_user"
62
+ decision = set_decision(str(target["category"]), str(target["id"]), state, reason=f"natural prompt: {action}")
63
+ return control_payload(
64
+ status="ok",
65
+ response=f"{target['id']} foi {'ativado' if state == 'enabled' else 'desativado'} para {target['category']}.",
66
+ result={"action": action, "category": target["category"], "id": target["id"], "state": state, "decision": decision.get("item")},
67
+ )
68
+
69
+
70
+ def plan_natural_control_prompt(root: Path, normalized_prompt: str) -> dict[str, Any] | None:
71
+ action = detect_action(normalized_prompt)
72
+ category = detect_category(normalized_prompt)
73
+ if action == "list":
74
+ payload = list_payload(root, category=category)
75
+ payload["dry_run"] = True
76
+ return payload
77
+ if action == "reset" and category:
78
+ return control_payload(
79
+ status="planned",
80
+ response=f"Dry-run: decisoes de {category} seriam resetadas.",
81
+ result={"action": "reset", "category": category},
82
+ ) | {"dry_run": True, "ok": True}
83
+ if action not in {"enable", "disable", "forget"}:
84
+ return None
85
+ index = build_control_index(root)
86
+ matches = resolve_target(index, normalized_prompt, category=category)
87
+ if not matches:
88
+ payload = needs_input_unknown(root, normalized_prompt, category=category)
89
+ payload["dry_run"] = True
90
+ return payload
91
+ if len(matches) > 1:
92
+ payload = needs_input_ambiguous(matches, normalized_prompt, category=category)
93
+ payload["dry_run"] = True
94
+ return payload
95
+ target = matches[0]
96
+ state = "enabled" if action == "enable" else "disabled_by_user"
97
+ return control_payload(
98
+ status="planned",
99
+ response=f"Dry-run: {target['id']} seria {'ativado' if action == 'enable' else 'desativado' if action == 'disable' else 'removido das decisoes'} para {target['category']}.",
100
+ result={"action": action, "category": target["category"], "id": target["id"], "state": state if action != "forget" else None},
101
+ ) | {"dry_run": True, "ok": True}
102
+
103
+
104
+ def build_control_index(root: Path) -> dict[str, list[dict[str, Any]]]:
105
+ return {
106
+ "tools": tool_items(root),
107
+ "integrations": integration_items(root),
108
+ "skills": skill_items(root),
109
+ "llms": llm_items(),
110
+ }
111
+
112
+
113
+ def tool_items(root: Path) -> list[dict[str, Any]]:
114
+ return [
115
+ item("tools", tool_id, label=str(spec.get("label") or tool_id), source="toolchain", aliases=aliases_for(tool_id, spec.get("label")))
116
+ for tool_id, spec in load_toolchain(root).items()
117
+ ]
118
+
119
+
120
+ def integration_items(root: Path) -> list[dict[str, Any]]:
121
+ items = []
122
+ for provider in load_providers(root):
123
+ provider_id = str(provider.get("id") or "")
124
+ if provider_id:
125
+ items.append(item("integrations", provider_id, label=str(provider.get("name") or provider_id), source="providers", aliases=aliases_for(provider_id, provider.get("name"))))
126
+ return items
127
+
128
+
129
+ def skill_items(root: Path) -> list[dict[str, Any]]:
130
+ catalog = root / "vendor" / "skills" / "CATALOG.md"
131
+ if not catalog.exists():
132
+ return []
133
+ items = []
134
+ for line in catalog.read_text(encoding="utf-8").splitlines():
135
+ match = SKILL_ROW_PATTERN.match(line)
136
+ if not match:
137
+ continue
138
+ skill_id = match.group(1).strip()
139
+ if skill_id and skill_id.lower() != "skill":
140
+ items.append(item("skills", skill_id, label=skill_id, source="vendor-skills", aliases=aliases_for(skill_id)))
141
+ return items
142
+
143
+
144
+ def llm_items() -> list[dict[str, Any]]:
145
+ return [
146
+ item("llms", backend_id, label=backend.display_name, source="llm-backends", aliases=aliases_for(backend_id, backend.display_name))
147
+ for backend_id, backend in BACKENDS.items()
148
+ ]
149
+
150
+
151
+ def item(category: str, item_id: str, *, label: str, source: str, aliases: set[str]) -> dict[str, Any]:
152
+ return {"category": category, "id": item_id, "label": label, "source": source, "aliases": sorted(aliases)}
153
+
154
+
155
+ def aliases_for(item_id: str, label: Any = None) -> set[str]:
156
+ values = {normalize(item_id)}
157
+ if label:
158
+ values.add(normalize(str(label)))
159
+ values.add(normalize(item_id.replace("-", " ")))
160
+ values.add(normalize(item_id.replace("_", " ")))
161
+ for token in re.split(r"[-_\s]+", item_id):
162
+ normalized = normalize(token)
163
+ if normalized and normalized not in {"cli", "mcp", "api", "tool", "skill", "plugin"}:
164
+ values.add(normalized)
165
+ if item_id.endswith("-cli"):
166
+ values.add(normalize(item_id.removesuffix("-cli")))
167
+ if item_id.endswith("-mcp"):
168
+ values.add(normalize(item_id.removesuffix("-mcp")))
169
+ return {value for value in values if value}
170
+
171
+
172
+ def detect_action(prompt: str) -> str | None:
173
+ if contains_any(prompt, LIST_MARKERS):
174
+ return "list"
175
+ if contains_any(prompt, FORGET_MARKERS) and contains_any(prompt, DECISION_MARKERS) and detect_category(prompt):
176
+ return "reset"
177
+ if contains_any(prompt, FORGET_MARKERS) and contains_any(prompt, DECISION_MARKERS):
178
+ return "forget"
179
+ if contains_any(prompt, FORGET_MARKERS) and detect_category(prompt):
180
+ return "reset"
181
+ if contains_any(prompt, DISABLE_MARKERS):
182
+ return "disable"
183
+ if contains_any(prompt, ENABLE_MARKERS):
184
+ return "enable"
185
+ return None
186
+
187
+
188
+ def detect_category(prompt: str) -> str | None:
189
+ for category, aliases in CATEGORY_ALIASES.items():
190
+ if contains_any(prompt, aliases):
191
+ return category
192
+ return None
193
+
194
+
195
+ def list_payload(root: Path, *, category: str | None) -> dict[str, Any]:
196
+ index = build_control_index(root)
197
+ if category:
198
+ result = {
199
+ "kind": "control-index",
200
+ "status": "ok",
201
+ "category": category,
202
+ "items": public_items(index.get(category, [])),
203
+ "decisions": list_decisions(category),
204
+ }
205
+ return control_payload(status="ok", response=f"Estes sao os itens catalogados para {category}.", result=result)
206
+ result = {
207
+ "kind": "control-index",
208
+ "status": "ok",
209
+ "categories": {key: public_items(value) for key, value in index.items()},
210
+ "decisions": list_decisions(),
211
+ }
212
+ return control_payload(status="ok", response="Estes sao os recursos controlaveis catalogados.", result=result)
213
+
214
+
215
+ def resolve_target(index: dict[str, list[dict[str, Any]]], prompt: str, *, category: str | None) -> list[dict[str, Any]]:
216
+ candidates = index.get(category, []) if category else [item for items in index.values() for item in items]
217
+ prompt_tokens = set(TOKEN_PATTERN.findall(prompt))
218
+ matches = []
219
+ for candidate in candidates:
220
+ aliases = set(candidate.get("aliases") or [])
221
+ if any(alias_matches_prompt(alias, prompt, prompt_tokens) for alias in aliases):
222
+ matches.append(candidate)
223
+ continue
224
+ candidate_tokens = set(TOKEN_PATTERN.findall(normalize(str(candidate.get("id") or ""))))
225
+ if candidate_tokens and candidate_tokens <= prompt_tokens:
226
+ matches.append(candidate)
227
+ return sorted(unique_matches(matches), key=lambda item: (str(item["category"]), str(item["id"])))
228
+
229
+
230
+ def unique_matches(matches: list[dict[str, Any]]) -> list[dict[str, Any]]:
231
+ seen: set[tuple[str, str]] = set()
232
+ unique = []
233
+ for match in matches:
234
+ key = (str(match.get("category")), str(match.get("id")))
235
+ if key not in seen:
236
+ seen.add(key)
237
+ unique.append(match)
238
+ return unique
239
+
240
+
241
+ def alias_matches_prompt(alias: str, prompt: str, prompt_tokens: set[str]) -> bool:
242
+ if not alias:
243
+ return False
244
+ alias_tokens = set(TOKEN_PATTERN.findall(alias))
245
+ if len(alias_tokens) == 1:
246
+ return alias in prompt_tokens
247
+ return alias in prompt
248
+
249
+
250
+ def needs_input_unknown(root: Path, prompt: str, *, category: str | None) -> dict[str, Any]:
251
+ index = build_control_index(root)
252
+ categories = [category] if category else sorted(index)
253
+ return {
254
+ "kind": "agent",
255
+ "status": "needs-input",
256
+ "ok": False,
257
+ "mode": "control-center-route",
258
+ "requires_llm": False,
259
+ "unknown_control_target": True,
260
+ "category": category,
261
+ "prompt": prompt,
262
+ "available_categories": categories,
263
+ "response": "Nao encontrei esse recurso nos catalogos locais controlaveis.",
264
+ "result": {"categories": {key: public_items(index.get(key, [])) for key in categories}},
265
+ "exit_code": 2,
266
+ }
267
+
268
+
269
+ def needs_input_ambiguous(matches: list[dict[str, Any]], prompt: str, *, category: str | None) -> dict[str, Any]:
270
+ return {
271
+ "kind": "agent",
272
+ "status": "needs-input",
273
+ "ok": False,
274
+ "mode": "control-center-route",
275
+ "requires_llm": False,
276
+ "ambiguous_control_target": True,
277
+ "category": category,
278
+ "prompt": prompt,
279
+ "matches": public_items(matches),
280
+ "response": "Encontrei mais de um recurso compativel. Informe a categoria ou id exato.",
281
+ "exit_code": 2,
282
+ }
283
+
284
+
285
+ def control_payload(*, status: str, response: str, result: dict[str, Any]) -> dict[str, Any]:
286
+ return {"kind": "agent", "status": status, "ok": status == "ok", "mode": "control-center-route", "requires_llm": False, "response": response, "result": result}
287
+
288
+
289
+ def public_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
290
+ return [{"category": item.get("category"), "id": item.get("id"), "label": item.get("label"), "source": item.get("source")} for item in items]
291
+
292
+
293
+ def contains_any(prompt: str, markers: tuple[str, ...]) -> bool:
294
+ return any(normalize(marker) in prompt for marker in markers)
295
+
296
+
297
+ def normalize(value: str) -> str:
298
+ return " ".join(value.lower().split())
@@ -122,6 +122,23 @@ def reset_decisions(category: str | None = None) -> dict[str, Any]:
122
122
  return {"kind": "decisions-reset", "status": "reset", "category": category, "path": str(path)}
123
123
 
124
124
 
125
+ def forget_decision(category: str, item_id: str) -> dict[str, Any]:
126
+ validate_category(category)
127
+ validate_item_id(item_id)
128
+ data = load_decisions()
129
+ key = decision_key(category, item_id)
130
+ removed = data.get("items", {}).pop(key, None)
131
+ path = save_decisions(data)
132
+ return {
133
+ "kind": "decision-forget",
134
+ "status": "forgotten" if isinstance(removed, dict) else "not-found",
135
+ "path": str(path),
136
+ "category": category,
137
+ "id": item_id,
138
+ "removed": public_decision(removed) if isinstance(removed, dict) else None,
139
+ }
140
+
141
+
125
142
  def is_disabled(category: str, item_id: str) -> bool:
126
143
  decision = get_decision(category, item_id)
127
144
  return bool(decision and decision.get("state") in {"disabled_by_user", "denied_by_user"})
@@ -7,7 +7,7 @@ import os
7
7
  from pathlib import Path
8
8
  from typing import Any
9
9
 
10
- from cli.aikit.app_home import APP_DIRS, app_home
10
+ from cli.aikit.app_home import APP_DIRS, app_home, app_home_status
11
11
  from cli.aikit.llm import config_path as llm_config_path
12
12
  from cli.aikit.llm import doctor_backends
13
13
  from cli.aikit.lock import lock_path, lock_status, read_lock
@@ -44,6 +44,7 @@ def runtime_diagnostics(
44
44
  "status": status,
45
45
  "root": str(root),
46
46
  "app_home": str(app_home()),
47
+ "app_home_status": app_home_status(),
47
48
  "app_dirs": {
48
49
  name: {
49
50
  "path": str(app_home() / name),