devcouncil 0.2.0 → 0.2.1
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 +191 -253
- package/bin/devcouncil.js +130 -32
- package/package.json +6 -17
- package/pyproject.toml +0 -66
- package/src/devcouncil/__init__.py +0 -0
- package/src/devcouncil/__main__.py +0 -4
- package/src/devcouncil/app/__init__.py +0 -28
- package/src/devcouncil/app/config.py +0 -320
- package/src/devcouncil/app/errors.py +0 -23
- package/src/devcouncil/app/events.py +0 -44
- package/src/devcouncil/app/orchestrator.py +0 -92
- package/src/devcouncil/app/project_status.py +0 -29
- package/src/devcouncil/app/run_context.py +0 -39
- package/src/devcouncil/app/state_machine.py +0 -108
- package/src/devcouncil/artifacts/__init__.py +0 -1
- package/src/devcouncil/artifacts/coverage.py +0 -96
- package/src/devcouncil/artifacts/graph.py +0 -163
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/artifacts/validators.py +0 -27
- package/src/devcouncil/assets/__init__.py +0 -1
- package/src/devcouncil/assets/devcouncil-logo.svg +0 -60
- package/src/devcouncil/assets/devcouncil_logo_premium.png +0 -0
- package/src/devcouncil/cli/__init__.py +0 -0
- package/src/devcouncil/cli/commands/__init__.py +0 -0
- package/src/devcouncil/cli/commands/agents.py +0 -292
- package/src/devcouncil/cli/commands/artifacts.py +0 -54
- package/src/devcouncil/cli/commands/ast.py +0 -22
- package/src/devcouncil/cli/commands/baseline.py +0 -35
- package/src/devcouncil/cli/commands/check.py +0 -209
- package/src/devcouncil/cli/commands/config.py +0 -115
- package/src/devcouncil/cli/commands/cost.py +0 -57
- package/src/devcouncil/cli/commands/dashboard.py +0 -31
- package/src/devcouncil/cli/commands/doctor.py +0 -340
- package/src/devcouncil/cli/commands/evidence.py +0 -48
- package/src/devcouncil/cli/commands/go.py +0 -656
- package/src/devcouncil/cli/commands/handoff.py +0 -69
- package/src/devcouncil/cli/commands/hook.py +0 -237
- package/src/devcouncil/cli/commands/init.py +0 -289
- package/src/devcouncil/cli/commands/integrate.py +0 -1515
- package/src/devcouncil/cli/commands/lsp.py +0 -20
- package/src/devcouncil/cli/commands/map.py +0 -112
- package/src/devcouncil/cli/commands/mcp_server.py +0 -18
- package/src/devcouncil/cli/commands/plan.py +0 -488
- package/src/devcouncil/cli/commands/prompt.py +0 -61
- package/src/devcouncil/cli/commands/repair.py +0 -89
- package/src/devcouncil/cli/commands/report.py +0 -137
- package/src/devcouncil/cli/commands/reset_demo_state.py +0 -33
- package/src/devcouncil/cli/commands/rollback.py +0 -59
- package/src/devcouncil/cli/commands/run.py +0 -289
- package/src/devcouncil/cli/commands/runs.py +0 -223
- package/src/devcouncil/cli/commands/scaffold.py +0 -32
- package/src/devcouncil/cli/commands/semantic.py +0 -47
- package/src/devcouncil/cli/commands/setup.py +0 -362
- package/src/devcouncil/cli/commands/shell.py +0 -73
- package/src/devcouncil/cli/commands/show.py +0 -76
- package/src/devcouncil/cli/commands/skills.py +0 -88
- package/src/devcouncil/cli/commands/status.py +0 -141
- package/src/devcouncil/cli/commands/tasks.py +0 -55
- package/src/devcouncil/cli/commands/trace.py +0 -88
- package/src/devcouncil/cli/commands/verify.py +0 -328
- package/src/devcouncil/cli/commands/version.py +0 -20
- package/src/devcouncil/cli/commands/watch.py +0 -574
- package/src/devcouncil/cli/commands/watch_fs.py +0 -40
- package/src/devcouncil/cli/main.py +0 -137
- package/src/devcouncil/council/__init__.py +0 -0
- package/src/devcouncil/council/prompts/__init__.py +0 -0
- package/src/devcouncil/council/prompts/arbiter.md +0 -19
- package/src/devcouncil/council/prompts/critic_a.md +0 -10
- package/src/devcouncil/council/prompts/critic_b.md +0 -10
- package/src/devcouncil/council/prompts/implementation_reviewer.md +0 -16
- package/src/devcouncil/council/prompts/planner_a.md +0 -16
- package/src/devcouncil/council/prompts/planner_b.md +0 -16
- package/src/devcouncil/council/prompts/rebuttal.md +0 -10
- package/src/devcouncil/council/prompts/spec_writer.md +0 -12
- package/src/devcouncil/domain/__init__.py +0 -0
- package/src/devcouncil/domain/assumption.py +0 -17
- package/src/devcouncil/domain/critique.py +0 -32
- package/src/devcouncil/domain/evidence.py +0 -47
- package/src/devcouncil/domain/gap.py +0 -52
- package/src/devcouncil/domain/requirement.py +0 -22
- package/src/devcouncil/domain/task.py +0 -55
- package/src/devcouncil/execution/__init__.py +0 -1
- package/src/devcouncil/execution/checkpoints.py +0 -246
- package/src/devcouncil/execution/context_builder.py +0 -60
- package/src/devcouncil/execution/executor.py +0 -15
- package/src/devcouncil/execution/fs_watcher.py +0 -180
- package/src/devcouncil/execution/handoff.py +0 -102
- package/src/devcouncil/execution/hook_policy.py +0 -253
- package/src/devcouncil/execution/patch.py +0 -77
- package/src/devcouncil/execution/paths.py +0 -14
- package/src/devcouncil/execution/permissions.py +0 -85
- package/src/devcouncil/execution/policy_engine.py +0 -343
- package/src/devcouncil/execution/prompt_builder.py +0 -671
- package/src/devcouncil/execution/shell_session.py +0 -225
- package/src/devcouncil/execution/task_runner.py +0 -170
- package/src/devcouncil/executors/__init__.py +0 -1
- package/src/devcouncil/executors/agent_registry.py +0 -575
- package/src/devcouncil/executors/coding_cli.py +0 -736
- package/src/devcouncil/executors/mini_swe.py +0 -73
- package/src/devcouncil/executors/native/__init__.py +0 -0
- package/src/devcouncil/executors/native/agent.py +0 -208
- package/src/devcouncil/executors/openhands.py +0 -71
- package/src/devcouncil/gating/__init__.py +0 -1
- package/src/devcouncil/gating/checks/__init__.py +0 -0
- package/src/devcouncil/gating/checks/clean_git.py +0 -52
- package/src/devcouncil/gating/checks/planned_files_check.py +0 -32
- package/src/devcouncil/gating/checks/requirement_coverage.py +0 -26
- package/src/devcouncil/gating/checks/secret_scan_check.py +0 -53
- package/src/devcouncil/gating/policy.py +0 -338
- package/src/devcouncil/hardware.py +0 -184
- package/src/devcouncil/indexing/__init__.py +0 -1
- package/src/devcouncil/indexing/ast_matcher.py +0 -168
- package/src/devcouncil/indexing/graph_index.py +0 -48
- package/src/devcouncil/indexing/lsp.py +0 -161
- package/src/devcouncil/indexing/repo_mapper.py +0 -1455
- package/src/devcouncil/indexing/semantic_index.py +0 -205
- package/src/devcouncil/integrations/actions.py +0 -146
- package/src/devcouncil/integrations/check.py +0 -423
- package/src/devcouncil/integrations/code_review_graph.py +0 -163
- package/src/devcouncil/integrations/github.py +0 -39
- package/src/devcouncil/integrations/github_intent.py +0 -142
- package/src/devcouncil/integrations/gitnexus.py +0 -62
- package/src/devcouncil/integrations/graphify.py +0 -34
- package/src/devcouncil/integrations/mcp/__init__.py +0 -0
- package/src/devcouncil/integrations/mcp/server.py +0 -2122
- package/src/devcouncil/integrations/opencode_devcouncil_plugin.mjs +0 -24
- package/src/devcouncil/integrations/pr_comments.py +0 -62
- package/src/devcouncil/live/__init__.py +0 -2
- package/src/devcouncil/live/cards.py +0 -349
- package/src/devcouncil/live/models.py +0 -63
- package/src/devcouncil/live/repair_prompt.py +0 -83
- package/src/devcouncil/live/reviewer.py +0 -70
- package/src/devcouncil/live/signals.py +0 -135
- package/src/devcouncil/live/summary.py +0 -34
- package/src/devcouncil/live/tasks.py +0 -18
- package/src/devcouncil/live/transcripts.py +0 -141
- package/src/devcouncil/llm/__init__.py +0 -1
- package/src/devcouncil/llm/cache.py +0 -42
- package/src/devcouncil/llm/model_defaults.yaml +0 -44
- package/src/devcouncil/llm/provider.py +0 -627
- package/src/devcouncil/llm/router.py +0 -310
- package/src/devcouncil/optimization/__init__.py +0 -1
- package/src/devcouncil/optimization/gepa_agent.py +0 -318
- package/src/devcouncil/planning/__init__.py +0 -1
- package/src/devcouncil/planning/arbiter_service.py +0 -57
- package/src/devcouncil/planning/correction_manifest.py +0 -303
- package/src/devcouncil/planning/critique_service.py +0 -71
- package/src/devcouncil/planning/plan_service.py +0 -60
- package/src/devcouncil/planning/prompt_enhancer_service.py +0 -167
- package/src/devcouncil/planning/repair_service.py +0 -39
- package/src/devcouncil/planning/spec_service.py +0 -70
- package/src/devcouncil/repo/__init__.py +0 -0
- package/src/devcouncil/repo/ci_scaffold.py +0 -157
- package/src/devcouncil/repo/gitignore.py +0 -123
- package/src/devcouncil/repo/sca.py +0 -374
- package/src/devcouncil/reporting/__init__.py +0 -0
- package/src/devcouncil/reporting/github_check.py +0 -32
- package/src/devcouncil/reporting/json_report.py +0 -30
- package/src/devcouncil/reporting/markdown_report.py +0 -83
- package/src/devcouncil/reporting/report_builder.py +0 -14
- package/src/devcouncil/skills/__init__.py +0 -19
- package/src/devcouncil/skills/library/README.md +0 -46
- package/src/devcouncil/skills/library/ai-training.md +0 -50
- package/src/devcouncil/skills/library/android.md +0 -50
- package/src/devcouncil/skills/library/backend.md +0 -52
- package/src/devcouncil/skills/library/core-engineering.md +0 -95
- package/src/devcouncil/skills/library/data-engineering.md +0 -47
- package/src/devcouncil/skills/library/desktop.md +0 -46
- package/src/devcouncil/skills/library/devops.md +0 -48
- package/src/devcouncil/skills/library/game-dev.md +0 -46
- package/src/devcouncil/skills/library/ios.md +0 -48
- package/src/devcouncil/skills/library/mobile-cross-platform.md +0 -46
- package/src/devcouncil/skills/library/security.md +0 -48
- package/src/devcouncil/skills/library/systems.md +0 -48
- package/src/devcouncil/skills/library/web.md +0 -47
- package/src/devcouncil/skills/library/windows.md +0 -47
- package/src/devcouncil/skills/registry.py +0 -330
- package/src/devcouncil/storage/__init__.py +0 -0
- package/src/devcouncil/storage/db.py +0 -147
- package/src/devcouncil/storage/models.py +0 -204
- package/src/devcouncil/storage/native.py +0 -557
- package/src/devcouncil/storage/repositories.py +0 -485
- package/src/devcouncil/telemetry/__init__.py +0 -0
- package/src/devcouncil/telemetry/cost.py +0 -140
- package/src/devcouncil/telemetry/model_pricing.yaml +0 -48
- package/src/devcouncil/telemetry/pricing.py +0 -28
- package/src/devcouncil/telemetry/traces.py +0 -146
- package/src/devcouncil/telemetry/tracker.py +0 -52
- package/src/devcouncil/ui/__init__.py +0 -1
- package/src/devcouncil/ui/dashboard.py +0 -423
- package/src/devcouncil/utils/__init__.py +0 -1
- package/src/devcouncil/utils/redaction.py +0 -147
- package/src/devcouncil/utils/subprocess_env.py +0 -69
- package/src/devcouncil/verification/__init__.py +0 -1
- package/src/devcouncil/verification/acceptance_compiler.py +0 -125
- package/src/devcouncil/verification/ad_hoc_check.py +0 -129
- package/src/devcouncil/verification/diff_coverage.py +0 -353
- package/src/devcouncil/verification/implementation_reviewer.py +0 -55
- package/src/devcouncil/verification/next_actions.py +0 -189
- package/src/devcouncil/verification/sandbox.py +0 -178
- package/src/devcouncil/verification/test_resolver.py +0 -91
- package/src/devcouncil/verification/verifier.py +0 -1548
- package/uv.lock +0 -1226
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
from typing import List, Dict, Any, Type, Optional, TypeVar
|
|
2
|
-
import copy
|
|
3
|
-
import json
|
|
4
|
-
import logging
|
|
5
|
-
import asyncio
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
from pydantic import BaseModel
|
|
9
|
-
from devcouncil.llm.provider import Provider, LLMResponse
|
|
10
|
-
from devcouncil.llm.cache import LLMCache
|
|
11
|
-
from devcouncil.telemetry.tracker import TelemetryTracker
|
|
12
|
-
from devcouncil.telemetry.traces import TraceLogger
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
StructuredModel = TypeVar("StructuredModel", bound=BaseModel)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class StructuredOutputError(RuntimeError):
|
|
19
|
-
"""A model could not produce valid structured output for a role, even after
|
|
20
|
-
a healing retry. Carries the role/model so the CLI can give actionable advice
|
|
21
|
-
(usually: switch that role to a more capable model)."""
|
|
22
|
-
|
|
23
|
-
def __init__(self, message: str, *, role: str, model: str):
|
|
24
|
-
super().__init__(message)
|
|
25
|
-
self.role = role
|
|
26
|
-
self.model = model
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class ModelRouter:
|
|
30
|
-
# Independent fresh attempts at producing valid structured output before
|
|
31
|
-
# giving up. Even capable models occasionally emit malformed JSON; a second
|
|
32
|
-
# clean attempt usually succeeds. Malformed responses are never cached, so a
|
|
33
|
-
# retry is genuinely fresh rather than re-serving the same bad JSON.
|
|
34
|
-
STRUCTURED_ATTEMPTS = 2
|
|
35
|
-
|
|
36
|
-
def __init__(
|
|
37
|
-
self,
|
|
38
|
-
provider: Provider,
|
|
39
|
-
role_config: Dict[str, Dict[str, Any]],
|
|
40
|
-
project_root: Path = Path("."),
|
|
41
|
-
):
|
|
42
|
-
self.provider = provider
|
|
43
|
-
self.role_config = role_config
|
|
44
|
-
self.project_root = project_root
|
|
45
|
-
|
|
46
|
-
@staticmethod
|
|
47
|
-
def _extract_json(content: str) -> str:
|
|
48
|
-
"""Best-effort extraction of a JSON document from a model response.
|
|
49
|
-
|
|
50
|
-
Handles the common ways a model wraps valid JSON: triple-backtick fences and
|
|
51
|
-
surrounding prose ("Here you go: {...} thanks"). Strips fences, returns the
|
|
52
|
-
whole thing if it already parses, otherwise scans for the first balanced
|
|
53
|
-
object/array (string- and escape-aware so braces inside string values don't
|
|
54
|
-
confuse it). Falls back to the de-fenced text so the existing healing path
|
|
55
|
-
still produces a meaningful error. A strict superset of plain fence-stripping
|
|
56
|
-
— clean/fenced JSON is returned unchanged."""
|
|
57
|
-
text = content.strip()
|
|
58
|
-
if "```json" in text:
|
|
59
|
-
text = text.split("```json", 1)[1].split("```", 1)[0].strip()
|
|
60
|
-
elif "```" in text:
|
|
61
|
-
text = text.split("```", 1)[1].split("```", 1)[0].strip()
|
|
62
|
-
try:
|
|
63
|
-
json.loads(text)
|
|
64
|
-
return text
|
|
65
|
-
except Exception:
|
|
66
|
-
pass
|
|
67
|
-
for opener, closer in (("{", "}"), ("[", "]")):
|
|
68
|
-
start = text.find(opener)
|
|
69
|
-
if start == -1:
|
|
70
|
-
continue
|
|
71
|
-
depth = 0
|
|
72
|
-
in_str = False
|
|
73
|
-
escaped = False
|
|
74
|
-
for i in range(start, len(text)):
|
|
75
|
-
ch = text[i]
|
|
76
|
-
if in_str:
|
|
77
|
-
if escaped:
|
|
78
|
-
escaped = False
|
|
79
|
-
elif ch == "\\":
|
|
80
|
-
escaped = True
|
|
81
|
-
elif ch == '"':
|
|
82
|
-
in_str = False
|
|
83
|
-
continue
|
|
84
|
-
if ch == '"':
|
|
85
|
-
in_str = True
|
|
86
|
-
elif ch == opener:
|
|
87
|
-
depth += 1
|
|
88
|
-
elif ch == closer:
|
|
89
|
-
depth -= 1
|
|
90
|
-
if depth == 0:
|
|
91
|
-
candidate = text[start:i + 1]
|
|
92
|
-
try:
|
|
93
|
-
json.loads(candidate)
|
|
94
|
-
return candidate
|
|
95
|
-
except Exception:
|
|
96
|
-
break
|
|
97
|
-
return text
|
|
98
|
-
|
|
99
|
-
async def _complete_with_retry(
|
|
100
|
-
self,
|
|
101
|
-
*,
|
|
102
|
-
model: str,
|
|
103
|
-
messages: List[Dict[str, str]],
|
|
104
|
-
temperature: float,
|
|
105
|
-
run_id: Optional[str],
|
|
106
|
-
attempts: int = 3,
|
|
107
|
-
) -> "LLMResponse":
|
|
108
|
-
"""Provider completion with bounded exponential-backoff retry. Used for BOTH the
|
|
109
|
-
initial call and the healing call so a transient fault in either is retried (and,
|
|
110
|
-
if still failing, surfaced to the caller's fallback logic) rather than aborting
|
|
111
|
-
the run."""
|
|
112
|
-
for attempt in range(attempts):
|
|
113
|
-
try:
|
|
114
|
-
return await self.provider.complete(
|
|
115
|
-
model=model,
|
|
116
|
-
messages=messages,
|
|
117
|
-
temperature=temperature,
|
|
118
|
-
json_mode=True,
|
|
119
|
-
run_id=run_id,
|
|
120
|
-
)
|
|
121
|
-
except Exception as exc:
|
|
122
|
-
if attempt == attempts - 1:
|
|
123
|
-
raise
|
|
124
|
-
logger.warning(
|
|
125
|
-
"LLM request failed (attempt %d/%d): %s. Retrying...",
|
|
126
|
-
attempt + 1, attempts, exc,
|
|
127
|
-
)
|
|
128
|
-
await asyncio.sleep(2 ** attempt)
|
|
129
|
-
raise RuntimeError("unreachable") # loop either returns or raises
|
|
130
|
-
|
|
131
|
-
async def complete_structured(
|
|
132
|
-
self,
|
|
133
|
-
role: str,
|
|
134
|
-
messages: List[Dict[str, str]],
|
|
135
|
-
schema: Type[StructuredModel],
|
|
136
|
-
temperature: Optional[float] = None,
|
|
137
|
-
run_id: Optional[str] = None,
|
|
138
|
-
fallback: Optional[StructuredModel] = None,
|
|
139
|
-
_attempt: int = 0,
|
|
140
|
-
) -> StructuredModel:
|
|
141
|
-
config = self.role_config.get(role)
|
|
142
|
-
if not config:
|
|
143
|
-
raise ValueError(f"No config found for role: {role}")
|
|
144
|
-
|
|
145
|
-
model = config["model"]
|
|
146
|
-
temp = temperature if temperature is not None else config.get("temperature", 0.0)
|
|
147
|
-
|
|
148
|
-
# Deep-copy to avoid mutating the caller's messages list
|
|
149
|
-
msgs = copy.deepcopy(messages)
|
|
150
|
-
|
|
151
|
-
# Add schema instructions to system or user message
|
|
152
|
-
schema_json = json.dumps(schema.model_json_schema(), indent=2)
|
|
153
|
-
instruction = f"\n\nYou MUST output a JSON object matching this schema:\n{schema_json}"
|
|
154
|
-
|
|
155
|
-
found_system = False
|
|
156
|
-
for msg in msgs:
|
|
157
|
-
if msg["role"] == "system":
|
|
158
|
-
msg["content"] += instruction
|
|
159
|
-
found_system = True
|
|
160
|
-
break
|
|
161
|
-
|
|
162
|
-
if not found_system:
|
|
163
|
-
msgs.insert(0, {"role": "system", "content": f"You are a helpful assistant.{instruction}"})
|
|
164
|
-
|
|
165
|
-
logger.info("LLM call: role=%s model=%s run_id=%s", role, model, run_id)
|
|
166
|
-
|
|
167
|
-
cache = LLMCache(self.project_root)
|
|
168
|
-
tracker = TelemetryTracker(self.project_root)
|
|
169
|
-
traces = TraceLogger(self.project_root)
|
|
170
|
-
|
|
171
|
-
# Provider knobs (e.g. Ollama num_ctx / base_url) that change the output for an
|
|
172
|
-
# identical prompt must be part of the cache key, else raising OLLAMA_NUM_CTX
|
|
173
|
-
# after a truncated answer would keep serving the stale response.
|
|
174
|
-
provider_fp = self.provider.cache_fingerprint()
|
|
175
|
-
# Zero local (Ollama) usage by provider so telemetry matches the cost ledger.
|
|
176
|
-
provider_local = self.provider.is_local_cost_free()
|
|
177
|
-
|
|
178
|
-
# Check cache first
|
|
179
|
-
response = cache.get(model, msgs, temp, True, provider_fp)
|
|
180
|
-
cache_hit = response is not None
|
|
181
|
-
|
|
182
|
-
if not response:
|
|
183
|
-
response = await self._complete_with_retry(
|
|
184
|
-
model=model, messages=msgs, temperature=temp, run_id=run_id
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
if response is None:
|
|
188
|
-
raise RuntimeError(f"LLM request for role {role} did not return a response.")
|
|
189
|
-
|
|
190
|
-
if not cache_hit:
|
|
191
|
-
tracker.log_usage(model, response.usage, local=provider_local)
|
|
192
|
-
|
|
193
|
-
logger.info(
|
|
194
|
-
"LLM response: role=%s model=%s tokens=%s",
|
|
195
|
-
role, response.model, response.usage,
|
|
196
|
-
)
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
# Extract JSON from fences/surrounding prose (balanced-aware).
|
|
200
|
-
content = self._extract_json(response.content)
|
|
201
|
-
data = json.loads(content)
|
|
202
|
-
result = schema.model_validate(data)
|
|
203
|
-
if not cache_hit:
|
|
204
|
-
cache.set(model, msgs, temp, True, response, provider_fp) # cache only validated output
|
|
205
|
-
return result
|
|
206
|
-
except Exception as e:
|
|
207
|
-
logger.warning(f"Initial parse failed for {role}, attempting healing: {e}")
|
|
208
|
-
traces.log_event(
|
|
209
|
-
"llm_structured_parse_failed",
|
|
210
|
-
{
|
|
211
|
-
"role": role,
|
|
212
|
-
"model": response.model,
|
|
213
|
-
"schema": schema.__name__,
|
|
214
|
-
"error": str(e),
|
|
215
|
-
"content_preview": response.content[:500],
|
|
216
|
-
},
|
|
217
|
-
run_id=run_id,
|
|
218
|
-
summary=f"Structured response parse failed for {role}; attempting repair.",
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
# Healing attempt: Ask the model to fix its own JSON
|
|
222
|
-
healing_prompt = f"""
|
|
223
|
-
The following JSON was returned but failed to parse or validate against the schema.
|
|
224
|
-
Error: {str(e)}
|
|
225
|
-
Content:
|
|
226
|
-
{response.content}
|
|
227
|
-
|
|
228
|
-
Please return the corrected JSON object only. No prose.
|
|
229
|
-
"""
|
|
230
|
-
# The healing completion runs INSIDE this try (with the same retry/backoff as
|
|
231
|
-
# the initial call). A transient failure here (429/timeout) must be treated as
|
|
232
|
-
# "healing failed" so it routes into the fresh-attempt/fallback logic below,
|
|
233
|
-
# not propagate as a raw provider error that defeats the supplied fallback.
|
|
234
|
-
healed_response = None
|
|
235
|
-
try:
|
|
236
|
-
# We use a lower temperature for healing
|
|
237
|
-
healed_response = await self._complete_with_retry(
|
|
238
|
-
model=model,
|
|
239
|
-
messages=[{"role": "user", "content": healing_prompt}],
|
|
240
|
-
temperature=0.0,
|
|
241
|
-
run_id=run_id,
|
|
242
|
-
)
|
|
243
|
-
tracker.log_usage(healed_response.model, healed_response.usage, local=provider_local)
|
|
244
|
-
healed_content = self._extract_json(healed_response.content)
|
|
245
|
-
data = json.loads(healed_content)
|
|
246
|
-
result = schema.model_validate(data)
|
|
247
|
-
cache.set(model, msgs, temp, True, healed_response, provider_fp)
|
|
248
|
-
return result
|
|
249
|
-
except Exception as final_e:
|
|
250
|
-
logger.error(f"Healing failed for {role}: {final_e}")
|
|
251
|
-
traces.log_event(
|
|
252
|
-
"llm_structured_parse_repair_failed",
|
|
253
|
-
{
|
|
254
|
-
"role": role,
|
|
255
|
-
"model": healed_response.model if healed_response else model,
|
|
256
|
-
"schema": schema.__name__,
|
|
257
|
-
"error": str(final_e),
|
|
258
|
-
"original_content_preview": response.content[:500],
|
|
259
|
-
"healed_content_preview": healed_response.content[:500] if healed_response else "(healing request failed)",
|
|
260
|
-
},
|
|
261
|
-
run_id=run_id,
|
|
262
|
-
summary=f"Structured response repair failed for {role}.",
|
|
263
|
-
)
|
|
264
|
-
if _attempt + 1 < self.STRUCTURED_ATTEMPTS:
|
|
265
|
-
# A fresh, independent attempt often succeeds where one bad draft
|
|
266
|
-
# (plus its repair) failed. The malformed response was never
|
|
267
|
-
# cached, so this re-runs the completion rather than re-reading it.
|
|
268
|
-
# Prepend a strict JSON-only instruction (on a copy, so the caller's
|
|
269
|
-
# messages are untouched) to nudge the retry toward parseable output.
|
|
270
|
-
logger.warning(
|
|
271
|
-
"Structured output failed for role '%s'; retrying fresh "
|
|
272
|
-
"(attempt %d/%d).",
|
|
273
|
-
role, _attempt + 2, self.STRUCTURED_ATTEMPTS,
|
|
274
|
-
)
|
|
275
|
-
strict_messages = copy.deepcopy(messages)
|
|
276
|
-
strict_messages.insert(0, {
|
|
277
|
-
"role": "system",
|
|
278
|
-
"content": (
|
|
279
|
-
"Respond with a single valid JSON object only — no prose, no "
|
|
280
|
-
"markdown fences, no trailing text. It must parse with a strict "
|
|
281
|
-
"JSON parser and match the requested schema."
|
|
282
|
-
),
|
|
283
|
-
})
|
|
284
|
-
return await self.complete_structured(
|
|
285
|
-
role,
|
|
286
|
-
strict_messages,
|
|
287
|
-
schema,
|
|
288
|
-
temperature=temperature,
|
|
289
|
-
run_id=run_id,
|
|
290
|
-
fallback=fallback,
|
|
291
|
-
_attempt=_attempt + 1,
|
|
292
|
-
)
|
|
293
|
-
if fallback is not None:
|
|
294
|
-
# Degradable role (e.g. critique/rebuttal/enhancement): keep
|
|
295
|
-
# planning alive on weaker models instead of crashing the run.
|
|
296
|
-
logger.warning(
|
|
297
|
-
"Role '%s' (model '%s') could not produce valid %s; "
|
|
298
|
-
"using a safe fallback so planning can continue.",
|
|
299
|
-
role, model, schema.__name__,
|
|
300
|
-
)
|
|
301
|
-
return fallback
|
|
302
|
-
raise StructuredOutputError(
|
|
303
|
-
f"Model '{model}' for role '{role}' could not produce valid "
|
|
304
|
-
f"{schema.__name__} JSON, even after a repair attempt. "
|
|
305
|
-
f"Use a more capable model for this role "
|
|
306
|
-
f"(e.g. 'dev config models --role {role} --model <model>'). "
|
|
307
|
-
f"Parser error: {final_e}",
|
|
308
|
-
role=role,
|
|
309
|
-
model=model,
|
|
310
|
-
)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Optimization integrations for DevCouncil prompt and workflow assets."""
|
|
@@ -1,318 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import importlib
|
|
4
|
-
import json
|
|
5
|
-
import sys
|
|
6
|
-
from collections.abc import Iterable
|
|
7
|
-
from dataclasses import dataclass
|
|
8
|
-
from datetime import datetime, timezone
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from typing import Any, Callable
|
|
11
|
-
|
|
12
|
-
import yaml # type: ignore[import-untyped]
|
|
13
|
-
|
|
14
|
-
from devcouncil.executors.agent_registry import (
|
|
15
|
-
load_agent_profiles,
|
|
16
|
-
load_cli_agent_specs,
|
|
17
|
-
normalize_agent_name,
|
|
18
|
-
)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
DEFAULT_OBJECTIVE = (
|
|
22
|
-
"Optimize the DevCouncil CLI-agent profile prompt preamble so coding agents stay inside "
|
|
23
|
-
"planned scope, run the expected verification commands, preserve evidence, avoid destructive "
|
|
24
|
-
"or out-of-policy changes, and address the observed failures in the evaluation examples."
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
class GepaUnavailableError(RuntimeError):
|
|
29
|
-
"""Raised when the optional GEPA package is not installed."""
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
@dataclass(frozen=True)
|
|
33
|
-
class AgentPromptOptimizationResult:
|
|
34
|
-
agent: str
|
|
35
|
-
profile_name: str
|
|
36
|
-
seed_preamble: str
|
|
37
|
-
best_preamble: str
|
|
38
|
-
best_score: float | None
|
|
39
|
-
artifact_path: Path
|
|
40
|
-
applied: bool
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def optimize_agent_profile(
|
|
44
|
-
*,
|
|
45
|
-
project_root: Path,
|
|
46
|
-
agent: str,
|
|
47
|
-
profile_name: str,
|
|
48
|
-
evals_path: Path,
|
|
49
|
-
max_metric_calls: int = 40,
|
|
50
|
-
objective: str | None = None,
|
|
51
|
-
apply: bool = False,
|
|
52
|
-
output_path: Path | None = None,
|
|
53
|
-
) -> AgentPromptOptimizationResult:
|
|
54
|
-
"""Run GEPA over a CLI-agent profile prompt preamble.
|
|
55
|
-
|
|
56
|
-
GEPA needs an evaluator. DevCouncil's evaluator consumes an offline JSON/JSONL
|
|
57
|
-
dataset of observed agent failures and desired prompt behavior, then feeds that
|
|
58
|
-
context back to GEPA as actionable side information.
|
|
59
|
-
"""
|
|
60
|
-
|
|
61
|
-
root = project_root.expanduser().resolve()
|
|
62
|
-
normalized_agent = normalize_agent_name(agent)
|
|
63
|
-
if normalized_agent not in load_cli_agent_specs(root):
|
|
64
|
-
raise ValueError(f"Agent '{agent}' is not registered or supported.")
|
|
65
|
-
|
|
66
|
-
profiles = load_agent_profiles(root)
|
|
67
|
-
profile = profiles.get(profile_name)
|
|
68
|
-
if profile is None:
|
|
69
|
-
raise ValueError(f"Profile '{profile_name}' is not configured.")
|
|
70
|
-
|
|
71
|
-
resolved_evals_path = evals_path.expanduser()
|
|
72
|
-
if not resolved_evals_path.is_absolute():
|
|
73
|
-
resolved_evals_path = root / resolved_evals_path
|
|
74
|
-
dataset = load_agent_eval_dataset(resolved_evals_path)
|
|
75
|
-
gepa_module = _load_gepa_optimize_anything()
|
|
76
|
-
seed_preamble = profile.prompt_preamble or ""
|
|
77
|
-
effective_objective = objective or DEFAULT_OBJECTIVE
|
|
78
|
-
|
|
79
|
-
def evaluator(candidate: Any, example: dict[str, Any] | None = None) -> float:
|
|
80
|
-
if example is None:
|
|
81
|
-
scores = [
|
|
82
|
-
_score_candidate(
|
|
83
|
-
candidate,
|
|
84
|
-
item,
|
|
85
|
-
log=getattr(gepa_module, "log", lambda message: None),
|
|
86
|
-
)
|
|
87
|
-
for item in dataset
|
|
88
|
-
]
|
|
89
|
-
return sum(scores) / len(scores)
|
|
90
|
-
return _score_candidate(
|
|
91
|
-
candidate,
|
|
92
|
-
example,
|
|
93
|
-
log=getattr(gepa_module, "log", lambda message: None),
|
|
94
|
-
)
|
|
95
|
-
|
|
96
|
-
engine = gepa_module.EngineConfig(max_metric_calls=max_metric_calls)
|
|
97
|
-
config = gepa_module.GEPAConfig(engine=engine)
|
|
98
|
-
gepa_result = gepa_module.optimize_anything(
|
|
99
|
-
seed_candidate=seed_preamble,
|
|
100
|
-
evaluator=evaluator,
|
|
101
|
-
dataset=dataset,
|
|
102
|
-
objective=effective_objective,
|
|
103
|
-
config=config,
|
|
104
|
-
)
|
|
105
|
-
|
|
106
|
-
best_preamble = _candidate_to_text(_best_candidate(gepa_result)).strip()
|
|
107
|
-
if not best_preamble:
|
|
108
|
-
best_preamble = seed_preamble
|
|
109
|
-
best_score = _best_score(gepa_result)
|
|
110
|
-
|
|
111
|
-
artifact_path = output_path or _default_artifact_path(root, normalized_agent, profile_name)
|
|
112
|
-
artifact_path = artifact_path.expanduser()
|
|
113
|
-
if not artifact_path.is_absolute():
|
|
114
|
-
artifact_path = root / artifact_path
|
|
115
|
-
_write_result_artifact(
|
|
116
|
-
artifact_path,
|
|
117
|
-
{
|
|
118
|
-
"optimizer": "gepa.optimize_anything",
|
|
119
|
-
"agent": normalized_agent,
|
|
120
|
-
"profile": profile_name,
|
|
121
|
-
"objective": effective_objective,
|
|
122
|
-
"evals_path": str(resolved_evals_path),
|
|
123
|
-
"max_metric_calls": max_metric_calls,
|
|
124
|
-
"example_count": len(dataset),
|
|
125
|
-
"seed_preamble": seed_preamble,
|
|
126
|
-
"best_preamble": best_preamble,
|
|
127
|
-
"best_score": best_score,
|
|
128
|
-
"applied": apply,
|
|
129
|
-
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
130
|
-
},
|
|
131
|
-
)
|
|
132
|
-
|
|
133
|
-
if apply:
|
|
134
|
-
_apply_profile_preamble(root, profile_name, best_preamble)
|
|
135
|
-
|
|
136
|
-
return AgentPromptOptimizationResult(
|
|
137
|
-
agent=normalized_agent,
|
|
138
|
-
profile_name=profile_name,
|
|
139
|
-
seed_preamble=seed_preamble,
|
|
140
|
-
best_preamble=best_preamble,
|
|
141
|
-
best_score=best_score,
|
|
142
|
-
artifact_path=artifact_path,
|
|
143
|
-
applied=apply,
|
|
144
|
-
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def load_agent_eval_dataset(path: Path) -> list[dict[str, Any]]:
|
|
148
|
-
evals_path = path.expanduser()
|
|
149
|
-
if not evals_path.exists():
|
|
150
|
-
raise ValueError(f"GEPA eval dataset not found: {evals_path}")
|
|
151
|
-
|
|
152
|
-
if evals_path.suffix.lower() == ".jsonl":
|
|
153
|
-
examples = [
|
|
154
|
-
json.loads(line)
|
|
155
|
-
for line in evals_path.read_text(encoding="utf-8").splitlines()
|
|
156
|
-
if line.strip()
|
|
157
|
-
]
|
|
158
|
-
else:
|
|
159
|
-
raw = json.loads(evals_path.read_text(encoding="utf-8"))
|
|
160
|
-
if isinstance(raw, dict):
|
|
161
|
-
if isinstance(raw.get("examples"), list):
|
|
162
|
-
examples = raw["examples"]
|
|
163
|
-
elif isinstance(raw.get("dataset"), list):
|
|
164
|
-
examples = raw["dataset"]
|
|
165
|
-
else:
|
|
166
|
-
examples = [raw]
|
|
167
|
-
elif isinstance(raw, list):
|
|
168
|
-
examples = raw
|
|
169
|
-
else:
|
|
170
|
-
raise ValueError("GEPA eval dataset must be a JSON object, JSON array, or JSONL file.")
|
|
171
|
-
|
|
172
|
-
normalized: list[dict[str, Any]] = []
|
|
173
|
-
for index, item in enumerate(examples, start=1):
|
|
174
|
-
if not isinstance(item, dict):
|
|
175
|
-
raise ValueError("Every GEPA eval example must be a JSON object.")
|
|
176
|
-
example = dict(item)
|
|
177
|
-
example.setdefault("id", f"example-{index}")
|
|
178
|
-
normalized.append(example)
|
|
179
|
-
|
|
180
|
-
if not normalized:
|
|
181
|
-
raise ValueError("GEPA eval dataset must contain at least one example.")
|
|
182
|
-
return normalized
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _load_gepa_optimize_anything() -> Any:
|
|
186
|
-
injected_module = sys.modules.get("gepa.optimize_anything")
|
|
187
|
-
if injected_module is not None:
|
|
188
|
-
return injected_module
|
|
189
|
-
try:
|
|
190
|
-
return importlib.import_module("gepa.optimize_anything")
|
|
191
|
-
except ImportError as exc:
|
|
192
|
-
raise GepaUnavailableError(
|
|
193
|
-
"GEPA is not installed in this environment. Reinstall or sync DevCouncil dependencies, "
|
|
194
|
-
"then rerun `dev agents optimize`."
|
|
195
|
-
) from exc
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def _score_candidate(candidate: Any, example: dict[str, Any], *, log: Callable[[str], None]) -> float:
|
|
199
|
-
candidate_text = _candidate_to_text(candidate)
|
|
200
|
-
lower_candidate = candidate_text.lower()
|
|
201
|
-
required_terms = _string_list(example, "required_terms", "expected_prompt_fragments", "must_include")
|
|
202
|
-
forbidden_terms = _string_list(example, "forbidden_terms", "must_avoid")
|
|
203
|
-
|
|
204
|
-
log(f"Example {example.get('id')}: {example.get('task', '')}".strip())
|
|
205
|
-
for key in ("observed_failure", "desired_behavior", "feedback", "rubric"):
|
|
206
|
-
value = example.get(key)
|
|
207
|
-
if value:
|
|
208
|
-
log(f"{key.replace('_', ' ').title()}: {value}")
|
|
209
|
-
if required_terms:
|
|
210
|
-
log(f"Required prompt terms: {', '.join(required_terms)}")
|
|
211
|
-
if forbidden_terms:
|
|
212
|
-
log(f"Forbidden prompt terms: {', '.join(forbidden_terms)}")
|
|
213
|
-
|
|
214
|
-
if not candidate_text.strip():
|
|
215
|
-
log("Candidate preamble is empty.")
|
|
216
|
-
return 0.0
|
|
217
|
-
|
|
218
|
-
required_hits = [term for term in required_terms if term.lower() in lower_candidate]
|
|
219
|
-
forbidden_hits = [term for term in forbidden_terms if term.lower() in lower_candidate]
|
|
220
|
-
missing_terms = [term for term in required_terms if term not in required_hits]
|
|
221
|
-
if missing_terms:
|
|
222
|
-
log(f"Missing required terms: {', '.join(missing_terms)}")
|
|
223
|
-
if forbidden_hits:
|
|
224
|
-
log(f"Forbidden terms present: {', '.join(forbidden_hits)}")
|
|
225
|
-
|
|
226
|
-
required_score = len(required_hits) / len(required_terms) if required_terms else 0.5
|
|
227
|
-
forbidden_score = 1.0 - (len(forbidden_hits) / len(forbidden_terms)) if forbidden_terms else 1.0
|
|
228
|
-
length_score = _length_score(candidate_text)
|
|
229
|
-
score = (required_score * 0.65) + (forbidden_score * 0.25) + (length_score * 0.10)
|
|
230
|
-
return max(0.0, min(1.0, score))
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
def _candidate_to_text(candidate: Any) -> str:
|
|
234
|
-
if candidate is None:
|
|
235
|
-
return ""
|
|
236
|
-
if isinstance(candidate, str):
|
|
237
|
-
return candidate
|
|
238
|
-
if isinstance(candidate, dict):
|
|
239
|
-
for key in ("prompt_preamble", "preamble", "prompt", "system_prompt", "text"):
|
|
240
|
-
value = candidate.get(key)
|
|
241
|
-
if isinstance(value, str):
|
|
242
|
-
return value
|
|
243
|
-
return json.dumps(candidate, sort_keys=True)
|
|
244
|
-
for attr in ("prompt_preamble", "preamble", "prompt", "system_prompt", "text"):
|
|
245
|
-
value = getattr(candidate, attr, None)
|
|
246
|
-
if isinstance(value, str):
|
|
247
|
-
return value
|
|
248
|
-
return str(candidate)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
def _best_candidate(result: Any) -> Any:
|
|
252
|
-
if isinstance(result, dict):
|
|
253
|
-
for key in ("best_candidate", "candidate", "best"):
|
|
254
|
-
if key in result:
|
|
255
|
-
return result[key]
|
|
256
|
-
return result
|
|
257
|
-
for attr in ("best_candidate", "candidate", "best"):
|
|
258
|
-
if hasattr(result, attr):
|
|
259
|
-
return getattr(result, attr)
|
|
260
|
-
return result
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def _best_score(result: Any) -> float | None:
|
|
264
|
-
raw_score: Any
|
|
265
|
-
if isinstance(result, dict):
|
|
266
|
-
raw_score = result.get("best_score", result.get("score"))
|
|
267
|
-
else:
|
|
268
|
-
raw_score = getattr(result, "best_score", getattr(result, "score", None))
|
|
269
|
-
if raw_score is None:
|
|
270
|
-
return None
|
|
271
|
-
try:
|
|
272
|
-
return float(raw_score)
|
|
273
|
-
except (TypeError, ValueError):
|
|
274
|
-
return None
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
def _string_list(example: dict[str, Any], *keys: str) -> list[str]:
|
|
278
|
-
values: list[str] = []
|
|
279
|
-
for key in keys:
|
|
280
|
-
raw = example.get(key)
|
|
281
|
-
if raw is None:
|
|
282
|
-
continue
|
|
283
|
-
if isinstance(raw, str):
|
|
284
|
-
values.append(raw)
|
|
285
|
-
elif isinstance(raw, Iterable):
|
|
286
|
-
values.extend(str(item) for item in raw if str(item).strip())
|
|
287
|
-
return [value.strip() for value in values if value.strip()]
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def _length_score(candidate_text: str) -> float:
|
|
291
|
-
words = candidate_text.split()
|
|
292
|
-
if 10 <= len(words) <= 180:
|
|
293
|
-
return 1.0
|
|
294
|
-
if len(words) < 10:
|
|
295
|
-
return len(words) / 10
|
|
296
|
-
return max(0.0, 1.0 - ((len(words) - 180) / 180))
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
def _default_artifact_path(project_root: Path, agent: str, profile_name: str) -> Path:
|
|
300
|
-
timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
301
|
-
safe_profile = profile_name.replace("/", "-").replace("\\", "-")
|
|
302
|
-
return project_root / ".devcouncil" / "optimizations" / f"{timestamp}-{agent}-{safe_profile}-gepa.json"
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
def _write_result_artifact(path: Path, payload: dict[str, Any]) -> None:
|
|
306
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
307
|
-
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
def _apply_profile_preamble(project_root: Path, profile_name: str, best_preamble: str) -> None:
|
|
311
|
-
config_path = project_root / ".devcouncil" / "config.yaml"
|
|
312
|
-
raw_config = yaml.safe_load(config_path.read_text(encoding="utf-8")) if config_path.exists() else {}
|
|
313
|
-
raw_config = raw_config or {}
|
|
314
|
-
profiles = raw_config.setdefault("integrations", {}).setdefault("cli_agents", {}).setdefault("profiles", {})
|
|
315
|
-
profile = profiles.setdefault(profile_name, {})
|
|
316
|
-
profile["prompt_preamble"] = best_preamble
|
|
317
|
-
config_path.parent.mkdir(parents=True, exist_ok=True)
|
|
318
|
-
config_path.write_text(yaml.safe_dump(raw_config, sort_keys=False), encoding="utf-8")
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
from typing import List, Dict
|
|
2
|
-
from pydantic import BaseModel
|
|
3
|
-
from devcouncil.domain.requirement import Requirement
|
|
4
|
-
from devcouncil.domain.task import Task
|
|
5
|
-
from devcouncil.llm.router import ModelRouter
|
|
6
|
-
|
|
7
|
-
class ArbiterDecision(BaseModel):
|
|
8
|
-
accepted_finding_ids: List[str]
|
|
9
|
-
rejected_finding_ids: List[Dict[str, str]] # id, reason
|
|
10
|
-
final_requirements: List[Requirement]
|
|
11
|
-
final_tasks: List[Task]
|
|
12
|
-
|
|
13
|
-
class ArbiterService:
|
|
14
|
-
def __init__(self, router: ModelRouter):
|
|
15
|
-
self.router = router
|
|
16
|
-
|
|
17
|
-
async def arbitrate(
|
|
18
|
-
self,
|
|
19
|
-
goal: str,
|
|
20
|
-
requirements_json: str,
|
|
21
|
-
plan_a_json: str,
|
|
22
|
-
plan_b_json: str,
|
|
23
|
-
critique_a_json: str,
|
|
24
|
-
critique_b_json: str,
|
|
25
|
-
rebuttal_a_json: str,
|
|
26
|
-
rebuttal_b_json: str
|
|
27
|
-
) -> ArbiterDecision:
|
|
28
|
-
prompt = f"""
|
|
29
|
-
Goal: {goal}
|
|
30
|
-
|
|
31
|
-
Initial Requirements:
|
|
32
|
-
{requirements_json}
|
|
33
|
-
|
|
34
|
-
Plan A: {plan_a_json}
|
|
35
|
-
Plan B: {plan_b_json}
|
|
36
|
-
|
|
37
|
-
Critique of Plan B by Critic A: {critique_a_json}
|
|
38
|
-
Critique of Plan A by Critic B: {critique_b_json}
|
|
39
|
-
|
|
40
|
-
Rebuttal of Critic B by Planner A: {rebuttal_a_json}
|
|
41
|
-
Rebuttal of Critic A by Planner B: {rebuttal_b_json}
|
|
42
|
-
|
|
43
|
-
You are the arbiter engineering manager. Your goal is to produce the final, definitive set of requirements and tasks.
|
|
44
|
-
- You do not decide by vibes.
|
|
45
|
-
- High-severity unrefuted findings from critics must be incorporated into the final requirements or tasks.
|
|
46
|
-
- If a planner successfully rebutted a finding, you may skip it.
|
|
47
|
-
- Produce a single, coherent task graph.
|
|
48
|
-
"""
|
|
49
|
-
messages = [
|
|
50
|
-
{"role": "user", "content": prompt}
|
|
51
|
-
]
|
|
52
|
-
|
|
53
|
-
return await self.router.complete_structured(
|
|
54
|
-
role="arbiter",
|
|
55
|
-
messages=messages,
|
|
56
|
-
schema=ArbiterDecision
|
|
57
|
-
)
|