opencode-orchestrator 0.2.2 → 0.2.3
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/dist/core/state.d.ts +2 -0
- package/dist/index.d.ts +5 -6
- package/dist/index.js +185 -2
- package/dist/utils/sanity.d.ts +31 -0
- package/package.json +1 -1
package/dist/core/state.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode Orchestrator Plugin
|
|
3
3
|
*
|
|
4
|
-
* 5-Agent
|
|
5
|
-
*
|
|
6
|
-
* Optimized for weak models through:
|
|
4
|
+
* This is the main entry point for the 5-Agent structured architecture.
|
|
5
|
+
* We've optimized it for weaker models by using:
|
|
7
6
|
* - XML-structured prompts with clear boundaries
|
|
8
|
-
* - Explicit reasoning patterns (THINK
|
|
7
|
+
* - Explicit reasoning patterns (THINK -> ACT -> OBSERVE -> ADJUST)
|
|
9
8
|
* - Evidence-based completion requirements
|
|
10
|
-
* - Autonomous execution loop
|
|
9
|
+
* - Autonomous execution loop that keeps going until done
|
|
11
10
|
*
|
|
12
|
-
*
|
|
11
|
+
* The agents are: Commander, Architect, Builder, Inspector, Recorder
|
|
13
12
|
*/
|
|
14
13
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
15
14
|
declare const OrchestratorPlugin: (input: PluginInput) => Promise<{
|
package/dist/index.js
CHANGED
|
@@ -780,6 +780,110 @@ function detectSlashCommand(text) {
|
|
|
780
780
|
return { command: match[1], args: match[2] || "" };
|
|
781
781
|
}
|
|
782
782
|
|
|
783
|
+
// src/utils/sanity.ts
|
|
784
|
+
function checkOutputSanity(text) {
|
|
785
|
+
if (!text || text.length < 50) {
|
|
786
|
+
return { isHealthy: true, severity: "ok" };
|
|
787
|
+
}
|
|
788
|
+
if (/(.)\1{15,}/.test(text)) {
|
|
789
|
+
return {
|
|
790
|
+
isHealthy: false,
|
|
791
|
+
reason: "Single character repetition detected",
|
|
792
|
+
severity: "critical"
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
if (/(.{2,6})\1{8,}/.test(text)) {
|
|
796
|
+
return {
|
|
797
|
+
isHealthy: false,
|
|
798
|
+
reason: "Pattern loop detected",
|
|
799
|
+
severity: "critical"
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
if (text.length > 200) {
|
|
803
|
+
const cleanText = text.replace(/\s/g, "");
|
|
804
|
+
if (cleanText.length > 100) {
|
|
805
|
+
const uniqueChars = new Set(cleanText).size;
|
|
806
|
+
const ratio = uniqueChars / cleanText.length;
|
|
807
|
+
if (ratio < 0.02) {
|
|
808
|
+
return {
|
|
809
|
+
isHealthy: false,
|
|
810
|
+
reason: "Low information density",
|
|
811
|
+
severity: "critical"
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
const boxChars = (text.match(/[\u2500-\u257f\u2580-\u259f\u2800-\u28ff]/g) || []).length;
|
|
817
|
+
if (boxChars > 100 && boxChars / text.length > 0.3) {
|
|
818
|
+
return {
|
|
819
|
+
isHealthy: false,
|
|
820
|
+
reason: "Visual gibberish detected",
|
|
821
|
+
severity: "critical"
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 10);
|
|
825
|
+
if (lines.length > 10) {
|
|
826
|
+
const lineSet = new Set(lines);
|
|
827
|
+
if (lineSet.size < lines.length * 0.2) {
|
|
828
|
+
return {
|
|
829
|
+
isHealthy: false,
|
|
830
|
+
reason: "Excessive line repetition",
|
|
831
|
+
severity: "warning"
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
const cjkChars = (text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []).length;
|
|
836
|
+
if (cjkChars > 200) {
|
|
837
|
+
const uniqueCjk = new Set(
|
|
838
|
+
text.match(/[\u4e00-\u9fff\u3400-\u4dbf]/g) || []
|
|
839
|
+
).size;
|
|
840
|
+
if (uniqueCjk < 10 && cjkChars / uniqueCjk > 20) {
|
|
841
|
+
return {
|
|
842
|
+
isHealthy: false,
|
|
843
|
+
reason: "CJK character spam detected",
|
|
844
|
+
severity: "critical"
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return { isHealthy: true, severity: "ok" };
|
|
849
|
+
}
|
|
850
|
+
var RECOVERY_PROMPT = `<anomaly_recovery>
|
|
851
|
+
\u26A0\uFE0F SYSTEM NOTICE: Previous output was malformed (gibberish/loop detected).
|
|
852
|
+
|
|
853
|
+
<recovery_protocol>
|
|
854
|
+
1. DISCARD the corrupted output completely - do not reference it
|
|
855
|
+
2. RECALL the original mission objective
|
|
856
|
+
3. IDENTIFY the last confirmed successful step
|
|
857
|
+
4. RESTART with a simpler, more focused approach
|
|
858
|
+
</recovery_protocol>
|
|
859
|
+
|
|
860
|
+
<instructions>
|
|
861
|
+
- If a sub-agent produced bad output: try a different agent or simpler task
|
|
862
|
+
- If stuck in a loop: break down the task into smaller pieces
|
|
863
|
+
- If context seems corrupted: call recorder to restore context
|
|
864
|
+
- THINK in English for maximum stability
|
|
865
|
+
</instructions>
|
|
866
|
+
|
|
867
|
+
What was the original task? Proceed from the last known good state.
|
|
868
|
+
</anomaly_recovery>`;
|
|
869
|
+
var ESCALATION_PROMPT = `<critical_anomaly>
|
|
870
|
+
\u{1F6A8} CRITICAL: Multiple consecutive malformed outputs detected.
|
|
871
|
+
|
|
872
|
+
<emergency_protocol>
|
|
873
|
+
1. STOP current execution path immediately
|
|
874
|
+
2. DO NOT continue with the same approach - it is failing
|
|
875
|
+
3. CALL architect for a completely new strategy
|
|
876
|
+
4. If architect also fails: report status to user and await guidance
|
|
877
|
+
</emergency_protocol>
|
|
878
|
+
|
|
879
|
+
<diagnosis>
|
|
880
|
+
The current approach is producing corrupted output.
|
|
881
|
+
This may indicate: context overload, model instability, or task complexity.
|
|
882
|
+
</diagnosis>
|
|
883
|
+
|
|
884
|
+
Request a fresh plan from architect with reduced scope.
|
|
885
|
+
</critical_anomaly>`;
|
|
886
|
+
|
|
783
887
|
// src/index.ts
|
|
784
888
|
var DEFAULT_MAX_STEPS = 500;
|
|
785
889
|
var TASK_COMMAND_MAX_STEPS = 1e3;
|
|
@@ -810,12 +914,18 @@ var OrchestratorPlugin = async (input) => {
|
|
|
810
914
|
const { directory, client } = input;
|
|
811
915
|
const sessions = /* @__PURE__ */ new Map();
|
|
812
916
|
return {
|
|
917
|
+
// -----------------------------------------------------------------
|
|
918
|
+
// Tools we expose to the LLM
|
|
919
|
+
// -----------------------------------------------------------------
|
|
813
920
|
tool: {
|
|
814
921
|
call_agent: callAgentTool,
|
|
815
922
|
slashcommand: createSlashcommandTool(),
|
|
816
923
|
grep_search: grepSearchTool(directory),
|
|
817
924
|
glob_search: globSearchTool(directory)
|
|
818
925
|
},
|
|
926
|
+
// -----------------------------------------------------------------
|
|
927
|
+
// Config hook - registers our commands and agents with OpenCode
|
|
928
|
+
// -----------------------------------------------------------------
|
|
819
929
|
config: async (config) => {
|
|
820
930
|
const existingCommands = config.command ?? {};
|
|
821
931
|
const existingAgents = config.agent ?? {};
|
|
@@ -837,6 +947,10 @@ var OrchestratorPlugin = async (input) => {
|
|
|
837
947
|
config.command = { ...orchestratorCommands, ...existingCommands };
|
|
838
948
|
config.agent = { ...orchestratorAgents, ...existingAgents };
|
|
839
949
|
},
|
|
950
|
+
// -----------------------------------------------------------------
|
|
951
|
+
// chat.message hook - runs when user sends a message
|
|
952
|
+
// This is where we intercept commands and set up sessions
|
|
953
|
+
// -----------------------------------------------------------------
|
|
840
954
|
"chat.message": async (msgInput, msgOutput) => {
|
|
841
955
|
const parts = msgOutput.parts;
|
|
842
956
|
const textPartIndex = parts.findIndex((p) => p.type === "text" && p.text);
|
|
@@ -857,7 +971,8 @@ var OrchestratorPlugin = async (input) => {
|
|
|
857
971
|
enabled: true,
|
|
858
972
|
iterations: 0,
|
|
859
973
|
taskRetries: /* @__PURE__ */ new Map(),
|
|
860
|
-
currentTask: ""
|
|
974
|
+
currentTask: "",
|
|
975
|
+
anomalyCount: 0
|
|
861
976
|
});
|
|
862
977
|
if (!parsed) {
|
|
863
978
|
const userMessage = originalText.trim();
|
|
@@ -881,7 +996,8 @@ var OrchestratorPlugin = async (input) => {
|
|
|
881
996
|
enabled: true,
|
|
882
997
|
iterations: 0,
|
|
883
998
|
taskRetries: /* @__PURE__ */ new Map(),
|
|
884
|
-
currentTask: ""
|
|
999
|
+
currentTask: "",
|
|
1000
|
+
anomalyCount: 0
|
|
885
1001
|
});
|
|
886
1002
|
parts[textPartIndex].text = COMMANDS["task"].template.replace(
|
|
887
1003
|
/\$ARGUMENTS/g,
|
|
@@ -897,12 +1013,39 @@ var OrchestratorPlugin = async (input) => {
|
|
|
897
1013
|
}
|
|
898
1014
|
}
|
|
899
1015
|
},
|
|
1016
|
+
// -----------------------------------------------------------------
|
|
1017
|
+
// tool.execute.after hook - runs after any tool call completes
|
|
1018
|
+
// We use this to track progress and detect problems
|
|
1019
|
+
// -----------------------------------------------------------------
|
|
900
1020
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
901
1021
|
const session = sessions.get(toolInput.sessionID);
|
|
902
1022
|
if (!session?.active) return;
|
|
903
1023
|
session.step++;
|
|
904
1024
|
session.timestamp = Date.now();
|
|
905
1025
|
const stateSession = state.sessions.get(toolInput.sessionID);
|
|
1026
|
+
if (toolInput.tool === "call_agent" && stateSession) {
|
|
1027
|
+
const sanityResult = checkOutputSanity(toolOutput.output);
|
|
1028
|
+
if (!sanityResult.isHealthy) {
|
|
1029
|
+
stateSession.anomalyCount = (stateSession.anomalyCount || 0) + 1;
|
|
1030
|
+
const agentName = toolInput.arguments?.agent || "unknown";
|
|
1031
|
+
toolOutput.output = `\u26A0\uFE0F [${agentName.toUpperCase()}] OUTPUT ANOMALY DETECTED
|
|
1032
|
+
|
|
1033
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1034
|
+
\u26A0\uFE0F Gibberish/loop detected: ${sanityResult.reason}
|
|
1035
|
+
Anomaly count: ${stateSession.anomalyCount}
|
|
1036
|
+
\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
|
|
1037
|
+
|
|
1038
|
+
` + (stateSession.anomalyCount >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT);
|
|
1039
|
+
return;
|
|
1040
|
+
} else {
|
|
1041
|
+
if (stateSession.anomalyCount > 0) {
|
|
1042
|
+
stateSession.anomalyCount = 0;
|
|
1043
|
+
}
|
|
1044
|
+
if (toolOutput.output.length < 5e3) {
|
|
1045
|
+
stateSession.lastHealthyOutput = toolOutput.output.substring(0, 1e3);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
906
1049
|
if (toolInput.tool === "call_agent" && toolInput.arguments?.task && stateSession) {
|
|
907
1050
|
const taskIdMatch = toolInput.arguments.task.match(/\[(TASK-\d+)\]/i);
|
|
908
1051
|
if (taskIdMatch) {
|
|
@@ -977,12 +1120,49 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
977
1120
|
|
|
978
1121
|
[${session.step}/${session.maxSteps}]`;
|
|
979
1122
|
},
|
|
1123
|
+
// -----------------------------------------------------------------
|
|
1124
|
+
// assistant.done hook - runs when the LLM finishes responding
|
|
1125
|
+
// This is the heart of the "relentless loop" - we keep pushing it
|
|
1126
|
+
// to continue until we see MISSION COMPLETE or hit the limit
|
|
1127
|
+
// -----------------------------------------------------------------
|
|
980
1128
|
"assistant.done": async (assistantInput, assistantOutput) => {
|
|
981
1129
|
const sessionID = assistantInput.sessionID;
|
|
982
1130
|
const session = sessions.get(sessionID);
|
|
983
1131
|
if (!session?.active) return;
|
|
984
1132
|
const parts = assistantOutput.parts;
|
|
985
1133
|
const textContent = parts?.filter((p) => p.type === "text" || p.type === "reasoning").map((p) => p.text || "").join("\n") || "";
|
|
1134
|
+
const stateSession = state.sessions.get(sessionID);
|
|
1135
|
+
const sanityResult = checkOutputSanity(textContent);
|
|
1136
|
+
if (!sanityResult.isHealthy && stateSession) {
|
|
1137
|
+
stateSession.anomalyCount = (stateSession.anomalyCount || 0) + 1;
|
|
1138
|
+
session.step++;
|
|
1139
|
+
session.timestamp = Date.now();
|
|
1140
|
+
const recoveryText = stateSession.anomalyCount >= 2 ? ESCALATION_PROMPT : RECOVERY_PROMPT;
|
|
1141
|
+
try {
|
|
1142
|
+
if (client?.session?.prompt) {
|
|
1143
|
+
await client.session.prompt({
|
|
1144
|
+
path: { id: sessionID },
|
|
1145
|
+
body: {
|
|
1146
|
+
parts: [{
|
|
1147
|
+
type: "text",
|
|
1148
|
+
text: `\u26A0\uFE0F ANOMALY #${stateSession.anomalyCount}: ${sanityResult.reason}
|
|
1149
|
+
|
|
1150
|
+
` + recoveryText + `
|
|
1151
|
+
|
|
1152
|
+
[Recovery Step ${session.step}/${session.maxSteps}]`
|
|
1153
|
+
}]
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
}
|
|
1157
|
+
} catch {
|
|
1158
|
+
session.active = false;
|
|
1159
|
+
state.missionActive = false;
|
|
1160
|
+
}
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (stateSession && stateSession.anomalyCount > 0) {
|
|
1164
|
+
stateSession.anomalyCount = 0;
|
|
1165
|
+
}
|
|
986
1166
|
if (textContent.includes("\u2705 MISSION COMPLETE") || textContent.includes("MISSION COMPLETE")) {
|
|
987
1167
|
session.active = false;
|
|
988
1168
|
state.missionActive = false;
|
|
@@ -1033,6 +1213,9 @@ ${stateSession.graph.getTaskSummary()}`;
|
|
|
1033
1213
|
}
|
|
1034
1214
|
}
|
|
1035
1215
|
},
|
|
1216
|
+
// -----------------------------------------------------------------
|
|
1217
|
+
// Event handler - cleans up when sessions are deleted
|
|
1218
|
+
// -----------------------------------------------------------------
|
|
1036
1219
|
handler: async ({ event }) => {
|
|
1037
1220
|
if (event.type === "session.deleted") {
|
|
1038
1221
|
const props = event.properties;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Sanity Check - LLM degeneration/gibberish detection
|
|
3
|
+
*
|
|
4
|
+
* Detects common LLM failure modes:
|
|
5
|
+
* - Single character repetition (SSSSSS...)
|
|
6
|
+
* - Pattern loops (茅茅茅茅...)
|
|
7
|
+
* - Low information density
|
|
8
|
+
* - Visual gibberish (box drawing characters)
|
|
9
|
+
* - Line repetition
|
|
10
|
+
*/
|
|
11
|
+
export interface SanityResult {
|
|
12
|
+
isHealthy: boolean;
|
|
13
|
+
reason?: string;
|
|
14
|
+
severity: "ok" | "warning" | "critical";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if LLM output shows signs of degeneration
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkOutputSanity(text: string): SanityResult;
|
|
20
|
+
/**
|
|
21
|
+
* Check if text is completely empty or meaningless
|
|
22
|
+
*/
|
|
23
|
+
export declare function isEmptyOrMeaningless(text: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Recovery prompt for single anomaly
|
|
26
|
+
*/
|
|
27
|
+
export declare const RECOVERY_PROMPT = "<anomaly_recovery>\n\u26A0\uFE0F SYSTEM NOTICE: Previous output was malformed (gibberish/loop detected).\n\n<recovery_protocol>\n1. DISCARD the corrupted output completely - do not reference it\n2. RECALL the original mission objective\n3. IDENTIFY the last confirmed successful step\n4. RESTART with a simpler, more focused approach\n</recovery_protocol>\n\n<instructions>\n- If a sub-agent produced bad output: try a different agent or simpler task\n- If stuck in a loop: break down the task into smaller pieces\n- If context seems corrupted: call recorder to restore context\n- THINK in English for maximum stability\n</instructions>\n\nWhat was the original task? Proceed from the last known good state.\n</anomaly_recovery>";
|
|
28
|
+
/**
|
|
29
|
+
* Escalation prompt for multiple consecutive anomalies
|
|
30
|
+
*/
|
|
31
|
+
export declare const ESCALATION_PROMPT = "<critical_anomaly>\n\uD83D\uDEA8 CRITICAL: Multiple consecutive malformed outputs detected.\n\n<emergency_protocol>\n1. STOP current execution path immediately\n2. DO NOT continue with the same approach - it is failing\n3. CALL architect for a completely new strategy\n4. If architect also fails: report status to user and await guidance\n</emergency_protocol>\n\n<diagnosis>\nThe current approach is producing corrupted output.\nThis may indicate: context overload, model instability, or task complexity.\n</diagnosis>\n\nRequest a fresh plan from architect with reduced scope.\n</critical_anomaly>";
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "opencode-orchestrator",
|
|
3
3
|
"displayName": "OpenCode Orchestrator",
|
|
4
4
|
"description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
|
|
5
|
-
"version": "0.2.
|
|
5
|
+
"version": "0.2.3",
|
|
6
6
|
"author": "agnusdei1207",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|