council.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. import { READONLY_FILE_OPERATIONS_RULES } from '../config';
  2. import { shortModelLabel } from '../utils/session';
  3. import { type AgentDefinition, resolvePrompt } from './orchestrator';
  4. import { createReadOnlyAgentPermission } from './permissions';
  5. // NOTE: Councillor system prompts live in the councillor agent factory.
  6. // The format functions below only structure the USER message content - the
  7. // agent factory provides the system prompt.
  8. const COUNCIL_AGENT_PROMPT = `You are the Council agent - a multi-LLM \
  9. orchestration system that runs consensus across multiple models.
  10. **Tool**: You have access to the \`council_session\` tool. You also have read-only codebase inspection tools. You do not have write, edit, shell, or subagent-delegation tools.
  11. **When to use**:
  12. - When invoked by a user with a request
  13. - When you want multiple expert opinions on a complex problem
  14. - When higher confidence is needed through model consensus
  15. **Usage**:
  16. 1. Call the \`council_session\` tool with the user's prompt
  17. 2. Optionally specify a preset (omit to use the configured default)
  18. 3. Receive the councillor responses formatted for synthesis
  19. 4. Follow the Synthesis Process below
  20. 5. Present the result to the user
  21. **Synthesis Process** (MANDATORY - follow in order):
  22. 1. Read the original user prompt
  23. 2. Review each councillor's response individually - note each councillor's \
  24. key insight and unique contribution by name
  25. 3. Identify agreements and contradictions between councillors
  26. 4. Resolve contradictions with explicit reasoning
  27. 5. Synthesize the optimal final answer
  28. 6. Format output per the Required Output Format below
  29. **Behavior**:
  30. - Delegate requests directly to council_session
  31. - Don't pre-analyze or filter the prompt before calling council_session
  32. - Credit specific insights from individual councillors using their names
  33. - If councillors disagree, explain why you chose one approach over another
  34. - Do not omit per-councillor details from the final response
  35. - Do not collapse the output into only a final summary
  36. - Be transparent about trade-offs when different approaches have valid pros/cons
  37. - Don't just average responses - choose the best approach and improve upon it
  38. ${READONLY_FILE_OPERATIONS_RULES}
  39. **Required Output Format**:
  40. Always include these sections in your final response:
  41. ## Council Response
  42. Provide the best synthesized answer. Integrate the strongest points from the \
  43. councillors, resolve disagreements, and give the user a clear final \
  44. recommendation or answer. Include relevant code examples and concrete details.
  45. ## Councillor Details
  46. Include each councillor's response separately.
  47. Use each councillor name exactly as provided in the tool result.
  48. Format each councillor like:
  49. ### <councillor name>
  50. <that councillor's response>
  51. If a councillor failed or timed out, include that status briefly.
  52. ## Council Summary
  53. Summarize where councillors agreed, where they disagreed, why you chose the \
  54. final answer, and any remaining uncertainty. Include a consensus confidence \
  55. rating: unanimous, majority, or split.`;
  56. export function createCouncilAgent(
  57. model: string,
  58. customPrompt?: string,
  59. customAppendPrompt?: string,
  60. ): AgentDefinition {
  61. const prompt = resolvePrompt(
  62. COUNCIL_AGENT_PROMPT,
  63. customPrompt,
  64. customAppendPrompt,
  65. );
  66. const definition: AgentDefinition = {
  67. name: 'council',
  68. description:
  69. 'Multi-LLM council agent that synthesizes responses from multiple models for higher-quality outputs',
  70. config: {
  71. temperature: 0.1,
  72. prompt,
  73. permission: {
  74. ...createReadOnlyAgentPermission(),
  75. council_session: 'allow',
  76. },
  77. },
  78. };
  79. // Council's model comes from config override or is resolved at
  80. // runtime; only set if a non-empty string is provided.
  81. if (model) {
  82. definition.config.model = model;
  83. }
  84. return definition;
  85. }
  86. /**
  87. * Build the prompt for a specific councillor session.
  88. *
  89. * Returns the raw user prompt - the agent factory (councillor.ts) provides
  90. * the system prompt with tool-aware instructions. No duplication.
  91. *
  92. * If a per-councillor prompt override is provided, it is prepended as
  93. * role/guidance context before the user's question.
  94. */
  95. export function formatCouncillorPrompt(
  96. userPrompt: string,
  97. councillorPrompt?: string,
  98. ): string {
  99. if (!councillorPrompt) return userPrompt;
  100. return `${councillorPrompt}\n\n---\n\n${userPrompt}`;
  101. }
  102. /**
  103. * Format councillor results for the council agent to synthesize.
  104. *
  105. * Formats councillor results as structured data that the council agent
  106. * (which called the tool) will receive as the tool response. The council
  107. * agent's system prompt contains synthesis instructions.
  108. * Returns a special message when all councillors failed to produce output.
  109. */
  110. export function formatCouncillorResults(
  111. originalPrompt: string,
  112. councillorResults: Array<{
  113. name: string;
  114. model: string;
  115. status: string;
  116. result?: string;
  117. error?: string;
  118. }>,
  119. ): string {
  120. const completedWithResults = councillorResults.filter(
  121. (cr) => cr.status === 'completed' && cr.result,
  122. );
  123. const councillorSection = completedWithResults
  124. .map((cr) => {
  125. const shortModel = shortModelLabel(cr.model);
  126. return `**${cr.name}** (${shortModel}):\n${cr.result}`;
  127. })
  128. .join('\n\n');
  129. const failedSection = councillorResults
  130. .filter((cr) => cr.status !== 'completed')
  131. .map((cr) => `**${cr.name}**: ${cr.status} - ${cr.error ?? 'Unknown'}`)
  132. .join('\n');
  133. // Defensive guard: caller (runCouncil) short-circuits when all fail,
  134. // but this function may be reused in other contexts.
  135. if (completedWithResults.length === 0) {
  136. const errorDetails = councillorResults
  137. .map(
  138. (cr) =>
  139. `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} - ${
  140. cr.error ?? 'Unknown'
  141. }`,
  142. )
  143. .join('\n');
  144. return `---\n\n**Original Prompt**:\n${originalPrompt}\n\n---\n\n**Councillor Responses**:\nAll councillors failed to produce output:\n${errorDetails}\n\nPlease generate a response based on the original prompt alone.`;
  145. }
  146. let prompt = `---\n\n**Original Prompt**:\n${originalPrompt}\n\n---\n\n**Councillor Responses**:\n${councillorSection}`;
  147. if (failedSection) {
  148. prompt += `\n\n---\n\n**Failed/Timed-out Councillors**:\n${failedSection}`;
  149. }
  150. prompt +=
  151. '\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).';
  152. return prompt;
  153. }