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.
- package/README.md +42 -2
- package/package.json +1 -1
- package/runtime/README.md +119 -5
- package/runtime/agents/README.md +8 -0
- package/runtime/agents/execution-reviewer/AGENTS.md +8 -0
- package/runtime/agents/execution-reviewer/README.md +13 -0
- package/runtime/agents/execution-reviewer/agent.yaml +25 -0
- package/runtime/agents/execution-reviewer/capabilities/review-agent-result/capability.yaml +15 -0
- package/runtime/agents/execution-reviewer/capabilities/review-agent-result/decision-rules.md +6 -0
- package/runtime/agents/execution-reviewer/capabilities/review-agent-result/workflow.md +7 -0
- package/runtime/agents/execution-reviewer/capabilities/review-final-output/capability.yaml +15 -0
- package/runtime/agents/execution-reviewer/capabilities/review-final-output/decision-rules.md +6 -0
- package/runtime/agents/execution-reviewer/capabilities/review-final-output/workflow.md +7 -0
- package/runtime/agents/execution-reviewer/capabilities/review-plan/capability.yaml +15 -0
- package/runtime/agents/execution-reviewer/capabilities/review-plan/decision-rules.md +6 -0
- package/runtime/agents/execution-reviewer/capabilities/review-plan/workflow.md +7 -0
- package/runtime/agents/execution-reviewer/infra/README.md +4 -0
- package/runtime/agents/execution-reviewer/knowledge/context.md +6 -0
- package/runtime/agents/execution-reviewer/knowledge/system.md +18 -0
- package/runtime/agents/execution-reviewer/templates/README.md +4 -0
- package/runtime/agents/local-llm-operator/AGENTS.md +8 -0
- package/runtime/agents/local-llm-operator/README.md +12 -0
- package/runtime/agents/local-llm-operator/agent.yaml +26 -0
- package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/capability.yaml +15 -0
- package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/decision-rules.md +6 -0
- package/runtime/agents/local-llm-operator/capabilities/delegate-operational-task/workflow.md +7 -0
- package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/capability.yaml +15 -0
- package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/decision-rules.md +5 -0
- package/runtime/agents/local-llm-operator/capabilities/inspect-local-models/workflow.md +6 -0
- package/runtime/agents/local-llm-operator/capabilities/select-local-worker/capability.yaml +15 -0
- package/runtime/agents/local-llm-operator/capabilities/select-local-worker/decision-rules.md +6 -0
- package/runtime/agents/local-llm-operator/capabilities/select-local-worker/workflow.md +7 -0
- package/runtime/agents/local-llm-operator/infra/README.md +4 -0
- package/runtime/agents/local-llm-operator/knowledge/context.md +6 -0
- package/runtime/agents/local-llm-operator/knowledge/system.md +18 -0
- package/runtime/agents/local-llm-operator/templates/README.md +4 -0
- package/runtime/agents/provider-configurator/AGENTS.md +8 -0
- package/runtime/agents/provider-configurator/README.md +5 -0
- package/runtime/agents/provider-configurator/agent.yaml +25 -0
- package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/capability.yaml +15 -0
- package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/decision-rules.md +6 -0
- package/runtime/agents/provider-configurator/capabilities/collect-provider-credentials/workflow.md +7 -0
- package/runtime/agents/provider-configurator/capabilities/configure-provider-source/capability.yaml +15 -0
- package/runtime/agents/provider-configurator/capabilities/configure-provider-source/decision-rules.md +6 -0
- package/runtime/agents/provider-configurator/capabilities/configure-provider-source/workflow.md +8 -0
- package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/capability.yaml +15 -0
- package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/decision-rules.md +6 -0
- package/runtime/agents/provider-configurator/capabilities/validate-provider-readiness/workflow.md +7 -0
- package/runtime/agents/provider-configurator/infra/README.md +5 -0
- package/runtime/agents/provider-configurator/knowledge/context.md +8 -0
- package/runtime/agents/provider-configurator/knowledge/system.md +19 -0
- package/runtime/agents/provider-configurator/templates/README.md +4 -0
- package/runtime/agents/task-orchestrator/AGENTS.md +8 -0
- package/runtime/agents/task-orchestrator/README.md +7 -0
- package/runtime/agents/task-orchestrator/agent.yaml +31 -0
- package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/capability.yaml +15 -0
- package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/decision-rules.md +6 -0
- package/runtime/agents/task-orchestrator/capabilities/coordinate-execution/workflow.md +7 -0
- package/runtime/agents/task-orchestrator/capabilities/plan-task/capability.yaml +15 -0
- package/runtime/agents/task-orchestrator/capabilities/plan-task/decision-rules.md +6 -0
- package/runtime/agents/task-orchestrator/capabilities/plan-task/workflow.md +8 -0
- package/runtime/agents/task-orchestrator/capabilities/select-specialists/capability.yaml +15 -0
- package/runtime/agents/task-orchestrator/capabilities/select-specialists/decision-rules.md +6 -0
- package/runtime/agents/task-orchestrator/capabilities/select-specialists/workflow.md +7 -0
- package/runtime/agents/task-orchestrator/infra/README.md +5 -0
- package/runtime/agents/task-orchestrator/knowledge/context.md +9 -0
- package/runtime/agents/task-orchestrator/knowledge/system.md +20 -0
- package/runtime/agents/task-orchestrator/templates/README.md +4 -0
- package/runtime/cli/README.md +111 -10
- package/runtime/cli/aikit/__init__.py +1 -1
- package/runtime/cli/aikit/agent_executor.py +66 -0
- package/runtime/cli/aikit/agent_registry.py +80 -0
- package/runtime/cli/aikit/app_home.py +87 -2
- package/runtime/cli/aikit/audit.py +44 -0
- package/runtime/cli/aikit/configuration_orchestrator.py +291 -0
- package/runtime/cli/aikit/control_router.py +298 -0
- package/runtime/cli/aikit/decision_store.py +175 -0
- package/runtime/cli/aikit/diagnostics.py +2 -1
- package/runtime/cli/aikit/execution_reviewer.py +174 -0
- package/runtime/cli/aikit/fallback.py +48 -2
- package/runtime/cli/aikit/install.py +4 -3
- package/runtime/cli/aikit/llm.py +9 -0
- package/runtime/cli/aikit/local_llm_operator.py +112 -0
- package/runtime/cli/aikit/lock.py +4 -1
- package/runtime/cli/aikit/main.py +604 -21
- package/runtime/cli/aikit/model_router.py +79 -0
- package/runtime/cli/aikit/ollama.py +237 -0
- package/runtime/cli/aikit/orchestrator.py +535 -0
- package/runtime/cli/aikit/provider_wizard.py +19 -0
- package/runtime/cli/aikit/review_gate.py +40 -0
- package/runtime/cli/aikit/sessions.py +37 -0
- package/runtime/cli/aikit/sources.py +6 -2
- package/runtime/cli/aikit/wizard_state.py +503 -0
|
@@ -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())
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Persistent local decisions for tools, integrations, skills and LLMs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from cli.aikit.app_home import app_path, ensure_app_home
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DECISION_VERSION = 1
|
|
15
|
+
VALID_CATEGORIES = {"tools", "integrations", "skills", "llms"}
|
|
16
|
+
VALID_STATES = {"enabled", "disabled_by_user", "denied_by_user", "needs_setup", "unavailable", "available", "requires_permission"}
|
|
17
|
+
ITEM_ID_PATTERN = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def decisions_path() -> Path:
|
|
21
|
+
ensure_app_home()
|
|
22
|
+
path = app_path("config", "decisions.json")
|
|
23
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
return path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def empty_decisions() -> dict[str, Any]:
|
|
28
|
+
return {"version": DECISION_VERSION, "items": {}}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_decisions() -> dict[str, Any]:
|
|
32
|
+
path = decisions_path()
|
|
33
|
+
if not path.exists():
|
|
34
|
+
return empty_decisions()
|
|
35
|
+
try:
|
|
36
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
37
|
+
except (OSError, json.JSONDecodeError):
|
|
38
|
+
return empty_decisions()
|
|
39
|
+
if not isinstance(data, dict):
|
|
40
|
+
return empty_decisions()
|
|
41
|
+
data.setdefault("version", DECISION_VERSION)
|
|
42
|
+
if not isinstance(data.get("items"), dict):
|
|
43
|
+
data["items"] = {}
|
|
44
|
+
return data
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def save_decisions(data: dict[str, Any]) -> Path:
|
|
48
|
+
path = decisions_path()
|
|
49
|
+
path.write_text(json.dumps(data, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
50
|
+
return path
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def list_decisions(category: str | None = None) -> dict[str, Any]:
|
|
54
|
+
data = load_decisions()
|
|
55
|
+
items = [public_decision(item) for item in data.get("items", {}).values() if not category or item.get("category") == category]
|
|
56
|
+
items.sort(key=lambda item: (item["category"], item["id"]))
|
|
57
|
+
return {
|
|
58
|
+
"kind": "decisions",
|
|
59
|
+
"status": "ok",
|
|
60
|
+
"category": category,
|
|
61
|
+
"path": str(decisions_path()),
|
|
62
|
+
"items": items,
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def set_decision(
|
|
67
|
+
category: str,
|
|
68
|
+
item_id: str,
|
|
69
|
+
state: str,
|
|
70
|
+
*,
|
|
71
|
+
reason: str | None = None,
|
|
72
|
+
scope: str = "persistent",
|
|
73
|
+
) -> dict[str, Any]:
|
|
74
|
+
validate_category(category)
|
|
75
|
+
validate_item_id(item_id)
|
|
76
|
+
if state not in VALID_STATES:
|
|
77
|
+
raise ValueError(f"invalid decision state: {state}")
|
|
78
|
+
data = load_decisions()
|
|
79
|
+
key = decision_key(category, item_id)
|
|
80
|
+
item = {
|
|
81
|
+
"key": key,
|
|
82
|
+
"category": category,
|
|
83
|
+
"id": item_id,
|
|
84
|
+
"state": state,
|
|
85
|
+
"scope": scope,
|
|
86
|
+
"reason": reason,
|
|
87
|
+
"updated_at": now_iso(),
|
|
88
|
+
}
|
|
89
|
+
existing = data["items"].get(key)
|
|
90
|
+
if isinstance(existing, dict) and existing.get("created_at"):
|
|
91
|
+
item["created_at"] = existing["created_at"]
|
|
92
|
+
else:
|
|
93
|
+
item["created_at"] = item["updated_at"]
|
|
94
|
+
data["items"][key] = item
|
|
95
|
+
path = save_decisions(data)
|
|
96
|
+
return {
|
|
97
|
+
"kind": "decision",
|
|
98
|
+
"status": "updated",
|
|
99
|
+
"path": str(path),
|
|
100
|
+
"item": public_decision(item),
|
|
101
|
+
"category": category,
|
|
102
|
+
"id": item_id,
|
|
103
|
+
"state": state,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def get_decision(category: str, item_id: str) -> dict[str, Any] | None:
|
|
108
|
+
validate_category(category)
|
|
109
|
+
validate_item_id(item_id)
|
|
110
|
+
item = load_decisions().get("items", {}).get(decision_key(category, item_id))
|
|
111
|
+
return public_decision(item) if isinstance(item, dict) else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def reset_decisions(category: str | None = None) -> dict[str, Any]:
|
|
115
|
+
data = load_decisions()
|
|
116
|
+
if category:
|
|
117
|
+
validate_category(category)
|
|
118
|
+
data["items"] = {key: value for key, value in data.get("items", {}).items() if value.get("category") != category}
|
|
119
|
+
else:
|
|
120
|
+
data["items"] = {}
|
|
121
|
+
path = save_decisions(data)
|
|
122
|
+
return {"kind": "decisions-reset", "status": "reset", "category": category, "path": str(path)}
|
|
123
|
+
|
|
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
|
+
|
|
142
|
+
def is_disabled(category: str, item_id: str) -> bool:
|
|
143
|
+
decision = get_decision(category, item_id)
|
|
144
|
+
return bool(decision and decision.get("state") in {"disabled_by_user", "denied_by_user"})
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def public_decision(item: dict[str, Any]) -> dict[str, Any]:
|
|
148
|
+
return {
|
|
149
|
+
"category": item.get("category"),
|
|
150
|
+
"id": item.get("id"),
|
|
151
|
+
"state": item.get("state"),
|
|
152
|
+
"scope": item.get("scope"),
|
|
153
|
+
"reason": item.get("reason"),
|
|
154
|
+
"created_at": item.get("created_at"),
|
|
155
|
+
"updated_at": item.get("updated_at"),
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def decision_key(category: str, item_id: str) -> str:
|
|
160
|
+
return f"{category}:{item_id}"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def validate_category(category: str) -> None:
|
|
164
|
+
if category not in VALID_CATEGORIES:
|
|
165
|
+
available = ", ".join(sorted(VALID_CATEGORIES))
|
|
166
|
+
raise ValueError(f"invalid decision category: {category}. available: {available}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def validate_item_id(item_id: str) -> None:
|
|
170
|
+
if not item_id or not ITEM_ID_PATTERN.fullmatch(item_id):
|
|
171
|
+
raise ValueError("item id must use letters, numbers, dots, dashes, underscores or colons")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def now_iso() -> str:
|
|
175
|
+
return datetime.now(timezone.utc).isoformat()
|
|
@@ -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),
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""Runtime execution for the execution-reviewer agent."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from cli.aikit.llm import BACKENDS, candidate_backend_ids, doctor_backend, invoke_agent_prompt, load_config
|
|
8
|
+
from cli.aikit.review_gate import mark_reviewed
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
PREFERRED_REVIEW_BACKENDS = ("claude-code", "codex-cli")
|
|
12
|
+
REVIEW_OK_MARKER = "REVIEW OK"
|
|
13
|
+
REVIEW_BLOCKED_MARKER = "REVIEW BLOCKED"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def enforce_execution_review(
|
|
17
|
+
*,
|
|
18
|
+
prompt: str,
|
|
19
|
+
result: dict[str, Any],
|
|
20
|
+
review_gate: dict[str, Any],
|
|
21
|
+
execution_plan: dict[str, Any] | None = None,
|
|
22
|
+
producer_backend: str | None = None,
|
|
23
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
24
|
+
"""Run a second concrete review when required and apply the gate outcome."""
|
|
25
|
+
if not review_gate.get("required"):
|
|
26
|
+
review_result = {
|
|
27
|
+
"kind": "execution-review",
|
|
28
|
+
"agent_id": "execution-reviewer",
|
|
29
|
+
"capability_id": "review-final-output",
|
|
30
|
+
"status": "not-required",
|
|
31
|
+
"ok": True,
|
|
32
|
+
}
|
|
33
|
+
return result, review_gate, review_result
|
|
34
|
+
|
|
35
|
+
response = str(result.get("response") or result.get("message") or "")
|
|
36
|
+
review_backend = select_review_backend(producer_backend=producer_backend, preferred=review_gate.get("preferred_reviewers") or [])
|
|
37
|
+
if not review_backend:
|
|
38
|
+
review_result = needs_review("No configured independent reviewer backend is available.", producer_backend=producer_backend)
|
|
39
|
+
return block_for_review(result, review_gate, review_result)
|
|
40
|
+
|
|
41
|
+
review_prompt = build_review_prompt(prompt=prompt, response=response, review_gate=review_gate, execution_plan=execution_plan)
|
|
42
|
+
invoked = invoke_agent_prompt(
|
|
43
|
+
review_prompt,
|
|
44
|
+
review_backend,
|
|
45
|
+
public_name="Execution Reviewer",
|
|
46
|
+
allow_fallback=False,
|
|
47
|
+
)
|
|
48
|
+
decision = parse_review_decision(str(invoked.get("response") or ""))
|
|
49
|
+
review_result = {
|
|
50
|
+
"kind": "execution-review",
|
|
51
|
+
"agent_id": "execution-reviewer",
|
|
52
|
+
"capability_id": "review-final-output",
|
|
53
|
+
"capability": "execution-reviewer.review-final-output",
|
|
54
|
+
"status": review_status(invoked=invoked, decision=decision),
|
|
55
|
+
"ok": bool(invoked.get("ok")) and decision == "approved",
|
|
56
|
+
"llm_backend": invoked.get("llm_backend"),
|
|
57
|
+
"producer_backend": producer_backend,
|
|
58
|
+
"decision": decision,
|
|
59
|
+
"response": invoked.get("response"),
|
|
60
|
+
"message": invoked.get("message"),
|
|
61
|
+
"attempts": invoked.get("llm_backend_attempts"),
|
|
62
|
+
}
|
|
63
|
+
if not invoked.get("ok"):
|
|
64
|
+
return block_for_review(result, review_gate, review_result)
|
|
65
|
+
if decision == "blocked":
|
|
66
|
+
review_result["message"] = "Execution reviewer blocked the proposed final output."
|
|
67
|
+
return block_for_review(result, review_gate, review_result)
|
|
68
|
+
if decision != "approved":
|
|
69
|
+
review_result["message"] = "Execution reviewer did not return REVIEW OK."
|
|
70
|
+
return block_for_review(result, review_gate, review_result)
|
|
71
|
+
reviewed_gate = mark_reviewed(review_gate, reviewer=str(invoked.get("llm_backend") or review_backend), notes=str(invoked.get("response") or "Reviewed."))
|
|
72
|
+
return result, reviewed_gate, review_result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def select_review_backend(*, producer_backend: str | None, preferred: list[str]) -> str | None:
|
|
76
|
+
config = load_config()
|
|
77
|
+
candidates = candidate_backend_ids(config=config, allow_fallback=True)
|
|
78
|
+
preferred_order = [item for item in list(preferred or []) + list(PREFERRED_REVIEW_BACKENDS) if item in BACKENDS]
|
|
79
|
+
ordered = [item for item in preferred_order if item in candidates]
|
|
80
|
+
ordered.extend(item for item in candidates if item not in ordered and item != "ollama")
|
|
81
|
+
for backend_id in ordered:
|
|
82
|
+
if backend_id == producer_backend:
|
|
83
|
+
continue
|
|
84
|
+
backend = doctor_backend(BACKENDS[backend_id], config)
|
|
85
|
+
if backend.get("status") == "ok":
|
|
86
|
+
return backend_id
|
|
87
|
+
return None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def build_review_prompt(
|
|
91
|
+
*,
|
|
92
|
+
prompt: str,
|
|
93
|
+
response: str,
|
|
94
|
+
review_gate: dict[str, Any],
|
|
95
|
+
execution_plan: dict[str, Any] | None,
|
|
96
|
+
) -> str:
|
|
97
|
+
plan_summary = {}
|
|
98
|
+
if isinstance(execution_plan, dict):
|
|
99
|
+
plan_summary = {
|
|
100
|
+
"status": execution_plan.get("status"),
|
|
101
|
+
"coordinator": (execution_plan.get("coordinator_agent") or {}).get("id") if isinstance(execution_plan.get("coordinator_agent"), dict) else None,
|
|
102
|
+
"specialist_tasks": [
|
|
103
|
+
{
|
|
104
|
+
"agent_id": task.get("agent_id"),
|
|
105
|
+
"capability_id": task.get("capability_id"),
|
|
106
|
+
"status": task.get("status"),
|
|
107
|
+
}
|
|
108
|
+
for task in execution_plan.get("specialist_tasks") or []
|
|
109
|
+
if isinstance(task, dict)
|
|
110
|
+
],
|
|
111
|
+
}
|
|
112
|
+
return "\n".join(
|
|
113
|
+
[
|
|
114
|
+
"Revise a entrega final do Agent DevKit antes da conclusao.",
|
|
115
|
+
"Verifique aderencia ao pedido, riscos, lacunas, evidencias, uso indevido de credenciais e necessidade de bloquear.",
|
|
116
|
+
"Responda com uma decisao objetiva: REVIEW OK ou REVIEW BLOCKED, seguida de justificativa curta.",
|
|
117
|
+
"",
|
|
118
|
+
f"Motivo do review gate: {review_gate.get('reason')}",
|
|
119
|
+
f"Prompt original: {prompt}",
|
|
120
|
+
f"Resumo do plano: {plan_summary}",
|
|
121
|
+
"",
|
|
122
|
+
"Resposta final proposta:",
|
|
123
|
+
response,
|
|
124
|
+
]
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def parse_review_decision(response: str) -> str:
|
|
129
|
+
upper = response.upper()
|
|
130
|
+
if REVIEW_BLOCKED_MARKER in upper:
|
|
131
|
+
return "blocked"
|
|
132
|
+
if REVIEW_OK_MARKER in upper:
|
|
133
|
+
return "approved"
|
|
134
|
+
return "unstructured"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def review_status(*, invoked: dict[str, Any], decision: str) -> str:
|
|
138
|
+
if not invoked.get("ok"):
|
|
139
|
+
return str(invoked.get("status", "failed"))
|
|
140
|
+
if decision == "blocked":
|
|
141
|
+
return "blocked"
|
|
142
|
+
if decision != "approved":
|
|
143
|
+
return "needs-review"
|
|
144
|
+
return "ok"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def needs_review(message: str, *, producer_backend: str | None) -> dict[str, Any]:
|
|
148
|
+
return {
|
|
149
|
+
"kind": "execution-review",
|
|
150
|
+
"agent_id": "execution-reviewer",
|
|
151
|
+
"capability_id": "review-final-output",
|
|
152
|
+
"capability": "execution-reviewer.review-final-output",
|
|
153
|
+
"status": "needs-review",
|
|
154
|
+
"ok": False,
|
|
155
|
+
"producer_backend": producer_backend,
|
|
156
|
+
"message": message,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def block_for_review(
|
|
161
|
+
result: dict[str, Any],
|
|
162
|
+
review_gate: dict[str, Any],
|
|
163
|
+
review_result: dict[str, Any],
|
|
164
|
+
) -> tuple[dict[str, Any], dict[str, Any], dict[str, Any]]:
|
|
165
|
+
blocked = dict(result)
|
|
166
|
+
blocked["status"] = "needs-review"
|
|
167
|
+
blocked["ok"] = False
|
|
168
|
+
blocked["exit_code"] = 2
|
|
169
|
+
blocked["message"] = review_result.get("message") or "Execution requires review before completion."
|
|
170
|
+
gate = dict(review_gate)
|
|
171
|
+
gate["status"] = "needs-review"
|
|
172
|
+
gate["reviewer"] = None
|
|
173
|
+
gate["notes"] = review_result.get("message")
|
|
174
|
+
return blocked, gate, review_result
|