oh-my-opencode-cohub 1.12.13 → 1.13.0-beta.1
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 +1033 -111
- package/dist/prompts/designer.d.ts +1 -1
- package/dist/prompts/designer.d.ts.map +1 -1
- package/dist/prompts/fixer.d.ts +1 -1
- package/dist/prompts/fixer.d.ts.map +1 -1
- package/dist/prompts/oracle.d.ts +1 -1
- package/dist/prompts/oracle.d.ts.map +1 -1
- 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 +77 -8
- package/dist/task-manager/tracker.d.ts.map +1 -1
- package/dist/task-manager/tracker.test.d.ts +2 -0
- package/dist/task-manager/tracker.test.d.ts.map +1 -0
- 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/job-control.d.ts +14 -0
- package/dist/tools/job-control.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
|
@@ -144,6 +144,12 @@ var ORACLE_PROMPT = `你是 Oracle——战略技术顾问和代码审查者。
|
|
|
144
144
|
- 聚焦策略,不聚焦执行
|
|
145
145
|
- 必要时指出具体文件/行号
|
|
146
146
|
|
|
147
|
+
**代码审查清单**:
|
|
148
|
+
- 正确性与安全性:逻辑错误、边界情况、注入/越权风险
|
|
149
|
+
- 性能与可维护性:明显的性能陷阱、不必要的复杂度
|
|
150
|
+
- 该删没删的代码:调试残留、被替代的旧实现、失效的 import、为未来预留却从未使用的抽象
|
|
151
|
+
- 过度工程检测:引入了超出任务需求的依赖、配置项、封装层
|
|
152
|
+
|
|
147
153
|
**文件操作规则**:
|
|
148
154
|
- 只读:检查并报告,不修改文件
|
|
149
155
|
- 优先使用专用文件工具检查代码库:glob/grep/ast_grep_search 用于发现,read 用于读取文件内容
|
|
@@ -305,6 +311,11 @@ var DESIGNER_PROMPT = `你是 Designer——前端 UI/UX 专家,创造和审
|
|
|
305
311
|
- 极简主义设计 → 克制、精准、精心处理间距和排版
|
|
306
312
|
- 优雅来自完全执行所选愿景,而非半途而废
|
|
307
313
|
|
|
314
|
+
**最小实现自查**
|
|
315
|
+
- 实现代码时依次自查:能复用现有组件/样式模式吗?平台原生元素够用吗?
|
|
316
|
+
- 只写达成当前设计愿景所需的最小实现;未被要求的包装层与投机抽象不加
|
|
317
|
+
- ⚠️ 此原则约束的是代码实现的克制,不约束视觉表达——视觉上大胆丰富依然鼓励
|
|
318
|
+
|
|
308
319
|
## 约束
|
|
309
320
|
- 有现有设计系统时尊重它
|
|
310
321
|
- 有组件库时利用它
|
|
@@ -357,6 +368,12 @@ var FIXER_PROMPT = `你是 Fixer——快速、聚焦的实现专家。
|
|
|
357
368
|
- 只询问真正无法自己获取的缺失信息
|
|
358
369
|
- 不要充当主要审查者;实施请求的变更并简要指出明显问题
|
|
359
370
|
|
|
371
|
+
**最小实现自查阶梯**:
|
|
372
|
+
- 写代码前依次自问,停在第一级成立处:这个改动必须存在吗?代码库里已有可复用的实现吗?标准库/平台原生能力/已安装依赖能完成吗?
|
|
373
|
+
- 上述都不满足时,只写能工作的最小实现
|
|
374
|
+
- 未被要求的防御性代码、配置项、提前抽象——一律不加
|
|
375
|
+
- 验证、错误处理、安全相关代码永不在省略之列
|
|
376
|
+
|
|
360
377
|
**输出格式**:
|
|
361
378
|
<summary>
|
|
362
379
|
简要总结实施内容
|
|
@@ -560,6 +577,62 @@ var CHINESE_LANGUAGE_INSTRUCTION = `# 中文语言要求
|
|
|
560
577
|
- 跨所有项目生效,优先级高于项目级规则`;
|
|
561
578
|
|
|
562
579
|
// src/task-manager/tracker.ts
|
|
580
|
+
import * as fs from "node:fs";
|
|
581
|
+
import * as os from "node:os";
|
|
582
|
+
import * as path from "node:path";
|
|
583
|
+
var DEFAULT_STATS_WINDOW = 50;
|
|
584
|
+
var STATS_FILE = path.join(os.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub", "stats.json");
|
|
585
|
+
var AGENT_DEFAULT_STRATEGY = {
|
|
586
|
+
"co-fixer": "relevant",
|
|
587
|
+
"co-designer": "relevant",
|
|
588
|
+
"co-planner": "relevant",
|
|
589
|
+
"co-oracle": "summary",
|
|
590
|
+
"co-council": "summary"
|
|
591
|
+
};
|
|
592
|
+
function defaultStrategyFor(agent) {
|
|
593
|
+
return AGENT_DEFAULT_STRATEGY[agent] ?? "none";
|
|
594
|
+
}
|
|
595
|
+
function computeStats(records, window = DEFAULT_STATS_WINDOW) {
|
|
596
|
+
const recent = records.slice(-Math.max(1, window));
|
|
597
|
+
const buckets = new Map;
|
|
598
|
+
for (const r of recent) {
|
|
599
|
+
const key = r.strategy + "\x00" + r.agent;
|
|
600
|
+
let bucket = buckets.get(key);
|
|
601
|
+
if (!bucket) {
|
|
602
|
+
bucket = {
|
|
603
|
+
strategy: r.strategy,
|
|
604
|
+
agent: r.agent,
|
|
605
|
+
count: 0,
|
|
606
|
+
successCount: 0,
|
|
607
|
+
successRate: 0,
|
|
608
|
+
avgLatencyMs: 0,
|
|
609
|
+
latencySamples: 0,
|
|
610
|
+
avgTokens: 0,
|
|
611
|
+
tokenSamples: 0
|
|
612
|
+
};
|
|
613
|
+
buckets.set(key, bucket);
|
|
614
|
+
}
|
|
615
|
+
bucket.count += 1;
|
|
616
|
+
if (r.status === "completed")
|
|
617
|
+
bucket.successCount += 1;
|
|
618
|
+
if (typeof r.latencyMs === "number") {
|
|
619
|
+
bucket.avgLatencyMs = (bucket.avgLatencyMs * bucket.latencySamples + r.latencyMs) / (bucket.latencySamples + 1);
|
|
620
|
+
bucket.latencySamples += 1;
|
|
621
|
+
}
|
|
622
|
+
if (r.tokens && typeof r.tokens.input === "number" && typeof r.tokens.output === "number") {
|
|
623
|
+
const total = r.tokens.input + r.tokens.output;
|
|
624
|
+
bucket.avgTokens = (bucket.avgTokens * bucket.tokenSamples + total) / (bucket.tokenSamples + 1);
|
|
625
|
+
bucket.tokenSamples += 1;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const result = Array.from(buckets.values());
|
|
629
|
+
for (const b of result) {
|
|
630
|
+
b.successRate = b.count > 0 ? b.successCount / b.count * 100 : 0;
|
|
631
|
+
}
|
|
632
|
+
result.sort((a, b) => a.strategy.localeCompare(b.strategy) || a.agent.localeCompare(b.agent));
|
|
633
|
+
return result;
|
|
634
|
+
}
|
|
635
|
+
|
|
563
636
|
class TaskTracker {
|
|
564
637
|
jobs = new Map;
|
|
565
638
|
counters = new Map;
|
|
@@ -592,6 +665,7 @@ class TaskTracker {
|
|
|
592
665
|
sessionId: args.task_id ?? "",
|
|
593
666
|
parentSessionId,
|
|
594
667
|
agent,
|
|
668
|
+
contextStrategy: defaultStrategyFor(agent),
|
|
595
669
|
label,
|
|
596
670
|
status: "running",
|
|
597
671
|
background: args.background ?? false,
|
|
@@ -618,12 +692,16 @@ class TaskTracker {
|
|
|
618
692
|
latest.terminalReconciled = true;
|
|
619
693
|
if (sessionId)
|
|
620
694
|
latest.sessionId = sessionId;
|
|
695
|
+
this.finalize(latest);
|
|
696
|
+
this.persistStats();
|
|
621
697
|
}
|
|
622
698
|
}
|
|
623
699
|
updateByChildSessionId(sessionId, status) {
|
|
624
700
|
for (const job of this.jobs.values()) {
|
|
625
701
|
if (job.sessionId === sessionId && job.background && job.status === "running") {
|
|
626
702
|
job.status = status;
|
|
703
|
+
this.finalize(job);
|
|
704
|
+
this.persistStats();
|
|
627
705
|
return;
|
|
628
706
|
}
|
|
629
707
|
}
|
|
@@ -699,11 +777,17 @@ class TaskTracker {
|
|
|
699
777
|
}
|
|
700
778
|
cleanupStaleJobs(timeoutMs) {
|
|
701
779
|
const now = Date.now();
|
|
780
|
+
const staleSessions = [];
|
|
702
781
|
for (const job of this.jobs.values()) {
|
|
703
782
|
if (job.background && job.status === "running" && now - job.createdAt > timeoutMs) {
|
|
704
783
|
job.status = "errored";
|
|
784
|
+
this.finalize(job);
|
|
785
|
+
if (job.sessionId)
|
|
786
|
+
staleSessions.push(job.sessionId);
|
|
705
787
|
}
|
|
706
788
|
}
|
|
789
|
+
this.persistStats();
|
|
790
|
+
return staleSessions;
|
|
707
791
|
}
|
|
708
792
|
isReusable(alias) {
|
|
709
793
|
const job = this.jobs.get(alias);
|
|
@@ -730,11 +814,49 @@ class TaskTracker {
|
|
|
730
814
|
getJobBySessionId(sessionId) {
|
|
731
815
|
for (const job of this.jobs.values()) {
|
|
732
816
|
if (job.sessionId === sessionId) {
|
|
733
|
-
return
|
|
817
|
+
return job;
|
|
734
818
|
}
|
|
735
819
|
}
|
|
736
820
|
return;
|
|
737
821
|
}
|
|
822
|
+
updateQuality(sessionId, quality) {
|
|
823
|
+
for (const job of this.jobs.values()) {
|
|
824
|
+
if (job.sessionId === sessionId) {
|
|
825
|
+
job.quality = quality;
|
|
826
|
+
this.finalize(job);
|
|
827
|
+
this.persistStats();
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
finalize(job) {
|
|
833
|
+
const latencyMs = Date.now() - job.createdAt;
|
|
834
|
+
if (!job.quality) {
|
|
835
|
+
job.quality = { latencyMs };
|
|
836
|
+
} else if (job.quality.latencyMs === undefined) {
|
|
837
|
+
job.quality.latencyMs = latencyMs;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
collectStatsRecords() {
|
|
841
|
+
const jobs = Array.from(this.jobs.values()).filter((j) => j.status !== "running").sort((a, b) => a.createdAt - b.createdAt);
|
|
842
|
+
return jobs.map((job) => ({
|
|
843
|
+
agent: job.agent,
|
|
844
|
+
strategy: job.contextStrategy ?? defaultStrategyFor(job.agent),
|
|
845
|
+
status: job.status,
|
|
846
|
+
...job.quality?.latencyMs !== undefined ? { latencyMs: job.quality.latencyMs } : {},
|
|
847
|
+
...job.quality?.tokens ? { tokens: job.quality.tokens } : {}
|
|
848
|
+
}));
|
|
849
|
+
}
|
|
850
|
+
getStats(window = DEFAULT_STATS_WINDOW) {
|
|
851
|
+
return computeStats(this.collectStatsRecords(), window);
|
|
852
|
+
}
|
|
853
|
+
persistStats(window = DEFAULT_STATS_WINDOW) {
|
|
854
|
+
try {
|
|
855
|
+
const recent = this.collectStatsRecords().slice(-Math.max(1, window));
|
|
856
|
+
fs.mkdirSync(path.dirname(STATS_FILE), { recursive: true });
|
|
857
|
+
fs.writeFileSync(STATS_FILE, JSON.stringify(recent, null, 2), "utf8");
|
|
858
|
+
} catch {}
|
|
859
|
+
}
|
|
738
860
|
markCancelled(taskId) {
|
|
739
861
|
let job = this.jobs.get(taskId);
|
|
740
862
|
if (!job) {
|
|
@@ -748,10 +870,183 @@ class TaskTracker {
|
|
|
748
870
|
if (job) {
|
|
749
871
|
job.status = "cancelled";
|
|
750
872
|
job.terminalReconciled = true;
|
|
873
|
+
this.finalize(job);
|
|
874
|
+
this.persistStats();
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
abortJob(taskId) {
|
|
878
|
+
let job = this.jobs.get(taskId);
|
|
879
|
+
if (!job) {
|
|
880
|
+
for (const j of this.jobs.values()) {
|
|
881
|
+
if (j.sessionId === taskId) {
|
|
882
|
+
job = j;
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
751
886
|
}
|
|
887
|
+
if (!job)
|
|
888
|
+
return;
|
|
889
|
+
if (job.status !== "running") {
|
|
890
|
+
return { sessionId: job.sessionId || undefined, job };
|
|
891
|
+
}
|
|
892
|
+
job.status = "cancelled";
|
|
893
|
+
job.terminalReconciled = true;
|
|
894
|
+
this.finalize(job);
|
|
895
|
+
this.persistStats();
|
|
896
|
+
return { sessionId: job.sessionId || undefined, job };
|
|
752
897
|
}
|
|
753
898
|
}
|
|
754
899
|
|
|
900
|
+
// src/task-manager/quality.ts
|
|
901
|
+
var DEFAULT_QUALITY_CONFIG = {
|
|
902
|
+
enabled: true
|
|
903
|
+
};
|
|
904
|
+
var ERROR_KEYWORDS = [
|
|
905
|
+
"error",
|
|
906
|
+
"failed",
|
|
907
|
+
"failure",
|
|
908
|
+
"exception",
|
|
909
|
+
"traceback",
|
|
910
|
+
"unable to",
|
|
911
|
+
"失败",
|
|
912
|
+
"错误",
|
|
913
|
+
"异常",
|
|
914
|
+
"无法完成",
|
|
915
|
+
"未能"
|
|
916
|
+
];
|
|
917
|
+
var BOUNDARY_KEYWORDS = new Set(["error", "failed", "failure", "exception"]);
|
|
918
|
+
var NEGATION_PHRASES = [
|
|
919
|
+
"no error",
|
|
920
|
+
"no errors",
|
|
921
|
+
"no failure",
|
|
922
|
+
"no failures",
|
|
923
|
+
"no exception",
|
|
924
|
+
"no exceptions",
|
|
925
|
+
"without error",
|
|
926
|
+
"without errors",
|
|
927
|
+
"without any error",
|
|
928
|
+
"without any errors",
|
|
929
|
+
"without failure",
|
|
930
|
+
"without failures",
|
|
931
|
+
"without exception",
|
|
932
|
+
"without exceptions",
|
|
933
|
+
"without any failure",
|
|
934
|
+
"without any failures",
|
|
935
|
+
"without any exception",
|
|
936
|
+
"without any exceptions",
|
|
937
|
+
"with no error",
|
|
938
|
+
"with no errors",
|
|
939
|
+
"with no failure",
|
|
940
|
+
"with no failures",
|
|
941
|
+
"not an error",
|
|
942
|
+
"not a failure",
|
|
943
|
+
"is not an error",
|
|
944
|
+
"never failed",
|
|
945
|
+
"never fails",
|
|
946
|
+
"did not fail",
|
|
947
|
+
"does not fail",
|
|
948
|
+
"not failed",
|
|
949
|
+
"无错误",
|
|
950
|
+
"没有错误",
|
|
951
|
+
"未失败",
|
|
952
|
+
"没有失败",
|
|
953
|
+
"未发现错误",
|
|
954
|
+
"未出现错误",
|
|
955
|
+
"未发生错误",
|
|
956
|
+
"不存在错误",
|
|
957
|
+
"无异常",
|
|
958
|
+
"没有异常",
|
|
959
|
+
"未发现异常",
|
|
960
|
+
"未出现异常",
|
|
961
|
+
"没有发现错误",
|
|
962
|
+
"没有发现任何错误",
|
|
963
|
+
"未发现任何错误",
|
|
964
|
+
"无任何错误",
|
|
965
|
+
"未出现任何错误",
|
|
966
|
+
"未发生任何错误",
|
|
967
|
+
"不存在任何错误",
|
|
968
|
+
"没有发现异常",
|
|
969
|
+
"未发现任何异常",
|
|
970
|
+
"无任何异常",
|
|
971
|
+
"未遇到错误",
|
|
972
|
+
"未产生错误",
|
|
973
|
+
"未遇到异常",
|
|
974
|
+
"未产生异常"
|
|
975
|
+
];
|
|
976
|
+
var NEGATION_WINDOW = 16;
|
|
977
|
+
function isNegated(lower, hitStart, hitLength) {
|
|
978
|
+
const windowStart = Math.max(0, hitStart - NEGATION_WINDOW);
|
|
979
|
+
const window = lower.slice(windowStart, hitStart + hitLength);
|
|
980
|
+
return NEGATION_PHRASES.some((phrase) => window.includes(phrase));
|
|
981
|
+
}
|
|
982
|
+
function hasErrorKeywords(text) {
|
|
983
|
+
const lower = text.toLowerCase();
|
|
984
|
+
for (const kw of BOUNDARY_KEYWORDS) {
|
|
985
|
+
const re = new RegExp(`\\b${kw}\\b`, "g");
|
|
986
|
+
let m;
|
|
987
|
+
while ((m = re.exec(lower)) !== null) {
|
|
988
|
+
const before = m.index > 0 ? lower[m.index - 1] : "";
|
|
989
|
+
const after = lower[m.index + kw.length] ?? "";
|
|
990
|
+
if (before === "." || after === "(")
|
|
991
|
+
continue;
|
|
992
|
+
if (isNegated(lower, m.index, kw.length))
|
|
993
|
+
continue;
|
|
994
|
+
return true;
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
for (const kw of ERROR_KEYWORDS) {
|
|
998
|
+
if (BOUNDARY_KEYWORDS.has(kw))
|
|
999
|
+
continue;
|
|
1000
|
+
let idx = lower.indexOf(kw);
|
|
1001
|
+
while (idx !== -1) {
|
|
1002
|
+
if (!isNegated(lower, idx, kw.length))
|
|
1003
|
+
return true;
|
|
1004
|
+
idx = lower.indexOf(kw, idx + kw.length);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
return false;
|
|
1008
|
+
}
|
|
1009
|
+
function assessQuality(input) {
|
|
1010
|
+
const outputOk = input.output.trim().length > 0;
|
|
1011
|
+
const exitOk = input.exitCode === undefined || input.exitCode === 0;
|
|
1012
|
+
const keywordOk = !hasErrorKeywords(input.output);
|
|
1013
|
+
const decisionsOk = input.decisions >= 1;
|
|
1014
|
+
const timedOut = input.timedOut === true;
|
|
1015
|
+
let score = 0;
|
|
1016
|
+
if (outputOk)
|
|
1017
|
+
score += 0.25;
|
|
1018
|
+
if (exitOk && !timedOut)
|
|
1019
|
+
score += 0.25;
|
|
1020
|
+
if (keywordOk && !timedOut)
|
|
1021
|
+
score += 0.25;
|
|
1022
|
+
if (decisionsOk && !timedOut)
|
|
1023
|
+
score += 0.25;
|
|
1024
|
+
if (!outputOk)
|
|
1025
|
+
score = 0;
|
|
1026
|
+
const passed = !timedOut && outputOk && exitOk && keywordOk && decisionsOk;
|
|
1027
|
+
let failureCategory;
|
|
1028
|
+
if (!passed) {
|
|
1029
|
+
if (timedOut)
|
|
1030
|
+
failureCategory = "timeout";
|
|
1031
|
+
else if (!outputOk)
|
|
1032
|
+
failureCategory = "empty";
|
|
1033
|
+
else if (!exitOk || !keywordOk)
|
|
1034
|
+
failureCategory = "error";
|
|
1035
|
+
else
|
|
1036
|
+
failureCategory = "quality-low";
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
score,
|
|
1040
|
+
passed,
|
|
1041
|
+
...failureCategory ? { failureCategory } : {},
|
|
1042
|
+
...input.latencyMs !== undefined ? { latencyMs: input.latencyMs } : {},
|
|
1043
|
+
...input.tokens ? { tokens: input.tokens } : {}
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
function isQualityEnabled(config) {
|
|
1047
|
+
return config?.enabled ?? DEFAULT_QUALITY_CONFIG.enabled;
|
|
1048
|
+
}
|
|
1049
|
+
|
|
755
1050
|
// src/context/types.ts
|
|
756
1051
|
var DEFAULT_CONTEXT_CONFIG = {
|
|
757
1052
|
strategy: {
|
|
@@ -776,6 +1071,9 @@ var DEFAULT_CONTEXT_CONFIG = {
|
|
|
776
1071
|
relevantMessageWindow: 20
|
|
777
1072
|
};
|
|
778
1073
|
|
|
1074
|
+
// src/context/engine.ts
|
|
1075
|
+
import * as fs2 from "node:fs";
|
|
1076
|
+
|
|
779
1077
|
// src/context/extractor.ts
|
|
780
1078
|
var KNOWN_EXTENSIONS = new Set([
|
|
781
1079
|
"ts",
|
|
@@ -827,12 +1125,12 @@ function extractRelevantFiles(messages, maxFiles, windowSize) {
|
|
|
827
1125
|
if (part.type === "tool" && part.state) {
|
|
828
1126
|
const state = part.state;
|
|
829
1127
|
const input = state.input ?? {};
|
|
830
|
-
const
|
|
831
|
-
if (
|
|
832
|
-
if (!fileMap.has(
|
|
833
|
-
fileMap.set(
|
|
1128
|
+
const path2 = extractPath(input);
|
|
1129
|
+
if (path2) {
|
|
1130
|
+
if (!fileMap.has(path2)) {
|
|
1131
|
+
fileMap.set(path2, { path: path2, summary: "" });
|
|
834
1132
|
}
|
|
835
|
-
const existing = fileMap.get(
|
|
1133
|
+
const existing = fileMap.get(path2);
|
|
836
1134
|
if (typeof input.offset === "number") {
|
|
837
1135
|
const limit = typeof input.limit === "number" ? input.limit : 50;
|
|
838
1136
|
existing.lines = `${input.offset}-${input.offset + limit}`;
|
|
@@ -952,6 +1250,25 @@ function extractErrors(messages, maxErrors, windowSize) {
|
|
|
952
1250
|
}
|
|
953
1251
|
return errors;
|
|
954
1252
|
}
|
|
1253
|
+
function truncateByTokens(text, maxTokens) {
|
|
1254
|
+
if (!text || maxTokens <= 0)
|
|
1255
|
+
return "";
|
|
1256
|
+
let used = 0;
|
|
1257
|
+
let cut = text.length;
|
|
1258
|
+
for (let i = 0;i < text.length; i++) {
|
|
1259
|
+
const code = text.charCodeAt(i);
|
|
1260
|
+
const isCjk = code >= 19968 && code <= 40959 || code >= 13312 && code <= 19903 || code >= 63744 && code <= 64255 || code >= 12352 && code <= 12543 || code >= 44032 && code <= 55215;
|
|
1261
|
+
used += isCjk ? 1 : 0.25;
|
|
1262
|
+
if (used > maxTokens) {
|
|
1263
|
+
cut = i;
|
|
1264
|
+
break;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
if (cut >= text.length)
|
|
1268
|
+
return text;
|
|
1269
|
+
return text.slice(0, cut) + `
|
|
1270
|
+
… [正文已按 token 预算截断]`;
|
|
1271
|
+
}
|
|
955
1272
|
|
|
956
1273
|
// src/context/formatter.ts
|
|
957
1274
|
var CONTEXT_MARKER_PATTERN = /<!-- CONTEXT:ID=([a-f0-9-]+) -->/;
|
|
@@ -1043,6 +1360,7 @@ class ContextEngine {
|
|
|
1043
1360
|
return;
|
|
1044
1361
|
if (args.strategy === "none")
|
|
1045
1362
|
return;
|
|
1363
|
+
context.strategy = args.strategy;
|
|
1046
1364
|
try {
|
|
1047
1365
|
const windowSize = this.config.relevantMessageWindow;
|
|
1048
1366
|
const messagesResult = await this.client.session.messages({
|
|
@@ -1055,11 +1373,25 @@ class ContextEngine {
|
|
|
1055
1373
|
context.decisions = extractDecisions(messages, this.config.maxDecisions, windowSize);
|
|
1056
1374
|
context.errors = extractErrors(messages, this.config.maxErrors, windowSize);
|
|
1057
1375
|
}
|
|
1376
|
+
if (args.strategy === "summary") {
|
|
1377
|
+
this.attachTruncatedBodies(context.relevantFiles);
|
|
1378
|
+
}
|
|
1058
1379
|
if (this.config.dependencyPropagation && this.dependencyCache.size > 0) {
|
|
1059
1380
|
context.dependencies = Array.from(this.dependencyCache.values()).slice(-this.config.maxDependencies);
|
|
1060
1381
|
}
|
|
1061
1382
|
} catch {}
|
|
1062
1383
|
}
|
|
1384
|
+
attachTruncatedBodies(files) {
|
|
1385
|
+
if (files.length === 0)
|
|
1386
|
+
return;
|
|
1387
|
+
const budgetPerFile = Math.max(1, Math.floor(this.config.summarizeMaxTokens / files.length));
|
|
1388
|
+
for (const file of files) {
|
|
1389
|
+
try {
|
|
1390
|
+
const content = fs2.readFileSync(file.path, "utf-8");
|
|
1391
|
+
file.body = truncateByTokens(content, budgetPerFile);
|
|
1392
|
+
} catch {}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1063
1395
|
consumeMarkedContext(messageText) {
|
|
1064
1396
|
const markerMatch = messageText.match(CONTEXT_MARKER_PATTERN);
|
|
1065
1397
|
if (!markerMatch)
|
|
@@ -1078,10 +1410,23 @@ class ContextEngine {
|
|
|
1078
1410
|
if (!context)
|
|
1079
1411
|
return "";
|
|
1080
1412
|
const parts = [];
|
|
1413
|
+
const isSummary = context.strategy === "summary";
|
|
1081
1414
|
if (context.relevantFiles.length > 0) {
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1415
|
+
if (isSummary) {
|
|
1416
|
+
parts.push("### \uD83D\uDCC1 相关文件(正文按 token 预算截断)");
|
|
1417
|
+
for (const file of context.relevantFiles) {
|
|
1418
|
+
const loc = file.lines ? ":" + file.lines : "";
|
|
1419
|
+
if (file.body) {
|
|
1420
|
+
parts.push("- `" + file.path + loc + "`:\n```text\n" + file.body + "\n```");
|
|
1421
|
+
} else {
|
|
1422
|
+
parts.push("- `" + file.path + loc + "` — " + (file.summary || "(正文不可用)"));
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
} else {
|
|
1426
|
+
parts.push("### \uD83D\uDCC1 相关文件");
|
|
1427
|
+
for (const file of context.relevantFiles) {
|
|
1428
|
+
parts.push("- `" + file.path + "`" + (file.summary ? " — " + file.summary : ""));
|
|
1429
|
+
}
|
|
1085
1430
|
}
|
|
1086
1431
|
}
|
|
1087
1432
|
if (context.decisions.length > 0) {
|
|
@@ -1108,27 +1453,31 @@ class ContextEngine {
|
|
|
1108
1453
|
` : "";
|
|
1109
1454
|
}
|
|
1110
1455
|
async captureResult(childSessionId, alias, agent) {
|
|
1111
|
-
if (!this.config.dependencyPropagation)
|
|
1112
|
-
return;
|
|
1113
1456
|
try {
|
|
1114
1457
|
const messagesResult = await this.client.session.messages({
|
|
1115
1458
|
path: { id: childSessionId }
|
|
1116
1459
|
});
|
|
1117
1460
|
const messages = messagesResult.data ?? [];
|
|
1118
1461
|
let keyOutput = "";
|
|
1462
|
+
let tokens;
|
|
1119
1463
|
for (let i = messages.length - 1;i >= 0; i--) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if (
|
|
1464
|
+
const info = messages[i].info;
|
|
1465
|
+
if (!info || info.role !== "assistant")
|
|
1466
|
+
continue;
|
|
1467
|
+
if (typeof info.tokens?.input === "number" && typeof info.tokens?.output === "number") {
|
|
1468
|
+
tokens = { input: info.tokens.input, output: info.tokens.output };
|
|
1469
|
+
}
|
|
1470
|
+
for (const part of messages[i].parts ?? []) {
|
|
1471
|
+
if (part.type === "text" && part.text) {
|
|
1472
|
+
keyOutput = part.text.slice(0, 500).replace(/\n/g, " ");
|
|
1128
1473
|
break;
|
|
1474
|
+
}
|
|
1129
1475
|
}
|
|
1476
|
+
if (keyOutput)
|
|
1477
|
+
break;
|
|
1130
1478
|
}
|
|
1131
|
-
|
|
1479
|
+
const decisions = extractDecisions(messages, this.config.maxDecisions, this.config.relevantMessageWindow);
|
|
1480
|
+
if (keyOutput && this.config.dependencyPropagation) {
|
|
1132
1481
|
this.dependencyCache.set(alias, {
|
|
1133
1482
|
alias,
|
|
1134
1483
|
agent,
|
|
@@ -1136,7 +1485,14 @@ class ContextEngine {
|
|
|
1136
1485
|
capturedAt: Date.now()
|
|
1137
1486
|
});
|
|
1138
1487
|
}
|
|
1139
|
-
|
|
1488
|
+
return {
|
|
1489
|
+
output: keyOutput,
|
|
1490
|
+
decisions: decisions.length,
|
|
1491
|
+
...tokens ? { tokens } : {}
|
|
1492
|
+
};
|
|
1493
|
+
} catch {
|
|
1494
|
+
return null;
|
|
1495
|
+
}
|
|
1140
1496
|
}
|
|
1141
1497
|
formatMarker(contextId) {
|
|
1142
1498
|
return formatContextMarker(contextId);
|
|
@@ -1155,15 +1511,15 @@ class ContextEngine {
|
|
|
1155
1511
|
}
|
|
1156
1512
|
|
|
1157
1513
|
// src/config/loader.ts
|
|
1158
|
-
import * as
|
|
1159
|
-
import * as
|
|
1160
|
-
import * as
|
|
1161
|
-
var USER_CONFIG_PATH =
|
|
1514
|
+
import * as fs3 from "node:fs";
|
|
1515
|
+
import * as path2 from "node:path";
|
|
1516
|
+
import * as os2 from "node:os";
|
|
1517
|
+
var USER_CONFIG_PATH = path2.join(os2.homedir(), ".config", "opencode", "oh-my-opencode-cohub.json");
|
|
1162
1518
|
function loadCoHubConfig(projectDir) {
|
|
1163
1519
|
const base = readJSON(USER_CONFIG_PATH);
|
|
1164
1520
|
if (!projectDir)
|
|
1165
1521
|
return base;
|
|
1166
|
-
const projectPath =
|
|
1522
|
+
const projectPath = path2.join(projectDir, ".opencode", "oh-my-opencode-cohub.json");
|
|
1167
1523
|
const project = readJSON(projectPath);
|
|
1168
1524
|
if (!project || Object.keys(project).length === 0)
|
|
1169
1525
|
return base;
|
|
@@ -1177,13 +1533,16 @@ function loadCoHubConfig(projectDir) {
|
|
|
1177
1533
|
if (project.context) {
|
|
1178
1534
|
merged.context = { ...base.context, ...project.context };
|
|
1179
1535
|
}
|
|
1536
|
+
if (project.quality) {
|
|
1537
|
+
merged.quality = { ...base.quality, ...project.quality };
|
|
1538
|
+
}
|
|
1180
1539
|
return merged;
|
|
1181
1540
|
}
|
|
1182
1541
|
function readJSON(filePath) {
|
|
1183
1542
|
try {
|
|
1184
|
-
if (!
|
|
1543
|
+
if (!fs3.existsSync(filePath))
|
|
1185
1544
|
return {};
|
|
1186
|
-
const raw =
|
|
1545
|
+
const raw = fs3.readFileSync(filePath, "utf-8");
|
|
1187
1546
|
return JSON.parse(raw);
|
|
1188
1547
|
} catch {
|
|
1189
1548
|
return {};
|
|
@@ -1194,10 +1553,10 @@ function readJSON(filePath) {
|
|
|
1194
1553
|
import { tool } from "@opencode-ai/plugin";
|
|
1195
1554
|
|
|
1196
1555
|
// src/utils/log.ts
|
|
1197
|
-
import * as
|
|
1198
|
-
import * as
|
|
1199
|
-
import * as
|
|
1200
|
-
var LOG_DIR =
|
|
1556
|
+
import * as fs4 from "fs/promises";
|
|
1557
|
+
import * as path3 from "path";
|
|
1558
|
+
import * as os3 from "os";
|
|
1559
|
+
var LOG_DIR = path3.join(os3.homedir(), ".local", "share", "opencode", "log");
|
|
1201
1560
|
var LOG_PREFIX = "oh-my-opencode-cohub";
|
|
1202
1561
|
var KEEP_DAYS = 7;
|
|
1203
1562
|
var _today = "";
|
|
@@ -1207,7 +1566,7 @@ function getLogPath() {
|
|
|
1207
1566
|
const today = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
1208
1567
|
if (today !== _today) {
|
|
1209
1568
|
_today = today;
|
|
1210
|
-
_logPath =
|
|
1569
|
+
_logPath = path3.join(LOG_DIR, `${LOG_PREFIX}.${today}.log`);
|
|
1211
1570
|
_cleanedToday = false;
|
|
1212
1571
|
}
|
|
1213
1572
|
return _logPath;
|
|
@@ -1220,7 +1579,7 @@ async function cleanOldLogsOnce() {
|
|
|
1220
1579
|
const cutoff = new Date;
|
|
1221
1580
|
cutoff.setUTCDate(cutoff.getUTCDate() - KEEP_DAYS);
|
|
1222
1581
|
const cutoffStr = cutoff.toISOString().slice(0, 10).replace(/-/g, "");
|
|
1223
|
-
const files = await
|
|
1582
|
+
const files = await fs4.readdir(LOG_DIR);
|
|
1224
1583
|
for (const file of files) {
|
|
1225
1584
|
if (!file.startsWith(LOG_PREFIX + "."))
|
|
1226
1585
|
continue;
|
|
@@ -1229,7 +1588,7 @@ async function cleanOldLogsOnce() {
|
|
|
1229
1588
|
if (!match)
|
|
1230
1589
|
continue;
|
|
1231
1590
|
if (match[1] < cutoffStr) {
|
|
1232
|
-
await
|
|
1591
|
+
await fs4.unlink(path3.join(LOG_DIR, file)).catch(() => {});
|
|
1233
1592
|
}
|
|
1234
1593
|
}
|
|
1235
1594
|
} catch {}
|
|
@@ -1237,7 +1596,7 @@ async function cleanOldLogsOnce() {
|
|
|
1237
1596
|
async function appendLog(tag, message, err) {
|
|
1238
1597
|
try {
|
|
1239
1598
|
const logPath = getLogPath();
|
|
1240
|
-
await
|
|
1599
|
+
await fs4.mkdir(LOG_DIR, { recursive: true });
|
|
1241
1600
|
const now = new Date().toISOString();
|
|
1242
1601
|
let line = `[${now}] [oh-my-opencode-cohub] [${tag}] ${message}`;
|
|
1243
1602
|
if (err instanceof Error) {
|
|
@@ -1249,13 +1608,158 @@ async function appendLog(tag, message, err) {
|
|
|
1249
1608
|
}
|
|
1250
1609
|
line += `
|
|
1251
1610
|
`;
|
|
1252
|
-
await
|
|
1611
|
+
await fs4.appendFile(logPath, line, "utf-8");
|
|
1253
1612
|
await cleanOldLogsOnce();
|
|
1254
1613
|
} catch {}
|
|
1255
1614
|
}
|
|
1256
1615
|
|
|
1616
|
+
// src/tools/model-stats.ts
|
|
1617
|
+
var DEFAULT_MODEL_STATS_CONFIG = {
|
|
1618
|
+
windowSize: 20,
|
|
1619
|
+
highFailureRate: 0.5,
|
|
1620
|
+
minSamples: 5
|
|
1621
|
+
};
|
|
1622
|
+
var config = { ...DEFAULT_MODEL_STATS_CONFIG };
|
|
1623
|
+
var history = new Map;
|
|
1624
|
+
function recordModelResult(model, category) {
|
|
1625
|
+
const window = history.get(model) ?? [];
|
|
1626
|
+
window.push(category);
|
|
1627
|
+
if (window.length > config.windowSize) {
|
|
1628
|
+
window.splice(0, window.length - config.windowSize);
|
|
1629
|
+
}
|
|
1630
|
+
history.set(model, window);
|
|
1631
|
+
}
|
|
1632
|
+
function getModelStats(model) {
|
|
1633
|
+
const window = history.get(model) ?? [];
|
|
1634
|
+
let successes = 0;
|
|
1635
|
+
let empty = 0;
|
|
1636
|
+
let timeout = 0;
|
|
1637
|
+
let error = 0;
|
|
1638
|
+
let qualityLow = 0;
|
|
1639
|
+
for (const c of window) {
|
|
1640
|
+
if (c === "success") {
|
|
1641
|
+
successes += 1;
|
|
1642
|
+
} else if (c === "empty") {
|
|
1643
|
+
empty += 1;
|
|
1644
|
+
} else if (c === "timeout") {
|
|
1645
|
+
timeout += 1;
|
|
1646
|
+
} else if (c === "error") {
|
|
1647
|
+
error += 1;
|
|
1648
|
+
} else {
|
|
1649
|
+
qualityLow += 1;
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
const total = window.length;
|
|
1653
|
+
const failures = total - successes;
|
|
1654
|
+
return {
|
|
1655
|
+
model,
|
|
1656
|
+
total,
|
|
1657
|
+
successes,
|
|
1658
|
+
failures,
|
|
1659
|
+
empty,
|
|
1660
|
+
timeout,
|
|
1661
|
+
error,
|
|
1662
|
+
qualityLow,
|
|
1663
|
+
failureRate: total === 0 ? 0 : failures / total,
|
|
1664
|
+
successRate: total === 0 ? 1 : successes / total
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
function orderCouncillorsByFailure(entries, statsProvider = getModelStats, cfg = config) {
|
|
1668
|
+
const isHighRisk = ([, c]) => {
|
|
1669
|
+
const s = statsProvider(c.model);
|
|
1670
|
+
return s.total >= cfg.minSamples && s.failureRate > cfg.highFailureRate;
|
|
1671
|
+
};
|
|
1672
|
+
const preferred = entries.filter((entry) => !isHighRisk(entry));
|
|
1673
|
+
const chosen = preferred.length > 0 ? preferred : [...entries];
|
|
1674
|
+
return chosen.sort((a, b) => statsProvider(a[1].model).failureRate - statsProvider(b[1].model).failureRate);
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// src/tools/adaptive-params.ts
|
|
1678
|
+
var DEFAULT_ADAPTIVE_CONFIG = {
|
|
1679
|
+
highRate: 0.8,
|
|
1680
|
+
lowRate: 0.5,
|
|
1681
|
+
minSamples: 5,
|
|
1682
|
+
hysteresisSamples: 5,
|
|
1683
|
+
minRetries: 1,
|
|
1684
|
+
maxRetries: 5,
|
|
1685
|
+
minTimeoutMs: 60000,
|
|
1686
|
+
maxTimeoutMs: 300000,
|
|
1687
|
+
defaultRetries: 3,
|
|
1688
|
+
defaultTimeoutMs: 180000,
|
|
1689
|
+
timeoutStepMs: 30000,
|
|
1690
|
+
windowSize: 20
|
|
1691
|
+
};
|
|
1692
|
+
var config2 = { ...DEFAULT_ADAPTIVE_CONFIG };
|
|
1693
|
+
var samples = new Map;
|
|
1694
|
+
function evaluateAdjustment(successRate, sampleCount, cfg = config2) {
|
|
1695
|
+
if (sampleCount < cfg.minSamples)
|
|
1696
|
+
return 0;
|
|
1697
|
+
if (successRate > cfg.highRate)
|
|
1698
|
+
return -1;
|
|
1699
|
+
if (successRate < cfg.lowRate)
|
|
1700
|
+
return 1;
|
|
1701
|
+
return 0;
|
|
1702
|
+
}
|
|
1703
|
+
function clampAdaptiveParams(params, cfg = config2) {
|
|
1704
|
+
return {
|
|
1705
|
+
retries: Math.min(cfg.maxRetries, Math.max(cfg.minRetries, Math.round(params.retries))),
|
|
1706
|
+
timeoutMs: Math.min(cfg.maxTimeoutMs, Math.max(cfg.minTimeoutMs, Math.round(params.timeoutMs)))
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
function adjustAdaptiveParams(current, direction, cfg = config2) {
|
|
1710
|
+
if (direction === 0)
|
|
1711
|
+
return clampAdaptiveParams(current, cfg);
|
|
1712
|
+
return clampAdaptiveParams({
|
|
1713
|
+
retries: current.retries + direction,
|
|
1714
|
+
timeoutMs: current.timeoutMs + direction * cfg.timeoutStepMs
|
|
1715
|
+
}, cfg);
|
|
1716
|
+
}
|
|
1717
|
+
function recordAdaptiveSample(key, success, cfg = config2) {
|
|
1718
|
+
let st = samples.get(key);
|
|
1719
|
+
if (!st) {
|
|
1720
|
+
st = {
|
|
1721
|
+
recent: [],
|
|
1722
|
+
current: { retries: cfg.defaultRetries, timeoutMs: cfg.defaultTimeoutMs },
|
|
1723
|
+
lastDirection: 0,
|
|
1724
|
+
streak: 0
|
|
1725
|
+
};
|
|
1726
|
+
samples.set(key, st);
|
|
1727
|
+
}
|
|
1728
|
+
st.recent.push(success);
|
|
1729
|
+
if (st.recent.length > cfg.windowSize) {
|
|
1730
|
+
st.recent.splice(0, st.recent.length - cfg.windowSize);
|
|
1731
|
+
}
|
|
1732
|
+
const total = st.recent.length;
|
|
1733
|
+
if (total < cfg.minSamples)
|
|
1734
|
+
return;
|
|
1735
|
+
const successes = st.recent.filter(Boolean).length;
|
|
1736
|
+
const direction = evaluateAdjustment(successes / total, total, cfg);
|
|
1737
|
+
if (direction === st.lastDirection) {
|
|
1738
|
+
st.streak += 1;
|
|
1739
|
+
} else {
|
|
1740
|
+
st.lastDirection = direction;
|
|
1741
|
+
st.streak = 1;
|
|
1742
|
+
}
|
|
1743
|
+
if (st.streak >= cfg.hysteresisSamples) {
|
|
1744
|
+
st.current = adjustAdaptiveParams(st.current, direction, cfg);
|
|
1745
|
+
st.streak = 0;
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
function getAdaptiveParams(key, cfg = config2) {
|
|
1749
|
+
const st = samples.get(key);
|
|
1750
|
+
if (!st)
|
|
1751
|
+
return { retries: cfg.defaultRetries, timeoutMs: cfg.defaultTimeoutMs };
|
|
1752
|
+
return { ...st.current };
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1257
1755
|
// src/tools/council.ts
|
|
1258
1756
|
var z = tool.schema;
|
|
1757
|
+
var RETRY_LIMITS = {
|
|
1758
|
+
empty: 3,
|
|
1759
|
+
timeout: 1,
|
|
1760
|
+
error: 0,
|
|
1761
|
+
"quality-low": 1
|
|
1762
|
+
};
|
|
1259
1763
|
|
|
1260
1764
|
class OperationTimeoutError extends Error {
|
|
1261
1765
|
constructor(message) {
|
|
@@ -1285,12 +1789,12 @@ async function abortSession(client, sessionId) {
|
|
|
1285
1789
|
appendLog("abortSession", "中止会话失败", err);
|
|
1286
1790
|
}
|
|
1287
1791
|
}
|
|
1288
|
-
async function promptWithTimeout(client,
|
|
1289
|
-
const sessionId =
|
|
1792
|
+
async function promptWithTimeout(client, path4, body, timeoutMs, directory) {
|
|
1793
|
+
const sessionId = path4.id;
|
|
1290
1794
|
let timer;
|
|
1291
1795
|
try {
|
|
1292
1796
|
const promptPromise = client.session.prompt({
|
|
1293
|
-
path:
|
|
1797
|
+
path: path4,
|
|
1294
1798
|
body,
|
|
1295
1799
|
query: directory ? { directory } : undefined
|
|
1296
1800
|
});
|
|
@@ -1316,6 +1820,7 @@ async function promptWithTimeout(client, path3, body, timeoutMs, directory) {
|
|
|
1316
1820
|
}
|
|
1317
1821
|
async function extractSessionResult(client, sessionId, options) {
|
|
1318
1822
|
const includeReasoning = options?.includeReasoning ?? true;
|
|
1823
|
+
const lastTextOnly = options?.lastTextOnly === true;
|
|
1319
1824
|
const messagesResult = await client.session.messages({
|
|
1320
1825
|
path: { id: sessionId }
|
|
1321
1826
|
});
|
|
@@ -1332,7 +1837,8 @@ async function extractSessionResult(client, sessionId, options) {
|
|
|
1332
1837
|
}
|
|
1333
1838
|
}
|
|
1334
1839
|
}
|
|
1335
|
-
const
|
|
1840
|
+
const texts = extractedContent.filter((t) => t.length > 0);
|
|
1841
|
+
const text = lastTextOnly ? texts.length > 0 ? texts[texts.length - 1] : "" : texts.join(`
|
|
1336
1842
|
|
|
1337
1843
|
`);
|
|
1338
1844
|
return { text, empty: text.length === 0 };
|
|
@@ -1395,18 +1901,254 @@ ${cr.result}`;
|
|
|
1395
1901
|
return parts.join(`
|
|
1396
1902
|
`);
|
|
1397
1903
|
}
|
|
1904
|
+
function buildDeliberationPrompt(originalPrompt, previousRound, currentName) {
|
|
1905
|
+
const others = previousRound.filter((cr) => cr.status === "completed" && cr.result && cr.name !== currentName).map((cr) => `**${cr.name}** (${shortModelLabel(cr.model)}):
|
|
1906
|
+
${cr.result}`).join(`
|
|
1907
|
+
|
|
1908
|
+
`);
|
|
1909
|
+
if (!others)
|
|
1910
|
+
return originalPrompt;
|
|
1911
|
+
return [
|
|
1912
|
+
originalPrompt,
|
|
1913
|
+
"",
|
|
1914
|
+
"---",
|
|
1915
|
+
"",
|
|
1916
|
+
"以下是其他成员第一轮意见,请回应并修正或坚持你的立场:",
|
|
1917
|
+
"",
|
|
1918
|
+
others
|
|
1919
|
+
].join(`
|
|
1920
|
+
`);
|
|
1921
|
+
}
|
|
1398
1922
|
function formatModelComposition(councillorResults) {
|
|
1399
1923
|
return councillorResults.map((cr) => `${cr.name}: ${shortModelLabel(cr.model)}`).join(", ");
|
|
1400
1924
|
}
|
|
1925
|
+
function adaptiveKey(config3) {
|
|
1926
|
+
return `${config3.variant ?? "default"}\x00${config3.model}`;
|
|
1927
|
+
}
|
|
1928
|
+
function classifyFailure(message) {
|
|
1929
|
+
if (message.includes("Empty response from provider"))
|
|
1930
|
+
return "empty";
|
|
1931
|
+
if (message.includes("timed out") || message.includes("OperationTimeoutError"))
|
|
1932
|
+
return "timeout";
|
|
1933
|
+
return "error";
|
|
1934
|
+
}
|
|
1935
|
+
var CONCLUSION_MARKERS = [
|
|
1936
|
+
"综上",
|
|
1937
|
+
"结论",
|
|
1938
|
+
"因此",
|
|
1939
|
+
"总之",
|
|
1940
|
+
"建议",
|
|
1941
|
+
"推荐",
|
|
1942
|
+
"我认为",
|
|
1943
|
+
"最终",
|
|
1944
|
+
"关键结论",
|
|
1945
|
+
"conclusion",
|
|
1946
|
+
"therefore",
|
|
1947
|
+
"in summary",
|
|
1948
|
+
"in conclusion",
|
|
1949
|
+
"recommend",
|
|
1950
|
+
"suggest",
|
|
1951
|
+
"overall"
|
|
1952
|
+
];
|
|
1953
|
+
var ENTITY_MARKERS = [
|
|
1954
|
+
"决策",
|
|
1955
|
+
"决定",
|
|
1956
|
+
"方案",
|
|
1957
|
+
"采用",
|
|
1958
|
+
"选择",
|
|
1959
|
+
"关键",
|
|
1960
|
+
"实体",
|
|
1961
|
+
"要点",
|
|
1962
|
+
"关键决策",
|
|
1963
|
+
"decision",
|
|
1964
|
+
"entity",
|
|
1965
|
+
"key point",
|
|
1966
|
+
"option",
|
|
1967
|
+
"recommendation",
|
|
1968
|
+
"key entity"
|
|
1969
|
+
];
|
|
1970
|
+
function extractCouncillorSummary(text) {
|
|
1971
|
+
const sentences = text.split(/(?<=[。!?!?;;])\s*|\n+/).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
1972
|
+
const conclusions = sentences.filter((s) => CONCLUSION_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
1973
|
+
const entities = sentences.filter((s) => ENTITY_MARKERS.some((m) => s.toLowerCase().includes(m)));
|
|
1974
|
+
return {
|
|
1975
|
+
conclusions: conclusions.length > 0 ? conclusions : sentences.length > 0 ? [sentences[sentences.length - 1]] : [],
|
|
1976
|
+
entities
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
var STOPWORDS = new Set([
|
|
1980
|
+
"the",
|
|
1981
|
+
"a",
|
|
1982
|
+
"an",
|
|
1983
|
+
"and",
|
|
1984
|
+
"or",
|
|
1985
|
+
"of",
|
|
1986
|
+
"to",
|
|
1987
|
+
"in",
|
|
1988
|
+
"on",
|
|
1989
|
+
"for",
|
|
1990
|
+
"with",
|
|
1991
|
+
"is",
|
|
1992
|
+
"are",
|
|
1993
|
+
"was",
|
|
1994
|
+
"were",
|
|
1995
|
+
"be",
|
|
1996
|
+
"been",
|
|
1997
|
+
"this",
|
|
1998
|
+
"that",
|
|
1999
|
+
"it",
|
|
2000
|
+
"as",
|
|
2001
|
+
"at",
|
|
2002
|
+
"by",
|
|
2003
|
+
"from",
|
|
2004
|
+
"我们",
|
|
2005
|
+
"的",
|
|
2006
|
+
"了",
|
|
2007
|
+
"是",
|
|
2008
|
+
"在",
|
|
2009
|
+
"和",
|
|
2010
|
+
"与",
|
|
2011
|
+
"及",
|
|
2012
|
+
"或",
|
|
2013
|
+
"一个",
|
|
2014
|
+
"这个",
|
|
2015
|
+
"进行",
|
|
2016
|
+
"以及",
|
|
2017
|
+
"结论",
|
|
2018
|
+
"综上",
|
|
2019
|
+
"总之",
|
|
2020
|
+
"因此",
|
|
2021
|
+
"建议",
|
|
2022
|
+
"推荐",
|
|
2023
|
+
"最终",
|
|
2024
|
+
"我认为",
|
|
2025
|
+
"关键",
|
|
2026
|
+
"要点",
|
|
2027
|
+
"采用",
|
|
2028
|
+
"方案",
|
|
2029
|
+
"决策",
|
|
2030
|
+
"决定",
|
|
2031
|
+
"选择",
|
|
2032
|
+
"实体",
|
|
2033
|
+
"conclusion",
|
|
2034
|
+
"therefore",
|
|
2035
|
+
"summary",
|
|
2036
|
+
"recommend",
|
|
2037
|
+
"suggest",
|
|
2038
|
+
"overall",
|
|
2039
|
+
"decision",
|
|
2040
|
+
"option",
|
|
2041
|
+
"recommendation",
|
|
2042
|
+
"key",
|
|
2043
|
+
"point",
|
|
2044
|
+
"entity",
|
|
2045
|
+
"error"
|
|
2046
|
+
]);
|
|
2047
|
+
function tokenize(text) {
|
|
2048
|
+
return text.toLowerCase().split(/[^a-z0-9\u4e00-\u9fff]+/).filter((t) => t.length > 0 && !STOPWORDS.has(t));
|
|
2049
|
+
}
|
|
2050
|
+
function jaccard(x, y) {
|
|
2051
|
+
if (x.length === 0 || y.length === 0)
|
|
2052
|
+
return 0;
|
|
2053
|
+
const xs = new Set(x);
|
|
2054
|
+
const ys = new Set(y);
|
|
2055
|
+
const inter = [...xs].filter((t) => ys.has(t)).length;
|
|
2056
|
+
const union = new Set([...xs, ...ys]).size;
|
|
2057
|
+
return union === 0 ? 0 : inter / union;
|
|
2058
|
+
}
|
|
2059
|
+
function pairwiseAgreement(a, b) {
|
|
2060
|
+
const aConcl = tokenize(a.conclusions.join(" "));
|
|
2061
|
+
const bConcl = tokenize(b.conclusions.join(" "));
|
|
2062
|
+
const aEnt = tokenize(a.entities.join(" "));
|
|
2063
|
+
const bEnt = tokenize(b.entities.join(" "));
|
|
2064
|
+
return 0.5 * jaccard(aConcl, bConcl) + 0.5 * jaccard(aEnt, bEnt);
|
|
2065
|
+
}
|
|
2066
|
+
function computeConsensus(inputs, threshold) {
|
|
2067
|
+
const n = inputs.length;
|
|
2068
|
+
if (n === 0)
|
|
2069
|
+
return { consensus: false, agreementScores: {}, averageAgreement: 0 };
|
|
2070
|
+
if (n === 1) {
|
|
2071
|
+
return { consensus: true, agreementScores: { [inputs[0].name]: 1 }, averageAgreement: 1 };
|
|
2072
|
+
}
|
|
2073
|
+
const agreementScores = {};
|
|
2074
|
+
for (const a of inputs) {
|
|
2075
|
+
let sum = 0;
|
|
2076
|
+
for (const b of inputs) {
|
|
2077
|
+
if (a.name === b.name)
|
|
2078
|
+
continue;
|
|
2079
|
+
sum += pairwiseAgreement(a.summary, b.summary);
|
|
2080
|
+
}
|
|
2081
|
+
agreementScores[a.name] = sum / (n - 1);
|
|
2082
|
+
}
|
|
2083
|
+
const values = Object.values(agreementScores);
|
|
2084
|
+
const averageAgreement = values.reduce((acc, v) => acc + v, 0) / values.length;
|
|
2085
|
+
const agreeing = values.filter((v) => v >= threshold).length;
|
|
2086
|
+
const required = Math.ceil(2 * n / 3);
|
|
2087
|
+
return { consensus: agreeing >= required, agreementScores, averageAgreement };
|
|
2088
|
+
}
|
|
2089
|
+
function decideRetryAction(category, consecutiveStreak, categoryRetriesUsed, budgetAvailable, categoryLimit = RETRY_LIMITS[category]) {
|
|
2090
|
+
if (!budgetAvailable)
|
|
2091
|
+
return "budget-exhausted";
|
|
2092
|
+
if (consecutiveStreak >= 2)
|
|
2093
|
+
return "needs-human";
|
|
2094
|
+
if (categoryRetriesUsed >= categoryLimit)
|
|
2095
|
+
return "give-up";
|
|
2096
|
+
return "retry";
|
|
2097
|
+
}
|
|
2098
|
+
var VARIANT_ORDER = ["max", "high", "medium", "low"];
|
|
2099
|
+
function downgradeVariant(variant) {
|
|
2100
|
+
if (!variant)
|
|
2101
|
+
return;
|
|
2102
|
+
const idx = VARIANT_ORDER.indexOf(variant);
|
|
2103
|
+
if (idx < 0 || idx >= VARIANT_ORDER.length - 1)
|
|
2104
|
+
return;
|
|
2105
|
+
return VARIANT_ORDER[idx + 1];
|
|
2106
|
+
}
|
|
2107
|
+
function appendRetryContext(prompt, category) {
|
|
2108
|
+
if (category === "timeout") {
|
|
2109
|
+
return `${prompt}
|
|
2110
|
+
|
|
2111
|
+
[retry] 上次尝试超时。请精炼作答:先给出结论与关键决策,避免冗长过程。`;
|
|
2112
|
+
}
|
|
2113
|
+
if (category === "quality-low") {
|
|
2114
|
+
return `${prompt}
|
|
2115
|
+
|
|
2116
|
+
[retry] 上次回答缺少明确的结论与关键实体/决策。请以"结论:..."开头,并列出关键决策/实体要点。`;
|
|
2117
|
+
}
|
|
2118
|
+
return prompt;
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
class RetryBudget {
|
|
2122
|
+
retriesLeft;
|
|
2123
|
+
maxElapsedMs;
|
|
2124
|
+
startedAt;
|
|
2125
|
+
constructor(maxTotalRetries, maxElapsedMs, startedAt = Date.now()) {
|
|
2126
|
+
this.retriesLeft = Math.max(0, maxTotalRetries);
|
|
2127
|
+
this.maxElapsedMs = maxElapsedMs;
|
|
2128
|
+
this.startedAt = startedAt;
|
|
2129
|
+
}
|
|
2130
|
+
elapsedMs() {
|
|
2131
|
+
return Date.now() - this.startedAt;
|
|
2132
|
+
}
|
|
2133
|
+
expired() {
|
|
2134
|
+
return this.elapsedMs() >= this.maxElapsedMs;
|
|
2135
|
+
}
|
|
2136
|
+
consumeRetry() {
|
|
2137
|
+
if (this.retriesLeft <= 0 || this.expired())
|
|
2138
|
+
return false;
|
|
2139
|
+
this.retriesLeft -= 1;
|
|
2140
|
+
return true;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
1401
2143
|
|
|
1402
2144
|
class CouncilManager {
|
|
1403
2145
|
client;
|
|
1404
2146
|
directory;
|
|
1405
2147
|
config;
|
|
1406
|
-
constructor(client, directory,
|
|
2148
|
+
constructor(client, directory, config3) {
|
|
1407
2149
|
this.client = client;
|
|
1408
2150
|
this.directory = directory;
|
|
1409
|
-
this.config =
|
|
2151
|
+
this.config = config3;
|
|
1410
2152
|
}
|
|
1411
2153
|
async runCouncil(prompt, presetName, parentSessionId) {
|
|
1412
2154
|
const resolvedPreset = presetName ?? this.config.default_preset ?? "default";
|
|
@@ -1428,8 +2170,10 @@ class CouncilManager {
|
|
|
1428
2170
|
}
|
|
1429
2171
|
const timeout = this.config.timeout ?? 180000;
|
|
1430
2172
|
const executionMode = this.config.councillor_execution_mode ?? "parallel";
|
|
1431
|
-
const
|
|
1432
|
-
const
|
|
2173
|
+
const maxRounds = this.config.max_rounds ?? 2;
|
|
2174
|
+
const consensusThreshold = this.config.consensus_threshold ?? 0.6;
|
|
2175
|
+
const budget = new RetryBudget(this.config.max_total_retries ?? 6, this.config.max_elapsed_ms ?? 3 * timeout);
|
|
2176
|
+
const councillorResults = await this.runCouncillors(prompt, preset, parentSessionId, timeout, executionMode, budget, { maxRounds, consensusThreshold });
|
|
1433
2177
|
const completedCount = councillorResults.filter((r) => r.status === "completed").length;
|
|
1434
2178
|
if (completedCount === 0) {
|
|
1435
2179
|
return {
|
|
@@ -1438,23 +2182,56 @@ class CouncilManager {
|
|
|
1438
2182
|
councillorResults
|
|
1439
2183
|
};
|
|
1440
2184
|
}
|
|
2185
|
+
const consensusInputs = councillorResults.filter((r) => r.status === "completed" && !!r.summary).map((r) => ({ name: r.name, summary: r.summary }));
|
|
2186
|
+
const consensus = computeConsensus(consensusInputs, consensusThreshold);
|
|
1441
2187
|
const formatted = formatCouncillorResults(prompt, councillorResults);
|
|
1442
2188
|
return {
|
|
1443
2189
|
success: true,
|
|
1444
2190
|
result: formatted,
|
|
1445
|
-
councillorResults
|
|
2191
|
+
councillorResults,
|
|
2192
|
+
consensus: consensus.consensus,
|
|
2193
|
+
agreement: consensus.averageAgreement
|
|
1446
2194
|
};
|
|
1447
2195
|
}
|
|
1448
|
-
async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode,
|
|
1449
|
-
const entries = Object.entries(councillors);
|
|
2196
|
+
async runCouncillors(prompt, councillors, parentSessionId, timeout, executionMode, budget, options) {
|
|
2197
|
+
const entries = orderCouncillorsByFailure(Object.entries(councillors));
|
|
1450
2198
|
const results = [];
|
|
2199
|
+
const explicitTimeout = this.config.timeout;
|
|
2200
|
+
const explicitRetries = this.config.councillor_retries;
|
|
2201
|
+
const resolveParams = (config3) => {
|
|
2202
|
+
const p = getAdaptiveParams(adaptiveKey(config3), {
|
|
2203
|
+
...DEFAULT_ADAPTIVE_CONFIG,
|
|
2204
|
+
defaultTimeoutMs: explicitTimeout ?? timeout,
|
|
2205
|
+
defaultRetries: explicitRetries ?? 3
|
|
2206
|
+
});
|
|
2207
|
+
return {
|
|
2208
|
+
timeout: explicitTimeout ?? p.timeoutMs,
|
|
2209
|
+
retries: explicitRetries ?? p.retries
|
|
2210
|
+
};
|
|
2211
|
+
};
|
|
1451
2212
|
if (executionMode === "serial") {
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
2213
|
+
let lastRound = [];
|
|
2214
|
+
for (let round = 1;round <= options.maxRounds; round++) {
|
|
2215
|
+
const roundResults = [];
|
|
2216
|
+
const firstAttemptIsExtra = round > 1;
|
|
2217
|
+
for (const [name, config3] of entries) {
|
|
2218
|
+
const params = resolveParams(config3);
|
|
2219
|
+
const roundPrompt = round > 1 ? buildDeliberationPrompt(prompt, lastRound, name) : prompt;
|
|
2220
|
+
const r = await this.runCouncillorWithRetry(name, config3, roundPrompt, parentSessionId, params.timeout, budget, { firstAttemptIsExtra, retries: params.retries });
|
|
2221
|
+
roundResults.push(r);
|
|
2222
|
+
}
|
|
2223
|
+
lastRound = roundResults;
|
|
2224
|
+
const consensusInputs = roundResults.filter((r) => r.status === "completed" && !!r.summary).map((r) => ({ name: r.name, summary: r.summary }));
|
|
2225
|
+
if (computeConsensus(consensusInputs, options.consensusThreshold).consensus) {
|
|
2226
|
+
break;
|
|
2227
|
+
}
|
|
1455
2228
|
}
|
|
2229
|
+
results.push(...lastRound);
|
|
1456
2230
|
} else {
|
|
1457
|
-
const promises = entries.map(([name,
|
|
2231
|
+
const promises = entries.map(([name, config3]) => {
|
|
2232
|
+
const params = resolveParams(config3);
|
|
2233
|
+
return this.runCouncillorWithRetry(name, config3, prompt, parentSessionId, params.timeout, budget, { retries: params.retries });
|
|
2234
|
+
});
|
|
1458
2235
|
const settled = await Promise.allSettled(promises);
|
|
1459
2236
|
for (let i = 0;i < settled.length; i++) {
|
|
1460
2237
|
const s = settled[i];
|
|
@@ -1473,39 +2250,80 @@ class CouncilManager {
|
|
|
1473
2250
|
}
|
|
1474
2251
|
return results;
|
|
1475
2252
|
}
|
|
1476
|
-
async runCouncillorWithRetry(name,
|
|
1477
|
-
|
|
1478
|
-
|
|
2253
|
+
async runCouncillorWithRetry(name, config3, prompt, parentSessionId, timeout, budget, options) {
|
|
2254
|
+
let attemptPrompt = formatCouncillorPrompt(prompt, config3.prompt);
|
|
2255
|
+
let attemptVariant = config3.variant;
|
|
2256
|
+
let lastCategory = null;
|
|
2257
|
+
let streak = 0;
|
|
2258
|
+
const categoryRetries = {};
|
|
2259
|
+
let attempt = 1;
|
|
2260
|
+
for (;; ) {
|
|
2261
|
+
if (budget.expired()) {
|
|
2262
|
+
return { name, model: config3.model, status: "error", error: "budget-exhausted" };
|
|
2263
|
+
}
|
|
2264
|
+
const isExtraAttempt = attempt > 1 || options?.firstAttemptIsExtra === true;
|
|
2265
|
+
if (isExtraAttempt && !budget.consumeRetry()) {
|
|
2266
|
+
return { name, model: config3.model, status: "error", error: "budget-exhausted" };
|
|
2267
|
+
}
|
|
2268
|
+
let category;
|
|
2269
|
+
let errorInfo;
|
|
1479
2270
|
try {
|
|
1480
|
-
const
|
|
2271
|
+
const session = await this.runAgentSession({
|
|
1481
2272
|
parentSessionId,
|
|
1482
|
-
title: `Council ${name} (${shortModelLabel(
|
|
1483
|
-
model:
|
|
1484
|
-
promptText:
|
|
1485
|
-
variant:
|
|
2273
|
+
title: `Council ${name} (${shortModelLabel(config3.model)})`,
|
|
2274
|
+
model: config3.model,
|
|
2275
|
+
promptText: attemptPrompt,
|
|
2276
|
+
variant: attemptVariant,
|
|
1486
2277
|
timeout
|
|
1487
2278
|
});
|
|
1488
|
-
|
|
2279
|
+
const quality = assessQuality({ output: session.text, decisions: session.summary.entities.length });
|
|
2280
|
+
if (quality.passed) {
|
|
2281
|
+
recordModelResult(config3.model, "success");
|
|
2282
|
+
recordAdaptiveSample(adaptiveKey(config3), true);
|
|
2283
|
+
return { name, model: config3.model, status: "completed", result: session.text, summary: session.summary };
|
|
2284
|
+
}
|
|
2285
|
+
category = quality.failureCategory ?? "quality-low";
|
|
2286
|
+
errorInfo = `Councillor "${name}": quality score ${quality.score} (${category})`;
|
|
1489
2287
|
} catch (error) {
|
|
1490
2288
|
const msg = error instanceof Error ? error.message : String(error);
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
if (!canRetry) {
|
|
1494
|
-
return {
|
|
1495
|
-
name,
|
|
1496
|
-
model: config.model,
|
|
1497
|
-
status: msg.includes("timed out") ? "timed_out" : "failed",
|
|
1498
|
-
error: `Councillor "${name}": ${msg}`
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
2289
|
+
category = classifyFailure(msg);
|
|
2290
|
+
errorInfo = `Councillor "${name}": ${msg}`;
|
|
1501
2291
|
}
|
|
2292
|
+
recordModelResult(config3.model, category);
|
|
2293
|
+
recordAdaptiveSample(adaptiveKey(config3), false);
|
|
2294
|
+
streak = lastCategory === category ? streak + 1 : 1;
|
|
2295
|
+
lastCategory = category;
|
|
2296
|
+
const used = categoryRetries[category] ?? 0;
|
|
2297
|
+
const categoryLimit = category === "empty" ? options?.retries ?? this.config.councillor_retries ?? 3 : RETRY_LIMITS[category];
|
|
2298
|
+
const action = decideRetryAction(category, streak, used, budget.retriesLeft > 0 && !budget.expired(), categoryLimit);
|
|
2299
|
+
if (action === "needs-human") {
|
|
2300
|
+
return {
|
|
2301
|
+
name,
|
|
2302
|
+
model: config3.model,
|
|
2303
|
+
status: "needs_human",
|
|
2304
|
+
error: `${errorInfo} — ${streak} consecutive ${category} failures, needs human review`
|
|
2305
|
+
};
|
|
2306
|
+
}
|
|
2307
|
+
if (action === "budget-exhausted") {
|
|
2308
|
+
return { name, model: config3.model, status: "error", error: "budget-exhausted" };
|
|
2309
|
+
}
|
|
2310
|
+
if (action === "give-up") {
|
|
2311
|
+
return {
|
|
2312
|
+
name,
|
|
2313
|
+
model: config3.model,
|
|
2314
|
+
status: category === "timeout" ? "timed_out" : "failed",
|
|
2315
|
+
error: errorInfo
|
|
2316
|
+
};
|
|
2317
|
+
}
|
|
2318
|
+
categoryRetries[category] = used + 1;
|
|
2319
|
+
if (category === "timeout") {
|
|
2320
|
+
attemptVariant = downgradeVariant(attemptVariant);
|
|
2321
|
+
attemptPrompt = appendRetryContext(attemptPrompt, "timeout");
|
|
2322
|
+
} else if (category === "quality-low") {
|
|
2323
|
+
attemptPrompt = appendRetryContext(attemptPrompt, "quality-low");
|
|
2324
|
+
}
|
|
2325
|
+
attempt += 1;
|
|
1502
2326
|
}
|
|
1503
|
-
return {
|
|
1504
|
-
name,
|
|
1505
|
-
model: config.model,
|
|
1506
|
-
status: "failed",
|
|
1507
|
-
error: `Councillor "${name}": max retries exhausted`
|
|
1508
|
-
};
|
|
1509
2327
|
}
|
|
1510
2328
|
async runAgentSession(options) {
|
|
1511
2329
|
const modelRef = parseModelReference(options.model);
|
|
@@ -1545,11 +2363,15 @@ class CouncilManager {
|
|
|
1545
2363
|
promptBody.variant = options.variant;
|
|
1546
2364
|
}
|
|
1547
2365
|
await promptWithTimeout(this.client, { id: sessionId }, promptBody, options.timeout, this.directory);
|
|
1548
|
-
const extraction = await extractSessionResult(this.client, sessionId
|
|
2366
|
+
const extraction = await extractSessionResult(this.client, sessionId, {
|
|
2367
|
+
includeReasoning: false,
|
|
2368
|
+
lastTextOnly: true
|
|
2369
|
+
});
|
|
1549
2370
|
if (extraction.empty) {
|
|
1550
2371
|
throw new Error("[oh-my-opencode-cohub] Empty response from provider");
|
|
1551
2372
|
}
|
|
1552
|
-
|
|
2373
|
+
const summary = extractCouncillorSummary(extraction.text);
|
|
2374
|
+
return { text: extraction.text, summary };
|
|
1553
2375
|
} finally {
|
|
1554
2376
|
if (sessionId) {
|
|
1555
2377
|
abortSession(this.client, sessionId).catch(() => {});
|
|
@@ -1593,16 +2415,61 @@ function createCouncilTool(ctx, councilManager) {
|
|
|
1593
2415
|
|
|
1594
2416
|
---
|
|
1595
2417
|
*Council: ${completed}/${total} councillors responded (${composition})*`;
|
|
2418
|
+
const agreementNum = typeof result.agreement === "number" ? result.agreement : 0;
|
|
2419
|
+
const consensusLabel = result.consensus === true ? agreementNum >= 0.8 ? "unanimous" : "majority" : "split";
|
|
2420
|
+
output += `
|
|
2421
|
+
*Consensus: ${consensusLabel} (${agreementNum.toFixed(2)})*`;
|
|
1596
2422
|
return output;
|
|
1597
2423
|
}
|
|
1598
2424
|
});
|
|
1599
2425
|
return { council_session };
|
|
1600
2426
|
}
|
|
1601
2427
|
|
|
2428
|
+
// src/tools/job-control.ts
|
|
2429
|
+
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
2430
|
+
var z2 = tool2.schema;
|
|
2431
|
+
function createCloseJobTool(ctx, tracker, onClosed) {
|
|
2432
|
+
const close_job = tool2({
|
|
2433
|
+
description: "Close (abort) a stuck or unnecessary subagent background task. " + "Accepts the child Session ID (e.g. ses_xxx, shown in the Background Job Board) or the task alias (e.g. coe-1). " + "The underlying subagent session is aborted via session.abort() and the task is marked cancelled. " + "Only the co-orchestrator (primary) agent may invoke this tool.",
|
|
2434
|
+
args: {
|
|
2435
|
+
task_id: z2.string().describe("The child session ID (ses_xxx) or task alias (e.g. coe-1) of the task to close.")
|
|
2436
|
+
},
|
|
2437
|
+
async execute(args, toolContext) {
|
|
2438
|
+
if (toolContext.agent !== "co-orchestrator") {
|
|
2439
|
+
throw new Error(`[oh-my-opencode-cohub] close_job can only be invoked by co-orchestrator. Current agent: ${toolContext.agent}`);
|
|
2440
|
+
}
|
|
2441
|
+
const hit = tracker.abortJob(args.task_id);
|
|
2442
|
+
if (!hit) {
|
|
2443
|
+
return JSON.stringify({ ok: false, task_id: args.task_id, reason: "not_found" }, null, 2);
|
|
2444
|
+
}
|
|
2445
|
+
let aborted = false;
|
|
2446
|
+
if (hit.sessionId) {
|
|
2447
|
+
const res = await ctx.client.session.abort({ path: { id: hit.sessionId } });
|
|
2448
|
+
if (res.error) {
|
|
2449
|
+
appendLog("job-control", `abort session ${hit.sessionId} 失败`, res.error);
|
|
2450
|
+
aborted = false;
|
|
2451
|
+
} else {
|
|
2452
|
+
aborted = true;
|
|
2453
|
+
}
|
|
2454
|
+
}
|
|
2455
|
+
onClosed?.();
|
|
2456
|
+
return JSON.stringify({
|
|
2457
|
+
ok: true,
|
|
2458
|
+
task_id: args.task_id,
|
|
2459
|
+
alias: hit.job?.alias,
|
|
2460
|
+
agent: hit.job?.agent,
|
|
2461
|
+
session_aborted: aborted,
|
|
2462
|
+
status: "cancelled"
|
|
2463
|
+
}, null, 2);
|
|
2464
|
+
}
|
|
2465
|
+
});
|
|
2466
|
+
return { close_job };
|
|
2467
|
+
}
|
|
2468
|
+
|
|
1602
2469
|
// src/index.ts
|
|
1603
|
-
import * as
|
|
1604
|
-
import * as
|
|
1605
|
-
import * as
|
|
2470
|
+
import * as fs5 from "node:fs";
|
|
2471
|
+
import * as path4 from "node:path";
|
|
2472
|
+
import * as os4 from "node:os";
|
|
1606
2473
|
function loadFileOverrides(projectDir) {
|
|
1607
2474
|
const overrides = {};
|
|
1608
2475
|
const agentNames = [
|
|
@@ -1621,23 +2488,23 @@ function loadFileOverrides(projectDir) {
|
|
|
1621
2488
|
];
|
|
1622
2489
|
const searchDirs = [];
|
|
1623
2490
|
if (projectDir) {
|
|
1624
|
-
searchDirs.push(
|
|
2491
|
+
searchDirs.push(path4.join(projectDir, ".opencode", "oh-my-opencode-cohub"));
|
|
1625
2492
|
}
|
|
1626
|
-
searchDirs.push(
|
|
2493
|
+
searchDirs.push(path4.join(os4.homedir(), ".config", "opencode", "oh-my-opencode-cohub"));
|
|
1627
2494
|
for (const agent of agentNames) {
|
|
1628
2495
|
for (const dir of searchDirs) {
|
|
1629
|
-
const replacePath =
|
|
1630
|
-
if (!overrides[agent]?.replace &&
|
|
2496
|
+
const replacePath = path4.join(dir, `${agent}.md`);
|
|
2497
|
+
if (!overrides[agent]?.replace && fs5.existsSync(replacePath)) {
|
|
1631
2498
|
try {
|
|
1632
|
-
overrides[agent] = { ...overrides[agent], replace:
|
|
2499
|
+
overrides[agent] = { ...overrides[agent], replace: fs5.readFileSync(replacePath, "utf-8") };
|
|
1633
2500
|
} catch (err) {
|
|
1634
2501
|
appendLog("loadFileOverrides", "读取replace文件失败", err);
|
|
1635
2502
|
}
|
|
1636
2503
|
}
|
|
1637
|
-
const appendPath =
|
|
1638
|
-
if (!overrides[agent]?.append &&
|
|
2504
|
+
const appendPath = path4.join(dir, `${agent}_append.md`);
|
|
2505
|
+
if (!overrides[agent]?.append && fs5.existsSync(appendPath)) {
|
|
1639
2506
|
try {
|
|
1640
|
-
overrides[agent] = { ...overrides[agent], append:
|
|
2507
|
+
overrides[agent] = { ...overrides[agent], append: fs5.readFileSync(appendPath, "utf-8") };
|
|
1641
2508
|
} catch (err) {
|
|
1642
2509
|
appendLog("loadFileOverrides", "读取append文件失败", err);
|
|
1643
2510
|
}
|
|
@@ -1668,26 +2535,26 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1668
2535
|
}
|
|
1669
2536
|
const promptOverrides = { ...configOverrides, ...fileOverrides };
|
|
1670
2537
|
const tracker = new TaskTracker;
|
|
1671
|
-
const STATE_DIR =
|
|
1672
|
-
const STATE_FILE =
|
|
2538
|
+
const STATE_DIR = path4.join(os4.homedir(), ".local", "share", "opencode", "storage", "oh-my-opencode-cohub");
|
|
2539
|
+
const STATE_FILE = path4.join(STATE_DIR, "tracker-state.json");
|
|
1673
2540
|
function syncTrackerState(sessionId) {
|
|
1674
2541
|
try {
|
|
1675
|
-
if (!
|
|
1676
|
-
|
|
2542
|
+
if (!fs5.existsSync(STATE_DIR)) {
|
|
2543
|
+
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
1677
2544
|
}
|
|
1678
2545
|
const state = {
|
|
1679
2546
|
updatedAt: Date.now(),
|
|
1680
2547
|
runningAgents: tracker.getRunningAgents(sessionId),
|
|
1681
2548
|
runningCount: tracker.getRunningCount(sessionId)
|
|
1682
2549
|
};
|
|
1683
|
-
|
|
2550
|
+
fs5.writeFileSync(STATE_FILE, JSON.stringify(state), "utf-8");
|
|
1684
2551
|
} catch {}
|
|
1685
2552
|
}
|
|
1686
|
-
const AGENT_CONFIG_FILE =
|
|
2553
|
+
const AGENT_CONFIG_FILE = path4.join(STATE_DIR, "cohub-state.json");
|
|
1687
2554
|
function syncAgentConfig() {
|
|
1688
2555
|
try {
|
|
1689
|
-
if (!
|
|
1690
|
-
|
|
2556
|
+
if (!fs5.existsSync(STATE_DIR)) {
|
|
2557
|
+
fs5.mkdirSync(STATE_DIR, { recursive: true });
|
|
1691
2558
|
}
|
|
1692
2559
|
const configs = agents.filter((a) => a.config.model).map((a) => {
|
|
1693
2560
|
const modelStr = a.config.model;
|
|
@@ -1702,7 +2569,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1702
2569
|
provider
|
|
1703
2570
|
};
|
|
1704
2571
|
});
|
|
1705
|
-
|
|
2572
|
+
fs5.writeFileSync(AGENT_CONFIG_FILE, JSON.stringify({ updatedAt: Date.now(), agents: configs }), "utf-8");
|
|
1706
2573
|
appendLog("syncAgentConfig", `已同步 ${configs.length} 个 agent: ${JSON.stringify(configs.map((c) => ({ name: c.name, model: c.model, variant: c.variant })))}`);
|
|
1707
2574
|
} catch {}
|
|
1708
2575
|
}
|
|
@@ -1803,6 +2670,7 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1803
2670
|
const councilConfig = userConfig.council ?? DEFAULT_COUNCIL_CONFIG;
|
|
1804
2671
|
const councilManager = new CouncilManager(input.client, input.directory, councilConfig);
|
|
1805
2672
|
const councilTools = createCouncilTool(input, councilManager);
|
|
2673
|
+
const jobTools = createCloseJobTool(input, tracker, () => syncTrackerState(tracker.currentParentSessionId));
|
|
1806
2674
|
function extractChildSessionId(output) {
|
|
1807
2675
|
if (!output || typeof output !== "object")
|
|
1808
2676
|
return;
|
|
@@ -1840,7 +2708,19 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1840
2708
|
const STALE_TIMEOUT_MS = 30 * 60 * 1000;
|
|
1841
2709
|
const cleanupTimer = setInterval(() => {
|
|
1842
2710
|
try {
|
|
1843
|
-
tracker.cleanupStaleJobs(STALE_TIMEOUT_MS);
|
|
2711
|
+
const staleSessions = tracker.cleanupStaleJobs(STALE_TIMEOUT_MS);
|
|
2712
|
+
for (const sid of staleSessions) {
|
|
2713
|
+
(async () => {
|
|
2714
|
+
try {
|
|
2715
|
+
const res = await input.client.session.abort({ path: { id: sid } });
|
|
2716
|
+
if (res.error) {
|
|
2717
|
+
appendLog("cleanupStaleJobs", `abort stale session ${sid} 失败`, res.error);
|
|
2718
|
+
}
|
|
2719
|
+
} catch (err) {
|
|
2720
|
+
appendLog("cleanupStaleJobs", `abort stale session ${sid} 失败`, err);
|
|
2721
|
+
}
|
|
2722
|
+
})();
|
|
2723
|
+
}
|
|
1844
2724
|
} catch (err) {
|
|
1845
2725
|
appendLog("cleanupStaleJobs", "定时清理过期任务失败", err);
|
|
1846
2726
|
}
|
|
@@ -1853,17 +2733,29 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1853
2733
|
}
|
|
1854
2734
|
}, 60000);
|
|
1855
2735
|
const agentConfigs = {};
|
|
2736
|
+
const COUNCIL_ONLY_TOOLS = { council_session: "deny" };
|
|
2737
|
+
const ORCHESTRATOR_ONLY_TOOLS = { close_job: "deny" };
|
|
1856
2738
|
for (const agent of agents) {
|
|
1857
|
-
|
|
2739
|
+
const base = {
|
|
1858
2740
|
...agent.config,
|
|
1859
2741
|
name: agent.name,
|
|
1860
2742
|
description: agent.description
|
|
1861
2743
|
};
|
|
2744
|
+
const existingPermission = base.permission ?? {};
|
|
2745
|
+
if (agent.name !== "co-council") {
|
|
2746
|
+
base.permission = { ...existingPermission, ...COUNCIL_ONLY_TOOLS };
|
|
2747
|
+
}
|
|
2748
|
+
if (agent.name !== "co-orchestrator") {
|
|
2749
|
+
base.permission = { ...base.permission, ...ORCHESTRATOR_ONLY_TOOLS };
|
|
2750
|
+
} else {
|
|
2751
|
+
base.permission = { ...base.permission, close_job: "allow" };
|
|
2752
|
+
}
|
|
2753
|
+
agentConfigs[agent.name] = base;
|
|
1862
2754
|
}
|
|
1863
2755
|
return {
|
|
1864
2756
|
name: "oh-my-opencode-cohub",
|
|
1865
2757
|
agent: agentConfigs,
|
|
1866
|
-
tool: councilTools,
|
|
2758
|
+
tool: { ...councilTools, ...jobTools },
|
|
1867
2759
|
config: async (cfg) => {
|
|
1868
2760
|
const c = cfg;
|
|
1869
2761
|
c.agent ??= {};
|
|
@@ -1878,10 +2770,22 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1878
2770
|
merged.model = pc.model;
|
|
1879
2771
|
if (pc.variant)
|
|
1880
2772
|
merged.variant = pc.variant;
|
|
2773
|
+
const existingPerm0 = merged.permission ?? {};
|
|
2774
|
+
if (name === "co-council") {
|
|
2775
|
+
merged.permission = { ...existingPerm0, council_session: "allow", ...ORCHESTRATOR_ONLY_TOOLS };
|
|
2776
|
+
} else if (name === "co-orchestrator") {
|
|
2777
|
+
merged.permission = {
|
|
2778
|
+
...existingPerm0,
|
|
2779
|
+
...COUNCIL_ONLY_TOOLS,
|
|
2780
|
+
close_job: "allow"
|
|
2781
|
+
};
|
|
2782
|
+
} else {
|
|
2783
|
+
merged.permission = { ...existingPerm0, ...COUNCIL_ONLY_TOOLS, ...ORCHESTRATOR_ONLY_TOOLS };
|
|
2784
|
+
}
|
|
1881
2785
|
c.agent[name] = merged;
|
|
1882
2786
|
}
|
|
1883
2787
|
try {
|
|
1884
|
-
|
|
2788
|
+
fs5.writeFileSync(path4.join(STATE_DIR, "config-hook-ran.json"), JSON.stringify({ ran: true, count: agents.length }));
|
|
1885
2789
|
} catch (err) {
|
|
1886
2790
|
appendLog("config", "双重注册写入文件失败", err);
|
|
1887
2791
|
}
|
|
@@ -1932,19 +2836,19 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1932
2836
|
if (details) {
|
|
1933
2837
|
output.args[targetField] += details;
|
|
1934
2838
|
}
|
|
1935
|
-
const logPath =
|
|
2839
|
+
const logPath = path4.join(os4.tmpdir(), "opencode", "ctx-diag.log");
|
|
1936
2840
|
try {
|
|
1937
|
-
const stat =
|
|
2841
|
+
const stat = fs5.statSync(logPath);
|
|
1938
2842
|
if (stat.size > 50 * 1024) {
|
|
1939
|
-
const lines =
|
|
2843
|
+
const lines = fs5.readFileSync(logPath, "utf-8").trim().split(`
|
|
1940
2844
|
`);
|
|
1941
|
-
|
|
2845
|
+
fs5.writeFileSync(logPath, lines.slice(-30).join(`
|
|
1942
2846
|
`) + `
|
|
1943
2847
|
`);
|
|
1944
2848
|
}
|
|
1945
2849
|
} catch {}
|
|
1946
2850
|
const finalPrompt = typeof output.args?.prompt === "string" ? output.args.prompt : "";
|
|
1947
|
-
|
|
2851
|
+
fs5.appendFileSync(logPath, JSON.stringify({
|
|
1948
2852
|
time: new Date().toISOString(),
|
|
1949
2853
|
session: input2.sessionID?.slice(0, 20) ?? "?",
|
|
1950
2854
|
subagent: subagentType,
|
|
@@ -1992,7 +2896,25 @@ var CoHubPlugin = async (input, options) => {
|
|
|
1992
2896
|
syncTrackerState(tracker.currentParentSessionId);
|
|
1993
2897
|
const job = tracker.getJobBySessionId(sessionId);
|
|
1994
2898
|
if (job) {
|
|
1995
|
-
contextEngine.captureResult(sessionId, job.alias, job.agent)
|
|
2899
|
+
contextEngine.captureResult(sessionId, job.alias, job.agent).then((captured) => {
|
|
2900
|
+
if (!captured)
|
|
2901
|
+
return;
|
|
2902
|
+
if (!isQualityEnabled(userConfig.quality))
|
|
2903
|
+
return;
|
|
2904
|
+
const quality = assessQuality({
|
|
2905
|
+
output: captured.output,
|
|
2906
|
+
decisions: captured.decisions,
|
|
2907
|
+
latencyMs: Date.now() - job.createdAt,
|
|
2908
|
+
tokens: captured.tokens
|
|
2909
|
+
});
|
|
2910
|
+
tracker.updateQuality(sessionId, {
|
|
2911
|
+
score: quality.score,
|
|
2912
|
+
...quality.failureCategory ? { failureCategory: quality.failureCategory } : {},
|
|
2913
|
+
...quality.latencyMs !== undefined ? { latencyMs: quality.latencyMs } : {},
|
|
2914
|
+
...quality.tokens ? { tokens: quality.tokens } : {}
|
|
2915
|
+
});
|
|
2916
|
+
syncTrackerState(tracker.currentParentSessionId);
|
|
2917
|
+
}).catch((err) => appendLog("event", "质量回送失败", err));
|
|
1996
2918
|
}
|
|
1997
2919
|
} else if (e.type === "session.deleted") {
|
|
1998
2920
|
tracker.updateByChildSessionId(sessionId, "errored");
|
|
@@ -2096,6 +3018,6 @@ var CoHubPlugin = async (input, options) => {
|
|
|
2096
3018
|
var server = CoHubPlugin;
|
|
2097
3019
|
var src_default = CoHubPlugin;
|
|
2098
3020
|
export {
|
|
2099
|
-
|
|
2100
|
-
|
|
3021
|
+
src_default as default,
|
|
3022
|
+
server
|
|
2101
3023
|
};
|