council-schema.ts 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237
  1. import { z } from 'zod';
  2. /**
  3. * Validates model IDs in "provider/model" format.
  4. * Inlined here to avoid circular dependency with schema.ts.
  5. */
  6. const ModelIdSchema = z
  7. .string()
  8. .regex(
  9. /^[^/\s]+\/[^\s]+$/,
  10. 'Expected provider/model format (e.g. "openai/gpt-5.4-mini")',
  11. );
  12. /**
  13. * Configuration for a single councillor within a preset.
  14. * Each councillor is an independent LLM that processes the same prompt.
  15. *
  16. * Councillors run as agent sessions with read-only codebase access
  17. * (read, glob, grep, lsp, list). They can examine the codebase but
  18. * cannot modify files or spawn subagents.
  19. */
  20. export const CouncillorConfigSchema = z.object({
  21. model: ModelIdSchema.describe(
  22. 'Model ID in provider/model format (e.g. "openai/gpt-5.4-mini")',
  23. ),
  24. variant: z.string().optional(),
  25. prompt: z
  26. .string()
  27. .optional()
  28. .describe(
  29. 'Optional role/guidance injected into the councillor user prompt',
  30. ),
  31. });
  32. export type CouncillorConfig = z.infer<typeof CouncillorConfigSchema>;
  33. /**
  34. * Per-preset master override. All fields are optional — any field
  35. * provided here overrides the global `council.master` for this preset.
  36. * Fields not provided fall back to the global master config.
  37. */
  38. export const PresetMasterOverrideSchema = z.object({
  39. model: ModelIdSchema.optional().describe(
  40. 'Override the master model for this preset',
  41. ),
  42. variant: z
  43. .string()
  44. .optional()
  45. .describe('Override the master variant for this preset'),
  46. prompt: z
  47. .string()
  48. .optional()
  49. .describe('Override the master synthesis guidance for this preset'),
  50. });
  51. export type PresetMasterOverride = z.infer<typeof PresetMasterOverrideSchema>;
  52. /**
  53. * A named preset grouping several councillors with an optional master override.
  54. *
  55. * The reserved key `"master"` provides per-preset overrides for the council
  56. * master (model, variant, prompt). All other keys are treated as councillor
  57. * names mapping to councillor configs.
  58. *
  59. * After parsing, the preset resolves to:
  60. * `{ councillors: Record<string, CouncillorConfig>, master?: PresetMasterOverride }`
  61. */
  62. export const CouncilPresetSchema = z
  63. .record(z.string(), z.record(z.string(), z.unknown()))
  64. .transform((entries, ctx) => {
  65. const councillors: Record<string, CouncillorConfig> = {};
  66. let masterOverride: PresetMasterOverride | undefined;
  67. for (const [key, raw] of Object.entries(entries)) {
  68. if (key === 'master') {
  69. const parsed = PresetMasterOverrideSchema.safeParse(raw);
  70. if (!parsed.success) {
  71. ctx.addIssue(
  72. `Invalid master override in preset: ${parsed.error.issues.map((i) => i.message).join(', ')}`,
  73. );
  74. return z.NEVER;
  75. }
  76. masterOverride = parsed.data;
  77. } else {
  78. const parsed = CouncillorConfigSchema.safeParse(raw);
  79. if (!parsed.success) {
  80. ctx.addIssue(
  81. `Invalid councillor "${key}": ${parsed.error.issues.map((i) => i.message).join(', ')}`,
  82. );
  83. return z.NEVER;
  84. }
  85. councillors[key] = parsed.data;
  86. }
  87. }
  88. return { councillors, master: masterOverride };
  89. });
  90. export type CouncilPreset = z.infer<typeof CouncilPresetSchema>;
  91. /**
  92. * Council Master configuration.
  93. * The master receives all councillor responses and produces the final synthesis.
  94. *
  95. * Note: The master runs as a council-master agent session with zero
  96. * permissions (deny all). Synthesis is a text-in/text-out operation —
  97. * no tools or MCPs are needed.
  98. */
  99. export const CouncilMasterConfigSchema = z.object({
  100. model: ModelIdSchema.describe(
  101. 'Model ID for the council master (e.g. "anthropic/claude-opus-4-6")',
  102. ),
  103. variant: z.string().optional(),
  104. prompt: z
  105. .string()
  106. .optional()
  107. .describe(
  108. 'Optional role/guidance injected into the master synthesis prompt',
  109. ),
  110. });
  111. export type CouncilMasterConfig = z.infer<typeof CouncilMasterConfigSchema>;
  112. /**
  113. * Execution mode for councillors.
  114. * - parallel: Run all councillors concurrently (default, fastest for multi-model systems)
  115. * - serial: Run councillors one at a time (required for single-model systems to avoid conflicts)
  116. */
  117. export const CouncillorExecutionModeSchema = z
  118. .enum(['parallel', 'serial'])
  119. .default('parallel')
  120. .describe(
  121. 'Execution mode for councillors. Use "serial" for single-model systems to avoid conflicts. ' +
  122. 'Use "parallel" for multi-model systems for faster execution.',
  123. );
  124. /**
  125. * Top-level council configuration.
  126. *
  127. * Example JSONC:
  128. * ```jsonc
  129. * {
  130. * "council": {
  131. * "master": { "model": "anthropic/claude-opus-4-6" },
  132. * "presets": {
  133. * "default": {
  134. * "alpha": { "model": "openai/gpt-5.4-mini" },
  135. * "beta": { "model": "openai/gpt-5.3-codex" },
  136. * "gamma": { "model": "google/gemini-3-pro" }
  137. * }
  138. * },
  139. * "master_timeout": 300000,
  140. * "councillors_timeout": 180000,
  141. * "councillor_execution_mode": "serial"
  142. * }
  143. * }
  144. * ```
  145. */
  146. export const CouncilConfigSchema = z.object({
  147. master: CouncilMasterConfigSchema,
  148. presets: z.record(z.string(), CouncilPresetSchema),
  149. master_timeout: z.number().min(0).default(300000),
  150. councillors_timeout: z.number().min(0).default(180000),
  151. default_preset: z.string().default('default'),
  152. master_fallback: z
  153. .array(ModelIdSchema)
  154. .optional()
  155. .transform((val) => {
  156. if (!val) return val;
  157. const unique = [...new Set(val)];
  158. if (unique.length !== val.length) {
  159. // Silently deduplicate — no validation error is raised for
  160. // duplicate entries; duplicates are removed transparently.
  161. return unique;
  162. }
  163. return val;
  164. })
  165. .describe(
  166. 'Fallback models for the council master. Tried in order if the primary model fails. ' +
  167. 'Example: ["anthropic/claude-sonnet-4-6", "openai/gpt-5.4"]',
  168. ),
  169. councillor_execution_mode: CouncillorExecutionModeSchema.describe(
  170. 'Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).',
  171. ),
  172. councillor_retries: z
  173. .number()
  174. .int()
  175. .min(0)
  176. .max(5)
  177. .default(3)
  178. .describe(
  179. 'Number of retry attempts for councillors and master that return empty responses ' +
  180. '(e.g. due to provider rate limiting). Default: 3 retries.',
  181. ),
  182. });
  183. export type CouncilConfig = z.infer<typeof CouncilConfigSchema>;
  184. export type CouncillorExecutionMode = z.infer<
  185. typeof CouncillorExecutionModeSchema
  186. >;
  187. /**
  188. * A sensible default council configuration that users can copy into their
  189. * opencode.jsonc. Provides a 3-councillor preset using common models.
  190. *
  191. * Users should replace models with ones they have access to.
  192. *
  193. * ```jsonc
  194. * "council": DEFAULT_COUNCIL_CONFIG
  195. * ```
  196. */
  197. export const DEFAULT_COUNCIL_CONFIG: z.input<typeof CouncilConfigSchema> = {
  198. master: { model: 'anthropic/claude-opus-4-6' },
  199. presets: {
  200. default: {
  201. alpha: { model: 'openai/gpt-5.4-mini' },
  202. beta: { model: 'openai/gpt-5.3-codex' },
  203. gamma: { model: 'google/gemini-3-pro' },
  204. },
  205. },
  206. };
  207. /**
  208. * Result of a council session.
  209. */
  210. export interface CouncilResult {
  211. success: boolean;
  212. result?: string;
  213. error?: string;
  214. councillorResults: Array<{
  215. name: string;
  216. model: string;
  217. status: 'completed' | 'failed' | 'timed_out';
  218. result?: string;
  219. error?: string;
  220. }>;
  221. }