oh-my-opencode-cohub 1.12.13 → 1.13.0-beta.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/README.md +1 -0
- package/dist/cli/index.js +115 -15
- package/dist/config/loader.d.ts +5 -16
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/context/engine.d.ts +15 -1
- package/dist/context/engine.d.ts.map +1 -1
- package/dist/context/extractor.d.ts +8 -0
- package/dist/context/extractor.d.ts.map +1 -1
- package/dist/context/truncate.test.d.ts +2 -0
- package/dist/context/truncate.test.d.ts.map +1 -0
- package/dist/context/types.d.ts +2 -0
- package/dist/context/types.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +924 -109
- package/dist/task-manager/quality.d.ts +51 -0
- package/dist/task-manager/quality.d.ts.map +1 -0
- package/dist/task-manager/quality.test.d.ts +2 -0
- package/dist/task-manager/quality.test.d.ts.map +1 -0
- package/dist/task-manager/stats.test.d.ts +2 -0
- package/dist/task-manager/stats.test.d.ts.map +1 -0
- package/dist/task-manager/tracker.d.ts +57 -6
- package/dist/task-manager/tracker.d.ts.map +1 -1
- package/dist/task-manager/types.d.ts +11 -0
- package/dist/task-manager/types.d.ts.map +1 -1
- package/dist/tools/adaptive-params.d.ts +63 -0
- package/dist/tools/adaptive-params.d.ts.map +1 -0
- package/dist/tools/adaptive-params.test.d.ts +2 -0
- package/dist/tools/adaptive-params.test.d.ts.map +1 -0
- package/dist/tools/council.d.ts +78 -1
- package/dist/tools/council.d.ts.map +1 -1
- package/dist/tools/council.test.d.ts +2 -0
- package/dist/tools/council.test.d.ts.map +1 -0
- package/dist/tools/model-stats.d.ts +56 -0
- package/dist/tools/model-stats.d.ts.map +1 -0
- package/dist/tools/model-stats.test.d.ts +2 -0
- package/dist/tools/model-stats.test.d.ts.map +1 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +26 -5
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -560,6 +560,62 @@ var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
|
|
|
560
560
|
- 跨所有项目生效,优先级高于项目级规则`;
|
|
561
561
|
|
|
562
562
|
// src/task-manager/tracker.ts
|
|
563
|
+
import * as fs from "node:fs";
|
|
564
|
+
import * as os from "node:os";
|
|
565
|
+
import * as path from "node:path";
|
|
566
|
+
var DEFAULT_STATS_WINDOW = 50;
|
|
567
|
+
var STATS_FILE = path.join(os.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub", "stats.json");
|
|
568
|
+
var AGENT_DEFAULT_STRATEGY = {
|
|
569
|
+
"co-fixer": "relevant",
|
|
570
|
+
"co-designer": "relevant",
|
|
571
|
+
"co-planner": "relevant",
|
|
572
|
+
"co-oracle": "summary",
|
|
573
|
+
"co-council": "summary"
|
|
574
|
+
};
|
|
575
|
+
function defaultStrategyFor(agent) {
|
|
576
|
+
return AGENT_DEFAULT_STRATEGY[agent] ?? "none";
|
|
577
|
+
}
|
|
578
|
+
function computeStats(records, window = DEFAULT_STATS_WINDOW) {
|
|
579
|
+
const recent = records.slice(-Math.max(1, window));
|
|
580
|
+
const buckets = new Map;
|
|
581
|
+
for (const r of recent) {
|
|
582
|
+
const key = r.strategy + "\x00" + r.agent;
|
|
583
|
+
let bucket = buckets.get(key);
|
|
584
|
+
if (!bucket) {
|
|
585
|
+
bucket = {
|
|
586
|
+
strategy: r.strategy,
|
|
587
|
+
agent: r.agent,
|
|
588
|
+
count: 0,
|
|
589
|
+
successCount: 0,
|
|
590
|
+
successRate: 0,
|
|
591
|
+
avgLatencyMs: 0,
|
|
592
|
+
latencySamples: 0,
|
|
593
|
+
avgTokens: 0,
|
|
594
|
+
tokenSamples: 0
|
|
595
|
+
};
|
|
596
|
+
buckets.set(key, bucket);
|
|
597
|
+
}
|
|
598
|
+
bucket.count += 1;
|
|
599
|
+
if (r.status === "completed")
|
|
600
|
+
bucket.successCount += 1;
|
|
601
|
+
if (typeof r.latencyMs === "number") {
|
|
602
|
+
bucket.avgLatencyMs = (bucket.avgLatencyMs * bucket.latencySamples + r.latencyMs) / (bucket.latencySamples + 1);
|
|
603
|
+
bucket.latencySamples += 1;
|
|
604
|
+
}
|
|
605
|
+
if (r.tokens && typeof r.tokens.input === "number" && typeof r.tokens.output === "number") {
|
|
606
|
+
const total = r.tokens.input + r.tokens.output;
|
|
607
|
+
bucket.avgTokens = (bucket.avgTokens * bucket.tokenSamples + total) / (bucket.tokenSamples + 1);
|
|
608
|
+
bucket.tokenSamples += 1;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
const result = Array.from(buckets.values());
|
|
612
|
+
for (const b of result) {
|
|
613
|
+
b.successRate = b.count > 0 ? b.successCount / b.count * 100 : 0;
|
|
614
|
+
}
|
|
615
|
+
result.sort((a, b) => a.strategy.localeCompare(b.strategy) || a.agent.localeCompare(b.agent));
|
|
616
|
+
return result;
|
|
617
|
+
}
|
|
618
|
+
|
|
563
619
|
class TaskTracker {
|
|
564
620
|
jobs = new Map;
|
|
565
621
|
counters = new Map;
|
|
@@ -592,6 +648,7 @@ class TaskTracker {
|
|
|
592
648
|
sessionId: args.task_id ?? "",
|
|
593
649
|
parentSessionId,
|
|
594
650
|
agent,
|
|
651
|
+
contextStrategy: defaultStrategyFor(agent),
|
|
595
652
|
label,
|
|
596
653
|
status: "running",
|
|
597
654
|
background: args.background ?? false,
|
|
@@ -618,12 +675,16 @@ class TaskTracker {
|
|
|
618
675
|
latest.terminalReconciled = true;
|
|
619
676
|
if (sessionId)
|
|
620
677
|
latest.sessionId = sessionId;
|
|
678
|
+
this.finalize(latest);
|
|
679
|
+
this.persistStats();
|
|
621
680
|
}
|
|
622
681
|
}
|
|
623
682
|
updateByChildSessionId(sessionId, status) {
|
|
624
683
|
for (const job of this.jobs.values()) {
|
|
625
684
|
if (job.sessionId === sessionId && job.background && job.status === "running") {
|
|
626
685
|
job.status = status;
|
|
686
|
+
this.finalize(job);
|
|
687
|
+
this.persistStats();
|
|
627
688
|
return;
|
|
628
689
|
}
|
|
629
690
|
}
|
|
@@ -702,8 +763,10 @@ class TaskTracker {
|
|
|
702
763
|
for (const job of this.jobs.values()) {
|
|
703
764
|
if (job.background && job.status === "running" && now - job.createdAt > timeoutMs) {
|
|
704
765
|
job.status = "errored";
|
|
766
|
+
this.finalize(job);
|
|
705
767
|
}
|
|
706
768
|
}
|
|
769
|
+
this.persistStats();
|
|
707
770
|
}
|
|
708
771
|
isReusable(alias) {
|
|
709
772
|
const job = this.jobs.get(alias);
|
|
@@ -730,11 +793,49 @@ class TaskTracker {
|
|
|
730
793
|
getJobBySessionId(sessionId) {
|
|
731
794
|
for (const job of this.jobs.values()) {
|
|
732
795
|
if (job.sessionId === sessionId) {
|
|
733
|
-
return
|
|
796
|
+
return job;
|
|
734
797
|
}
|
|
735
798
|
}
|
|
736
799
|
return;
|
|
737
800
|
}
|
|
801
|
+
updateQuality(sessionId, quality) {
|
|
802
|
+
for (const job of this.jobs.values()) {
|
|
803
|
+
if (job.sessionId === sessionId) {
|
|
804
|
+
job.quality = quality;
|
|
805
|
+
this.finalize(job);
|
|
806
|
+
this.persistStats();
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
finalize(job) {
|
|
812
|
+
const latencyMs = Date.now() - job.createdAt;
|
|
813
|
+
if (!job.quality) {
|
|
814
|
+
job.quality = { latencyMs };
|
|
815
|
+
} else if (job.quality.latencyMs === undefined) {
|
|
816
|
+
job.quality.latencyMs = latencyMs;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
collectStatsRecords() {
|
|
820
|
+
const jobs = Array.from(this.jobs.values()).filter((j) => j.status !== "running").sort((a, b) => a.createdAt - b.createdAt);
|
|
821
|
+
return jobs.map((job) => ({
|
|
822
|
+
agent: job.agent,
|
|
823
|
+
strategy: job.contextStrategy ?? defaultStrategyFor(job.agent),
|
|
824
|
+
status: job.status,
|
|
825
|
+
...job.quality?.latencyMs !== undefined ? { latencyMs: job.quality.latencyMs } : {},
|
|
826
|
+
...job.quality?.tokens ? { tokens: job.quality.tokens } : {}
|
|
827
|
+
}));
|
|
828
|
+
}
|
|
829
|
+
getStats(window = DEFAULT_STATS_WINDOW) {
|
|
830
|
+
return computeStats(this.collectStatsRecords(), window);
|
|
831
|
+
}
|
|
832
|
+
persistStats(window = DEFAULT_STATS_WINDOW) {
|
|
833
|
+
try {
|
|
834
|
+
const recent = this.collectStatsRecords().slice(-Math.max(1, window));
|
|
835
|
+
fs.mkdirSync(path.dirname(STATS_FILE), { recursive: true });
|
|
836
|
+
fs.writeFileSync(STATS_FILE, JSON.stringify(recent, null, 2), "utf8");
|
|
837
|
+
} catch {}
|
|
838
|
+
}
|
|
738
839
|
markCancelled(taskId) {
|
|
739
840
|
let job = this.jobs.get(taskId);
|
|
740
841
|
if (!job) {
|
|
@@ -748,10 +849,162 @@ class TaskTracker {
|
|
|
748
849
|
if (job) {
|
|
749
850
|
job.status = "cancelled";
|
|
750
851
|
job.terminalReconciled = true;
|
|
852
|
+
this.finalize(job);
|
|
853
|
+
this.persistStats();
|
|
751
854
|
}
|
|
752
855
|
}
|
|
753
856
|
}
|
|
754
857
|
|
|
858
|
+
// src/task-manager/quality.ts
|
|
859
|
+
var DEFAULT_QUALITY_CONFIG = {
|
|
860
|
+
enabled: true
|
|
861
|
+
};
|
|
862
|
+
var ERROR_KEYWORDS = [
|
|
863
|
+
"error",
|
|
864
|
+
"failed",
|
|
865
|
+
"failure",
|
|
866
|
+
"exception",
|
|
867
|
+
"traceback",
|
|
868
|
+
"unable to",
|
|
869
|
+
"失败",
|
|
870
|
+
"错误",
|
|
871
|
+
"异常",
|
|
872
|
+
"无法完成",
|
|
873
|
+
"未能"
|
|
874
|
+
];
|
|
875
|
+
var BOUNDARY_KEYWORDS = new Set(["error", "failed", "failure", "exception"]);
|
|
876
|
+
var NEGATION_PHRASES = [
|
|
877
|
+
"no error",
|
|
878
|
+
"no errors",
|
|
879
|
+
"no failure",
|
|
880
|
+
"no failures",
|
|
881
|
+
"no exception",
|
|
882
|
+
"no exceptions",
|
|
883
|
+
"without error",
|
|
884
|
+
"without errors",
|
|
885
|
+
"without any error",
|
|
886
|
+
"without any errors",
|
|
887
|
+
"without failure",
|
|
888
|
+
"without failures",
|
|
889
|
+
"without exception",
|
|
890
|
+
"without exceptions",
|
|
891
|
+
"without any failure",
|
|
892
|
+
"without any failures",
|
|
893
|
+
"without any exception",
|
|
894
|
+
"without any exceptions",
|
|
895
|
+
"with no error",
|
|
896
|
+
"with no errors",
|
|
897
|
+
"with no failure",
|
|
898
|
+
"with no failures",
|
|
899
|
+
"not an error",
|
|
900
|
+
"not a failure",
|
|
901
|
+
"is not an error",
|
|
902
|
+
"never failed",
|
|
903
|
+
"never fails",
|
|
904
|
+
"did not fail",
|
|
905
|
+
"does not fail",
|
|
906
|
+
"not failed",
|
|
907
|
+
"无错误",
|
|
908
|
+
"没有错误",
|
|
909
|
+
"未失败",
|
|
910
|
+
"没有失败",
|
|
911
|
+
"未发现错误",
|
|
912
|
+
"未出现错误",
|
|
913
|
+
"未发生错误",
|
|
914
|
+
"不存在错误",
|
|
915
|
+
"无异常",
|
|
916
|
+
"没有异常",
|
|
917
|
+
"未发现异常",
|
|
918
|
+
"未出现异常",
|
|
919
|
+
"没有发现错误",
|
|
920
|
+
"没有发现任何错误",
|
|
921
|
+
"未发现任何错误",
|
|
922
|
+
"无任何错误",
|
|
923
|
+
"未出现任何错误",
|
|
924
|
+
"未发生任何错误",
|
|
925
|
+
"不存在任何错误",
|
|
926
|
+
"没有发现异常",
|
|
927
|
+
"未发现任何异常",
|
|
928
|
+
"无任何异常",
|
|
929
|
+
"未遇到错误",
|
|
930
|
+
"未产生错误",
|
|
931
|
+
"未遇到异常",
|
|
932
|
+
"未产生异常"
|
|
933
|
+
];
|
|
934
|
+
var NEGATION_WINDOW = 16;
|
|
935
|
+
function isNegated(lower, hitStart, hitLength) {
|
|
936
|
+
const windowStart = Math.max(0, hitStart - NEGATION_WINDOW);
|
|
937
|
+
const window = lower.slice(windowStart, hitStart + hitLength);
|
|
938
|
+
return NEGATION_PHRASES.some((phrase) => window.includes(phrase));
|
|
939
|
+
}
|
|
940
|
+
function hasErrorKeywords(text) {
|
|
941
|
+
const lower = text.toLowerCase();
|
|
942
|
+
for (const kw of BOUNDARY_KEYWORDS) {
|
|
943
|
+
const re = new RegExp(`\\b${kw}\\b`, "g");
|
|
944
|
+
let m;
|
|
945
|
+
while ((m = re.exec(lower)) !== null) {
|
|
946
|
+
const before = m.index > 0 ? lower[m.index - 1] : "";
|
|
947
|
+
const after = lower[m.index + kw.length] ?? "";
|
|
948
|
+
if (before === "." || after === "(")
|
|
949
|
+
continue;
|
|
950
|
+
if (isNegated(lower, m.index, kw.length))
|
|
951
|
+
continue;
|
|
952
|
+
return true;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
for (const kw of ERROR_KEYWORDS) {
|
|
956
|
+
if (BOUNDARY_KEYWORDS.has(kw))
|
|
957
|
+
continue;
|
|
958
|
+
let idx = lower.indexOf(kw);
|
|
959
|
+
while (idx !== -1) {
|
|
960
|
+
if (!isNegated(lower, idx, kw.length))
|
|
961
|
+
return true;
|
|
962
|
+
idx = lower.indexOf(kw, idx + kw.length);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return false;
|
|
966
|
+
}
|
|
967
|
+
function assessQuality(input) {
|
|
968
|
+
const outputOk = input.output.trim().length > 0;
|
|
969
|
+
const exitOk = input.exitCode === undefined || input.exitCode === 0;
|
|
970
|
+
const keywordOk = !hasErrorKeywords(input.output);
|
|
971
|
+
const decisionsOk = input.decisions >= 1;
|
|
972
|
+
const timedOut = input.timedOut === true;
|
|
973
|
+
let score = 0;
|
|
974
|
+
if (outputOk)
|
|
975
|
+
score += 0.25;
|
|
976
|
+
if (exitOk && !timedOut)
|
|
977
|
+
score += 0.25;
|
|
978
|
+
if (keywordOk && !timedOut)
|
|
979
|
+
score += 0.25;
|
|
980
|
+
if (decisionsOk && !timedOut)
|
|
981
|
+
score += 0.25;
|
|
982
|
+
if (!outputOk)
|
|
983
|
+
score = 0;
|
|
984
|
+
const passed = !timedOut && outputOk && exitOk && keywordOk && decisionsOk;
|
|
985
|
+
let failureCategory;
|
|
986
|
+
if (!passed) {
|
|
987
|
+
if (timedOut)
|
|
988
|
+
failureCategory = "timeout";
|
|
989
|
+
else if (!outputOk)
|
|
990
|
+
failureCategory = "empty";
|
|
991
|
+
else if (!exitOk || !keywordOk)
|
|
992
|
+
failureCategory = "error";
|
|
993
|
+
else
|
|
994
|
+
failureCategory = "quality-low";
|
|
995
|
+
}
|
|
996
|
+
return {
|
|
997
|
+
score,
|
|
998
|
+
passed,
|
|
999
|
+
...failureCategory ? { failureCategory } : {},
|
|
1000
|
+
...input.latencyMs !== undefined ? { latencyMs: input.latencyMs } : {},
|
|
1001
|
+
...input.tokens ? { tokens: input.tokens } : {}
|
|
1002
|
+
};
|
|
1003
|
+
}
|
|
1004
|
+
function isQualityEnabled(config) {
|
|
1005
|
+
return config?.enabled ?? DEFAULT_QUALITY_CONFIG.enabled;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
755
1008
|
// src/context/types.ts
|
|
756
1009
|
var DEFAULT_CONTEXT_CONFIG = {
|
|
757
1010
|
strategy: {
|
|
@@ -776,6 +1029,9 @@ var DEFAULT_CONTEXT_CONFIG = {
|
|
|
776
1029
|
relevantMessageWindow: 20
|
|
777
1030
|
};
|
|
778
1031
|
|
|
1032
|
+
// src/context/engine.ts
|
|
1033
|
+
import * as fs2 from "node:fs";
|
|
1034
|
+
|
|
779
1035
|
// src/context/extractor.ts
|
|
780
1036
|
var KNOWN_EXTENSIONS = new Set([
|
|
781
1037
|
"ts",
|
|
@@ -827,12 +1083,12 @@ function extractRelevantFiles(messages, maxFiles, windowSize) {
|
|
|
827
1083
|
if (part.type === "tool" && part.state) {
|
|
828
1084
|
const state = part.state;
|
|
829
1085
|
const input = state.input ?? {};
|
|
830
|
-
const
|
|
831
|
-
if (
|
|
832
|
-
if (!fileMap.has(
|
|
833
|
-
fileMap.set(
|
|
1086
|
+
const path2 = extractPath(input);
|
|
1087
|
+
if (path2) {
|
|
1088
|
+
if (!fileMap.has(path2)) {
|
|
1089
|
+
fileMap.set(path2, { path: path2, summary: "" });
|
|
834
1090
|
}
|
|
835
|
-
const existing = fileMap.get(
|
|
1091
|
+
const existing = fileMap.get(path2);
|
|
836
1092
|
if (typeof input.offset === "number") {
|
|
837
1093
|
const limit = typeof input.limit === "number" ? input.limit : 50;
|
|
838
1094
|
existing.lines = `${input.offset}-${input.offset + limit}`;
|
|
@@ -952,6 +1208,25 @@ function extractErrors(messages, maxErrors, windowSize) {
|
|
|
952
1208
|
}
|
|
953
1209
|
return errors;
|
|
954
1210
|
}
|
|
1211
|
+
function truncateByTokens(text, maxTokens) {
|
|
1212
|
+
if (!text || maxTokens <= 0)
|
|
1213
|
+
return "";
|
|
1214
|
+
let used = 0;
|
|
1215
|
+
let cut = text.length;
|
|
1216
|
+
for (let i = 0;i < text.length; i++) {
|
|
1217
|
+
const code = text.charCodeAt(i);
|
|
1218
|
+
const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
|
|
1219
|
+
used += isCjk ? 1 : 0.25;
|
|
1220
|
+
if (used > maxTokens) {
|
|
1221
|
+
cut = i;
|
|
1222
|
+
break;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (cut >= text.length)
|
|
1226
|
+
return text;
|
|
1227
|
+
return text.slice(0, cut) + `
|
|
1228
|
+
… [正文已按 token 预算截断]`;
|
|
1229
|
+
}
|
|
955
1230
|
|
|
956
1231
|
// src/context/formatter.ts
|
|
957
1232
|
var CONTEXT_MARKER_PATTERN = /<!-- CONTEXT:ID=([a-f0-9-]+) -->/;
|
|
@@ -1043,6 +1318,7 @@ class ContextEngine {
|
|
|
1043
1318
|
return;
|
|
1044
1319
|
if (args.strategy === "none")
|
|
1045
1320
|
return;
|
|
1321
|
+
context.strategy = args.strategy;
|
|
1046
1322
|
try {
|
|
1047
1323
|
const windowSize = this.config.relevantMessageWindow;
|
|
1048
1324
|
const messagesResult = await this.client.session.messages({
|
|
@@ -1055,11 +1331,25 @@ class ContextEngine {
|
|
|
1055
1331
|
context.decisions = extractDecisions(messages, this.config.maxDecisions, windowSize);
|
|
1056
1332
|
context.errors = extractErrors(messages, this.config.maxErrors, windowSize);
|
|
1057
1333
|
}
|
|
1334
|
+
if (args.strategy === "summary") {
|
|
1335
|
+
this.attachTruncatedBodies(context.relevantFiles);
|
|
1336
|
+
}
|
|
1058
1337
|
if (this.config.dependencyPropagation && this.dependencyCache.size > 0) {
|
|
1059
1338
|
context.dependencies = Array.from(this.dependencyCache.values()).slice(-this.config.maxDependencies);
|
|
1060
1339
|
}
|
|
1061
1340
|
} catch {}
|
|
1062
1341
|
}
|
|
1342
|
+
attachTruncatedBodies(files) {
|
|
1343
|
+
if (files.length === 0)
|
|
1344
|
+
return;
|
|
1345
|
+
const budgetPerFile = Math.max(1, Math.floor(this.config.summarizeMaxTokens / files.length));
|
|
1346
|
+
for (const file of files) {
|
|
1347
|
+
try {
|
|
1348
|
+
const content = fs2.readFileSync(file.path, "utf-8");
|
|
1349
|
+
file.body = truncateByTokens(content, budgetPerFile);
|
|
1350
|
+
} catch {}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1063
1353
|
consumeMarkedContext(messageText) {
|
|
1064
1354
|
const markerMatch = messageText.match(CONTEXT_MARKER_PATTERN);
|
|
1065
1355
|
if (!markerMatch)
|
|
@@ -1078,10 +1368,23 @@ class ContextEngine {
|
|
|
1078
1368
|
if (!context)
|
|
1079
1369
|
return "";
|
|
1080
1370
|
const parts = [];
|
|
1371
|
+
const isSummary = context.strategy === "summary";
|
|
1081
1372
|
if (context.relevantFiles.length > 0) {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1373
|
+
if (isSummary) {
|
|
1374
|
+
parts.push("### \uD83D\uDCC1 相关文件(正文按 token 预算截断)");
|
|
1375
|
+
for (const file of context.relevantFiles) {
|
|
1376
|
+
const loc = file.lines ? ":" + file.lines : "";
|
|
1377
|
+
if (file.body) {
|
|
1378
|
+
parts.push("- `" + file.path + loc + "`:\n```text\n" + file.body + "\n```");
|
|
1379
|
+
} else {
|
|
1380
|
+
parts.push("- `" + file.path + loc + "` — " + (file.summary || "(正文不可用)"));
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
} else {
|
|
1384
|
+
parts.push("### \uD83D\uDCC1 相关文件");
|
|
1385
|
+
for (const file of context.relevantFiles) {
|
|
1386
|
+
parts.push("- `" + file.path + "`" + (file.summary ? " — " + file.summary : ""));
|
|
1387
|
+
}
|
|
1085
1388
|
}
|
|
1086
1389
|
}
|
|
1087
1390
|
if (context.decisions.length > 0) {
|
|
@@ -1108,27 +1411,31 @@ class ContextEngine {
|
|
|
1108
1411
|
` : "";
|
|
1109
1412
|
}
|
|
1110
1413
|
async captureResult(childSessionId, alias, agent) {
|
|
1111
|
-
if (!this.config.dependencyPropagation)
|
|
1112
|
-
return;
|
|
1113
1414
|
try {
|
|
1114
1415
|
const messagesResult = await this.client.session.messages({
|
|
1115
1416
|
path: { id: childSessionId }
|
|
1116
1417
|
});
|
|
1117
1418
|
const messages = messagesResult.data ?? [];
|
|
1118
1419
|
let keyOutput = "";
|
|
1420
|
+
let tokens;
|
|
1119
1421
|
for (let i = messages.length - 1;i >= 0; i--) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if (
|
|
1422
|
+
const info = messages[i].info;
|
|
1423
|
+
if (!info || info.role !== "assistant")
|
|
1424
|
+
continue;
|
|
1425
|
+
if (typeof info.tokens?.input === "number" && typeof info.tokens?.output === "number") {
|
|
1426
|
+
tokens = { input: info.tokens.input, output: info.tokens.output };
|
|
1427
|
+
}
|
|
1428
|
+
for (const part of messages[i].parts ?? []) {
|
|
1429
|
+
if (part.type === "text" && part.text) {
|
|
1430
|
+
keyOutput = part.text.slice(0, 500).replace(/\n/g, " ");
|
|
1128
1431
|
break;
|
|
1432
|
+
}
|
|
1129
1433
|
}
|
|
1434
|
+
if (keyOutput)
|
|
1435
|
+
break;
|
|
1130
1436
|
}
|
|
1131
|
-
|
|
1437
|
+
const decisions = extractDecisions(messages, this.config.maxDecisions, this.config.relevantMessageWindow);
|
|
1438
|
+
if (keyOutput && this.config.dependencyPropagation) {
|
|
1132
1439
|
this.dependencyCache.set(alias, {
|
|
1133
1440
|
alias,
|
|
1134
1441
|
agent,
|
|
@@ -1136,7 +1443,14 @@ class ContextEngine {
|
|
|
1136
1443
|
capturedAt: Date.now()
|
|
1137
1444
|
});
|
|
1138
1445
|
}
|
|
1139
|
-
|
|
1446
|
+
return {
|
|
1447
|
+
output: keyOutput,
|
|
1448
|
+
decisions: decisions.length,
|
|
1449
|
+
...tokens ? { tokens } : {}
|
|
1450
|
+
};
|
|
1451
|
+
} catch {
|
|
1452
|
+
return null;
|
|
1453
|
+
}
|
|
1140
1454
|
}
|
|
1141
1455
|
formatMarker(contextId) {
|
|
1142
1456
|
return formatContextMarker(contextId);
|
|
@@ -1155,15 +1469,15 @@ class ContextEngine {
|
|
|
1155
1469
|
}
|
|
1156
1470
|
|
|
1157
1471
|
// src/config/loader.ts
|
|
1158
|
-
import * as
|
|
1159
|
-
import * as
|
|
1160
|
-
import * as
|
|
1161
|
-
var USER_CONFIG_PATH =
|
|
1472
|
+
import * as fs3 from "node:fs";
|
|
1473
|
+
import * as path2 from "node:path";
|
|
1474
|
+
import * as os2 from "node:os";
|
|
1475
|
+
var USER_CONFIG_PATH = path2.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
|
|
1162
1476
|
function loadCoHubConfig(projectDir) {
|
|
1163
1477
|
const base = readJSON(USER_CONFIG_PATH);
|
|
1164
1478
|
if (!projectDir)
|
|
1165
1479
|
return base;
|
|
1166
|
-
const projectPath =
|
|
1480
|
+
const projectPath = path2.join(projectDir, ".opencode", "oh-my-opencode-cohub.json");
|
|
1167
1481
|
const project = readJSON(projectPath);
|
|
1168
1482
|
if (!project || Object.keys(project).length === 0)
|
|
1169
1483
|
return base;
|
|
@@ -1177,13 +1491,16 @@ function loadCoHubConfig(projectDir) {
|
|
|
1177
1491
|
if (project.context) {
|
|
1178
1492
|
merged.context = { ...base.context, ...project.context };
|
|
1179
1493
|
}
|
|
1494
|
+
if (project.quality) {
|
|
1495
|
+
merged.quality = { ...base.quality, ...project.quality };
|
|
1496
|
+
}
|
|
1180
1497
|
return merged;
|
|
1181
1498
|
}
|
|
1182
1499
|
function readJSON(filePath) {
|
|
1183
1500
|
try {
|
|
1184
|
-
if (!
|
|
1501
|
+
if (!fs3.existsSync(filePath))
|
|
1185
1502
|
return {};
|
|
1186
|
-
const raw =
|
|
1503
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
1187
1504
|
return JSON.parse(raw);
|
|
1188
1505
|
} catch {
|
|
1189
1506
|
return {};
|
|
@@ -1194,10 +1511,10 @@ function readJSON(filePath) {
|
|
|
1194
1511
|
import { tool } from "@opencode-ai/plugin";
|
|
1195
1512
|
|
|
1196
1513
|
// src/utils/log.ts
|
|
1197
|
-
import * as
|
|
1198
|
-
import * as
|
|
1199
|
-
import * as
|
|
1200
|
-
var LOG_DIR =
|
|
1514
|
+
import * as fs4 from "fs/promises";
|
|
1515
|
+
import * as path3 from "path";
|
|
1516
|
+
import * as os3 from "os";
|
|
1517
|
+
var LOG_DIR = path3.join(os3.homedir(), ".local", "share", "opencode", "log");
|
|
1201
1518
|
var LOG_PREFIX = "oh-my-opencode-cohub";
|
|
1202
1519
|
var KEEP_DAYS = 7;
|
|
1203
1520
|
var _today = "";
|
|
@@ -1207,7 +1524,7 @@ function getLogPath() {
|
|
|
1207
1524
|
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
1208
1525
|
if (today !== _today) {
|
|
1209
1526
|
_today = today;
|
|
1210
|
-
_logPath =
|
|
1527
|
+
_logPath = path3.join(LOG_DIR, `${LOG_PREFIX}.${today}.log`);
|
|
1211
1528
|
_cleanedToday = false;
|
|
1212
1529
|
}
|
|
1213
1530
|
return _logPath;
|
|
@@ -1220,7 +1537,7 @@ async function cleanOldLogsOnce() {
|
|
|
1220
1537
|
const cutoff = new Date;
|
|
1221
1538
|
cutoff.setUTCDate(cutoff.getUTCDate() - KEEP_DAYS);
|
|
1222
1539
|
const cutoffStr = cutoff.toISOString().slice(0, 10).replace(/-/g, "");
|
|
1223
|
-
const files = await
|
|
1540
|
+
const files = await fs4.readdir(LOG_DIR);
|
|
1224
1541
|
for (const file of files) {
|
|
1225
1542
|
if (!file.startsWith(LOG_PREFIX + "."))
|
|
1226
1543
|
continue;
|
|
@@ -1229,7 +1546,7 @@ async function cleanOldLogsOnce() {
|
|
|
1229
1546
|
if (!match)
|
|
1230
1547
|
continue;
|
|
1231
1548
|
if (match[1] < cutoffStr) {
|
|
1232
|
-
await
|
|
1549
|
+
await fs4.unlink(path3.join(LOG_DIR, file)).catch(() => {});
|
|
1233
1550
|
}
|
|
1234
1551
|
}
|
|
1235
1552
|
} catch {}
|
|
@@ -1237,7 +1554,7 @@ async function cleanOldLogsOnce() {
|
|
|
1237
1554
|
async function appendLog(tag, message, err) {
|
|
1238
1555
|
try {
|
|
1239
1556
|
const logPath = getLogPath();
|
|
1240
|
-
await
|
|
1557
|
+
await fs4.mkdir(LOG_DIR, { recursive: true });
|
|
1241
1558
|
const now = new Date().toISOString();
|
|
1242
1559
|
let line = `[${now}] [oh-my-opencode-cohub] [${tag}] ${message}`;
|
|
1243
1560
|
if (err instanceof Error) {
|
|
@@ -1249,13 +1566,158 @@ async function appendLog(tag, message, err) {
|
|
|
1249
1566
|
}
|
|
1250
1567
|
line += `
|
|
1251
1568
|
`;
|
|
1252
|
-
await
|
|
1569
|
+
await fs4.appendFile(logPath, line, "utf-8");
|
|
1253
1570
|
await cleanOldLogsOnce();
|
|
1254
1571
|
} catch {}
|
|
1255
1572
|
}
|
|
1256
1573
|
|
|
1574
|
+
// src/tools/model-stats.ts
|
|
1575
|
+
var DEFAULT_MODEL_STATS_CONFIG = {
|
|
1576
|
+
windowSize: 20,
|
|
1577
|
+
highFailureRate: 0.5,
|
|
1578
|
+
minSamples: 5
|
|
1579
|
+
};
|
|
1580
|
+
var config = { ...DEFAULT_MODEL_STATS_CONFIG };
|
|
1581
|
+
var history = new Map;
|
|
1582
|
+
function recordModelResult(model, category) {
|
|
1583
|
+
const window = history.get(model) ?? [];
|
|
1584
|
+
window.push(category);
|
|
1585
|
+
if (window.length > config.windowSize) {
|
|
1586
|
+
window.splice(0, window.length - config.windowSize);
|
|
1587
|
+
}
|
|
1588
|
+
history.set(model, window);
|
|
1589
|
+
}
|
|
1590
|
+
function getModelStats(model) {
|
|
1591
|
+
const window = history.get(model) ?? [];
|
|
1592
|
+
let successes = 0;
|
|
1593
|
+
let empty = 0;
|
|
1594
|
+
let timeout = 0;
|
|
1595
|
+
let error = 0;
|
|
1596
|
+
let qualityLow = 0;
|
|
1597
|
+
for (const c of window) {
|
|
1598
|
+
if (c === "success") {
|
|
1599
|
+
successes += 1;
|
|
1600
|
+
} else if (c === "empty") {
|
|
1601
|
+
empty += 1;
|
|
1602
|
+
} else if (c === "timeout") {
|
|
1603
|
+
timeout += 1;
|
|
1604
|
+
} else if (c === "error") {
|
|
1605
|
+
error += 1;
|
|
1606
|
+
} else {
|
|
1607
|
+
qualityLow += 1;
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
const total = window.length;
|
|
1611
|
+
const failures = total - successes;
|
|
1612
|
+
return {
|
|
1613
|
+
model,
|
|
1614
|
+
total,
|
|
1615
|
+
successes,
|
|
1616
|
+
failures,
|
|
1617
|
+
empty,
|
|
1618
|
+
timeout,
|
|
1619
|
+
error,
|
|
1620
|
+
qualityLow,
|
|
1621
|
+
failureRate: total === 0 ? 0 : failures / total,
|
|
1622
|
+
successRate: total === 0 ? 1 : successes / total
|
|
1623
|
+
};
|
|
1624
|
+
}
|
|
1625
|
+
function orderCouncillorsByFailure(entries, statsProvider = getModelStats, cfg = config) {
|
|
1626
|
+
const isHighRisk = ([, c]) => {
|
|
1627
|
+
const s = statsProvider(c.model);
|
|
1628
|
+
return s.total >= cfg.minSamples && s.failureRate > cfg.highFailureRate;
|
|
1629
|
+
};
|
|
1630
|
+
const preferred = entries.filter((entry) => !isHighRisk(entry));
|
|
1631
|
+
const chosen = preferred.length > 0 ? preferred : [...entries];
|
|
1632
|
+
return chosen.sort((a, b) => statsProvider(a[1].model).failureRate - statsProvider(b[1].model).failureRate);
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/tools/adaptive-params.ts
|
|
1636
|
+
var DEFAULT_ADAPTIVE_CONFIG = {
|
|
1637
|
+
highRate: 0.8,
|
|
1638
|
+
lowRate: 0.5,
|
|
1639
|
+
minSamples: 5,
|
|
1640
|
+
hysteresisSamples: 5,
|
|
1641
|
+
minRetries: 1,
|
|
1642
|
+
maxRetries: 5,
|
|
1643
|
+
minTimeoutMs: 60000,
|
|
1644
|
+
maxTimeoutMs: 300000,
|
|
1645
|
+
defaultRetries: 3,
|
|
1646
|
+
defaultTimeoutMs: 180000,
|
|
1647
|
+
timeoutStepMs: 30000,
|
|
1648
|
+
windowSize: 20
|
|
1649
|
+
};
|
|
1650
|
+
var config2 = { ...DEFAULT_ADAPTIVE_CONFIG };
|
|
1651
|
+
var samples = new Map;
|
|
1652
|
+
function evaluateAdjustment(successRate, sampleCount, cfg = config2) {
|
|
1653
|
+
if (sampleCount < cfg.minSamples)
|
|
1654
|
+
return 0;
|
|
1655
|
+
if (successRate > cfg.highRate)
|
|
1656
|
+
return -1;
|
|
1657
|
+
if (successRate < cfg.lowRate)
|
|
1658
|
+
return 1;
|
|
1659
|
+
return 0;
|
|
1660
|
+
}
|
|
1661
|
+
function clampAdaptiveParams(params, cfg = config2) {
|
|
1662
|
+
return {
|
|
1663
|
+
retries: Math.min(cfg.maxRetries, Math.max(cfg.minRetries, Math.round(params.retries))),
|
|
1664
|
+
timeoutMs: Math.min(cfg.maxTimeoutMs, Math.max(cfg.minTimeoutMs, Math.round(params.timeoutMs)))
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function adjustAdaptiveParams(current, direction, cfg = config2) {
|
|
1668
|
+
if (direction === 0)
|
|
1669
|
+
return clampAdaptiveParams(current, cfg);
|
|
1670
|
+
return clampAdaptiveParams({
|
|
1671
|
+
retries: current.retries + direction,
|
|
1672
|
+
timeoutMs: current.timeoutMs + direction * cfg.timeoutStepMs
|
|
1673
|
+
}, cfg);
|
|
1674
|
+
}
|
|
1675
|
+
function recordAdaptiveSample(key, success, cfg = config2) {
|
|
1676
|
+
let st = samples.get(key);
|
|
1677
|
+
if (!st) {
|
|
1678
|
+
st = {
|
|
1679
|
+
recent: [],
|
|
1680
|
+
current: { retries: cfg.defaultRetries, timeoutMs: cfg.defaultTimeoutMs },
|
|
1681
|
+
lastDirection: 0,
|
|
1682
|
+
streak: 0
|
|
1683
|
+
};
|
|
1684
|
+
samples.set(key, st);
|
|
1685
|
+
}
|
|
1686
|
+
st.recent.push(success);
|
|
1687
|
+
if (st.recent.length > cfg.windowSize) {
|
|
1688
|
+
st.recent.splice(0, st.recent.length - cfg.windowSize);
|
|
1689
|
+
}
|
|
1690
|
+
const total = st.recent.length;
|
|
1691
|
+
if (total < cfg.minSamples)
|
|
1692
|
+
return;
|
|
1693
|
+
const successes = st.recent.filter(Boolean).length;
|
|
1694
|
+
const direction = evaluateAdjustment(successes / total, total, cfg);
|
|
1695
|
+
if (direction === st.lastDirection) {
|
|
1696
|
+
st.streak += 1;
|
|
1697
|
+
} else {
|
|
1698
|
+
st.lastDirection = direction;
|
|
1699
|
+
st.streak = 1;
|
|
1700
|
+
}
|
|
1701
|
+
if (st.streak >= cfg.hysteresisSamples) {
|
|
1702
|
+
st.current = adjustAdaptiveParams(st.current, direction, cfg);
|
|
1703
|
+
st.streak = 0;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
function getAdaptiveParams(key, cfg = config2) {
|
|
1707
|
+
const st = samples.get(key);
|
|
1708
|
+
if (!st)
|
|
1709
|
+
return { retries: cfg.defaultRetries, timeoutMs: cfg.defaultTimeoutMs };
|
|
1710
|
+
return { ...st.current };
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1257
1713
|
// src/tools/council.ts
|
|
1258
1714
|
var z = tool.schema;
|
|
1715
|
+
var RETRY_LIMITS = {
|
|
1716
|
+
empty: 3,
|
|
1717
|
+
timeout: 1,
|
|
1718
|
+
error: 0,
|
|
1719
|
+
"quality-low": 1
|
|
1720
|
+
};
|
|
1259
1721
|
|
|
1260
1722
|
class OperationTimeoutError extends Error {
|
|
1261
1723
|
constructor(message) {
|
|
@@ -1285,12 +1747,12 @@ async function abortSession(client, sessionId) {
|
|
|
1285
1747
|
appendLog("abortSession", "中止会话失败", err);
|
|
1286
1748
|
}
|
|
1287
1749
|
}
|
|
1288
|
-
async function promptWithTimeout(client,
|
|
1289
|
-
const sessionId =
|
|
1750
|
+
async function promptWithTimeout(client, path4, body, timeoutMs, directory) {
|
|
1751
|
+
const sessionId = path4.id;
|
|
1290
1752
|
let timer;
|
|
1291
1753
|
try {
|
|
1292
1754
|
const promptPromise = client.session.prompt({
|
|
1293
|
-
path:
|
|
1755
|
+
path: path4,
|
|
1294
1756
|
body,
|
|
1295
1757
|
query: directory ? { directory } : undefined
|
|
1296
1758
|
});
|
|
@@ -1316,6 +1778,7 @@ async function promptWithTimeout(client, path3, body, timeoutMs, directory) {
|
|
|
1316
1778
|
}
|
|
1317
1779
|
async function extractSessionResult(client, sessionId, options) {
|
|
1318
1780
|
const includeReasoning = options?.includeReasoning ?? true;
|
|
1781
|
+
const lastTextOnly = options?.lastTextOnly === true;
|
|
1319
1782
|
const messagesResult = await client.session.messages({
|
|
1320
1783
|
path: { id: sessionId }
|
|
1321
1784
|
});
|
|
@@ -1332,7 +1795,8 @@ async function extractSessionResult(client, sessionId, options) {
|
|
|
1332
1795
|
}
|
|
1333
1796
|
}
|
|
1334
1797
|
}
|
|
1335
|
-
const
|
|
1798
|
+
const texts = extractedContent.filter((t) => t.length > 0);
|
|
1799
|
+
const text = lastTextOnly ? texts.length > 0 ? texts[texts.length - 1] : "" : texts.join(`
|
|
1336
1800
|
|
|
1337
1801
|
`);
|
|
1338
1802
|
return { text, empty: text.length === 0 };
|
|
@@ -1395,18 +1859,254 @@ ${cr.result}`;
|
|
|
1395
1859
|
return parts.join(`
|
|
1396
1860
|
`);
|
|
1397
1861
|
}
|
|
1862
|
+
function buildDeliberationPrompt(originalPrompt, previousRound, currentName) {
|
|
1863
|
+
const others = previousRound.filter((cr) => cr.status === "completed" && cr.result && cr.name !== currentName).map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}):
|
|
1864
|
+
${cr.result}`).join(`
|
|
1865
|
+
|
|
1866
|
+
`);
|
|
1867
|
+
if (!others)
|
|
1868
|
+
return originalPrompt;
|
|
1869
|
+
return [
|
|
1870
|
+
originalPrompt,
|
|
1871
|
+
"",
|
|
1872
|
+
"---",
|
|
1873
|
+
"",
|
|
1874
|
+
"以下是其他成员第一轮意见,请回应并修正或坚持你的立场:",
|
|
1875
|
+
"",
|
|
1876
|
+
others
|
|
1877
|
+
].join(`
|
|
1878
|
+
`);
|
|
1879
|
+
}
|
|
1398
1880
|
function formatModelComposition(councillorResults) {
|
|
1399
1881
|
return councillorResults.map((cr) => `${cr.name}: ${shortModelLabel(cr.model)}`).join(", ");
|
|
1400
1882
|
}
|
|
1883
|
+
function adaptiveKey(config3) {
|
|
1884
|
+
return `${config3.variant ?? "default"}\x00${config3.model}`;
|
|
1885
|
+
}
|
|
1886
|
+
function classifyFailure(message) {
|
|
1887
|
+
if (message.includes("Empty response from provider"))
|
|
1888
|
+
return "empty";
|
|
1889
|
+
if (message.includes("timed out") || message.includes("OperationTimeoutError"))
|
|
1890
|
+
return "timeout";
|
|
1891
|
+
return "error";
|
|
1892
|
+
}
|
|
1893
|
+
var CONCLUSION_MARKERS = [
|
|
1894
|
+
"综上",
|
|
1895
|
+
"结论",
|
|
1896
|
+
"因此",
|
|
1897
|
+
"总之",
|
|
1898
|
+
"建议",
|
|
1899
|
+
"推荐",
|
|
1900
|
+
"我认为",
|
|
1901
|
+
"最终",
|
|
1902
|
+
"关键结论",
|
|
1903
|
+
"conclusion",
|
|
1904
|
+
"therefore",
|
|
1905
|
+
"in summary",
|
|
1906
|
+
"in conclusion",
|
|
1907
|
+
"recommend",
|
|
1908
|
+
"suggest",
|
|
1909
|
+
"overall"
|
|
1910
|
+
];
|
|
1911
|
+
var ENTITY_MARKERS = [
|
|
1912
|
+
"决策",
|
|
1913
|
+
"决定",
|
|
1914
|
+
"方案",
|
|
1915
|
+
"采用",
|
|
1916
|
+
"选择",
|
|
1917
|
+
"关键",
|
|
1918
|
+
"实体",
|
|
1919
|
+
"要点",
|
|
1920
|
+
"关键决策",
|
|
1921
|
+
"decision",
|
|
1922
|
+
"entity",
|
|
1923
|
+
"key point",
|
|
1924
|
+
"option",
|
|
1925
|
+
"recommendation",
|
|
1926
|
+
"key entity"
|
|
1927
|
+
];
|
|
1928
|
+
function extractCouncillorSummary(text) {
|
|
1929
|
+
const sentences = text.split(/(?<=[。!?!?;;])\s*|\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1930
|
+
const conclusions = sentences.filter((s) => CONCLUSION_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
1931
|
+
const entities = sentences.filter((s) => ENTITY_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
1932
|
+
return {
|
|
1933
|
+
conclusions: conclusions.length > 0 ? conclusions : sentences.length > 0 ? [sentences[sentences.length - 1]] : [],
|
|
1934
|
+
entities
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
var STOPWORDS = new Set([
|
|
1938
|
+
"the",
|
|
1939
|
+
"a",
|
|
1940
|
+
"an",
|
|
1941
|
+
"and",
|
|
1942
|
+
"or",
|
|
1943
|
+
"of",
|
|
1944
|
+
"to",
|
|
1945
|
+
"in",
|
|
1946
|
+
"on",
|
|
1947
|
+
"for",
|
|
1948
|
+
"with",
|
|
1949
|
+
"is",
|
|
1950
|
+
"are",
|
|
1951
|
+
"was",
|
|
1952
|
+
"were",
|
|
1953
|
+
"be",
|
|
1954
|
+
"been",
|
|
1955
|
+
"this",
|
|
1956
|
+
"that",
|
|
1957
|
+
"it",
|
|
1958
|
+
"as",
|
|
1959
|
+
"at",
|
|
1960
|
+
"by",
|
|
1961
|
+
"from",
|
|
1962
|
+
"我们",
|
|
1963
|
+
"的",
|
|
1964
|
+
"了",
|
|
1965
|
+
"是",
|
|
1966
|
+
"在",
|
|
1967
|
+
"和",
|
|
1968
|
+
"与",
|
|
1969
|
+
"及",
|
|
1970
|
+
"或",
|
|
1971
|
+
"一个",
|
|
1972
|
+
"这个",
|
|
1973
|
+
"进行",
|
|
1974
|
+
"以及",
|
|
1975
|
+
"结论",
|
|
1976
|
+
"综上",
|
|
1977
|
+
"总之",
|
|
1978
|
+
"因此",
|
|
1979
|
+
"建议",
|
|
1980
|
+
"推荐",
|
|
1981
|
+
"最终",
|
|
1982
|
+
"我认为",
|
|
1983
|
+
"关键",
|
|
1984
|
+
"要点",
|
|
1985
|
+
"采用",
|
|
1986
|
+
"方案",
|
|
1987
|
+
"决策",
|
|
1988
|
+
"决定",
|
|
1989
|
+
"选择",
|
|
1990
|
+
"实体",
|
|
1991
|
+
"conclusion",
|
|
1992
|
+
"therefore",
|
|
1993
|
+
"summary",
|
|
1994
|
+
"recommend",
|
|
1995
|
+
"suggest",
|
|
1996
|
+
"overall",
|
|
1997
|
+
"decision",
|
|
1998
|
+
"option",
|
|
1999
|
+
"recommendation",
|
|
2000
|
+
"key",
|
|
2001
|
+
"point",
|
|
2002
|
+
"entity",
|
|
2003
|
+
"error"
|
|
2004
|
+
]);
|
|
2005
|
+
function tokenize(text) {
|
|
2006
|
+
return text.toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/).filter((t) => t.length > 0 && !STOPWORDS.has(t));
|
|
2007
|
+
}
|
|
2008
|
+
function jaccard(x, y) {
|
|
2009
|
+
if (x.length === 0 || y.length === 0)
|
|
2010
|
+
return 0;
|
|
2011
|
+
const xs = new Set(x);
|
|
2012
|
+
const ys = new Set(y);
|
|
2013
|
+
const inter = [...xs].filter((t) => ys.has(t)).length;
|
|
2014
|
+
const union = new Set([...xs, ...ys]).size;
|
|
2015
|
+
return union === 0 ? 0 : inter / union;
|
|
2016
|
+
}
|
|
2017
|
+
function pairwiseAgreement(a, b) {
|
|
2018
|
+
const aConcl = tokenize(a.conclusions.join(" "));
|
|
2019
|
+
const bConcl = tokenize(b.conclusions.join(" "));
|
|
2020
|
+
const aEnt = tokenize(a.entities.join(" "));
|
|
2021
|
+
const bEnt = tokenize(b.entities.join(" "));
|
|
2022
|
+
return 0.5 * jaccard(aConcl, bConcl) + 0.5 * jaccard(aEnt, bEnt);
|
|
2023
|
+
}
|
|
2024
|
+
function computeConsensus(inputs, threshold) {
|
|
2025
|
+
const n = inputs.length;
|
|
2026
|
+
if (n === 0)
|
|
2027
|
+
return { consensus: false, agreementScores: {}, averageAgreement: 0 };
|
|
2028
|
+
if (n === 1) {
|
|
2029
|
+
return { consensus: true, agreementScores: { [inputs[0].name]: 1 }, averageAgreement: 1 };
|
|
2030
|
+
}
|
|
2031
|
+
const agreementScores = {};
|
|
2032
|
+
for (const a of inputs) {
|
|
2033
|
+
let sum = 0;
|
|
2034
|
+
for (const b of inputs) {
|
|
2035
|
+
if (a.name === b.name)
|
|
2036
|
+
continue;
|
|
2037
|
+
sum += pairwiseAgreement(a.summary, b.summary);
|
|
2038
|
+
}
|
|
2039
|
+
agreementScores[a.name] = sum / (n - 1);
|
|
2040
|
+
}
|
|
2041
|
+
const values = Object.values(agreementScores);
|
|
2042
|
+
const averageAgreement = values.reduce((acc, v) => acc + v, 0) / values.length;
|
|
2043
|
+
const agreeing = values.filter((v) => v >= threshold).length;
|
|
2044
|
+
const required = Math.ceil(2 * n / 3);
|
|
2045
|
+
return { consensus: agreeing >= required, agreementScores, averageAgreement };
|
|
2046
|
+
}
|
|
2047
|
+
function decideRetryAction(category, consecutiveStreak, categoryRetriesUsed, budgetAvailable, categoryLimit = RETRY_LIMITS[category]) {
|
|
2048
|
+
if (!budgetAvailable)
|
|
2049
|
+
return "budget-exhausted";
|
|
2050
|
+
if (consecutiveStreak >= 2)
|
|
2051
|
+
return "needs-human";
|
|
2052
|
+
if (categoryRetriesUsed >= categoryLimit)
|
|
2053
|
+
return "give-up";
|
|
2054
|
+
return "retry";
|
|
2055
|
+
}
|
|
2056
|
+
var VARIANT_ORDER = ["max", "high", "medium", "low"];
|
|
2057
|
+
function downgradeVariant(variant) {
|
|
2058
|
+
if (!variant)
|
|
2059
|
+
return;
|
|
2060
|
+
const idx = VARIANT_ORDER.indexOf(variant);
|
|
2061
|
+
if (idx < 0 || idx >= VARIANT_ORDER.length - 1)
|
|
2062
|
+
return;
|
|
2063
|
+
return VARIANT_ORDER[idx + 1];
|
|
2064
|
+
}
|
|
2065
|
+
function appendRetryContext(prompt, category) {
|
|
2066
|
+
if (category === "timeout") {
|
|
2067
|
+
return `${prompt}
|
|
2068
|
+
|
|
2069
|
+
[retry] 上次尝试超时。请精炼作答:先给出结论与关键决策,避免冗长过程。`;
|
|
2070
|
+
}
|
|
2071
|
+
if (category === "quality-low") {
|
|
2072
|
+
return `${prompt}
|
|
2073
|
+
|
|
2074
|
+
[retry] 上次回答缺少明确的结论与关键实体/决策。请以"结论:..."开头,并列出关键决策/实体要点。`;
|
|
2075
|
+
}
|
|
2076
|
+
return prompt;
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
class RetryBudget {
|
|
2080
|
+
retriesLeft;
|
|
2081
|
+
maxElapsedMs;
|
|
2082
|
+
startedAt;
|
|
2083
|
+
constructor(maxTotalRetries, maxElapsedMs, startedAt = Date.now()) {
|
|
2084
|
+
this.retriesLeft = Math.max(0, maxTotalRetries);
|
|
2085
|
+
this.maxElapsedMs = maxElapsedMs;
|
|
2086
|
+
this.startedAt = startedAt;
|
|
2087
|
+
}
|
|
2088
|
+
elapsedMs() {
|
|
2089
|
+
return Date.now() - this.startedAt;
|
|
2090
|
+
}
|
|
2091
|
+
expired() {
|
|
2092
|
+
return this.elapsedMs() >= this.maxElapsedMs;
|
|
2093
|
+
}
|
|
2094
|
+
consumeRetry() {
|
|
2095
|
+
if (this.retriesLeft <= 0 || this.expired())
|
|
2096
|
+
return false;
|
|
2097
|
+
this.retriesLeft -= 1;
|
|
2098
|
+
return true;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
1401
2101
|
|
|
1402
2102
|
class CouncilManager {
|
|
1403
2103
|
client;
|
|
1404
2104
|
directory;
|
|
1405
2105
|
config;
|
|
1406
|
-
constructor(client, directory,
|
|
2106
|
+
constructor(client, directory, config3) {
|
|
1407
2107
|
this.client = client;
|
|
1408
2108
|
this.directory = directory;
|
|
1409
|
-
this.config =
|
|
2109
|
+
this.config = config3;
|
|
1410
2110
|
}
|
|
1411
2111
|
async runCouncil(prompt, presetName, parentSessionId) {
|
|
1412
2112
|
const resolvedPreset = presetName ?? this.config.default_preset ?? "default";
|
|
@@ -1428,8 +2128,10 @@ class CouncilManager {
|
|
|
1428
2128
|
}
|
|
1429
2129
|
const timeout = this.config.timeout ?? 180000;
|
|
1430
2130
|
const executionMode = this.config.councillor_execution_mode ?? "parallel";
|
|
1431
|
-
const
|
|
1432
|
-
const
|
|
2131
|
+
const maxRounds = this.config.max_rounds ?? 2;
|
|
2132
|
+
const consensusThreshold = this.config.consensus_threshold ?? 0.6;
|
|
2133
|
+
const budget = new RetryBudget(this.config.max_total_retries ?? 6, this.config.max_elapsed_ms ?? 3 * timeout);
|
|
2134
|
+
const councillorResults = await this.runCouncillors(prompt, preset, parentSessionId, timeout, executionMode, budget, { maxRounds, consensusThreshold });
|
|
1433
2135
|
const completedCount = councillorResults.filter((r) => r.status === "completed").length;
|
|
1434
2136
|
if (completedCount === 0) {
|
|
1435
2137
|
return {
|
|
@@ -1438,23 +2140,56 @@ class CouncilManager {
|
|
|
1438
2140
|
councillorResults
|
|
1439
2141
|
};
|
|
1440
2142
|
}
|
|
2143
|
+
const consensusInputs = councillorResults.filter((r) => r.status === "completed" && !!r.summary).map((r) => ({ name: r.name, summary: r.summary }));
|
|
2144
|
+
const consensus = computeConsensus(consensusInputs, consensusThreshold);
|
|
1441
2145
|
const formatted = formatCouncillorResults(prompt, councillorResults);
|
|
1442
2146
|
return {
|
|
1443
2147
|
success: true,
|
|
1444
2148
|
result: formatted,
|
|
1445
|
-
councillorResults
|
|
2149
|
+
councillorResults,
|
|
2150
|
+
consensus: consensus.consensus,
|
|
2151
|
+
agreement: consensus.averageAgreement
|
|
1446
2152
|
};
|
|
1447
2153
|
}
|
|
1448
|
-
async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode,
|
|
1449
|
-
const entries = Object.entries(councillors);
|
|
2154
|
+
async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode, budget, options) {
|
|
2155
|
+
const entries = orderCouncillorsByFailure(Object.entries(councillors));
|
|
1450
2156
|
const results = [];
|
|
2157
|
+
const explicitTimeout = this.config.timeout;
|
|
2158
|
+
const explicitRetries = this.config.councillor_retries;
|
|
2159
|
+
const resolveParams = (config3) => {
|
|
2160
|
+
const p = getAdaptiveParams(adaptiveKey(config3), {
|
|
2161
|
+
...DEFAULT_ADAPTIVE_CONFIG,
|
|
2162
|
+
defaultTimeoutMs: explicitTimeout ?? timeout,
|
|
2163
|
+
defaultRetries: explicitRetries ?? 3
|
|
2164
|
+
});
|
|
2165
|
+
return {
|
|
2166
|
+
timeout: explicitTimeout ?? p.timeoutMs,
|
|
2167
|
+
retries: explicitRetries ?? p.retries
|
|
2168
|
+
};
|
|
2169
|
+
};
|
|
1451
2170
|
if (executionMode === "serial") {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
2171
|
+
let lastRound = [];
|
|
2172
|
+
for (let round = 1;round <= options.maxRounds; round++) {
|
|
2173
|
+
const roundResults = [];
|
|
2174
|
+
const firstAttemptIsExtra = round > 1;
|
|
2175
|
+
for (const [name, config3] of entries) {
|
|
2176
|
+
const params = resolveParams(config3);
|
|
2177
|
+
const roundPrompt = round > 1 ? buildDeliberationPrompt(prompt, lastRound, name) : prompt;
|
|
2178
|
+
const r = await this.runCouncillorWithRetry(name, config3, roundPrompt, parentSessionId, params.timeout, budget, { firstAttemptIsExtra, retries: params.retries });
|
|
2179
|
+
roundResults.push(r);
|
|
2180
|
+
}
|
|
2181
|
+
lastRound = roundResults;
|
|
2182
|
+
const consensusInputs = roundResults.filter((r) => r.status === "completed" && !!r.summary).map((r) => ({ name: r.name, summary: r.summary }));
|
|
2183
|
+
if (computeConsensus(consensusInputs, options.consensusThreshold).consensus) {
|
|
2184
|
+
break;
|
|
2185
|
+
}
|
|
1455
2186
|
}
|
|
2187
|
+
results.push(...lastRound);
|
|
1456
2188
|
} else {
|
|
1457
|
-
const promises = entries.map(([name,
|
|
2189
|
+
const promises = entries.map(([name, config3]) => {
|
|
2190
|
+
const params = resolveParams(config3);
|
|
2191
|
+
return this.runCouncillorWithRetry(name, config3, prompt, parentSessionId, params.timeout, budget, { retries: params.retries });
|
|
2192
|
+
});
|
|
1458
2193
|
const settled = await Promise.allSettled(promises);
|
|
1459
2194
|
for (let i = 0;i < settled.length; i++) {
|
|
1460
2195
|
const s = settled[i];
|
|
@@ -1473,39 +2208,80 @@ class CouncilManager {
|
|
|
1473
2208
|
}
|
|
1474
2209
|
return results;
|
|
1475
2210
|
}
|
|
1476
|
-
async runCouncillorWithRetry(name,
|
|
1477
|
-
|
|
1478
|
-
|
|
2211
|
+
async runCouncillorWithRetry(name, config3, prompt, parentSessionId, timeout, budget, options) {
|
|
2212
|
+
let attemptPrompt = formatCouncillorPrompt(prompt, config3.prompt);
|
|
2213
|
+
let attemptVariant = config3.variant;
|
|
2214
|
+
let lastCategory = null;
|
|
2215
|
+
let streak = 0;
|
|
2216
|
+
const categoryRetries = {};
|
|
2217
|
+
let attempt = 1;
|
|
2218
|
+
for (;; ) {
|
|
2219
|
+
if (budget.expired()) {
|
|
2220
|
+
return { name, model: config3.model, status: "error", error: "budget-exhausted" };
|
|
2221
|
+
}
|
|
2222
|
+
const isExtraAttempt = attempt > 1 || options?.firstAttemptIsExtra === true;
|
|
2223
|
+
if (isExtraAttempt && !budget.consumeRetry()) {
|
|
2224
|
+
return { name, model: config3.model, status: "error", error: "budget-exhausted" };
|
|
2225
|
+
}
|
|
2226
|
+
let category;
|
|
2227
|
+
let errorInfo;
|
|
1479
2228
|
try {
|
|
1480
|
-
const
|
|
2229
|
+
const session = await this.runAgentSession({
|
|
1481
2230
|
parentSessionId,
|
|
1482
|
-
title: `Council ${name} (${shortModelLabel(
|
|
1483
|
-
model:
|
|
1484
|
-
promptText:
|
|
1485
|
-
variant:
|
|
2231
|
+
title: `Council ${name} (${shortModelLabel(config3.model)})`,
|
|
2232
|
+
model: config3.model,
|
|
2233
|
+
promptText: attemptPrompt,
|
|
2234
|
+
variant: attemptVariant,
|
|
1486
2235
|
timeout
|
|
1487
2236
|
});
|
|
1488
|
-
|
|
2237
|
+
const quality = assessQuality({ output: session.text, decisions: session.summary.entities.length });
|
|
2238
|
+
if (quality.passed) {
|
|
2239
|
+
recordModelResult(config3.model, "success");
|
|
2240
|
+
recordAdaptiveSample(adaptiveKey(config3), true);
|
|
2241
|
+
return { name, model: config3.model, status: "completed", result: session.text, summary: session.summary };
|
|
2242
|
+
}
|
|
2243
|
+
category = quality.failureCategory ?? "quality-low";
|
|
2244
|
+
errorInfo = `Councillor "${name}": quality score ${quality.score} (${category})`;
|
|
1489
2245
|
} catch (error) {
|
|
1490
2246
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
2247
|
+
category = classifyFailure(msg);
|
|
2248
|
+
errorInfo = `Councillor "${name}": ${msg}`;
|
|
2249
|
+
}
|
|
2250
|
+
recordModelResult(config3.model, category);
|
|
2251
|
+
recordAdaptiveSample(adaptiveKey(config3), false);
|
|
2252
|
+
streak = lastCategory === category ? streak + 1 : 1;
|
|
2253
|
+
lastCategory = category;
|
|
2254
|
+
const used = categoryRetries[category] ?? 0;
|
|
2255
|
+
const categoryLimit = category === "empty" ? options?.retries ?? this.config.councillor_retries ?? 3 : RETRY_LIMITS[category];
|
|
2256
|
+
const action = decideRetryAction(category, streak, used, budget.retriesLeft > 0 && !budget.expired(), categoryLimit);
|
|
2257
|
+
if (action === "needs-human") {
|
|
2258
|
+
return {
|
|
2259
|
+
name,
|
|
2260
|
+
model: config3.model,
|
|
2261
|
+
status: "needs_human",
|
|
2262
|
+
error: `${errorInfo} — ${streak} consecutive ${category} failures, needs human review`
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
if (action === "budget-exhausted") {
|
|
2266
|
+
return { name, model: config3.model, status: "error", error: "budget-exhausted" };
|
|
2267
|
+
}
|
|
2268
|
+
if (action === "give-up") {
|
|
2269
|
+
return {
|
|
2270
|
+
name,
|
|
2271
|
+
model: config3.model,
|
|
2272
|
+
status: category === "timeout" ? "timed_out" : "failed",
|
|
2273
|
+
error: errorInfo
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
categoryRetries[category] = used + 1;
|
|
2277
|
+
if (category === "timeout") {
|
|
2278
|
+
attemptVariant = downgradeVariant(attemptVariant);
|
|
2279
|
+
attemptPrompt = appendRetryContext(attemptPrompt, "timeout");
|
|
2280
|
+
} else if (category === "quality-low") {
|
|
2281
|
+
attemptPrompt = appendRetryContext(attemptPrompt, "quality-low");
|
|
1501
2282
|
}
|
|
2283
|
+
attempt += 1;
|
|
1502
2284
|
}
|
|
1503
|
-
return {
|
|
1504
|
-
name,
|
|
1505
|
-
model: config.model,
|
|
1506
|
-
status: "failed",
|
|
1507
|
-
error: `Councillor "${name}": max retries exhausted`
|
|
1508
|
-
};
|
|
1509
2285
|
}
|
|
1510
2286
|
async runAgentSession(options) {
|
|
1511
2287
|
const modelRef = parseModelReference(options.model);
|
|
@@ -1545,11 +2321,15 @@ class CouncilManager {
|
|
|
1545
2321
|
promptBody.variant = options.variant;
|
|
1546
2322
|
}
|
|
1547
2323
|
await promptWithTimeout(this.client, { id: sessionId }, promptBody, options.timeout, this.directory);
|
|
1548
|
-
const extraction = await extractSessionResult(this.client, sessionId
|
|
2324
|
+
const extraction = await extractSessionResult(this.client, sessionId, {
|
|
2325
|
+
includeReasoning: false,
|
|
2326
|
+
lastTextOnly: true
|
|
2327
|
+
});
|
|
1549
2328
|
if (extraction.empty) {
|
|
1550
2329
|
throw new Error("[oh-my-opencode-cohub] Empty response from provider");
|
|
1551
2330
|
}
|
|
1552
|
-
|
|
2331
|
+
const summary = extractCouncillorSummary(extraction.text);
|
|
2332
|
+
return { text: extraction.text, summary };
|
|
1553
2333
|
} finally {
|
|
1554
2334
|
if (sessionId) {
|
|
1555
2335
|
abortSession(this.client, sessionId).catch(() => {});
|
|
@@ -1593,6 +2373,10 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
1593
2373
|
|
|
1594
2374
|
---
|
|
1595
2375
|
*Council: ${completed}/${total} councillors responded (${composition})*`;
|
|
2376
|
+
const agreementNum = typeof result.agreement === "number" ? result.agreement : 0;
|
|
2377
|
+
const consensusLabel = result.consensus === true ? agreementNum >= 0.8 ? "unanimous" : "majority" : "split";
|
|
2378
|
+
output += `
|
|
2379
|
+
*Consensus: ${consensusLabel} (${agreementNum.toFixed(2)})*`;
|
|
1596
2380
|
return output;
|
|
1597
2381
|
}
|
|
1598
2382
|
});
|
|
@@ -1600,9 +2384,9 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
1600
2384
|
}
|
|
1601
2385
|
|
|
1602
2386
|
// src/index.ts
|
|
1603
|
-
import * as
|
|
1604
|
-
import * as
|
|
1605
|
-
import * as
|
|
2387
|
+
import * as fs5 from "node:fs";
|
|
2388
|
+
import * as path4 from "node:path";
|
|
2389
|
+
import * as os4 from "node:os";
|
|
1606
2390
|
function loadFileOverrides(projectDir) {
|
|
1607
2391
|
const overrides = {};
|
|
1608
2392
|
const agentNames = [
|
|
@@ -1621,23 +2405,23 @@ function loadFileOverrides(projectDir) {
|
|
|
1621
2405
|
];
|
|
1622
2406
|
const searchDirs = [];
|
|
1623
2407
|
if (projectDir) {
|
|
1624
|
-
searchDirs.push(
|
|
2408
|
+
searchDirs.push(path4.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
|
|
1625
2409
|
}
|
|
1626
|
-
searchDirs.push(
|
|
2410
|
+
searchDirs.push(path4.join(os4.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
|
|
1627
2411
|
for (const agent of agentNames) {
|
|
1628
2412
|
for (const dir of searchDirs) {
|
|
1629
|
-
const replacePath =
|
|
1630
|
-
if (!overrides[agent]?.replace &&
|
|
2413
|
+
const replacePath = path4.join(dir, `${agent}.md`);
|
|
2414
|
+
if (!overrides[agent]?.replace && fs5.existsSync(replacePath)) {
|
|
1631
2415
|
try {
|
|
1632
|
-
overrides[agent] = { ...overrides[agent], replace:
|
|
2416
|
+
overrides[agent] = { ...overrides[agent], replace: fs5.readFileSync(replacePath, "utf-8") };
|
|
1633
2417
|
} catch (err) {
|
|
1634
2418
|
appendLog("loadFileOverrides", "读取replace文件失败", err);
|
|
1635
2419
|
}
|
|
1636
2420
|
}
|
|
1637
|
-
const appendPath =
|
|
1638
|
-
if (!overrides[agent]?.append &&
|
|
2421
|
+
const appendPath = path4.join(dir, `${agent}_append.md`);
|
|
2422
|
+
if (!overrides[agent]?.append && fs5.existsSync(appendPath)) {
|
|
1639
2423
|
try {
|
|
1640
|
-
overrides[agent] = { ...overrides[agent], append:
|
|
2424
|
+
overrides[agent] = { ...overrides[agent], append: fs5.readFileSync(appendPath, "utf-8") };
|
|
1641
2425
|
} catch (err) {
|
|
1642
2426
|
appendLog("loadFileOverrides", "读取append文件失败", err);
|
|
1643
2427
|
}
|
|
@@ -1668,26 +2452,26 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1668
2452
|
}
|
|
1669
2453
|
const promptOverrides = { ...configOverrides, ...fileOverrides };
|
|
1670
2454
|
const tracker = new TaskTracker;
|
|
1671
|
-
const STATE_DIR =
|
|
1672
|
-
const STATE_FILE =
|
|
2455
|
+
const STATE_DIR = path4.join(os4.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
2456
|
+
const STATE_FILE = path4.join(STATE_DIR, "tracker-state.json");
|
|
1673
2457
|
function syncTrackerState(sessionId) {
|
|
1674
2458
|
try {
|
|
1675
|
-
if (!
|
|
1676
|
-
|
|
2459
|
+
if (!fs5.existsSync(STATE_DIR)) {
|
|
2460
|
+
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
1677
2461
|
}
|
|
1678
2462
|
const state = {
|
|
1679
2463
|
updatedAt: Date.now(),
|
|
1680
2464
|
runningAgents: tracker.getRunningAgents(sessionId),
|
|
1681
2465
|
runningCount: tracker.getRunningCount(sessionId)
|
|
1682
2466
|
};
|
|
1683
|
-
|
|
2467
|
+
fs5.writeFileSync(STATE_FILE, JSON.stringify(state), "utf-8");
|
|
1684
2468
|
} catch {}
|
|
1685
2469
|
}
|
|
1686
|
-
const AGENT_CONFIG_FILE =
|
|
2470
|
+
const AGENT_CONFIG_FILE = path4.join(STATE_DIR, "cohub-state.json");
|
|
1687
2471
|
function syncAgentConfig() {
|
|
1688
2472
|
try {
|
|
1689
|
-
if (!
|
|
1690
|
-
|
|
2473
|
+
if (!fs5.existsSync(STATE_DIR)) {
|
|
2474
|
+
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
1691
2475
|
}
|
|
1692
2476
|
const configs = agents.filter((a) => a.config.model).map((a) => {
|
|
1693
2477
|
const modelStr = a.config.model;
|
|
@@ -1702,7 +2486,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1702
2486
|
provider
|
|
1703
2487
|
};
|
|
1704
2488
|
});
|
|
1705
|
-
|
|
2489
|
+
fs5.writeFileSync(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }), "utf-8");
|
|
1706
2490
|
appendLog("syncAgentConfig", `已同步 ${configs.length} 个 agent: ${JSON.stringify(configs.map((c) => ({ name: c.name, model: c.model, variant: c.variant })))}`);
|
|
1707
2491
|
} catch {}
|
|
1708
2492
|
}
|
|
@@ -1853,12 +2637,18 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1853
2637
|
}
|
|
1854
2638
|
}, 60000);
|
|
1855
2639
|
const agentConfigs = {};
|
|
2640
|
+
const COUNCIL_ONLY_TOOLS = { council_session: "deny" };
|
|
1856
2641
|
for (const agent of agents) {
|
|
1857
|
-
|
|
2642
|
+
const base = {
|
|
1858
2643
|
...agent.config,
|
|
1859
2644
|
name: agent.name,
|
|
1860
2645
|
description: agent.description
|
|
1861
2646
|
};
|
|
2647
|
+
if (agent.name !== "co-council") {
|
|
2648
|
+
const existingPermission = base.permission ?? {};
|
|
2649
|
+
base.permission = { ...existingPermission, ...COUNCIL_ONLY_TOOLS };
|
|
2650
|
+
}
|
|
2651
|
+
agentConfigs[agent.name] = base;
|
|
1862
2652
|
}
|
|
1863
2653
|
return {
|
|
1864
2654
|
name: "oh-my-opencode-cohub",
|
|
@@ -1878,10 +2668,17 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1878
2668
|
merged.model = pc.model;
|
|
1879
2669
|
if (pc.variant)
|
|
1880
2670
|
merged.variant = pc.variant;
|
|
2671
|
+
if (name !== "co-council") {
|
|
2672
|
+
const existingPerm = merged.permission ?? {};
|
|
2673
|
+
merged.permission = { ...existingPerm, ...COUNCIL_ONLY_TOOLS };
|
|
2674
|
+
} else {
|
|
2675
|
+
const existingPerm = merged.permission ?? {};
|
|
2676
|
+
merged.permission = { ...existingPerm, council_session: "allow" };
|
|
2677
|
+
}
|
|
1881
2678
|
c.agent[name] = merged;
|
|
1882
2679
|
}
|
|
1883
2680
|
try {
|
|
1884
|
-
|
|
2681
|
+
fs5.writeFileSync(path4.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
|
|
1885
2682
|
} catch (err) {
|
|
1886
2683
|
appendLog("config", "双重注册写入文件失败", err);
|
|
1887
2684
|
}
|
|
@@ -1932,19 +2729,19 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1932
2729
|
if (details) {
|
|
1933
2730
|
output.args[targetField] += details;
|
|
1934
2731
|
}
|
|
1935
|
-
const logPath =
|
|
2732
|
+
const logPath = path4.join(os4.tmpdir(), "opencode", "ctx-diag.log");
|
|
1936
2733
|
try {
|
|
1937
|
-
const stat =
|
|
2734
|
+
const stat = fs5.statSync(logPath);
|
|
1938
2735
|
if (stat.size > 50 * 1024) {
|
|
1939
|
-
const lines =
|
|
2736
|
+
const lines = fs5.readFileSync(logPath, "utf-8").trim().split(`
|
|
1940
2737
|
`);
|
|
1941
|
-
|
|
2738
|
+
fs5.writeFileSync(logPath, lines.slice(-30).join(`
|
|
1942
2739
|
`) + `
|
|
1943
2740
|
`);
|
|
1944
2741
|
}
|
|
1945
2742
|
} catch {}
|
|
1946
2743
|
const finalPrompt = typeof output.args?.prompt === "string" ? output.args.prompt : "";
|
|
1947
|
-
|
|
2744
|
+
fs5.appendFileSync(logPath, JSON.stringify({
|
|
1948
2745
|
time: new Date().toISOString(),
|
|
1949
2746
|
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
1950
2747
|
subagent: subagentType,
|
|
@@ -1992,7 +2789,25 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1992
2789
|
syncTrackerState(tracker.currentParentSessionId);
|
|
1993
2790
|
const job = tracker.getJobBySessionId(sessionId);
|
|
1994
2791
|
if (job) {
|
|
1995
|
-
contextEngine.captureResult(sessionId, job.alias, job.agent)
|
|
2792
|
+
contextEngine.captureResult(sessionId, job.alias, job.agent).then((captured) => {
|
|
2793
|
+
if (!captured)
|
|
2794
|
+
return;
|
|
2795
|
+
if (!isQualityEnabled(userConfig.quality))
|
|
2796
|
+
return;
|
|
2797
|
+
const quality = assessQuality({
|
|
2798
|
+
output: captured.output,
|
|
2799
|
+
decisions: captured.decisions,
|
|
2800
|
+
latencyMs: Date.now() - job.createdAt,
|
|
2801
|
+
tokens: captured.tokens
|
|
2802
|
+
});
|
|
2803
|
+
tracker.updateQuality(sessionId, {
|
|
2804
|
+
score: quality.score,
|
|
2805
|
+
...quality.failureCategory ? { failureCategory: quality.failureCategory } : {},
|
|
2806
|
+
...quality.latencyMs !== undefined ? { latencyMs: quality.latencyMs } : {},
|
|
2807
|
+
...quality.tokens ? { tokens: quality.tokens } : {}
|
|
2808
|
+
});
|
|
2809
|
+
syncTrackerState(tracker.currentParentSessionId);
|
|
2810
|
+
}).catch((err) => appendLog("event", "质量回送失败", err));
|
|
1996
2811
|
}
|
|
1997
2812
|
} else if (e.type === "session.deleted") {
|
|
1998
2813
|
tracker.updateByChildSessionId(sessionId, "errored");
|
|
@@ -2096,6 +2911,6 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2096
2911
|
var server = CoHubPlugin;
|
|
2097
2912
|
var src_default = CoHubPlugin;
|
|
2098
2913
|
export {
|
|
2099
|
-
|
|
2100
|
-
|
|
2914
|
+
src_default as default,
|
|
2915
|
+
server
|
|
2101
2916
|
};
|