council.ts 6.2 KB

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