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.
@@ -5,6 +5,8 @@ export interface SessionState {
5
5
  taskRetries: Map<string, number>;
6
6
  currentTask: string;
7
7
  graph?: TaskGraph;
8
+ anomalyCount: number;
9
+ lastHealthyOutput?: string;
8
10
  }
9
11
  export declare const state: {
10
12
  missionActive: boolean;
package/dist/index.d.ts CHANGED
@@ -1,15 +1,14 @@
1
1
  /**
2
2
  * OpenCode Orchestrator Plugin
3
3
  *
4
- * 5-Agent Structured Architecture
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 ACT OBSERVE ADJUST)
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
- * Agents: Commander, Architect, Builder, Inspector, Recorder
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.2",
5
+ "version": "0.2.3",
6
6
  "author": "agnusdei1207",
7
7
  "license": "MIT",
8
8
  "repository": {