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
@@ -5,12 +5,12 @@ from __future__ import annotations
5
5
  import re
6
6
  from typing import Any
7
7
 
8
- from cli.aikit.llm import llm_preference, load_config
8
+ from cli.aikit.llm import BACKENDS, doctor_backend, llm_preference, load_config
9
9
  from cli.aikit.ollama import ollama_status
10
10
 
11
11
 
12
12
  OPERATIONAL_PATTERN = re.compile(
13
- r"(?i)\b(resum|sumari|classifi|extra(?:i|ir)|normaliz|compar|logs?|rascunho|agrupe|agrupar)\b"
13
+ r"(?i)\b(resum\w*|sumari\w*|classifi\w*|extra(?:i|ir|ia|cao|ção)\w*|normaliz\w*|compar\w*|logs?|rascunho|agrupe|agrupar)\b"
14
14
  )
15
15
  HIGH_LEVEL_PATTERN = re.compile(
16
16
  r"(?i)\b(arquitet|decid|aprovar|reprovar|especifica|requisit|implemente|codigo|c[oó]digo|documento|automac|deploy|seguran)\b"
@@ -18,9 +18,11 @@ HIGH_LEVEL_PATTERN = re.compile(
18
18
 
19
19
 
20
20
  def build_model_plan(prompt: str, *, route: dict[str, Any] | None = None) -> dict[str, Any]:
21
- preference = llm_preference(load_config())
21
+ config = load_config()
22
+ preference = llm_preference(config)
22
23
  ollama = ollama_status()
23
- local_available = ollama.get("status") == "ok"
24
+ ollama_backend = doctor_backend(BACKENDS["ollama"], config)
25
+ local_available = ollama.get("status") == "ok" or ollama_backend.get("status") == "ok"
24
26
  operational = bool(OPERATIONAL_PATTERN.search(prompt))
25
27
  high_level = bool(HIGH_LEVEL_PATTERN.search(prompt))
26
28
  use_local = operational and local_available
@@ -32,6 +34,13 @@ def build_model_plan(prompt: str, *, route: dict[str, Any] | None = None) -> dic
32
34
  "local_llm_role": "operational-worker",
33
35
  "local_llm_available": local_available,
34
36
  "local_llm_provider": "ollama",
37
+ "local_llm_backend_configured": ollama_backend.get("status") == "ok",
38
+ "local_llm_runtime": {
39
+ "binary_status": ollama.get("status"),
40
+ "backend_status": ollama_backend.get("status"),
41
+ "model": ollama_backend.get("model"),
42
+ "base_url": ollama_backend.get("base_url"),
43
+ },
35
44
  "local_llm_recommended": operational,
36
45
  "local_llm_selected": use_local,
37
46
  "delegation": {
@@ -0,0 +1,535 @@
1
+ """Multi-agent planning for natural-language Agent DevKit prompts."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ import unicodedata
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from cli.aikit.agent_registry import find_capability, load_agent_registry
11
+ from cli.aikit.configuration_orchestrator import provider_setup_wizard
12
+ from cli.aikit.memory import redact_secrets
13
+ from cli.aikit.model_router import build_model_plan
14
+ from cli.aikit.review_gate import build_review_gate
15
+ from cli.aikit.router import route_prompt
16
+ from cli.aikit.sources import SourceRegistryError, public_source, resolve_source
17
+
18
+
19
+ RUNTIME_COORDINATOR = {
20
+ "id": "task-orchestrator",
21
+ "kind": "runtime-agent",
22
+ "name": "Task Orchestrator",
23
+ "role": "coordinator",
24
+ }
25
+ REVIEWER_AGENT_ID = "execution-reviewer"
26
+ PROVIDER_CONFIGURATOR_AGENT_ID = "provider-configurator"
27
+ LOCAL_LLM_OPERATOR_AGENT_ID = "local-llm-operator"
28
+ STOP_WORDS = {
29
+ "a",
30
+ "as",
31
+ "com",
32
+ "da",
33
+ "de",
34
+ "do",
35
+ "dos",
36
+ "e",
37
+ "em",
38
+ "essa",
39
+ "esse",
40
+ "no",
41
+ "na",
42
+ "o",
43
+ "os",
44
+ "para",
45
+ "por",
46
+ "uma",
47
+ "um",
48
+ }
49
+ TOKEN_ALIASES = {
50
+ "analise": {"analysis", "analyze", "analisar"},
51
+ "analisar": {"analysis", "analyze", "analise"},
52
+ "cloudwatch": {"aws", "logs", "log"},
53
+ "demanda": {"demand", "requirement", "requirements", "requisitos"},
54
+ "erro": {"error", "errors", "falha"},
55
+ "falha": {"error", "errors", "erro"},
56
+ "gere": {"generate", "create", "criar"},
57
+ "gerar": {"generate", "create", "criar"},
58
+ "logs": {"log", "cloudwatch", "elasticsearch"},
59
+ "servico": {"service", "serviço"},
60
+ "especificacao": {"specification", "spec", "requirements", "requisitos"},
61
+ "requisito": {"requirement", "requirements", "specification"},
62
+ "requisitos": {"requirement", "requirements", "specification"},
63
+ "tecnica": {"technical", "architecture", "component"},
64
+ "tecnico": {"technical", "architecture", "component"},
65
+ }
66
+ AGENT_ANCHORS = {
67
+ "aws-architecture-analyst": {"aws", "arquitetura", "architecture", "workload", "resiliencia", "observability", "vpc"},
68
+ "aws-cloudwatch-log-analyzer": {"aws", "cloudwatch", "log", "logs"},
69
+ "aws-operations-operator": {"aws", "ecs", "lambda", "sqs", "cloudfront", "eventbridge"},
70
+ "aws-security-governance-auditor": {"aws", "iam", "s3", "cloudtrail", "security", "seguranca"},
71
+ "azure-devops-orchestrator": {"azure", "devops", "card", "workitem", "work", "item", "board"},
72
+ "bpo-analyser": {"bpo", "cpf", "proposta", "proposal"},
73
+ "data-scientist-analyst": {"dataset", "dados", "data", "cohort", "modelo", "estatistica"},
74
+ "database-change-operator": {"postgres", "migration", "migracao", "database", "banco"},
75
+ "drawio-diagram-builder": {"drawio", "diagrama", "diagram"},
76
+ "elasticsearch-log-analyzer": {"elasticsearch", "elastic", "indice", "index", "logs", "log"},
77
+ "excel-workbook-builder": {"excel", "planilha", "spreadsheet", "workbook"},
78
+ "figma-ui-ux-product-designer": {"figma", "ui", "ux", "design", "tela", "wireframe"},
79
+ "github-pr-reviewer": {"github", "gh", "pr", "prs", "pull", "request"},
80
+ "knowledge-generator": {"knowledge", "conhecimento", "documentacao", "runbook"},
81
+ "postgres-data-analyzer": {"postgres", "postgresql", "sql", "database", "banco"},
82
+ "presentation-deck-builder": {"apresentacao", "presentation", "slides", "powerpoint", "deck"},
83
+ "software-specification-analyst": {"especificacao", "specification", "requisitos", "requirements", "historia", "stories", "demanda"},
84
+ "sqlserver-change-operator": {"sqlserver", "sql", "migration", "migracao", "banco"},
85
+ "sqlserver-data-analyzer": {"sqlserver", "sql", "database", "banco", "tabela"},
86
+ "technical-integration-analyst": {"integracao", "integration", "api", "rest", "soap", "sftp", "mcp"},
87
+ "topdesk-orchestrator": {"topdesk", "chamado"},
88
+ }
89
+
90
+
91
+ def build_execution_plan(root: Path, prompt: str, *, dry_run: bool = False) -> dict[str, Any]:
92
+ registry = load_agent_registry(root)
93
+ safe_prompt = redact_secrets(prompt)
94
+ route = route_prompt(prompt)
95
+ domain_agent = select_domain_agent(registry, prompt, route)
96
+ specialist_tasks = specialist_tasks_for_prompt(registry, prompt, route, domain_agent)
97
+ configuration_tasks = configuration_tasks_for(registry, root, prompt, route, specialist_tasks, dry_run=dry_run)
98
+ model_plan = build_model_plan(prompt, route=route)
99
+ model_plan = attach_local_llm_agent_contract(registry, model_plan)
100
+ review_gate = build_review_gate(prompt, route=route, model_plan=model_plan)
101
+ status = "needs-input" if configuration_tasks and not dry_run else "planned"
102
+ return {
103
+ "kind": "agentic-execution-plan",
104
+ "schema_version": "ai-devkit.agentic-plan/v1",
105
+ "status": status,
106
+ "dry_run": dry_run,
107
+ "prompt": safe_prompt,
108
+ "coordinator_agent": runtime_agent(registry, "task-orchestrator", fallback=RUNTIME_COORDINATOR),
109
+ "domain_agent": domain_agent,
110
+ "route": route,
111
+ "model_plan": model_plan,
112
+ "review_gate": review_gate,
113
+ "specialist_tasks": specialist_tasks,
114
+ "configuration_tasks": configuration_tasks,
115
+ "review_task": review_task(registry, review_gate),
116
+ "executed_tasks": [],
117
+ "blocked_tasks": [],
118
+ "trace": trace_for_plan(specialist_tasks, configuration_tasks, status=status),
119
+ }
120
+
121
+
122
+ def select_domain_agent(registry: dict[str, Any], prompt: str, route: dict[str, Any] | None) -> dict[str, Any]:
123
+ normalized = normalize(prompt)
124
+ agents = registry.get("agents") or {}
125
+ if re.search(r"\bn1\b|primeiro nivel|1o nivel|1º nivel", normalized) and "n1-support-agent" in agents:
126
+ return slim_agent(agents["n1-support-agent"])
127
+ if re.search(r"\bn2\b|segundo nivel|2o nivel|2º nivel", normalized) and "n2-support-agent" in agents:
128
+ return slim_agent(agents["n2-support-agent"])
129
+ if route and route.get("agent_id") in agents:
130
+ return slim_agent(agents[str(route["agent_id"])])
131
+ matched_agent = best_agent_match(registry, prompt)
132
+ if matched_agent:
133
+ return slim_agent(matched_agent)
134
+ if "task-orchestrator" in agents:
135
+ return slim_agent(agents["task-orchestrator"])
136
+ return dict(RUNTIME_COORDINATOR)
137
+
138
+
139
+ def specialist_tasks_for_prompt(
140
+ registry: dict[str, Any],
141
+ prompt: str,
142
+ route: dict[str, Any] | None,
143
+ domain_agent: dict[str, Any],
144
+ ) -> list[dict[str, Any]]:
145
+ if domain_agent.get("id") in {"n1-support-agent", "n2-support-agent"}:
146
+ return orchestrated_tasks(registry, str(domain_agent["id"]), prompt, route)
147
+ if route:
148
+ task = task_from_route(registry, route)
149
+ return [task] if task else []
150
+ if domain_agent.get("id") and domain_agent.get("id") != "task-orchestrator":
151
+ task = best_capability_task(registry, str(domain_agent["id"]), prompt)
152
+ return [task] if task else []
153
+ return []
154
+
155
+
156
+ def orchestrated_tasks(registry: dict[str, Any], agent_id: str, prompt: str, route: dict[str, Any] | None) -> list[dict[str, Any]]:
157
+ agent = (registry.get("agents") or {}).get(agent_id) or {}
158
+ tasks: list[dict[str, Any]] = []
159
+ for pair in agent.get("orchestrated_agents") or []:
160
+ if "/" not in str(pair):
161
+ continue
162
+ specialist_agent, capability_id = str(pair).split("/", 1)
163
+ capability = find_capability(registry, specialist_agent, capability_id)
164
+ if not capability:
165
+ continue
166
+ task = base_task(capability, prompt=prompt)
167
+ if route and specialist_agent == route.get("agent_id") and capability_id == route.get("capability_id"):
168
+ task["args"] = list(route.get("args") or [])
169
+ task["entities"] = dict(route.get("entities") or {})
170
+ task["provider"] = route.get("provider") or task.get("provider")
171
+ task["primary"] = True
172
+ tasks.append(task)
173
+ if route and not any(task.get("primary") for task in tasks):
174
+ routed = task_from_route(registry, route)
175
+ if routed:
176
+ routed["primary"] = True
177
+ tasks.insert(0, routed)
178
+ return tasks
179
+
180
+
181
+ def task_from_route(registry: dict[str, Any], route: dict[str, Any]) -> dict[str, Any] | None:
182
+ capability = find_capability(registry, str(route.get("agent_id") or ""), str(route.get("capability_id") or ""))
183
+ if not capability:
184
+ return None
185
+ task = base_task(capability, prompt="")
186
+ task["args"] = list(route.get("args") or [])
187
+ task["entities"] = dict(route.get("entities") or {})
188
+ task["provider"] = route.get("provider") or task.get("provider")
189
+ task["primary"] = True
190
+ return task
191
+
192
+
193
+ def best_agent_match(registry: dict[str, Any], prompt: str) -> dict[str, Any] | None:
194
+ prompt_tokens = expanded_tokens(prompt)
195
+ normalized_prompt = normalize(prompt)
196
+ best: tuple[int, dict[str, Any] | None] = (0, None)
197
+ for agent in (registry.get("agents") or {}).values():
198
+ if not has_agent_anchor(str(agent.get("id") or ""), prompt_tokens, normalized_prompt):
199
+ continue
200
+ agent_tokens = expanded_tokens(
201
+ " ".join(
202
+ [
203
+ str(agent.get("id") or ""),
204
+ str(agent.get("name") or ""),
205
+ str(agent.get("purpose") or ""),
206
+ " ".join(
207
+ " ".join(
208
+ [
209
+ str(capability.get("short_id") or ""),
210
+ str(capability.get("name") or ""),
211
+ str(capability.get("purpose") or ""),
212
+ ]
213
+ )
214
+ for capability in (agent.get("capabilities_index") or {}).values()
215
+ if isinstance(capability, dict)
216
+ ),
217
+ ]
218
+ )
219
+ )
220
+ score = weighted_overlap(prompt_tokens, agent_tokens)
221
+ if score > best[0]:
222
+ best = (score, agent)
223
+ return best[1] if best[0] >= 3 else None
224
+
225
+
226
+ def has_agent_anchor(agent_id: str, prompt_tokens: set[str], normalized_prompt: str) -> bool:
227
+ anchors = AGENT_ANCHORS.get(agent_id)
228
+ if not anchors:
229
+ return False
230
+ if anchors & prompt_tokens:
231
+ return True
232
+ return any(anchor in normalized_prompt for anchor in anchors if len(anchor) >= 5)
233
+
234
+
235
+ def best_capability_task(registry: dict[str, Any], agent_id: str, prompt: str) -> dict[str, Any] | None:
236
+ agent = (registry.get("agents") or {}).get(agent_id) or {}
237
+ capabilities = [item for item in (agent.get("capabilities_index") or {}).values() if isinstance(item, dict)]
238
+ if not capabilities:
239
+ return None
240
+ hinted = hinted_capability(capabilities, prompt)
241
+ if hinted:
242
+ task = base_task(hinted, prompt=prompt)
243
+ task["primary"] = True
244
+ task["selection"] = {"method": "registry-hint", "score": None}
245
+ return task
246
+ prompt_tokens = expanded_tokens(prompt)
247
+ best: tuple[int, dict[str, Any] | None] = (0, None)
248
+ for capability in capabilities:
249
+ capability_tokens = expanded_tokens(
250
+ " ".join(
251
+ [
252
+ str(capability.get("short_id") or ""),
253
+ str(capability.get("name") or ""),
254
+ str(capability.get("purpose") or ""),
255
+ ]
256
+ )
257
+ )
258
+ score = weighted_overlap(prompt_tokens, capability_tokens)
259
+ if score > best[0]:
260
+ best = (score, capability)
261
+ if not best[1] or best[0] < 2:
262
+ return None
263
+ task = base_task(best[1], prompt=prompt)
264
+ task["primary"] = True
265
+ task["selection"] = {"method": "registry-score", "score": best[0]}
266
+ return task
267
+
268
+
269
+ def hinted_capability(capabilities: list[dict[str, Any]], prompt: str) -> dict[str, Any] | None:
270
+ normalized = normalize(prompt)
271
+ hints = []
272
+ if "especificacao" in normalized and ("tecnica" in normalized or "tecnico" in normalized or "technical" in normalized):
273
+ hints.append("create-technical-spec")
274
+ if "especificacao" in normalized and ("funcional" in normalized or "functional" in normalized):
275
+ hints.append("create-functional-spec")
276
+ if "historia" in normalized or "user stor" in normalized:
277
+ hints.append("write-user-stories")
278
+ if "cloudwatch" in normalized and ("erro" in normalized or "error" in normalized):
279
+ hints.append("analyze-service-error")
280
+ if ("elasticsearch" in normalized or "elastic" in normalized) and ("erro" in normalized or "error" in normalized):
281
+ hints.append("analyze-service-errors")
282
+ for hint in hints:
283
+ for capability in capabilities:
284
+ if capability.get("short_id") == hint:
285
+ return capability
286
+ return None
287
+
288
+
289
+ def base_task(capability: dict[str, Any], *, prompt: str) -> dict[str, Any]:
290
+ return {
291
+ "id": f"{capability['agent_id']}.{capability['short_id']}",
292
+ "agent_id": capability["agent_id"],
293
+ "capability_id": capability["short_id"],
294
+ "capability": capability["id"],
295
+ "purpose": capability.get("purpose"),
296
+ "write_policy": capability.get("write_policy") or "read_only",
297
+ "provider": infer_provider(capability),
298
+ "args": [],
299
+ "entities": {},
300
+ "status": "planned",
301
+ "executable": bool(capability.get("has_runner")),
302
+ "prompt": redact_secrets(prompt),
303
+ "primary": False,
304
+ }
305
+
306
+
307
+ def configuration_tasks_for(
308
+ registry: dict[str, Any],
309
+ root: Path,
310
+ prompt: str,
311
+ route: dict[str, Any] | None,
312
+ specialist_tasks: list[dict[str, Any]],
313
+ *,
314
+ dry_run: bool,
315
+ ) -> list[dict[str, Any]]:
316
+ providers = []
317
+ provider_agents: dict[str, str] = {}
318
+ if route and route.get("provider"):
319
+ providers.append(str(route["provider"]))
320
+ if route.get("agent_id"):
321
+ provider_agents[str(route["provider"])] = str(route["agent_id"])
322
+ for task in specialist_tasks:
323
+ provider = task.get("provider")
324
+ if provider:
325
+ provider_id = str(provider)
326
+ providers.append(provider_id)
327
+ provider_agents.setdefault(provider_id, str(task.get("agent_id") or ""))
328
+ result: list[dict[str, Any]] = []
329
+ seen: set[str] = set()
330
+ for provider in providers:
331
+ if provider in seen:
332
+ continue
333
+ seen.add(provider)
334
+ source = None
335
+ agent_id = (route or {}).get("agent_id") or provider_agents.get(provider)
336
+ try:
337
+ source = resolve_source(provider=provider, intent=(route or {}).get("intent"), agent_id=agent_id)
338
+ except SourceRegistryError:
339
+ source = None
340
+ if source:
341
+ continue
342
+ capability = find_capability(registry, PROVIDER_CONFIGURATOR_AGENT_ID, "configure-provider-source") or {}
343
+ wizard = provider_setup_wizard(root, provider, prompt=prompt, route=route, reason="Provider/source required by multi-agent plan.")
344
+ result.append(
345
+ {
346
+ "id": f"provider-configurator.{provider}",
347
+ "agent_id": PROVIDER_CONFIGURATOR_AGENT_ID,
348
+ "capability_id": "configure-provider-source",
349
+ "capability": capability.get("id") or "provider-configurator.configure-provider-source",
350
+ "purpose": capability.get("purpose"),
351
+ "write_policy": capability.get("write_policy") or "local-config-write",
352
+ "path": capability.get("path"),
353
+ "provider": provider,
354
+ "status": "waiting-for-user",
355
+ "setup_wizard": wizard,
356
+ }
357
+ )
358
+ return result
359
+
360
+
361
+ def attach_source_to_primary_task(plan: dict[str, Any]) -> dict[str, Any]:
362
+ route = plan.get("route") if isinstance(plan.get("route"), dict) else None
363
+ if not route:
364
+ return plan
365
+ try:
366
+ source = resolve_source(provider=route.get("provider"), intent=route.get("intent"), agent_id=route.get("agent_id"))
367
+ except SourceRegistryError:
368
+ source = None
369
+ if not source:
370
+ return plan
371
+ for task in plan.get("specialist_tasks") or []:
372
+ if task.get("primary"):
373
+ task["source"] = public_source(source)
374
+ task["args"] = [*list(task.get("args") or []), "--source", str(source["id"])]
375
+ task["status"] = "ready"
376
+ plan["configuration_tasks"] = []
377
+ if plan.get("status") == "needs-input":
378
+ plan["status"] = "planned"
379
+ return plan
380
+
381
+
382
+ def mark_plan_after_execution(plan: dict[str, Any], executed: list[dict[str, Any]], blocked: list[dict[str, Any]]) -> dict[str, Any]:
383
+ plan = dict(plan)
384
+ plan["executed_tasks"] = executed
385
+ plan["blocked_tasks"] = blocked
386
+ if blocked and not executed:
387
+ plan["status"] = "blocked"
388
+ elif blocked and executed:
389
+ plan["status"] = "partial"
390
+ elif executed:
391
+ plan["status"] = "ok"
392
+ plan["trace"] = trace_for_plan(plan.get("specialist_tasks") or [], plan.get("configuration_tasks") or [], executed=executed, blocked=blocked, status=plan["status"])
393
+ return plan
394
+
395
+
396
+ def review_task(registry: dict[str, Any], review_gate: dict[str, Any]) -> dict[str, Any]:
397
+ capability = find_capability(registry, REVIEWER_AGENT_ID, "review-final-output") or {}
398
+ task = {
399
+ "id": "execution-reviewer.review-final-output",
400
+ "agent_id": REVIEWER_AGENT_ID,
401
+ "capability_id": "review-final-output",
402
+ "capability": capability.get("id") or "execution-reviewer.review-final-output",
403
+ "purpose": capability.get("purpose"),
404
+ "write_policy": capability.get("write_policy") or "read-only",
405
+ "path": capability.get("path"),
406
+ "status": "pending" if review_gate.get("required") else "not-required",
407
+ "required": bool(review_gate.get("required")),
408
+ "preferred_reviewers": list(review_gate.get("preferred_reviewers") or []),
409
+ }
410
+ return task
411
+
412
+
413
+ def mark_review_task(plan: dict[str, Any], *, reviewer: str) -> dict[str, Any]:
414
+ task = dict(plan.get("review_task") or {"agent_id": REVIEWER_AGENT_ID, "capability_id": "review-final-output"})
415
+ if task.get("required"):
416
+ task["status"] = "reviewed"
417
+ task["reviewer"] = reviewer
418
+ plan["review_task"] = task
419
+ return plan
420
+
421
+
422
+ def trace_for_plan(
423
+ specialist_tasks: list[dict[str, Any]],
424
+ configuration_tasks: list[dict[str, Any]],
425
+ *,
426
+ executed: list[dict[str, Any]] | None = None,
427
+ blocked: list[dict[str, Any]] | None = None,
428
+ status: str,
429
+ ) -> list[dict[str, Any]]:
430
+ trace = [{"agent_id": RUNTIME_COORDINATOR["id"], "action": "plan", "status": status}]
431
+ trace.extend({"agent_id": task["agent_id"], "action": "configure", "status": task.get("status")} for task in configuration_tasks)
432
+ trace.extend({"agent_id": task["agent_id"], "action": "execute", "status": task.get("status")} for task in specialist_tasks)
433
+ trace.extend({"agent_id": item["agent_id"], "action": "executed", "status": item.get("status")} for item in executed or [])
434
+ trace.extend({"agent_id": item["agent_id"], "action": "blocked", "status": item.get("status")} for item in blocked or [])
435
+ return trace
436
+
437
+
438
+ def infer_provider(capability: dict[str, Any]) -> str | None:
439
+ requires = capability.get("requires") if isinstance(capability.get("requires"), dict) else {}
440
+ providers = requires.get("providers") if isinstance(requires.get("providers"), list) else []
441
+ if providers and isinstance(providers[0], dict):
442
+ return str(providers[0].get("id") or "") or None
443
+ integration = capability.get("integration") if isinstance(capability.get("integration"), dict) else {}
444
+ repository = str(integration.get("repository") or "")
445
+ agent_id = str(capability.get("agent_id") or "")
446
+ if "azure" in repository or agent_id == "azure-devops-orchestrator":
447
+ return "azure-devops"
448
+ if "topdesk" in repository or agent_id == "topdesk-orchestrator":
449
+ return "topdesk"
450
+ if "cloudwatch" in repository:
451
+ return "aws-cloudwatch"
452
+ if "elasticsearch" in repository:
453
+ return "elasticsearch"
454
+ if "sqlserver" in repository:
455
+ return "sqlserver"
456
+ if "postgres" in repository:
457
+ return "postgres"
458
+ if "/aws/" in repository or "aws-" in repository:
459
+ return "aws"
460
+ return None
461
+
462
+
463
+ def attach_local_llm_agent_contract(registry: dict[str, Any], model_plan: dict[str, Any]) -> dict[str, Any]:
464
+ plan = dict(model_plan)
465
+ agent = (registry.get("agents") or {}).get(LOCAL_LLM_OPERATOR_AGENT_ID)
466
+ select_capability = find_capability(registry, LOCAL_LLM_OPERATOR_AGENT_ID, "select-local-worker") or {}
467
+ delegate_capability = find_capability(registry, LOCAL_LLM_OPERATOR_AGENT_ID, "delegate-operational-task") or {}
468
+ plan["operator_agent"] = slim_agent(agent) if isinstance(agent, dict) else {
469
+ "id": LOCAL_LLM_OPERATOR_AGENT_ID,
470
+ "name": "Local LLM Operator",
471
+ "kind": "runtime-agent",
472
+ }
473
+ plan["selection_capability"] = {
474
+ "agent_id": LOCAL_LLM_OPERATOR_AGENT_ID,
475
+ "capability_id": "select-local-worker",
476
+ "capability": select_capability.get("id") or "local-llm-operator.select-local-worker",
477
+ }
478
+ plan["delegation_capability"] = {
479
+ "agent_id": LOCAL_LLM_OPERATOR_AGENT_ID,
480
+ "capability_id": "delegate-operational-task",
481
+ "capability": delegate_capability.get("id") or "local-llm-operator.delegate-operational-task",
482
+ }
483
+ return plan
484
+
485
+
486
+ def runtime_agent(registry: dict[str, Any], agent_id: str, *, fallback: dict[str, Any]) -> dict[str, Any]:
487
+ agent = (registry.get("agents") or {}).get(agent_id)
488
+ if isinstance(agent, dict):
489
+ public = slim_agent(agent)
490
+ public["role"] = fallback.get("role")
491
+ return public
492
+ return dict(fallback)
493
+
494
+
495
+ def slim_agent(agent: dict[str, Any]) -> dict[str, Any]:
496
+ return {
497
+ "id": agent.get("id"),
498
+ "name": agent.get("name"),
499
+ "kind": agent.get("kind") or "specialist-agent",
500
+ "purpose": agent.get("purpose"),
501
+ "path": agent.get("path"),
502
+ }
503
+
504
+
505
+ def normalize(prompt: str) -> str:
506
+ return " ".join(strip_accents(prompt).lower().split())
507
+
508
+
509
+ def strip_accents(value: str) -> str:
510
+ return "".join(char for char in unicodedata.normalize("NFKD", value) if not unicodedata.combining(char))
511
+
512
+
513
+ def expanded_tokens(value: str) -> set[str]:
514
+ tokens = {
515
+ token
516
+ for token in re.findall(r"[a-z0-9][a-z0-9._-]*", normalize(value))
517
+ if len(token) >= 3 and token not in STOP_WORDS
518
+ }
519
+ expanded = set(tokens)
520
+ for token in tokens:
521
+ expanded.update(TOKEN_ALIASES.get(token, set()))
522
+ if token.endswith("s") and len(token) > 4:
523
+ expanded.add(token[:-1])
524
+ return expanded
525
+
526
+
527
+ def weighted_overlap(prompt_tokens: set[str], candidate_tokens: set[str]) -> int:
528
+ score = 0
529
+ for token in prompt_tokens:
530
+ if token in candidate_tokens:
531
+ score += 2
532
+ continue
533
+ if any(token in candidate or candidate in token for candidate in candidate_tokens if len(candidate) >= 5):
534
+ score += 1
535
+ return score
@@ -168,6 +168,8 @@ def record_exchange(
168
168
  "ok": result.get("ok"),
169
169
  "backend": result.get("llm_backend") or backend,
170
170
  "requires_llm": result.get("requires_llm"),
171
+ "execution_plan": summarize_execution_plan(result.get("execution_plan")),
172
+ "orchestration_trace": result.get("orchestration_trace") or [],
171
173
  "token_estimate": token_delta,
172
174
  }
173
175
  with (path / "messages.jsonl").open("a", encoding="utf-8") as file:
@@ -189,6 +191,41 @@ def record_exchange(
189
191
  return public_session(state, active=True)
190
192
 
191
193
 
194
+ def summarize_execution_plan(plan: Any) -> dict[str, Any] | None:
195
+ if not isinstance(plan, dict):
196
+ return None
197
+ return {
198
+ "kind": plan.get("kind"),
199
+ "status": plan.get("status"),
200
+ "coordinator_agent": (plan.get("coordinator_agent") or {}).get("id") if isinstance(plan.get("coordinator_agent"), dict) else None,
201
+ "domain_agent": (plan.get("domain_agent") or {}).get("id") if isinstance(plan.get("domain_agent"), dict) else None,
202
+ "specialist_tasks": [
203
+ {
204
+ "agent_id": task.get("agent_id"),
205
+ "capability_id": task.get("capability_id"),
206
+ "status": task.get("status"),
207
+ }
208
+ for task in plan.get("specialist_tasks") or []
209
+ if isinstance(task, dict)
210
+ ],
211
+ "configuration_tasks": [
212
+ {
213
+ "agent_id": task.get("agent_id"),
214
+ "provider": task.get("provider"),
215
+ "status": task.get("status"),
216
+ }
217
+ for task in plan.get("configuration_tasks") or []
218
+ if isinstance(task, dict)
219
+ ],
220
+ "review_task": {
221
+ "agent_id": (plan.get("review_task") or {}).get("agent_id"),
222
+ "status": (plan.get("review_task") or {}).get("status"),
223
+ }
224
+ if isinstance(plan.get("review_task"), dict)
225
+ else None,
226
+ }
227
+
228
+
192
229
  def build_contextual_prompt(session_id: str, prompt: str) -> str:
193
230
  exchanges = recent_exchanges(session_id, limit=RECENT_CONTEXT_EXCHANGES)
194
231
  if not exchanges:
@@ -15,6 +15,9 @@ ENV_VAR_NAME_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
15
15
 
16
16
  PROVIDER_CONFIG_ENV: dict[str, dict[str, str]] = {
17
17
  "azure-devops": {
18
+ "AZURE_DEVOPS_ORG": "AZURE_DEVOPS_ORG",
19
+ "AZURE_DEVOPS_PROJECT": "AZURE_DEVOPS_PROJECT",
20
+ "AZURE_DEVOPS_API_VERSION": "AZURE_DEVOPS_API_VERSION",
18
21
  "org": "AZURE_DEVOPS_ORG",
19
22
  "organization": "AZURE_DEVOPS_ORG",
20
23
  "project": "AZURE_DEVOPS_PROJECT",
@@ -223,8 +226,9 @@ def apply_source_to_args(source: dict[str, Any] | None, agent_id: str, capabilit
223
226
  config = source.get("config") or {}
224
227
  result = list(args)
225
228
  if agent_id == "azure-devops-orchestrator" and capability_id == "read-card":
226
- if config.get("project") and not has_arg(result, "--project"):
227
- result.extend(["--project", str(config["project"])])
229
+ project = config.get("project") or config.get("AZURE_DEVOPS_PROJECT")
230
+ if project and not has_arg(result, "--project"):
231
+ result.extend(["--project", str(project)])
228
232
  if config.get("fixture") and not has_arg(result, "--fixture"):
229
233
  result.extend(["--fixture", str(config["fixture"])])
230
234
  return result