opencouncil 0.1.0
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/LICENSE +21 -0
- package/README.md +108 -0
- package/dist/index.js +11 -0
- package/dist/lib/config.js +13 -0
- package/dist/lib/models.js +4 -0
- package/dist/lib/schema.js +14 -0
- package/dist/lib/synthesize.js +20 -0
- package/dist/tools/init.js +51 -0
- package/dist/tools/run.js +116 -0
- package/package.json +28 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 OpenCouncil Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# OpenCouncil Plugin for OpenCode
|
|
2
|
+
|
|
3
|
+
A multi-model council plugin for OpenCode that runs prompts across multiple LLMs and synthesizes their responses to identify consensus and discrepancies.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add the plugin to your `opencode.json`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"plugin": ["opencode-opencouncil"]
|
|
12
|
+
}
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Then restart OpenCode. The plugin will be automatically installed via Bun.
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
Add the configuration to your `opencode.json`:
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"plugin": ["opencode-opencouncil"],
|
|
24
|
+
"opencouncil": {
|
|
25
|
+
"defaultCouncil": "standard",
|
|
26
|
+
"councils": {
|
|
27
|
+
"standard": {
|
|
28
|
+
"members": [
|
|
29
|
+
{ "name": "gemini", "model": "google/gemini-2.0-flash", "temperature": 0.2 },
|
|
30
|
+
{ "name": "codex", "model": "openai/codex", "temperature": 0.2 },
|
|
31
|
+
{ "name": "opus", "model": "anthropic/claude-opus-4", "temperature": 0.2 }
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Council Member Options
|
|
40
|
+
|
|
41
|
+
Each member supports:
|
|
42
|
+
- `name`: Identifier for the member
|
|
43
|
+
- `model`: Provider/model ID (format: `provider/model`)
|
|
44
|
+
- `temperature`: Sampling temperature (optional)
|
|
45
|
+
- `system`: Custom system prompt (optional)
|
|
46
|
+
- `options`: Additional model options (optional)
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Tool: `opencouncil.run`
|
|
51
|
+
|
|
52
|
+
Run a prompt through the configured council:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Use tool: opencouncil.run
|
|
56
|
+
Prompt: "Explain the benefits of TypeScript"
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Optional parameters:
|
|
60
|
+
- `council`: Specify a council name (defaults to `defaultCouncil`)
|
|
61
|
+
|
|
62
|
+
### Tool: `opencouncil.init`
|
|
63
|
+
|
|
64
|
+
Initialize configuration and optionally create an agent file:
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
Use tool: opencouncil.init
|
|
68
|
+
writeAgentFile: true
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
This will:
|
|
72
|
+
1. Print the configuration block to add to your `opencode.json`
|
|
73
|
+
2. If `writeAgentFile` is true, create `.opencode/agents/opencouncil.md`
|
|
74
|
+
|
|
75
|
+
### Agent: `@opencouncil`
|
|
76
|
+
|
|
77
|
+
If you ran `opencouncil.init` with `writeAgentFile: true`, you can use:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
@opencouncil "What are the best practices for error handling?"
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The agent will automatically call `opencouncil.run` and summarize the results.
|
|
84
|
+
|
|
85
|
+
## Output Format
|
|
86
|
+
|
|
87
|
+
The plugin returns:
|
|
88
|
+
1. **Member responses** - Individual answers from each model
|
|
89
|
+
2. **Consensus** - Claims endorsed by multiple members
|
|
90
|
+
3. **Discrepancies** - Claims mentioned by only one member
|
|
91
|
+
4. **Uncertainties** - Each member's expressed uncertainties
|
|
92
|
+
|
|
93
|
+
## Troubleshooting
|
|
94
|
+
|
|
95
|
+
### Plugin installation stuck
|
|
96
|
+
|
|
97
|
+
Clear the cache and restart:
|
|
98
|
+
```bash
|
|
99
|
+
rm -rf ~/.cache/opencode
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Model not found
|
|
103
|
+
|
|
104
|
+
Ensure the model IDs match those available in your OpenCode installation. Use OpenCode's CLI commands to discover available models.
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { makeRunTool } from "./tools/run.js";
|
|
2
|
+
import { makeInitTool } from "./tools/init.js";
|
|
3
|
+
const plugin = async (input) => {
|
|
4
|
+
return {
|
|
5
|
+
tool: {
|
|
6
|
+
"opencouncil.run": makeRunTool(input),
|
|
7
|
+
"opencouncil.init": makeInitTool(input)
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
export default plugin;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function getCouncil(cfg, requested) {
|
|
2
|
+
const oc = cfg.opencouncil ?? {};
|
|
3
|
+
const councilName = requested ?? oc.defaultCouncil ?? "standard";
|
|
4
|
+
const council = oc.councils?.[councilName];
|
|
5
|
+
const members = council?.members?.length
|
|
6
|
+
? council.members
|
|
7
|
+
: [
|
|
8
|
+
{ name: "gemini", model: "google/gemini-2.0-flash", temperature: 0.2 },
|
|
9
|
+
{ name: "codex", model: "openai/codex", temperature: 0.2 },
|
|
10
|
+
{ name: "opus", model: "anthropic/claude-opus-4", temperature: 0.2 }
|
|
11
|
+
];
|
|
12
|
+
return { councilName, members, rubric: council?.rubric };
|
|
13
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const memberOutputFormat = {
|
|
2
|
+
type: "json_schema",
|
|
3
|
+
name: "member_response",
|
|
4
|
+
schema: {
|
|
5
|
+
type: "object",
|
|
6
|
+
additionalProperties: false,
|
|
7
|
+
properties: {
|
|
8
|
+
answer: { type: "string" },
|
|
9
|
+
claims: { type: "array", items: { type: "string" } },
|
|
10
|
+
uncertainties: { type: "array", items: { type: "string" } }
|
|
11
|
+
},
|
|
12
|
+
required: ["answer", "claims", "uncertainties"]
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function normalize(s) {
|
|
2
|
+
return s.trim().toLowerCase();
|
|
3
|
+
}
|
|
4
|
+
export function synthesize(results) {
|
|
5
|
+
const claimIndex = new Map();
|
|
6
|
+
for (const r of results) {
|
|
7
|
+
for (const c of r.claims) {
|
|
8
|
+
const key = normalize(c);
|
|
9
|
+
const entry = claimIndex.get(key) ?? { claim: c, by: [] };
|
|
10
|
+
entry.by.push({ member: r.member, model: r.model });
|
|
11
|
+
claimIndex.set(key, entry);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const allClaims = [...claimIndex.values()].sort((a, b) => b.by.length - a.by.length);
|
|
15
|
+
const agreed = allClaims.filter(x => x.by.length > 1);
|
|
16
|
+
const disputedCandidates = allClaims.filter(x => x.by.length === 1);
|
|
17
|
+
// Youll improve this with semantic clustering / contradiction detection later.
|
|
18
|
+
const discrepancies = disputedCandidates.slice(0, 10);
|
|
19
|
+
return { agreed, discrepancies };
|
|
20
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
export function makeInitTool(pluginInput) {
|
|
3
|
+
return tool({
|
|
4
|
+
description: "Prints (and optionally writes) starter config + agent file for OpenCouncil.",
|
|
5
|
+
args: {
|
|
6
|
+
writeAgentFile: tool.schema.boolean().optional().describe("If true, writes .opencode/agents/opencouncil.md")
|
|
7
|
+
},
|
|
8
|
+
async execute(args, ctx) {
|
|
9
|
+
const { $, directory } = pluginInput;
|
|
10
|
+
if (args.writeAgentFile) {
|
|
11
|
+
await $ `mkdir -p ${directory}/.opencode/agents`;
|
|
12
|
+
await $ `cat > ${directory}/.opencode/agents/opencouncil.md << 'EOF'
|
|
13
|
+
---
|
|
14
|
+
description: Runs a multi-model council and synthesizes discrepancies
|
|
15
|
+
mode: primary
|
|
16
|
+
tools:
|
|
17
|
+
write: false
|
|
18
|
+
edit: false
|
|
19
|
+
bash: false
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
When asked a question, call the tool opencouncil.run with the users prompt.
|
|
23
|
+
Then summarize consensus and list key discrepancies.
|
|
24
|
+
EOF`;
|
|
25
|
+
}
|
|
26
|
+
return [
|
|
27
|
+
"Add this to your opencode.json (top-level):",
|
|
28
|
+
"",
|
|
29
|
+
"```jsonc",
|
|
30
|
+
"{",
|
|
31
|
+
' "plugin": ["opencode-opencouncil"],',
|
|
32
|
+
' "opencouncil": {',
|
|
33
|
+
' "defaultCouncil": "standard",',
|
|
34
|
+
' "councils": {',
|
|
35
|
+
' "standard": {',
|
|
36
|
+
' "members": [',
|
|
37
|
+
' { "name": "gemini", "model": "google/<your-gemini-model>", "temperature": 0.2 },',
|
|
38
|
+
' { "name": "codex", "model": "openai/<your-codex-model>", "temperature": 0.2 },',
|
|
39
|
+
' { "name": "opus", "model": "anthropic/<your-opus-model>", "temperature": 0.2 }',
|
|
40
|
+
" ]",
|
|
41
|
+
" }",
|
|
42
|
+
" }",
|
|
43
|
+
" }",
|
|
44
|
+
"}",
|
|
45
|
+
"```",
|
|
46
|
+
"",
|
|
47
|
+
"Then restart OpenCode (it will auto-install npm plugins)."
|
|
48
|
+
].join("\n");
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { getCouncil } from "../lib/config.js";
|
|
3
|
+
import { splitModel } from "../lib/models.js";
|
|
4
|
+
import { synthesize } from "../lib/synthesize.js";
|
|
5
|
+
function isTextPart(part) {
|
|
6
|
+
return (typeof part === "object" &&
|
|
7
|
+
part !== null &&
|
|
8
|
+
"type" in part &&
|
|
9
|
+
part.type === "text" &&
|
|
10
|
+
"text" in part &&
|
|
11
|
+
typeof part.text === "string");
|
|
12
|
+
}
|
|
13
|
+
const MEMBER_SYSTEM_PROMPT = `You are a council member evaluating a prompt. Respond with a JSON object containing:
|
|
14
|
+
- answer: your detailed response to the prompt
|
|
15
|
+
- claims: an array of key factual claims you make (as strings)
|
|
16
|
+
- uncertainties: an array of anything you're uncertain about (as strings)
|
|
17
|
+
|
|
18
|
+
Format your response as valid JSON only, no markdown code blocks.`;
|
|
19
|
+
export function makeRunTool(pluginInput) {
|
|
20
|
+
return tool({
|
|
21
|
+
description: "Runs multiple configured models on a prompt, then summarizes and identifies discrepancies.",
|
|
22
|
+
args: {
|
|
23
|
+
prompt: tool.schema.string().describe("The prompt to evaluate"),
|
|
24
|
+
council: tool.schema.string().optional().describe("Council name (defaults to configured defaultCouncil)")
|
|
25
|
+
},
|
|
26
|
+
async execute(args, ctx) {
|
|
27
|
+
const { client } = pluginInput;
|
|
28
|
+
const configResp = await client.config.get();
|
|
29
|
+
const cfg = configResp.data;
|
|
30
|
+
const { councilName, members } = getCouncil(cfg, args.council);
|
|
31
|
+
const results = await Promise.all(members.map(async (m) => {
|
|
32
|
+
const { providerID, modelID } = splitModel(m.model);
|
|
33
|
+
const systemPrompt = m.system?.trim()
|
|
34
|
+
? `${m.system.trim()}\n\n${MEMBER_SYSTEM_PROMPT}`
|
|
35
|
+
: MEMBER_SYSTEM_PROMPT;
|
|
36
|
+
const res = await client.session.prompt({
|
|
37
|
+
path: { id: cfg.session?.id ?? "current" },
|
|
38
|
+
body: {
|
|
39
|
+
model: { providerID, modelID },
|
|
40
|
+
system: systemPrompt,
|
|
41
|
+
parts: [{ type: "text", text: args.prompt }]
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
const data = res.data;
|
|
45
|
+
if (!data) {
|
|
46
|
+
return {
|
|
47
|
+
member: m.name,
|
|
48
|
+
model: m.model,
|
|
49
|
+
answer: "",
|
|
50
|
+
claims: [],
|
|
51
|
+
uncertainties: []
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Use unknown to bypass TypeScript's strict Part union type checking
|
|
55
|
+
const parts = data.parts;
|
|
56
|
+
const textPart = parts.find((p) => isTextPart(p));
|
|
57
|
+
const text = textPart?.text ?? "{}";
|
|
58
|
+
let parsed;
|
|
59
|
+
try {
|
|
60
|
+
// Try to extract JSON from markdown code blocks if present
|
|
61
|
+
const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/)
|
|
62
|
+
|| text.match(/\{[\s\S]*\}/);
|
|
63
|
+
const jsonText = jsonMatch ? jsonMatch[1] || jsonMatch[0] : text;
|
|
64
|
+
parsed = JSON.parse(jsonText);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
parsed = {
|
|
68
|
+
answer: text,
|
|
69
|
+
claims: [],
|
|
70
|
+
uncertainties: ["Failed to parse structured response"]
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
member: m.name,
|
|
75
|
+
model: m.model,
|
|
76
|
+
answer: parsed.answer ?? text ?? "",
|
|
77
|
+
claims: parsed.claims ?? [],
|
|
78
|
+
uncertainties: parsed.uncertainties ?? []
|
|
79
|
+
};
|
|
80
|
+
}));
|
|
81
|
+
const { agreed, discrepancies } = synthesize(results);
|
|
82
|
+
const lines = [];
|
|
83
|
+
lines.push(`# OpenCouncil (${councilName})`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
lines.push("## Member responses");
|
|
86
|
+
for (const r of results) {
|
|
87
|
+
lines.push(`### ${r.member} (${r.model})`);
|
|
88
|
+
lines.push(r.answer || "_(no answer)_");
|
|
89
|
+
if (r.uncertainties?.length) {
|
|
90
|
+
lines.push("");
|
|
91
|
+
lines.push("**Uncertainties:**");
|
|
92
|
+
for (const u of r.uncertainties)
|
|
93
|
+
lines.push(`- ${u}`);
|
|
94
|
+
}
|
|
95
|
+
lines.push("");
|
|
96
|
+
}
|
|
97
|
+
lines.push("## Consensus (claims repeated by >1 member)");
|
|
98
|
+
if (!agreed.length)
|
|
99
|
+
lines.push("- _(none detected)_");
|
|
100
|
+
for (const a of agreed) {
|
|
101
|
+
lines.push(`- ${a.claim} _(endorsed by: ${a.by.map(x => x.member).join(", ")})_`);
|
|
102
|
+
}
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push("## Discrepancies (claims only mentioned by one member)");
|
|
105
|
+
if (!discrepancies.length)
|
|
106
|
+
lines.push("- _(none detected)_");
|
|
107
|
+
for (const d of discrepancies) {
|
|
108
|
+
lines.push(`- ${d.claim} _(only: ${d.by[0].member})_`);
|
|
109
|
+
}
|
|
110
|
+
lines.push("");
|
|
111
|
+
lines.push("## Next improvement");
|
|
112
|
+
lines.push("- For higher quality discrepancy detection, add a second-pass judge prompt that clusters claims semantically and flags contradictions.");
|
|
113
|
+
return lines.join("\n");
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "opencouncil",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"exports": "./dist/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"README.md",
|
|
10
|
+
"LICENSE"
|
|
11
|
+
],
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@opencode-ai/plugin": "^1.1.65"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"typescript": "^5.5.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"opencode",
|
|
20
|
+
"plugin",
|
|
21
|
+
"agents",
|
|
22
|
+
"llm"
|
|
23
|
+
],
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "tsc -p tsconfig.json"
|
|
27
|
+
}
|
|
28
|
+
}
|