Merge branch 'KIN-DOCS-003-backend_dev'
This commit is contained in:
commit
12bf510bbc
6 changed files with 180 additions and 13 deletions
93
agents/prompts/knowledge_synthesizer.md
Normal file
93
agents/prompts/knowledge_synthesizer.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
You are a Knowledge Synthesizer for the Kin multi-agent orchestrator.
|
||||
|
||||
Your job: aggregate and synthesize outputs from multiple parallel research agents into a unified, confidence-rated knowledge base ready for the Architect.
|
||||
|
||||
## Input
|
||||
|
||||
You receive:
|
||||
- PROJECT: id, name, path, tech stack
|
||||
- TASK: id, title, brief with `phases_context` keyed by researcher role name (e.g. `business_analyst`, `market_researcher`)
|
||||
- DECISIONS: known architectural decisions and conventions
|
||||
- PREVIOUS STEP OUTPUT: latest approved researcher outputs (if phases_context is absent)
|
||||
|
||||
## Working Mode
|
||||
|
||||
1. Read `brief.phases_context` — each key is a researcher role, value is their output summary
|
||||
2. Identify consensus findings: claims supported by ≥2 researcher roles
|
||||
3. Identify conflicts: directly contradictory findings between roles; name the conflict explicitly
|
||||
4. Assign confidence rating to each conclusion: `high` (≥2 supporting roles), `medium` (1 role), `low` (inferred)
|
||||
5. Prioritize actions: order by impact and confidence, not by which role reported them first
|
||||
|
||||
## Focus On
|
||||
|
||||
- Synthesize, do not repeat raw data — extract conclusions, not summaries of summaries
|
||||
- Make conflicts explicit: name both positions and which roles hold them; do not paper over disagreements
|
||||
- Confidence must be grounded in cross-role agreement, not assertion strength
|
||||
- Unresolved conflicts = honest knowledge gaps, not speculation — flag them as open questions
|
||||
- `phases_context_used` must list every input role to prove completeness
|
||||
|
||||
## Quality Checks
|
||||
|
||||
- Every item in `confidence_rated_conclusions` cites at least one `supporting_roles` entry
|
||||
- `unified_findings` contains no raw data dumps — only synthesized insights
|
||||
- `unresolved_conflicts` is not silently omitted when contradictions exist
|
||||
- `prioritized_actions` are concrete and directed at the Architect, not vague recommendations
|
||||
- All input roles from `phases_context` appear in `phases_context_used`
|
||||
|
||||
## Return Format
|
||||
|
||||
Return ONLY valid JSON (no markdown, no explanation):
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "done",
|
||||
"unified_findings": [
|
||||
"Synthesized insight that multiple researchers converge on"
|
||||
],
|
||||
"confidence_rated_conclusions": [
|
||||
{
|
||||
"conclusion": "...",
|
||||
"confidence": "high | medium | low",
|
||||
"supporting_roles": ["business_analyst", "market_researcher"],
|
||||
"rationale": "Why this confidence level"
|
||||
}
|
||||
],
|
||||
"unresolved_conflicts": [
|
||||
{
|
||||
"topic": "...",
|
||||
"positions": {
|
||||
"role_a": "their position",
|
||||
"role_b": "their position"
|
||||
},
|
||||
"recommendation": "What the Architect should decide"
|
||||
}
|
||||
],
|
||||
"prioritized_actions": [
|
||||
"Action 1 for Architect — grounded in high-confidence findings",
|
||||
"Action 2 for Architect"
|
||||
],
|
||||
"phases_context_used": ["business_analyst", "market_researcher"]
|
||||
}
|
||||
```
|
||||
|
||||
Valid values for `status`: `"done"`, `"blocked"`.
|
||||
|
||||
If status is "blocked", include `"blocked_reason": "..."`.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Do NOT repeat raw researcher outputs — synthesize only
|
||||
- Do NOT assign `confidence: high` to findings supported by only one role
|
||||
- Do NOT omit `unresolved_conflicts` when contradictions exist between roles
|
||||
- Do NOT write implementation code or architectural decisions — that is the Architect's job
|
||||
- Do NOT speculate beyond what researchers reported; unknown = unresolved
|
||||
|
||||
## Blocked Protocol
|
||||
|
||||
If you cannot perform the task (phases_context is missing or empty, ambiguous requirements, task outside your scope), return this JSON **instead of** the normal output:
|
||||
|
||||
```json
|
||||
{"status": "blocked", "reason": "<clear explanation>", "blocked_at": "<ISO-8601 datetime>"}
|
||||
```
|
||||
|
||||
Use current datetime for `blocked_at`. Do NOT guess or partially complete — return blocked immediately.
|
||||
|
|
@ -227,6 +227,21 @@ specialists:
|
|||
context_rules:
|
||||
decisions: all
|
||||
|
||||
knowledge_synthesizer:
|
||||
name: "Knowledge Synthesizer"
|
||||
model: sonnet
|
||||
tools: [Read, Grep, Glob]
|
||||
description: "Aggregates multi-agent research outputs into unified, confidence-rated knowledge base for the Architect"
|
||||
permissions: read_only
|
||||
context_rules:
|
||||
decisions: all
|
||||
output_schema:
|
||||
unified_findings: "array of strings"
|
||||
confidence_rated_conclusions: "array of { conclusion, confidence: high|medium|low, supporting_roles, rationale }"
|
||||
unresolved_conflicts: "array of { topic, positions: { role: position }, recommendation }"
|
||||
prioritized_actions: "array of strings"
|
||||
phases_context_used: "array of role names"
|
||||
|
||||
research_head:
|
||||
name: "Research Department Head"
|
||||
model: opus
|
||||
|
|
|
|||
|
|
@ -75,6 +75,9 @@ def build_context(
|
|||
ctx["modules"] = models.get_modules(conn, project_id)
|
||||
ctx["decisions"] = models.get_decisions(conn, project_id)
|
||||
|
||||
elif role == "knowledge_synthesizer":
|
||||
ctx["decisions"] = models.get_decisions(conn, project_id)
|
||||
|
||||
elif role == "debugger":
|
||||
ctx["decisions"] = models.get_decisions(
|
||||
conn, project_id, types=["gotcha", "workaround"],
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import sqlite3
|
|||
|
||||
from core import models
|
||||
|
||||
# Canonical order of research roles (architect always last)
|
||||
# Canonical order of research roles (knowledge_synthesizer and architect always last)
|
||||
RESEARCH_ROLES = [
|
||||
"business_analyst",
|
||||
"market_researcher",
|
||||
|
|
@ -18,6 +18,7 @@ RESEARCH_ROLES = [
|
|||
"tech_researcher",
|
||||
"ux_designer",
|
||||
"marketer",
|
||||
"knowledge_synthesizer",
|
||||
"architect",
|
||||
]
|
||||
|
||||
|
|
@ -29,17 +30,18 @@ ROLE_LABELS = {
|
|||
"tech_researcher": "Tech Researcher",
|
||||
"ux_designer": "UX Designer",
|
||||
"marketer": "Marketer",
|
||||
"knowledge_synthesizer": "Knowledge Synthesizer",
|
||||
"architect": "Architect",
|
||||
}
|
||||
|
||||
|
||||
def validate_roles(roles: list[str]) -> list[str]:
|
||||
"""Filter unknown roles, remove duplicates, strip 'architect' (auto-added later)."""
|
||||
"""Filter unknown roles, remove duplicates, strip auto-managed roles (architect, knowledge_synthesizer)."""
|
||||
seen: set[str] = set()
|
||||
result = []
|
||||
for r in roles:
|
||||
r = r.strip().lower()
|
||||
if r == "architect":
|
||||
if r in ("architect", "knowledge_synthesizer"):
|
||||
continue
|
||||
if r in RESEARCH_ROLES and r not in seen:
|
||||
seen.add(r)
|
||||
|
|
@ -48,9 +50,18 @@ def validate_roles(roles: list[str]) -> list[str]:
|
|||
|
||||
|
||||
def build_phase_order(selected_roles: list[str]) -> list[str]:
|
||||
"""Return roles in canonical RESEARCH_ROLES order, append architect if any selected."""
|
||||
ordered = [r for r in RESEARCH_ROLES if r in selected_roles and r != "architect"]
|
||||
"""Return roles in canonical RESEARCH_ROLES order.
|
||||
|
||||
Auto-inserts knowledge_synthesizer before architect when ≥2 researchers selected.
|
||||
Architect always appended last when any researcher is selected.
|
||||
"""
|
||||
ordered = [
|
||||
r for r in RESEARCH_ROLES
|
||||
if r in selected_roles and r not in ("architect", "knowledge_synthesizer")
|
||||
]
|
||||
if ordered:
|
||||
if len(ordered) >= 2:
|
||||
ordered.append("knowledge_synthesizer")
|
||||
ordered.append("architect")
|
||||
return ordered
|
||||
|
||||
|
|
@ -114,6 +125,24 @@ def activate_phase(conn: sqlite3.Connection, phase_id: int) -> dict:
|
|||
"phase_order": phase["phase_order"],
|
||||
"workflow": "research",
|
||||
}
|
||||
|
||||
# knowledge_synthesizer: collect approved researcher outputs into phases_context
|
||||
if phase["role"] == "knowledge_synthesizer":
|
||||
all_phases = models.list_phases(conn, phase["project_id"])
|
||||
phases_context = {}
|
||||
for p in all_phases:
|
||||
if p["status"] == "approved" and p.get("task_id"):
|
||||
row = conn.execute(
|
||||
"""SELECT output_summary FROM agent_logs
|
||||
WHERE task_id = ? AND success = 1
|
||||
ORDER BY created_at DESC LIMIT 1""",
|
||||
(p["task_id"],),
|
||||
).fetchone()
|
||||
if row and row["output_summary"]:
|
||||
phases_context[p["role"]] = row["output_summary"]
|
||||
if phases_context:
|
||||
brief["phases_context"] = phases_context
|
||||
|
||||
task = models.create_task(
|
||||
conn, task_id, phase["project_id"],
|
||||
title=f"[Research] {ROLE_LABELS.get(phase['role'], phase['role'])}",
|
||||
|
|
|
|||
|
|
@ -115,11 +115,11 @@ class TestAllPromptsContainStandardStructure:
|
|||
class TestPromptCount:
|
||||
"""Проверяет, что число промптов не изменилось неожиданно."""
|
||||
|
||||
def test_prompt_count_is_25(self):
|
||||
"""В agents/prompts/ ровно 25 файлов .md."""
|
||||
def test_prompt_count_is_26(self):
|
||||
"""В agents/prompts/ ровно 26 файлов .md."""
|
||||
count = len(_prompt_files())
|
||||
assert count == 25, ( # 25 промптов — актуально на 2026-03-19 (см. git log agents/prompts/)
|
||||
f"Ожидалось 25 промптов, найдено {count}. "
|
||||
assert count == 26, ( # 26 промптов — актуально на 2026-03-19, +knowledge_synthesizer (KIN-DOCS-003, см. git log agents/prompts/)
|
||||
f"Ожидалось 26 промптов, найдено {count}. "
|
||||
"Если добавлен новый промпт — обнови этот тест."
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ def test_validate_roles_strips_and_lowercases():
|
|||
|
||||
|
||||
@pytest.mark.parametrize("roles,expected", [
|
||||
# 1 исследователь → нет knowledge_synthesizer, architect последний
|
||||
(
|
||||
["business_analyst"],
|
||||
["business_analyst", "architect"],
|
||||
|
|
@ -86,17 +87,21 @@ def test_validate_roles_strips_and_lowercases():
|
|||
["tech_researcher"],
|
||||
["tech_researcher", "architect"],
|
||||
),
|
||||
# ≥2 исследователей → knowledge_synthesizer авто-вставляется перед architect (KIN-DOCS-003, 2026-03-19)
|
||||
(
|
||||
["marketer", "business_analyst"],
|
||||
["business_analyst", "marketer", "architect"],
|
||||
["business_analyst", "marketer", "knowledge_synthesizer", "architect"],
|
||||
),
|
||||
(
|
||||
["ux_designer", "market_researcher", "tech_researcher"],
|
||||
["market_researcher", "tech_researcher", "ux_designer", "architect"],
|
||||
["market_researcher", "tech_researcher", "ux_designer", "knowledge_synthesizer", "architect"],
|
||||
),
|
||||
])
|
||||
def test_build_phase_order_canonical_order_and_appends_architect(roles, expected):
|
||||
"""KIN-059: роли сортируются в канонический порядок, architect добавляется последним."""
|
||||
"""KIN-059: роли сортируются в канонический порядок, architect добавляется последним.
|
||||
|
||||
При ≥2 исследователях knowledge_synthesizer авто-вставляется перед architect.
|
||||
"""
|
||||
assert build_phase_order(roles) == expected
|
||||
|
||||
|
||||
|
|
@ -113,6 +118,28 @@ def test_build_phase_order_architect_always_last():
|
|||
assert result[-1] == "architect"
|
||||
|
||||
|
||||
def test_build_phase_order_single_researcher_no_synthesizer():
|
||||
"""KIN-DOCS-003: 1 исследователь → knowledge_synthesizer НЕ вставляется."""
|
||||
result = build_phase_order(["business_analyst"])
|
||||
assert "knowledge_synthesizer" not in result
|
||||
assert result == ["business_analyst", "architect"]
|
||||
|
||||
|
||||
def test_build_phase_order_two_researchers_inserts_synthesizer():
|
||||
"""KIN-DOCS-003: 2 исследователя → knowledge_synthesizer авто-вставляется перед architect."""
|
||||
result = build_phase_order(["market_researcher", "tech_researcher"])
|
||||
assert "knowledge_synthesizer" in result
|
||||
assert result.index("knowledge_synthesizer") == len(result) - 2
|
||||
assert result[-1] == "architect"
|
||||
|
||||
|
||||
def test_validate_roles_strips_knowledge_synthesizer():
|
||||
"""KIN-DOCS-003: knowledge_synthesizer убирается из входных ролей — авто-управляемая роль."""
|
||||
result = validate_roles(["knowledge_synthesizer", "business_analyst"])
|
||||
assert "knowledge_synthesizer" not in result
|
||||
assert "business_analyst" in result
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# create_project_with_phases
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -146,7 +173,7 @@ def test_create_project_with_phases_other_phases_remain_pending(conn):
|
|||
conn, "proj1", "P1", "/path",
|
||||
description="Desc", selected_roles=["market_researcher", "tech_researcher"],
|
||||
)
|
||||
# market_researcher, tech_researcher, architect → 3 фазы
|
||||
# market_researcher, tech_researcher, knowledge_synthesizer, architect → 4 фазы (KIN-DOCS-003)
|
||||
for phase in result["phases"][1:]:
|
||||
assert phase["status"] == "pending"
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue