oh-my-opencode-cohub 1.0.17 → 1.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,13 +2,15 @@
2
2
 
3
3
  OpenCode 中文智能体编排插件 CoHub——纯调度模式、全中文提示词、规范分析代理、方案制定代理。
4
4
 
5
+ [![GitHub](https://img.shields.io/badge/GitHub-Mr--cjf%2Foh--my--opencode--cohub-blue?logo=github)](https://github.com/Mr-cjf/oh-my-opencode-cohub)
6
+
5
7
  ## 特性
6
8
 
7
9
  - 🚀 **独立运行**:纯 OpenCode Plugin,依赖 `@opencode-ai/plugin` SDK,零外部插件依赖
8
10
  - 🤖 **12 个内置代理**:8 个通用代理(orchestrator/oracle/librarian/explorer/designer/fixer/observer/council)+ 4 个规范/方案代理(rule-user/rule-project/rule-app/planner)
9
11
  - 🪝 **自动注入**:通过 `system.transform` 钩子在每次 LLM 调用时注入中文语言要求
10
12
  - 🧠 **纯调度 Orchestrator**:不碰任何文件工具,只负责规划、委派、验证
11
- - 🇨🇳 **全中文提示词**:8 个内置代理 + 4 个插件代理全中文
13
+ - 🇨🇳 **全中文提示词**:12 个代理全部使用中文提示词
12
14
  - 📋 **规范分析代理**:rule-user(用户AGENTS.md)、rule-project(项目AGENTS.md)、rule-app(.opencode/rules/)
13
15
  - 🎯 **方案制定代理**:planner 综合所有输入输出结构化任务分解
14
16
  - 🔗 **OpenSpec + Superpowers 死板路由**:收到需求即触发,不判断"大小"
@@ -78,7 +80,7 @@ CLI 会精确清理所有 `co-*` 代理和相关配置,不影响其他插件
78
80
 
79
81
  ```bash
80
82
  # 1. 克隆项目
81
- git clone <repo-url> && cd oh-my-opencode-cohub
83
+ git clone https://github.com/Mr-cjf/oh-my-opencode-cohub.git && cd oh-my-opencode-cohub
82
84
 
83
85
  # 2. 安装依赖
84
86
  npm install
@@ -149,6 +151,51 @@ CLI 安装后自动创建 `~/.config/opencode/oh-my-opencode-cohub.json`,这
149
151
 
150
152
  > **注意**:同一目录 `~/.config/opencode/oh-my-opencode-cohub/` 下也可放置 `{agent}.md` 提示词覆盖文件(见「自定义提示词」章节),与 JSON 配置文件共存。
151
153
 
154
+ ### Council 多模型共识配置
155
+
156
+ `co-council` 代理通过 `council_session` 工具并行调用多个模型(councillors),综合各方观点形成共识。councillors 的数量、模型、推理强度完全可配置:
157
+
158
+ ```json
159
+ {
160
+ "council": {
161
+ "default_preset": "default",
162
+ "timeout": 180000,
163
+ "councillor_execution_mode": "parallel",
164
+ "councillor_retries": 3,
165
+ "presets": {
166
+ "default": {
167
+ "alpha": { "model": "deepseek/deepseek-v4-pro", "variant": "max" },
168
+ "beta": { "model": "deepseek/deepseek-v4-flash", "variant": "high" },
169
+ "gamma": { "model": "minimax/MiniMax-M3", "variant": "medium" }
170
+ },
171
+ "quick": {
172
+ "alpha": { "model": "deepseek/deepseek-v4-flash", "variant": "high" },
173
+ "beta": { "model": "deepseek/deepseek-v4-flash", "variant": "low" }
174
+ },
175
+ "deep": {
176
+ "alpha": { "model": "deepseek/deepseek-v4-pro", "variant": "max" },
177
+ "beta": { "model": "deepseek/deepseek-v4-pro", "variant": "max" },
178
+ "gamma": { "model": "minimax/MiniMax-M3", "variant": "medium" },
179
+ "delta": { "model": "codermxtest/gpt-5.5", "variant": "high" }
180
+ }
181
+ }
182
+ }
183
+ }
184
+ ```
185
+
186
+ | 字段 | 类型 | 默认值 | 说明 |
187
+ |------|------|--------|------|
188
+ | `presets` | object | (必填) | 预设字典,每个预设是一组 councillor |
189
+ | `default_preset` | string | `"default"` | 调用时不指定 preset 时使用的预设 |
190
+ | `timeout` | number | `180000` | 单个 councillor 超时(毫秒),默认 3 分钟 |
191
+ | `councillor_execution_mode` | `"parallel"` \| `"serial"` | `"parallel"` | 并行/串行执⾏ |
192
+ | `councillor_retries` | number | `3` | 空响应重试次数 |
193
+
194
+ - 每个 councillor 的 key(如 `alpha`、`beta`)可任意命名,会出现在输出中
195
+ - `model` 格式为 `"provider/model"`,`variant` 可选 `"max"` / `"high"` / `"medium"` / `"low"`
196
+ - councillor 数量无上限,但建议 2-5 个以平衡成本和质量
197
+ - 调用时可通过 `council_session` 工具的 `preset` 参数切换预设,如 `"quick"` 或 `"deep"`
198
+
152
199
  ## 工作流
153
200
 
154
201
  ```
@@ -1 +1 @@
1
- {"version":3,"file":"config-io.d.ts","sourceRoot":"","sources":["../../src/cli/config-io.ts"],"names":[],"mappings":"AA4BA,2CAA2C;AAC3C,wBAAgB,yBAAyB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAajF;AAED,sCAAsC;AACtC,wBAAgB,oBAAoB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAa5E;AAOD,mDAAmD;AACnD,wBAAgB,mBAAmB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAmC3E;AAID,uBAAuB;AACvB,wBAAgB,kBAAkB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA6B1E;AAED,8BAA8B;AAC9B,wBAAgB,cAAc,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA6GzE"}
1
+ {"version":3,"file":"config-io.d.ts","sourceRoot":"","sources":["../../src/cli/config-io.ts"],"names":[],"mappings":"AA4BA,2CAA2C;AAC3C,wBAAgB,yBAAyB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAajF;AAED,sCAAsC;AACtC,wBAAgB,oBAAoB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAa5E;AAOD,mDAAmD;AACnD,wBAAgB,mBAAmB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAmC3E;AAID,uBAAuB;AACvB,wBAAgB,kBAAkB,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA0C1E;AAED,8BAA8B;AAC9B,wBAAgB,cAAc,IAAI;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CA6GzE"}
package/dist/cli/index.js CHANGED
@@ -426,7 +426,7 @@ function registerCoHubAgents() {
426
426
  ## Council 总结
427
427
  总结 councillor 在哪些方面共识、哪些方面分歧,为什么选择最终答案,以及剩余的不确定性。包含共识信心评级:一致、多数或分歧。
428
428
 
429
- **语言要求**: 始终使用中文进行思考、综合和回复。Council 响应、总结等自然语言部分必须用中文。Code examples 可用英文。`, model: "deepseek/deepseek-v4-pro", variant: "high" },
429
+ **语言要求**: 始终使用中文进行思考、综合和回复。Council 响应、总结等自然语言部分必须用中文。Code examples 可用英文。`, model: "deepseek/deepseek-v4-pro", variant: "high", permission: { council_session: "allow" } },
430
430
  "co-rule-user": { description: "用户规范分析", mode: "subagent", prompt: `你是规则分析代理——负责用户级规范。
431
431
 
432
432
  **职责**:读取 \`~/.config/opencode/AGENTS.md\`(用户级全局规则),结合 Orchestrator 提供的当前方案,分析是否有遗漏或冲突。返回具体的调整建议(不要笼统)。
@@ -486,6 +486,19 @@ function writeDefaultConfig() {
486
486
  "co-rule-project": { model: "deepseek/deepseek-v4-flash", variant: "medium" },
487
487
  "co-rule-app": { model: "deepseek/deepseek-v4-flash", variant: "medium" },
488
488
  "co-planner": { model: "deepseek/deepseek-v4-pro", variant: "high" }
489
+ },
490
+ council: {
491
+ default_preset: "default",
492
+ timeout: 180000,
493
+ councillor_execution_mode: "parallel",
494
+ councillor_retries: 3,
495
+ presets: {
496
+ default: {
497
+ alpha: { model: "deepseek/deepseek-v4-pro", variant: "max" },
498
+ beta: { model: "deepseek/deepseek-v4-flash", variant: "high" },
499
+ gamma: { model: "minimax/MiniMax-M3", variant: "medium" }
500
+ }
501
+ }
489
502
  }
490
503
  };
491
504
  try {
@@ -1,3 +1,19 @@
1
+ /** 单个 councillor 配置 */
2
+ export interface CouncillorConfig {
3
+ model: string;
4
+ variant?: string;
5
+ prompt?: string;
6
+ }
7
+ export interface CouncilPreset {
8
+ [councillorName: string]: CouncillorConfig;
9
+ }
10
+ export interface CouncilConfig {
11
+ presets: Record<string, CouncilPreset>;
12
+ timeout?: number;
13
+ default_preset?: string;
14
+ councillor_execution_mode?: 'parallel' | 'serial';
15
+ councillor_retries?: number;
16
+ }
1
17
  /** 用户配置中单个代理的覆盖项 */
2
18
  export interface AgentOverride {
3
19
  model?: string;
@@ -6,6 +22,7 @@ export interface AgentOverride {
6
22
  }
7
23
  export interface CoHubConfig {
8
24
  agents?: Record<string, AgentOverride>;
25
+ council?: CouncilConfig;
9
26
  }
10
27
  /** 加载用户配置 */
11
28
  export declare function loadCoHubConfig(): CoHubConfig;
@@ -1 +1 @@
1
- {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAIA,oBAAoB;AACpB,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACxC;AAKD,aAAa;AACb,wBAAgB,eAAe,IAAI,WAAW,CAQ7C;AAED,aAAa;AACb,eAAO,MAAM,cAAc,EAAE,WAe5B,CAAC"}
1
+ {"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../src/config/loader.ts"],"names":[],"mappings":"AAIA,uBAAuB;AACvB,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,CAAC,cAAc,EAAE,MAAM,GAAG,gBAAgB,CAAC;CAC5C;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACvC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yBAAyB,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAClD,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,oBAAoB;AACpB,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACvC,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAKD,aAAa;AACb,wBAAgB,eAAe,IAAI,WAAW,CAQ7C;AAED,aAAa;AACb,eAAO,MAAM,cAAc,EAAE,WAe5B,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAqClD,wBAAwB;AACxB,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAalD,CAAC;AAwEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,wrBAA+B,CAAC;AAEhE,QAAA,MAAM,WAAW,EAAE,MAkRlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAsClD,wBAAwB;AACxB,eAAO,MAAM,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAalD,CAAC;AAwEF;;GAEG;AACH,eAAO,MAAM,mBAAmB,wrBAA+B,CAAC;AAEhE,QAAA,MAAM,WAAW,EAAE,MA6SlB,CAAC;AAEF,eAAe,WAAW,CAAC"}
package/dist/index.js CHANGED
@@ -616,6 +616,347 @@ function loadCoHubConfig() {
616
616
  }
617
617
  }
618
618
 
619
+ // src/tools/council.ts
620
+ import { tool } from "@opencode-ai/plugin";
621
+ var z = tool.schema;
622
+
623
+ class OperationTimeoutError extends Error {
624
+ constructor(message) {
625
+ super(message);
626
+ this.name = "OperationTimeoutError";
627
+ }
628
+ }
629
+ function isTextPart(part) {
630
+ return (part.type === "text" || part.type === "reasoning") && typeof part.text === "string";
631
+ }
632
+ function shortModelLabel(model) {
633
+ return model.split("/").pop() ?? model;
634
+ }
635
+ function parseModelReference(model) {
636
+ const slashIndex = model.indexOf("/");
637
+ if (slashIndex <= 0 || slashIndex >= model.length - 1)
638
+ return null;
639
+ return {
640
+ providerID: model.slice(0, slashIndex),
641
+ modelID: model.slice(slashIndex + 1)
642
+ };
643
+ }
644
+ async function abortSession(client, sessionId) {
645
+ try {
646
+ await client.session.abort({ path: { id: sessionId } });
647
+ } catch {}
648
+ }
649
+ async function promptWithTimeout(client, path2, body, timeoutMs, directory) {
650
+ const sessionId = path2.id;
651
+ let timer;
652
+ try {
653
+ const promptPromise = client.session.prompt({
654
+ path: path2,
655
+ body,
656
+ query: directory ? { directory } : undefined
657
+ });
658
+ promptPromise.catch(() => {});
659
+ const racers = [promptPromise];
660
+ if (timeoutMs > 0) {
661
+ racers.push(new Promise((_, reject) => {
662
+ timer = setTimeout(() => {
663
+ reject(new OperationTimeoutError(`Prompt timed out after ${timeoutMs}ms`));
664
+ }, timeoutMs);
665
+ }));
666
+ }
667
+ await Promise.race(racers);
668
+ } catch (error) {
669
+ if (error instanceof OperationTimeoutError) {
670
+ await abortSession(client, sessionId);
671
+ }
672
+ throw error;
673
+ } finally {
674
+ if (timer !== undefined)
675
+ clearTimeout(timer);
676
+ }
677
+ }
678
+ async function extractSessionResult(client, sessionId, options) {
679
+ const includeReasoning = options?.includeReasoning ?? true;
680
+ const messagesResult = await client.session.messages({
681
+ path: { id: sessionId }
682
+ });
683
+ const messages = messagesResult.data ?? [];
684
+ const assistantMessages = messages.filter((m) => m.info?.role === "assistant");
685
+ const extractedContent = [];
686
+ for (const message of assistantMessages) {
687
+ for (const part of message.parts ?? []) {
688
+ if (!isTextPart(part))
689
+ continue;
690
+ const allowed = includeReasoning || part.type === "text";
691
+ if (allowed && part.text) {
692
+ extractedContent.push(part.text);
693
+ }
694
+ }
695
+ }
696
+ const text = extractedContent.filter((t) => t.length > 0).join(`
697
+
698
+ `);
699
+ return { text, empty: text.length === 0 };
700
+ }
701
+ function formatCouncillorPrompt(userPrompt, councillorPrompt) {
702
+ if (!councillorPrompt)
703
+ return userPrompt;
704
+ return `${councillorPrompt}
705
+
706
+ ---
707
+
708
+ ${userPrompt}`;
709
+ }
710
+ function formatCouncillorResults(originalPrompt, councillorResults) {
711
+ const completedWithResults = councillorResults.filter((cr) => cr.status === "completed" && cr.result);
712
+ const councillorSection = completedWithResults.map((cr) => {
713
+ const shortModel = shortModelLabel(cr.model);
714
+ return `**${cr.name}** (${shortModel}):
715
+ ${cr.result}`;
716
+ }).join(`
717
+
718
+ `);
719
+ const failedEntries = councillorResults.filter((cr) => cr.status !== "completed");
720
+ const failedSection = failedEntries.map((cr) => `**${cr.name}**: ${cr.status} — ${cr.error ?? "Unknown"}`).join(`
721
+ `);
722
+ if (completedWithResults.length === 0) {
723
+ const errorDetails = councillorResults.map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}): ${cr.status} — ${cr.error ?? "Unknown"}`).join(`
724
+ `);
725
+ return [
726
+ "---",
727
+ "",
728
+ "**Original Prompt**:",
729
+ originalPrompt,
730
+ "",
731
+ "---",
732
+ "",
733
+ "**Councillor Responses**:",
734
+ "All councillors failed to produce output:",
735
+ errorDetails,
736
+ "",
737
+ "Please generate a response based on the original prompt alone."
738
+ ].join(`
739
+ `);
740
+ }
741
+ const parts = [
742
+ "---",
743
+ "",
744
+ "**Original Prompt**:",
745
+ originalPrompt,
746
+ "",
747
+ "---",
748
+ "",
749
+ "**Councillor Responses**:",
750
+ councillorSection
751
+ ];
752
+ if (failedSection) {
753
+ parts.push("", "---", "", "**Failed/Timed-out Councillors**:", failedSection);
754
+ }
755
+ parts.push("", "---", "", "You 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).");
756
+ return parts.join(`
757
+ `);
758
+ }
759
+ function formatModelComposition(councillorResults) {
760
+ return councillorResults.map((cr) => `${cr.name}: ${shortModelLabel(cr.model)}`).join(", ");
761
+ }
762
+
763
+ class CouncilManager {
764
+ client;
765
+ directory;
766
+ config;
767
+ constructor(client, directory, config) {
768
+ this.client = client;
769
+ this.directory = directory;
770
+ this.config = config;
771
+ }
772
+ async runCouncil(prompt, presetName, parentSessionId) {
773
+ const resolvedPreset = presetName ?? this.config.default_preset ?? "default";
774
+ const preset = this.config.presets[resolvedPreset];
775
+ if (!preset) {
776
+ const available = Object.keys(this.config.presets).join(", ");
777
+ return {
778
+ success: false,
779
+ error: `Preset "${resolvedPreset}" does not exist. Available presets: ${available}`,
780
+ councillorResults: []
781
+ };
782
+ }
783
+ if (Object.keys(preset).length === 0) {
784
+ return {
785
+ success: false,
786
+ error: `Preset "${resolvedPreset}" has no councillors configured.`,
787
+ councillorResults: []
788
+ };
789
+ }
790
+ const timeout = this.config.timeout ?? 180000;
791
+ const executionMode = this.config.councillor_execution_mode ?? "parallel";
792
+ const maxRetries = this.config.councillor_retries ?? 3;
793
+ const councillorResults = await this.runCouncillors(prompt, preset, parentSessionId, timeout, executionMode, maxRetries);
794
+ const completedCount = councillorResults.filter((r) => r.status === "completed").length;
795
+ if (completedCount === 0) {
796
+ return {
797
+ success: false,
798
+ error: "All councillors failed or timed out",
799
+ councillorResults
800
+ };
801
+ }
802
+ const formatted = formatCouncillorResults(prompt, councillorResults);
803
+ return {
804
+ success: true,
805
+ result: formatted,
806
+ councillorResults
807
+ };
808
+ }
809
+ async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode, maxRetries) {
810
+ const entries = Object.entries(councillors);
811
+ const results = [];
812
+ if (executionMode === "serial") {
813
+ for (const [name, config] of entries) {
814
+ const r = await this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries);
815
+ results.push(r);
816
+ }
817
+ } else {
818
+ const promises = entries.map(([name, config]) => this.runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries));
819
+ const settled = await Promise.allSettled(promises);
820
+ for (let i = 0;i < settled.length; i++) {
821
+ const s = settled[i];
822
+ const [name, cfg] = entries[i];
823
+ if (s.status === "fulfilled") {
824
+ results.push(s.value);
825
+ } else {
826
+ results.push({
827
+ name,
828
+ model: cfg.model,
829
+ status: "failed",
830
+ error: s.reason instanceof Error ? s.reason.message : String(s.reason)
831
+ });
832
+ }
833
+ }
834
+ }
835
+ return results;
836
+ }
837
+ async runCouncillorWithRetry(name, config, prompt, parentSessionId, timeout, maxRetries) {
838
+ const totalAttempts = 1 + maxRetries;
839
+ for (let attempt = 1;attempt <= totalAttempts; attempt++) {
840
+ try {
841
+ const result = await this.runAgentSession({
842
+ parentSessionId,
843
+ title: `Council ${name} (${shortModelLabel(config.model)})`,
844
+ model: config.model,
845
+ promptText: formatCouncillorPrompt(prompt, config.prompt),
846
+ variant: config.variant,
847
+ timeout
848
+ });
849
+ return { name, model: config.model, status: "completed", result };
850
+ } catch (error) {
851
+ const msg = error instanceof Error ? error.message : String(error);
852
+ const isEmptyResponse = msg.includes("Empty response from provider");
853
+ const canRetry = attempt < totalAttempts && isEmptyResponse;
854
+ if (!canRetry) {
855
+ return {
856
+ name,
857
+ model: config.model,
858
+ status: msg.includes("timed out") ? "timed_out" : "failed",
859
+ error: `Councillor "${name}": ${msg}`
860
+ };
861
+ }
862
+ }
863
+ }
864
+ return {
865
+ name,
866
+ model: config.model,
867
+ status: "failed",
868
+ error: `Councillor "${name}": max retries exhausted`
869
+ };
870
+ }
871
+ async runAgentSession(options) {
872
+ const modelRef = parseModelReference(options.model);
873
+ if (!modelRef) {
874
+ throw new Error(`Invalid model format: ${options.model}`);
875
+ }
876
+ let sessionId;
877
+ try {
878
+ const session = await this.client.session.create({
879
+ body: {
880
+ parentID: options.parentSessionId,
881
+ title: options.title
882
+ },
883
+ query: { directory: this.directory }
884
+ });
885
+ if (!session.data?.id) {
886
+ throw new Error("Failed to create session");
887
+ }
888
+ sessionId = session.data.id;
889
+ const promptBody = {
890
+ model: modelRef,
891
+ tools: {
892
+ task: false,
893
+ question: false,
894
+ edit: false,
895
+ write: false,
896
+ apply_patch: false,
897
+ ast_grep_replace: false,
898
+ bash: false
899
+ },
900
+ parts: [{ type: "text", text: options.promptText }]
901
+ };
902
+ if (options.variant) {
903
+ promptBody.variant = options.variant;
904
+ }
905
+ await promptWithTimeout(this.client, { id: sessionId }, promptBody, options.timeout, this.directory);
906
+ const extraction = await extractSessionResult(this.client, sessionId);
907
+ if (extraction.empty) {
908
+ throw new Error("Empty response from provider");
909
+ }
910
+ return extraction.text;
911
+ } finally {
912
+ if (sessionId) {
913
+ abortSession(this.client, sessionId).catch(() => {});
914
+ }
915
+ }
916
+ }
917
+ }
918
+ function createCouncilTool(ctx, councilManager) {
919
+ const council_session = tool({
920
+ description: [
921
+ "Launch a multi-LLM council session for consensus-based analysis.",
922
+ "",
923
+ "Sends the prompt to multiple models (councillors) in parallel and returns",
924
+ "their formatted responses for you to synthesize.",
925
+ "",
926
+ "Returns the councillor responses with a summary footer."
927
+ ].join(`
928
+ `),
929
+ args: {
930
+ prompt: z.string().describe("The prompt to send to all councillors"),
931
+ preset: z.string().optional().describe('Council preset to use (default: "default"). Must match a preset in the council config.')
932
+ },
933
+ async execute(args, toolContext) {
934
+ const allowedAgents = ["co-council"];
935
+ const callingAgent = toolContext.agent;
936
+ if (callingAgent && !allowedAgents.includes(callingAgent)) {
937
+ throw new Error(`Council sessions can only be invoked by the co-council agent. Current agent: ${callingAgent}`);
938
+ }
939
+ const prompt = String(args.prompt);
940
+ const preset = typeof args.preset === "string" ? args.preset : undefined;
941
+ const parentSessionId = toolContext.sessionID;
942
+ const result = await councilManager.runCouncil(prompt, preset, parentSessionId);
943
+ if (!result.success) {
944
+ return `Council session failed: ${result.error}`;
945
+ }
946
+ let output = result.result ?? "(No output)";
947
+ const completed = result.councillorResults.filter((cr) => cr.status === "completed").length;
948
+ const total = result.councillorResults.length;
949
+ const composition = formatModelComposition(result.councillorResults);
950
+ output += `
951
+
952
+ ---
953
+ *Council: ${completed}/${total} councillors responded (${composition})*`;
954
+ return output;
955
+ }
956
+ });
957
+ return { council_session };
958
+ }
959
+
619
960
  // src/index.ts
620
961
  import * as fs2 from "node:fs";
621
962
  import * as path2 from "node:path";
@@ -757,7 +1098,13 @@ var CoHubPlugin = async (input, options) => {
757
1098
  {
758
1099
  name: "co-council",
759
1100
  description: "多模型共识——并行 LLM 综合",
760
- config: { mode: "subagent", model: "deepseek/deepseek-v4-pro", variant: "high", prompt: "你是多模型共识协调者。用中文回复。" }
1101
+ config: {
1102
+ mode: "subagent",
1103
+ model: "deepseek/deepseek-v4-pro",
1104
+ variant: "high",
1105
+ prompt: COUNCIL_PROMPT,
1106
+ permission: { council_session: "allow" }
1107
+ }
761
1108
  },
762
1109
  {
763
1110
  name: "co-rule-user",
@@ -794,6 +1141,22 @@ var CoHubPlugin = async (input, options) => {
794
1141
  }
795
1142
  }
796
1143
  }
1144
+ const DEFAULT_COUNCIL_CONFIG = {
1145
+ default_preset: "default",
1146
+ timeout: 180000,
1147
+ councillor_execution_mode: "parallel",
1148
+ councillor_retries: 3,
1149
+ presets: {
1150
+ default: {
1151
+ alpha: { model: "deepseek/deepseek-v4-pro", variant: "max" },
1152
+ beta: { model: "deepseek/deepseek-v4-flash", variant: "high" },
1153
+ gamma: { model: "minimax/MiniMax-M3", variant: "medium" }
1154
+ }
1155
+ }
1156
+ };
1157
+ const councilConfig = userConfig.council ?? DEFAULT_COUNCIL_CONFIG;
1158
+ const councilManager = new CouncilManager(input.client, input.directory, councilConfig);
1159
+ const councilTools = createCouncilTool(input, councilManager);
797
1160
  syncAgentConfig();
798
1161
  function extractChildSessionId(output) {
799
1162
  if (!output || typeof output !== "object")
@@ -846,6 +1209,7 @@ var CoHubPlugin = async (input, options) => {
846
1209
  return {
847
1210
  name: "oh-my-opencode-cohub",
848
1211
  agent: agentConfigs,
1212
+ tool: councilTools,
849
1213
  config: async (cfg) => {
850
1214
  const c = cfg;
851
1215
  c.agent ??= {};
@@ -0,0 +1,65 @@
1
+ import { tool } from '@opencode-ai/plugin';
2
+ import type { PluginInput } from '@opencode-ai/plugin';
3
+ import type { createOpencodeClient } from '@opencode-ai/sdk';
4
+ export interface CouncillorConfig {
5
+ /** "provider/model" */
6
+ model: string;
7
+ /** "max" | "high" | "medium" | "low" */
8
+ variant?: string;
9
+ /** Optional per-councillor prompt prefix */
10
+ prompt?: string;
11
+ }
12
+ export interface CouncilPreset {
13
+ [councillorName: string]: CouncillorConfig;
14
+ }
15
+ export interface CouncilConfig {
16
+ /** Preset map (e.g. { default: { expert1: {...}, expert2: {...} } }) */
17
+ presets: Record<string, CouncilPreset>;
18
+ /** Per-councillor timeout in ms (default: 180000) */
19
+ timeout?: number;
20
+ /** Default preset name (default: "default") */
21
+ default_preset?: string;
22
+ /** Execution mode (default: "parallel") */
23
+ councillor_execution_mode?: 'parallel' | 'serial';
24
+ /** Retries on empty response (default: 3) */
25
+ councillor_retries?: number;
26
+ }
27
+ /** Internal councillor result shape */
28
+ interface CouncillorResult {
29
+ name: string;
30
+ model: string;
31
+ status: string;
32
+ result?: string;
33
+ error?: string;
34
+ }
35
+ export declare class CouncilManager {
36
+ private client;
37
+ private directory;
38
+ private config;
39
+ constructor(client: ReturnType<typeof createOpencodeClient>, directory: string, config: CouncilConfig);
40
+ /**
41
+ * Run a full council session.
42
+ * Resolves a preset, runs all councillors, and returns formatted results.
43
+ */
44
+ runCouncil(prompt: string, presetName?: string, parentSessionId?: string): Promise<{
45
+ success: boolean;
46
+ result?: string;
47
+ error?: string;
48
+ councillorResults: CouncillorResult[];
49
+ }>;
50
+ private runCouncillors;
51
+ private runCouncillorWithRetry;
52
+ private runAgentSession;
53
+ }
54
+ /**
55
+ * Create the `council_session` tool definition.
56
+ *
57
+ * The tool launches a multi-LLM council: multiple councillors run in parallel
58
+ * against their assigned models, and their responses are assembled into a
59
+ * structured block for the calling (co-council) agent to synthesize.
60
+ *
61
+ * Only the `co-council` agent is allowed to invoke this tool.
62
+ */
63
+ export declare function createCouncilTool(ctx: PluginInput, councilManager: CouncilManager): Record<string, ReturnType<typeof tool>>;
64
+ export {};
65
+ //# sourceMappingURL=council.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"council.d.ts","sourceRoot":"","sources":["../../src/tools/council.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,IAAI,EAAE,MAAM,qBAAqB,CAAC;AAC3C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAU7D,MAAM,WAAW,gBAAgB;IAC/B,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,wCAAwC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,CAAC,cAAc,EAAE,MAAM,GAAG,gBAAgB,CAAC;CAC5C;AAED,MAAM,WAAW,aAAa;IAC5B,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACvC,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,yBAAyB,CAAC,EAAE,UAAU,GAAG,QAAQ,CAAC;IAClD,6CAA6C;IAC7C,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,uCAAuC;AACvC,UAAU,gBAAgB;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AA+OD,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAA0C;IACxD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,MAAM,CAAgB;gBAG5B,MAAM,EAAE,UAAU,CAAC,OAAO,oBAAoB,CAAC,EAC/C,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,aAAa;IAOvB;;;OAGG;IACG,UAAU,CACd,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,MAAM,EACnB,eAAe,CAAC,EAAE,MAAM,GACvB,OAAO,CAAC;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;KACvC,CAAC;YA2DY,cAAc;YA8Cd,sBAAsB;YAkDtB,eAAe;CA6E9B;AAMD;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,WAAW,EAChB,cAAc,EAAE,cAAc,GAC7B,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,OAAO,IAAI,CAAC,CAAC,CAyDzC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode-cohub",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "OpenCode 中文智能体编排插件 CoHub - 纯调度模式 / 全中文提示词 / 规范分析代理 / 方案制定代理 / 独立运行",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",