Browse Source

Merge pull request #405 from alvinunreal/council-prompt-tune

Alvin 3 months ago
parent
commit
e94e24e6a5
3 changed files with 163 additions and 123 deletions
  1. 11 0
      docs/council.md
  2. 105 104
      src/agents/council.test.ts
  3. 47 19
      src/agents/council.ts

+ 11 - 0
docs/council.md

@@ -313,6 +313,17 @@ The orchestrator may delegate to `@council` for high-stakes or ambiguous
 decisions, but it does so sparingly because council is usually the most
 decisions, but it does so sparingly because council is usually the most
 expensive path.
 expensive path.
 
 
+### Output format
+
+Council responses include:
+
+1. `Council Response` — the synthesized final answer.
+2. `Councillor Details` — each responding councillor's individual response,
+   using the councillor names from the configured preset.
+3. `Council Summary` — agreement, disagreement resolution, remaining
+   uncertainty, and a consensus confidence rating of `unanimous`, `majority`,
+   or `split`.
+
 ### Output footer
 ### Output footer
 
 
 Council responses include a footer like:
 Council responses include a footer like:

+ 105 - 104
src/agents/council.test.ts

@@ -1,224 +1,225 @@
-import { describe, expect, test } from "bun:test";
-import { formatCouncillorPrompt, formatCouncillorResults } from "./council";
+import { describe, expect, test } from 'bun:test';
+import { formatCouncillorPrompt, formatCouncillorResults } from './council';
 
 
-describe("formatCouncillorResults", () => {
+describe('formatCouncillorResults', () => {
   const originalPrompt =
   const originalPrompt =
-    "What is the best way to implement a REST API in TypeScript?";
+    'What is the best way to implement a REST API in TypeScript?';
 
 
-  test("formats completed councillor results correctly", () => {
+  test('formats completed councillor results correctly', () => {
     const councillorResults = [
     const councillorResults = [
       {
       {
-        name: "alpha",
-        model: "anthropic/claude-opus-4-6",
-        status: "completed",
-        result: "Use Express.js with TypeScript interfaces for type safety.",
+        name: 'alpha',
+        model: 'anthropic/claude-opus-4-6',
+        status: 'completed',
+        result: 'Use Express.js with TypeScript interfaces for type safety.',
       },
       },
       {
       {
-        name: "beta",
-        model: "openai/gpt-5.5",
-        status: "completed",
+        name: 'beta',
+        model: 'openai/gpt-5.5',
+        status: 'completed',
         result:
         result:
-          "Consider Fastify for better performance and built-in type validation.",
+          'Consider Fastify for better performance and built-in type validation.',
       },
       },
     ];
     ];
 
 
     const formatted = formatCouncillorResults(
     const formatted = formatCouncillorResults(
       originalPrompt,
       originalPrompt,
-      councillorResults
+      councillorResults,
     );
     );
 
 
-    expect(formatted).toContain("**Original Prompt**:");
+    expect(formatted).toContain('**Original Prompt**:');
     expect(formatted).toContain(originalPrompt);
     expect(formatted).toContain(originalPrompt);
-    expect(formatted).toContain("**alpha** (claude-opus-4-6):");
-    expect(formatted).toContain("**beta** (gpt-5.5):");
+    expect(formatted).toContain('**alpha** (claude-opus-4-6):');
+    expect(formatted).toContain('**beta** (gpt-5.5):');
     expect(formatted).toContain(
     expect(formatted).toContain(
-      "Use Express.js with TypeScript interfaces for type safety."
+      'Use Express.js with TypeScript interfaces for type safety.',
     );
     );
     expect(formatted).toContain(
     expect(formatted).toContain(
-      "Consider Fastify for better performance and built-in type validation."
+      'Consider Fastify for better performance and built-in type validation.',
     );
     );
-    expect(formatted).toContain("**Councillor Responses**:");
+    expect(formatted).toContain('**Councillor Responses**:');
     expect(formatted).toContain(
     expect(formatted).toContain(
-      "Synthesize the optimal response based on the above."
+      'You MUST follow the Synthesis Process steps before producing output',
     );
     );
-    expect(formatted).not.toContain("**Failed/Timed-out Councillors**:");
+    expect(formatted).toContain(
+      'consensus confidence rating (unanimous, majority, or split)',
+    );
+    expect(formatted).not.toContain('**Failed/Timed-out Councillors**:');
   });
   });
 
 
-  test("includes failed councillors section when some fail", () => {
+  test('includes failed councillors section when some fail', () => {
     const councillorResults = [
     const councillorResults = [
       {
       {
-        name: "alpha",
-        model: "anthropic/claude-opus-4-6",
-        status: "completed",
-        result: "Use Express.js with TypeScript interfaces for type safety.",
+        name: 'alpha',
+        model: 'anthropic/claude-opus-4-6',
+        status: 'completed',
+        result: 'Use Express.js with TypeScript interfaces for type safety.',
       },
       },
       {
       {
-        name: "beta",
-        model: "openai/gpt-5.5",
-        status: "timed_out",
-        error: "Request timed out after 180000ms",
+        name: 'beta',
+        model: 'openai/gpt-5.5',
+        status: 'timed_out',
+        error: 'Request timed out after 180000ms',
       },
       },
       {
       {
-        name: "gamma",
-        model: "google/gemini-pro",
-        status: "failed",
-        error: "Provider returned empty response",
+        name: 'gamma',
+        model: 'google/gemini-pro',
+        status: 'failed',
+        error: 'Provider returned empty response',
       },
       },
     ];
     ];
 
 
     const formatted = formatCouncillorResults(
     const formatted = formatCouncillorResults(
       originalPrompt,
       originalPrompt,
-      councillorResults
+      councillorResults,
     );
     );
 
 
-    expect(formatted).toContain("**Councillor Responses**:");
-    expect(formatted).toContain("**alpha** (claude-opus-4-6):");
+    expect(formatted).toContain('**Councillor Responses**:');
+    expect(formatted).toContain('**alpha** (claude-opus-4-6):');
     expect(formatted).toContain(
     expect(formatted).toContain(
-      "Use Express.js with TypeScript interfaces for type safety."
+      'Use Express.js with TypeScript interfaces for type safety.',
     );
     );
-    expect(formatted).toContain("**Failed/Timed-out Councillors**:");
+    expect(formatted).toContain('**Failed/Timed-out Councillors**:');
     expect(formatted).toContain(
     expect(formatted).toContain(
-      "**beta**: timed_out — Request timed out after 180000ms"
+      '**beta**: timed_out — Request timed out after 180000ms',
     );
     );
     expect(formatted).toContain(
     expect(formatted).toContain(
-      "**gamma**: failed — Provider returned empty response"
+      '**gamma**: failed — Provider returned empty response',
     );
     );
-    expect(formatted).not.toContain("**beta** (gpt-5.5):");
-    expect(formatted).not.toContain("**gamma** (gemini-pro):");
+    expect(formatted).not.toContain('**beta** (gpt-5.5):');
+    expect(formatted).not.toContain('**gamma** (gemini-pro):');
   });
   });
 
 
-  test("returns fallback message when all councillors fail", () => {
+  test('returns fallback message when all councillors fail', () => {
     const councillorResults = [
     const councillorResults = [
       {
       {
-        name: "alpha",
-        model: "anthropic/claude-opus-4-6",
-        status: "timeout",
-        error: "Request timed out",
+        name: 'alpha',
+        model: 'anthropic/claude-opus-4-6',
+        status: 'timeout',
+        error: 'Request timed out',
       },
       },
       {
       {
-        name: "beta",
-        model: "openai/gpt-5.5",
-        status: "error",
-        error: "Provider error",
+        name: 'beta',
+        model: 'openai/gpt-5.5',
+        status: 'error',
+        error: 'Provider error',
       },
       },
     ];
     ];
 
 
     const formatted = formatCouncillorResults(
     const formatted = formatCouncillorResults(
       originalPrompt,
       originalPrompt,
-      councillorResults
+      councillorResults,
     );
     );
 
 
-    expect(formatted).toContain("**Original Prompt**:");
+    expect(formatted).toContain('**Original Prompt**:');
     expect(formatted).toContain(originalPrompt);
     expect(formatted).toContain(originalPrompt);
-    expect(formatted).toContain("**Councillor Responses**:");
-    expect(formatted).toContain("All councillors failed to produce output:");
-    expect(formatted).toContain("**alpha** (claude-opus-4-6):");
-    expect(formatted).toContain("**beta** (gpt-5.5):");
-    expect(formatted).toContain("Request timed out");
-    expect(formatted).toContain("Provider error");
+    expect(formatted).toContain('**Councillor Responses**:');
+    expect(formatted).toContain('All councillors failed to produce output:');
+    expect(formatted).toContain('**alpha** (claude-opus-4-6):');
+    expect(formatted).toContain('**beta** (gpt-5.5):');
+    expect(formatted).toContain('Request timed out');
+    expect(formatted).toContain('Provider error');
   });
   });
 
 
-  test("handles councillors with result but completed status", () => {
+  test('handles councillors with result but completed status', () => {
     const councillorResults = [
     const councillorResults = [
       {
       {
-        name: "alpha",
-        model: "anthropic/claude-opus-4-6",
-        status: "completed",
-        result: "Valid response",
+        name: 'alpha',
+        model: 'anthropic/claude-opus-4-6',
+        status: 'completed',
+        result: 'Valid response',
       },
       },
       {
       {
-        name: "beta",
-        model: "openai/gpt-5.5",
-        status: "completed",
-        result: "Another valid response",
+        name: 'beta',
+        model: 'openai/gpt-5.5',
+        status: 'completed',
+        result: 'Another valid response',
       },
       },
     ];
     ];
 
 
     const formatted = formatCouncillorResults(
     const formatted = formatCouncillorResults(
       originalPrompt,
       originalPrompt,
-      councillorResults
+      councillorResults,
     );
     );
 
 
-    expect(formatted).toContain("**alpha** (claude-opus-4-6):");
-    expect(formatted).toContain("Valid response");
-    expect(formatted).toContain("**beta** (gpt-5.5):");
-    expect(formatted).toContain("Another valid response");
-    expect(formatted).toContain(
-      "Synthesize the optimal response based on the above."
-    );
+    expect(formatted).toContain('**alpha** (claude-opus-4-6):');
+    expect(formatted).toContain('Valid response');
+    expect(formatted).toContain('**beta** (gpt-5.5):');
+    expect(formatted).toContain('Another valid response');
+    expect(formatted).toContain('review each councillor response individually');
   });
   });
 });
 });
 
 
-describe("formatCouncillorPrompt", () => {
-  const userPrompt = "How do I implement async/await in TypeScript?";
+describe('formatCouncillorPrompt', () => {
+  const userPrompt = 'How do I implement async/await in TypeScript?';
 
 
-  test("returns user prompt unchanged when no councillor prompt is provided", () => {
+  test('returns user prompt unchanged when no councillor prompt is provided', () => {
     const formatted = formatCouncillorPrompt(userPrompt);
     const formatted = formatCouncillorPrompt(userPrompt);
     expect(formatted).toBe(userPrompt);
     expect(formatted).toBe(userPrompt);
   });
   });
 
 
-  test("prepends councillor prompt with separator when provided", () => {
+  test('prepends councillor prompt with separator when provided', () => {
     const councillorPrompt =
     const councillorPrompt =
-      "You are a TypeScript expert. Focus on practical examples.";
+      'You are a TypeScript expert. Focus on practical examples.';
     const formatted = formatCouncillorPrompt(userPrompt, councillorPrompt);
     const formatted = formatCouncillorPrompt(userPrompt, councillorPrompt);
 
 
     expect(formatted).toContain(councillorPrompt);
     expect(formatted).toContain(councillorPrompt);
     expect(formatted).toContain(userPrompt);
     expect(formatted).toContain(userPrompt);
-    expect(formatted).toContain("---");
+    expect(formatted).toContain('---');
     expect(formatted).toMatch(
     expect(formatted).toMatch(
       new RegExp(
       new RegExp(
         `^${councillorPrompt.replace(
         `^${councillorPrompt.replace(
           /[.*+?^${}()|[\]\\]/g,
           /[.*+?^${}()|[\]\\]/g,
-          "\\$&"
-        )}\\n\\n---\\n\\n${userPrompt.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`
-      )
+          '\\$&',
+        )}\\n\\n---\\n\\n${userPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
+      ),
     );
     );
   });
   });
 
 
-  test("handles multiline councillor prompt", () => {
+  test('handles multiline councillor prompt', () => {
     const councillorPrompt =
     const councillorPrompt =
-      "You are an expert.\nFocus on clarity.\nProvide code examples.";
+      'You are an expert.\nFocus on clarity.\nProvide code examples.';
     const formatted = formatCouncillorPrompt(userPrompt, councillorPrompt);
     const formatted = formatCouncillorPrompt(userPrompt, councillorPrompt);
 
 
     expect(formatted).toContain(councillorPrompt);
     expect(formatted).toContain(councillorPrompt);
     expect(formatted).toContain(userPrompt);
     expect(formatted).toContain(userPrompt);
-    expect(formatted).toContain("---");
+    expect(formatted).toContain('---');
     expect(formatted).toMatch(
     expect(formatted).toMatch(
       new RegExp(
       new RegExp(
         `^${councillorPrompt.replace(
         `^${councillorPrompt.replace(
           /[.*+?^${}()|[\]\\]/g,
           /[.*+?^${}()|[\]\\]/g,
-          "\\$&"
-        )}\\n\\n---\\n\\n${userPrompt.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}$`
-      )
+          '\\$&',
+        )}\\n\\n---\\n\\n${userPrompt.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`,
+      ),
     );
     );
   });
   });
 
 
-  test("handles empty councillor prompt", () => {
-    const formatted = formatCouncillorPrompt(userPrompt, "");
+  test('handles empty councillor prompt', () => {
+    const formatted = formatCouncillorPrompt(userPrompt, '');
     expect(formatted).toBe(userPrompt);
     expect(formatted).toBe(userPrompt);
   });
   });
 
 
-  test("handles multiline user prompt with councillor prompt", () => {
-    const councillorPrompt = "You are an expert.";
-    const multilineUserPrompt = "Line 1\nLine 2\nLine 3";
+  test('handles multiline user prompt with councillor prompt', () => {
+    const councillorPrompt = 'You are an expert.';
+    const multilineUserPrompt = 'Line 1\nLine 2\nLine 3';
     const formatted = formatCouncillorPrompt(
     const formatted = formatCouncillorPrompt(
       multilineUserPrompt,
       multilineUserPrompt,
-      councillorPrompt
+      councillorPrompt,
     );
     );
 
 
     expect(formatted).toContain(councillorPrompt);
     expect(formatted).toContain(councillorPrompt);
     expect(formatted).toContain(multilineUserPrompt);
     expect(formatted).toContain(multilineUserPrompt);
-    expect(formatted).toContain("---");
+    expect(formatted).toContain('---');
     expect(formatted).toMatch(
     expect(formatted).toMatch(
       new RegExp(
       new RegExp(
         `^${councillorPrompt.replace(
         `^${councillorPrompt.replace(
           /[.*+?^${}()|[\]\\]/g,
           /[.*+?^${}()|[\]\\]/g,
-          "\\$&"
+          '\\$&',
         )}\\n\\n---\\n\\n${multilineUserPrompt.replace(
         )}\\n\\n---\\n\\n${multilineUserPrompt.replace(
           /[.*+?^${}()|[\]\\]/g,
           /[.*+?^${}()|[\]\\]/g,
-          "\\$&"
-        )}$`
-      )
+          '\\$&',
+        )}$`,
+      ),
     );
     );
   });
   });
 });
 });

+ 47 - 19
src/agents/council.ts

@@ -19,27 +19,52 @@ orchestration system that runs consensus across multiple models.
 1. Call the \`council_session\` tool with the user's prompt
 1. Call the \`council_session\` tool with the user's prompt
 2. Optionally specify a preset (default: "default")
 2. Optionally specify a preset (default: "default")
 3. Receive the councillor responses formatted for synthesis
 3. Receive the councillor responses formatted for synthesis
-4. Synthesize the optimal final answer from the councillor responses
-5. Present the synthesized result to the user
-
-**Synthesis Guidelines**:
-When you receive councillor responses, synthesize them into the optimal final answer:
-- Review all councillor responses thoroughly and create the best possible answer
-- Credit specific insights from individual councillors by name (e.g., "alpha noted that...", "beta suggested...")
-- Clearly explain your reasoning for the chosen approach
-- Be transparent about trade-offs when different approaches have valid pros/cons
-- Note any remaining uncertainties or areas where further investigation is needed
-- If councillors disagree, explain the resolution and your reasoning
-- Acknowledge if consensus was impossible and explain why
-- Don't just average responses — choose the best approach and improve upon it
-- Present the synthesized solution with relevant code examples, concrete details, and clear explanations
+4. Follow the Synthesis Process below
+5. Present the result to the user
+
+**Synthesis Process** (MANDATORY — follow in order):
+1. Read the original user prompt
+2. Review each councillor's response individually — note each councillor's \
+key insight and unique contribution by name
+3. Identify agreements and contradictions between councillors
+4. Resolve contradictions with explicit reasoning
+5. Synthesize the optimal final answer
+6. Format output per the Required Output Format below
 
 
 **Behavior**:
 **Behavior**:
 - Delegate requests directly to council_session
 - Delegate requests directly to council_session
 - Don't pre-analyze or filter the prompt before calling council_session
 - Don't pre-analyze or filter the prompt before calling council_session
-- Synthesize the councillor results into a comprehensive, coherent answer
-- Include attribution for valuable insights from specific councillors
-- If councillors disagree, explain why you chose one approach over another`;
+- Credit specific insights from individual councillors using their names
+- If councillors disagree, explain why you chose one approach over another
+- Do not omit per-councillor details from the final response
+- Do not collapse the output into only a final summary
+- Be transparent about trade-offs when different approaches have valid pros/cons
+- Don't just average responses — choose the best approach and improve upon it
+
+**Required Output Format**:
+Always include these sections in your final response:
+
+## Council Response
+Provide the best synthesized answer. Integrate the strongest points from the \
+councillors, resolve disagreements, and give the user a clear final \
+recommendation or answer. Include relevant code examples and concrete details.
+
+## Councillor Details
+Include each councillor's response separately.
+
+Use each councillor name exactly as provided in the tool result.
+
+Format each councillor like:
+
+### <councillor name>
+<that councillor's response>
+
+If a councillor failed or timed out, include that status briefly.
+
+## Council Summary
+Summarize where councillors agreed, where they disagreed, why you chose the \
+final answer, and any remaining uncertainty. Include a consensus confidence \
+rating: unanimous, majority, or split.`;
 
 
 export function createCouncilAgent(
 export function createCouncilAgent(
   model: string,
   model: string,
@@ -128,7 +153,9 @@ export function formatCouncillorResults(
     const errorDetails = councillorResults
     const errorDetails = councillorResults
       .map(
       .map(
         (cr) =>
         (cr) =>
-          `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} — ${cr.error ?? 'Unknown'}`,
+          `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} — ${
+            cr.error ?? 'Unknown'
+          }`,
       )
       )
       .join('\n');
       .join('\n');
 
 
@@ -141,7 +168,8 @@ export function formatCouncillorResults(
     prompt += `\n\n---\n\n**Failed/Timed-out Councillors**:\n${failedSection}`;
     prompt += `\n\n---\n\n**Failed/Timed-out Councillors**:\n${failedSection}`;
   }
   }
 
 
-  prompt += '\n\n---\n\nSynthesize the optimal response based on the above.';
+  prompt +=
+    '\n\n---\n\nYou MUST follow the Synthesis Process steps before producing output: review each councillor response individually, then produce the required output with a synthesized Council Response, per-councillor details using their exact names, and a Council Summary with consensus confidence rating (unanimous, majority, or split).';
 
 
   return prompt;
   return prompt;
 }
 }