@bolloon/bolloon-agent 0.5.0 → 0.5.2
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 +39 -0
- package/dist/agents/agent-lang.js +101 -0
- package/dist/agents/agent-manifest-protocol.js +4 -0
- package/dist/agents/execution-supervisor.js +87 -5
- package/dist/agents/gateway-group.js +44 -3
- package/dist/agents/model-policy.js +651 -0
- package/dist/agents/pi-sdk-tools.js +84 -33
- package/dist/agents/pi-sdk.js +184 -31
- package/dist/agents/run-store.js +53 -0
- package/dist/agents/task/task-runner.js +8 -1
- package/dist/agents/workflow-pivot-loop.js +22 -13
- package/dist/cli/clipboard.js +65 -0
- package/dist/cli/ink-app.js +145 -17
- package/dist/cli/log-gate.js +386 -0
- package/dist/cli/mention-data.js +2 -0
- package/dist/cli/model-selector.js +538 -0
- package/dist/cli/reply-hygiene.js +54 -0
- package/dist/cli/setup-wizard.js +686 -69
- package/dist/cli/startup-notice.js +160 -0
- package/dist/cli/stores.js +22 -1
- package/dist/cli/theme.js +49 -0
- package/dist/cli/tui-select.js +970 -0
- package/dist/cli-entry.js +125 -18
- package/dist/efficode/compress.js +78 -0
- package/dist/efficode/crc32.js +38 -0
- package/dist/efficode/did.js +66 -0
- package/dist/efficode/index.js +24 -0
- package/dist/efficode/lz77.js +170 -0
- package/dist/efficode/negotiate.js +187 -0
- package/dist/efficode/ops.js +87 -0
- package/dist/efficode/packet.js +271 -0
- package/dist/efficode/types.js +45 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js +4 -0
- package/dist/electron-build/agents/agent-manifest-protocol.js.map +1 -1
- package/dist/electron-build/agents/execution-supervisor.js +86 -4
- package/dist/electron-build/agents/execution-supervisor.js.map +1 -1
- package/dist/electron-build/agents/goal-store.js.map +1 -1
- package/dist/electron-build/agents/model-policy.js +706 -0
- package/dist/electron-build/agents/model-policy.js.map +1 -0
- package/dist/electron-build/agents/pi-sdk-tools.js +84 -33
- package/dist/electron-build/agents/pi-sdk-tools.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk-types.js.map +1 -1
- package/dist/electron-build/agents/pi-sdk.js +185 -31
- package/dist/electron-build/agents/pi-sdk.js.map +1 -1
- package/dist/electron-build/agents/run-store.js +55 -1
- package/dist/electron-build/agents/run-store.js.map +1 -1
- package/dist/electron-build/agents/task/task-runner.js +8 -1
- package/dist/electron-build/agents/task/task-runner.js.map +1 -1
- package/dist/electron-build/agents/workflow-pivot-loop.js +22 -13
- package/dist/electron-build/agents/workflow-pivot-loop.js.map +1 -1
- package/dist/electron-build/llm/config-store.js +146 -6
- package/dist/electron-build/llm/config-store.js.map +1 -1
- package/dist/electron-build/llm/connection-probe.js +764 -0
- package/dist/electron-build/llm/connection-probe.js.map +1 -0
- package/dist/electron-build/llm/custom-provider-store.js +272 -0
- package/dist/electron-build/llm/custom-provider-store.js.map +1 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js +23 -0
- package/dist/electron-build/llm/data/provider-catalog-baked.js.map +1 -0
- package/dist/electron-build/llm/model-catalog.js +778 -0
- package/dist/electron-build/llm/model-catalog.js.map +1 -0
- package/dist/electron-build/llm/model-discovery.js +1202 -0
- package/dist/electron-build/llm/model-discovery.js.map +1 -0
- package/dist/electron-build/llm/model-selection.js +1315 -0
- package/dist/electron-build/llm/model-selection.js.map +1 -0
- package/dist/electron-build/llm/pi-ai.js +751 -55
- package/dist/electron-build/llm/pi-ai.js.map +1 -1
- package/dist/electron-build/llm/provider-catalog.js +969 -0
- package/dist/electron-build/llm/provider-catalog.js.map +1 -0
- package/dist/electron-build/llm/provider-registry.js +950 -0
- package/dist/electron-build/llm/provider-registry.js.map +1 -0
- package/dist/electron-build/llm/tool-name.js +266 -0
- package/dist/electron-build/llm/tool-name.js.map +1 -0
- package/dist/electron-build/utils/dual-source-facts.js +236 -0
- package/dist/electron-build/utils/dual-source-facts.js.map +1 -0
- package/dist/electron-build/utils/dual-source.js +32 -156
- package/dist/electron-build/utils/dual-source.js.map +1 -1
- package/dist/electron-build/utils/update-manager.js +22 -72
- package/dist/electron-build/utils/update-manager.js.map +1 -1
- package/dist/electron-build/utils/update-state.js +2 -19
- package/dist/electron-build/utils/update-state.js.map +1 -1
- package/dist/electron-build/utils/version-identity.js +193 -0
- package/dist/electron-build/utils/version-identity.js.map +1 -0
- package/dist/electron-build/utils/version-info.js +47 -101
- package/dist/electron-build/utils/version-info.js.map +1 -1
- package/dist/index.js +282 -96
- package/dist/ios/bolloon-web.json +10 -0
- package/dist/ios/mobile-contacts.js +44 -11
- package/dist/ios/mobile-core.js +1723 -84
- package/dist/ios/mobile-tasks.js +339 -0
- package/dist/ios/mobile-update.js +1442 -0
- package/dist/ios/mobile.js +308 -35
- package/dist/ios/routes-llm-config.js +344 -52
- package/dist/ios/routes-mobile-tasks.js +353 -0
- package/dist/ios/server.js +163 -4
- package/dist/ios/sw.js +1 -1
- package/dist/llm/config-store.js +146 -6
- package/dist/llm/connection-probe.js +744 -0
- package/dist/llm/custom-provider-store.js +224 -0
- package/dist/llm/data/provider-catalog-baked.js +19 -0
- package/dist/llm/model-catalog.js +717 -0
- package/dist/llm/model-discovery.js +1137 -0
- package/dist/llm/model-selection.js +1241 -0
- package/dist/llm/pi-ai.js +732 -54
- package/dist/llm/provider-catalog.js +898 -0
- package/dist/llm/provider-registry.js +917 -0
- package/dist/llm/tool-name.js +220 -0
- package/dist/setup/onboard.js +34 -8
- package/dist/utils/dual-source-facts.js +223 -0
- package/dist/utils/dual-source.js +12 -141
- package/dist/utils/update-manager.js +16 -59
- package/dist/utils/update-state.js +6 -18
- package/dist/utils/version-identity.js +176 -0
- package/dist/utils/version-info.js +9 -78
- package/dist/web/bolloon-web.json +10 -0
- package/dist/web/mobile-core.js +1721 -84
- package/dist/web/mobile-update.js +1442 -0
- package/dist/web/mobile.js +372 -35
- package/dist/web/routes-llm-config.js +344 -52
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/scripts/build-web.ts +36 -0
package/README.md
CHANGED
|
@@ -91,6 +91,32 @@ npm start
|
|
|
91
91
|
|
|
92
92
|
---
|
|
93
93
|
|
|
94
|
+
## Docker
|
|
95
|
+
|
|
96
|
+
不想在本机装 Node 与依赖, 可以直接跑容器 (多阶段镜像 · 非 root 用户 uid 1001 · 状态全部落在卷里):
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
docker build -t bolloon-agent:0.5.1 .
|
|
100
|
+
docker run -d --name bolloon-agent \
|
|
101
|
+
-p 127.0.0.1:54188:54188 \
|
|
102
|
+
-v bolloon-data:/home/bolloon/.bolloon \
|
|
103
|
+
bolloon-agent:0.5.1 --web
|
|
104
|
+
# → http://127.0.0.1:54188
|
|
105
|
+
|
|
106
|
+
docker compose up -d # 同上的 compose 写法
|
|
107
|
+
docker compose run --rm bolloon-cli --version # 一次性 CLI (不用起服务)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
- **密钥别写进 compose**: 复制 `.env.example` → `.env.docker` (已在 `.gitignore`), 或者把本机
|
|
111
|
+
0600 的 `llm-config.json` **只读挂载**进容器 (更安全: 不进环境变量、不进 `docker inspect`)。
|
|
112
|
+
- **数据卷里有身份私钥与 API key 配置** (`/home/bolloon/.bolloon`): 别导出、别提交、别 `docker commit`。
|
|
113
|
+
- 端口只认环境变量 `PORT` (默认 `54188`); `--port` 参数无效。
|
|
114
|
+
|
|
115
|
+
完整说明 (镜像怎么搭的 · 卷与 uid 权限 · HEALTHCHECK 探什么/不探什么 · 常见问题) 见
|
|
116
|
+
[docs/wiki/docker-deployment.md](./docs/wiki/docker-deployment.md)。
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
94
120
|
## 环境变量
|
|
95
121
|
|
|
96
122
|
| 变量 | 说明 |
|
|
@@ -136,6 +162,19 @@ bolloon --help # All commands
|
|
|
136
162
|
|
|
137
163
|
Requires Node.js ≥ 18 and an LLM API key (`OPENAI_API_KEY` or `DEEPSEEK_API_KEY`).
|
|
138
164
|
|
|
165
|
+
### Docker
|
|
166
|
+
|
|
167
|
+
Run it in a container instead (multi-stage image, non-root user, state in a volume):
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
docker build -t bolloon-agent:0.5.1 .
|
|
171
|
+
docker run -d --name bolloon-agent -p 127.0.0.1:54188:54188 \
|
|
172
|
+
-v bolloon-data:/home/bolloon/.bolloon bolloon-agent:0.5.1 --web
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Copy `.env.example` to `.env.docker` (gitignored) for keys, or mount your `llm-config.json` read-only
|
|
176
|
+
(safer). Full guide: [docs/wiki/docker-deployment.md](./docs/wiki/docker-deployment.md).
|
|
177
|
+
|
|
139
178
|
### License
|
|
140
179
|
|
|
141
180
|
This project is released under the [MIT License](./LICENSE). You are free to use, modify, and distribute this project, provided that you retain the copyright and permission notices. See the [LICENSE](./LICENSE) file for details.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agents/agent-lang - 智能体交流语言的**声明层**与消息封装层接线
|
|
3
|
+
*
|
|
4
|
+
* 这一层只做三件事:
|
|
5
|
+
* ① 声明: manifest 里带上 `supportedLangs` (本机支持哪些语言);
|
|
6
|
+
* ② 出站: 拿"本机声明 + 对端声明"裁决 → 要么真编 (双方 efficode), 要么原文直发;
|
|
7
|
+
* ③ 入站: 只按对端**声明**的语言分派 —— 声明不是 efficode 的载荷一律不进解码器。
|
|
8
|
+
*
|
|
9
|
+
* 不支持的场景怎么处理 (照 leo 的要求, 不许静默):
|
|
10
|
+
* - 对端没声明 / 只声明 natural → 自然语言, 记一笔 `fallback=false`
|
|
11
|
+
* - 本机想要 efficode 但对端没声明 → 回落, 记一笔 `fallback=true` + 原因
|
|
12
|
+
* - 对端声明了本机不认识的语言字符串 → 回落, 原样呈现, 记 `unknownSide`
|
|
13
|
+
* - 对端声明 efficode 但本机没声明支持 → 拒绝解码并抛 (绝不硬解)
|
|
14
|
+
*/
|
|
15
|
+
import { decodeFromPeer, encodeForPeer, defaultDeclaredLang, negotiateLang, recordLangDecision, unknownLangNames, } from '../efficode/negotiate.js';
|
|
16
|
+
import { normalizeLang } from '../efficode/index.js';
|
|
17
|
+
/** 本机支持的语言声明 (进程内, 由 manifest 装配处设置) */
|
|
18
|
+
let localLangs = [];
|
|
19
|
+
/** 本机默认声明 (env 可覆盖; 默认 natural) */
|
|
20
|
+
let defaultLang = null;
|
|
21
|
+
export function setLocalSupportedLangs(langs) {
|
|
22
|
+
const arr = Array.isArray(langs) ? langs : langs == null ? [] : [langs];
|
|
23
|
+
localLangs = arr.map((x) => String(x).trim()).filter(Boolean);
|
|
24
|
+
return localLangs;
|
|
25
|
+
}
|
|
26
|
+
export function getLocalSupportedLangs() {
|
|
27
|
+
return [...localLangs];
|
|
28
|
+
}
|
|
29
|
+
/** 本机声明 (没显式设置就回落到 env/默认; 声明不变量绝不"猜"成 efficode) */
|
|
30
|
+
export function localDeclaredLang(env = process.env) {
|
|
31
|
+
if (defaultLang)
|
|
32
|
+
return defaultLang;
|
|
33
|
+
return defaultDeclaredLang(env);
|
|
34
|
+
}
|
|
35
|
+
export function setLocalDeclaredLang(lang) {
|
|
36
|
+
defaultLang = normalizeLang(lang);
|
|
37
|
+
return defaultLang ?? 'natural';
|
|
38
|
+
}
|
|
39
|
+
/** 本机要写进 manifest 的声明: 显式语言列表优先, 否则 [默认声明] */
|
|
40
|
+
export function manifestLangDeclaration(env = process.env) {
|
|
41
|
+
if (localLangs.length)
|
|
42
|
+
return [...localLangs];
|
|
43
|
+
return [localDeclaredLang(env)];
|
|
44
|
+
}
|
|
45
|
+
/** 从 manifest 里读对端声明 (老 manifest 没这个字段 → 空数组 = 没声明) */
|
|
46
|
+
export function peerLangsOf(manifest) {
|
|
47
|
+
const raw = manifest?.supportedLangs;
|
|
48
|
+
const arr = Array.isArray(raw) ? raw : raw == null ? [] : [raw];
|
|
49
|
+
return arr.map((x) => String(x).trim()).filter(Boolean);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 出站消息: 双方声明都是 efficode 才编码, 否则原文直发并在帧里如实声明 `lang: 'natural'`。
|
|
53
|
+
*/
|
|
54
|
+
export function buildAgentMessage(opts) {
|
|
55
|
+
const mine = opts.mine ?? manifestLangDeclaration(opts.env);
|
|
56
|
+
const enc = encodeForPeer({ text: opts.text, mine, theirs: opts.theirs, from: opts.from, op: opts.op });
|
|
57
|
+
const frame = {
|
|
58
|
+
type: 'agent_message',
|
|
59
|
+
payload: { text: enc.text, lang: enc.lang },
|
|
60
|
+
ts: Date.now(),
|
|
61
|
+
fromDid: opts.from ?? '',
|
|
62
|
+
...(opts.to ? { toDid: opts.to } : {}),
|
|
63
|
+
};
|
|
64
|
+
recordLangDecision(enc.decision, { channel: 'agent_message', peer: opts.to });
|
|
65
|
+
return { frame: JSON.stringify(frame), lang: enc.lang, decision: enc.decision, bytes: enc.bytes, encoded: enc.lang === 'efficode' };
|
|
66
|
+
}
|
|
67
|
+
/** 解析线上帧 (非 agent_message / 坏 JSON → null; 不抛, 调用方按"不是我的帧"处理) */
|
|
68
|
+
export function parseAgentMessageFrame(text) {
|
|
69
|
+
try {
|
|
70
|
+
const v = JSON.parse(text);
|
|
71
|
+
if (!v || v.type !== 'agent_message' || !v.payload || typeof v.payload.text !== 'string')
|
|
72
|
+
return null;
|
|
73
|
+
return v;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* 入站消息: 严格按帧里 `lang` 分派。
|
|
81
|
+
* - `lang: 'efficode'` 且本机也声明 efficode → 真解包 (畸形包抛 EfficodeError)
|
|
82
|
+
* - 其它一切 (含缺字段 / 未知字符串) → 原样文本 + 明确 note, **不进解码器**
|
|
83
|
+
*/
|
|
84
|
+
export function readAgentMessage(frameText, opts = {}) {
|
|
85
|
+
const frame = parseAgentMessageFrame(frameText);
|
|
86
|
+
if (!frame)
|
|
87
|
+
return null;
|
|
88
|
+
const mine = opts.mine ?? manifestLangDeclaration(opts.env);
|
|
89
|
+
const declared = frame.payload.lang;
|
|
90
|
+
const inc = decodeFromPeer({ payload: frame.payload.text, declaredLang: declared, mine });
|
|
91
|
+
const decision = negotiateLang(mine, declared);
|
|
92
|
+
recordLangDecision(decision, { channel: 'agent_message_recv' });
|
|
93
|
+
return {
|
|
94
|
+
...inc,
|
|
95
|
+
declared: inc.declaredRaw,
|
|
96
|
+
unknownLangs: unknownLangNames(declared),
|
|
97
|
+
fromDid: frame.fromDid ?? '',
|
|
98
|
+
toDid: frame.toDid,
|
|
99
|
+
decision,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -54,6 +54,10 @@ export function setLocalManifest(m) {
|
|
|
54
54
|
localManifest.exportments = [];
|
|
55
55
|
if (!('sciences' in m))
|
|
56
56
|
localManifest.sciences = [];
|
|
57
|
+
// 2026-09-28: 交流语言声明同规矩 —— 不给就显式清空, 免得旧的 efficode 声明在对端已经不支持时还留着.
|
|
58
|
+
// (老 manifest 语义不变: 没有 supportedLangs = 没声明 = 自然语言)
|
|
59
|
+
if (!('supportedLangs' in m))
|
|
60
|
+
localManifest.supportedLangs = [];
|
|
57
61
|
// 2026-09-18: 网络脉冲 (只记公开统计; 不落 DID/正文; 统计失败绝不影响主路径)
|
|
58
62
|
try {
|
|
59
63
|
const did = String(localManifest.ownerPublicKey || '');
|
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as os from 'os';
|
|
15
15
|
import { listRunnableGoals, claimGoal, heartbeatGoal, releaseGoal, readGoal, setContinuation, bumpContinuationAttempts, evaluateGoalCompletion, addEvidence, } from './goal-store.js';
|
|
16
|
-
|
|
16
|
+
// 2026-09-26 (P7 接线): 长期任务的模型策略 —— 「下一个 Run 用哪一份模型」的唯一决定函数,
|
|
17
|
+
// 以及「这次失败换模型能解决吗」的唯一判定函数 (Supervisor 不再自己看失败类别猜)。
|
|
18
|
+
import { resolveNextRunModel, supervisorMaySwitchModel, parseGoalModelPolicy, formatModelDecision, isTerminalRunStatus, } from './model-policy.js';
|
|
19
|
+
import { readRun, reconcileOrphans, superviseRuns, buildContinuationPlan, recordModelSwitch, RESUMABLE_STATUSES, } from './run-store.js';
|
|
17
20
|
// 2026-09-25 (飞轮接线): P0–P4 的七个模块经**唯一**适配层接进真实执行路径
|
|
18
21
|
// (节奏由进展决定 · Run 收尾必过 closeRun · 子 Agent 走工作合同 · 阻塞监控 · 变更注入 · 用户可见态)
|
|
19
22
|
// 2026-09-25 (串行收口): 四条线各自报"只有主线能加"的钩子在这里补齐 ——
|
|
@@ -96,13 +99,19 @@ export function decideGoalOutcome(goal, run, opts = {}) {
|
|
|
96
99
|
case 'failed':
|
|
97
100
|
case 'needs_human': {
|
|
98
101
|
const cls = run.errorClass || 'unknown';
|
|
102
|
+
// ★ 2026-09-26 (P7 接线): 这次失败"换一个模型能不能解决"由**唯一判定函数**回答, 不再由
|
|
103
|
+
// Supervisor 这边看类别自己猜。结论照实写进 reason (含策略模式与理由), 但**不改写**任何
|
|
104
|
+
// 在跑的 Run —— 真正换模型发生在下一个 Run 的决定里 (`resolveNextRunModel`, 见 runGoal)。
|
|
105
|
+
const policyMode = parseGoalModelPolicy(goal.modelPolicy).policy.mode;
|
|
106
|
+
const modelVerdict = supervisorMaySwitchModel({ mode: policyMode, errorClass: run.errorClass });
|
|
107
|
+
const modelHint = `; 模型: 策略=${policyMode} · ${modelVerdict.reason}`;
|
|
99
108
|
const needsHuman = ['auth', 'persist_failed', 'corrupt_state', 'policy_denied', 'repeat_failure', 'bad_args', 'no_such_tool'].includes(cls)
|
|
100
109
|
|| attempts >= maxAttempts;
|
|
101
110
|
if (needsHuman) {
|
|
102
111
|
return {
|
|
103
112
|
goalStatus: 'needs_human',
|
|
104
113
|
continuation: { ...base, autoContinue: false, wakeReason: 'needs_human', attempts },
|
|
105
|
-
reason: `${cls} 需要人工介入${attempts >= maxAttempts ? ` (自动继续已试 ${attempts} 次)` : ''}`,
|
|
114
|
+
reason: `${cls} 需要人工介入${attempts >= maxAttempts ? ` (自动继续已试 ${attempts} 次)` : ''}${modelHint}`,
|
|
106
115
|
};
|
|
107
116
|
}
|
|
108
117
|
const wait = continuationBackoffMs(attempts);
|
|
@@ -114,7 +123,7 @@ export function decideGoalOutcome(goal, run, opts = {}) {
|
|
|
114
123
|
wakeAt: new Date(now + wait).toISOString(),
|
|
115
124
|
attempts: attempts + 1,
|
|
116
125
|
},
|
|
117
|
-
reason: `可恢复错误 ${cls}: 第 ${attempts + 1} 次自动继续, ${Math.round(wait / 1000)}s
|
|
126
|
+
reason: `可恢复错误 ${cls}: 第 ${attempts + 1} 次自动继续, ${Math.round(wait / 1000)}s 后唤醒${modelHint}`,
|
|
118
127
|
};
|
|
119
128
|
}
|
|
120
129
|
case 'recovering':
|
|
@@ -500,6 +509,55 @@ export class ExecutionSupervisor {
|
|
|
500
509
|
const prevRun = prevRunId ? await readRun(prevRunId) : null;
|
|
501
510
|
const plan = prevRunId ? await buildContinuationPlan(prevRunId).catch(() => null) : null;
|
|
502
511
|
const guards = plan?.replayGuards || [];
|
|
512
|
+
// ★ 2026-09-26 (P7 接线): 下一个 Run 该用**哪一份**模型配置 —— 唯一决定函数 `resolveNextRunModel`。
|
|
513
|
+
// 它自己走完四步: ① 读 Goal 策略 (记录上的 modelPolicy 优先, 没有才读 sidecar);
|
|
514
|
+
// ② 定稿配置 (auto → 最新 Global 默认; pinned → 固定三元组; session → 会话绑定);
|
|
515
|
+
// ③ 按**失败类别**判定能不能挑备用模型 (内部就是 `supervisorMaySwitchModel`);
|
|
516
|
+
// ④ 跨 Run 非幂等守卫照带 (模型换了也不重做副作用), 并把这次决定写成 Run 事件。
|
|
517
|
+
// 它算的是**下一条** Run: 在跑的那条 Run 的快照一个字都不动 (旧 Run 可追溯)。
|
|
518
|
+
//
|
|
519
|
+
// ⚠ **事件落点** (接线时真跑逼出来的, 不是设计洁癖): P7 自己文档里的事实边界是
|
|
520
|
+
// 「本层算出来的只用于①下一个 Run 的启动快照 ②切换事件 —— **它不回头改老 Run**」,
|
|
521
|
+
// 飞轮规则 ⑦ (`goal-flywheel-wiring.test.ts`) 更是**逐字节**钉住「已发生的 Run 记录不被改写」。
|
|
522
|
+
// 而决定函数的事件缺省落点是 `prevRunId` —— 上一条 Run **已经收尾**时, 那条写入等于
|
|
523
|
+
// 往历史记录上追加一格 (真跑实测: `updatedAt`/`modelSwitches`/`harness` 全变 ⇒ 判红)。
|
|
524
|
+
// 所以分两种, 都说清: 上一条 Run **还活着** → 事件就写它 (它是当事 Run, 也是缺省);
|
|
525
|
+
// **已收尾** → 它是历史, 决定"只算不写", 等新 Run 起来后把同一条决定记到**新 Run 自己**的
|
|
526
|
+
// 账本上 (`RunModelSwitchEvent.to` 的语义本来就是"这个 Run **或下一个 Run** 该用的那一份")。
|
|
527
|
+
const prevIsHistory = prevRun ? isTerminalRunStatus(prevRun.status) : true;
|
|
528
|
+
let modelResolution = null;
|
|
529
|
+
try {
|
|
530
|
+
// 备用候选: 只有上一个 Run 是**模型相关失败**时才值得去凑 (凑法在切换入口里, 只挑现在真能用的)
|
|
531
|
+
const fallbackCandidates = prevRun?.errorClass
|
|
532
|
+
? await (async () => {
|
|
533
|
+
const ms = await import('../llm/model-selection.js');
|
|
534
|
+
return await ms.usableFallbackCandidates(prevRun.modelConfig ?? null);
|
|
535
|
+
})().catch(() => [])
|
|
536
|
+
: [];
|
|
537
|
+
modelResolution = await resolveNextRunModel({
|
|
538
|
+
goalId: goal.goalId,
|
|
539
|
+
// 上一条 Run 已收尾 = 历史 → 不给事件落点 (P7 契约里"只算不写"的那一档), 免得往历史追加
|
|
540
|
+
...(prevIsHistory ? {} : { prevRunId: prevRunId || undefined }),
|
|
541
|
+
policy: goal.modelPolicy,
|
|
542
|
+
errorClass: prevRun?.errorClass,
|
|
543
|
+
...(fallbackCandidates.length ? { fallbackCandidates } : {}),
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
// 算不出来**不猜**: 下一句日志说清, 执行器按自己的默认启动 (Run 记录里因此没有模型快照)
|
|
548
|
+
this.log(`[supervisor] goal=${goal.goalId} 下一个 Run 的模型决定算不出来 (按无快照启动): ${String(err?.message || err).slice(0, 160)}`);
|
|
549
|
+
}
|
|
550
|
+
const nextModelConfig = modelResolution?.startRunModelConfig;
|
|
551
|
+
if (modelResolution) {
|
|
552
|
+
this.log(`[supervisor] goal=${goal.goalId} 下一个 Run 模型: ${formatModelDecision(modelResolution.decision)}`
|
|
553
|
+
+ `; 策略来源=${modelResolution.policyOrigin}`
|
|
554
|
+
+ `${modelResolution.startRunModelConfig ? `; 启动快照 hash=${modelResolution.startRunModelConfig.configHash}` : '; 没有可信快照 → 不写快照'}`
|
|
555
|
+
+ `; 事件=${modelResolution.event.ok
|
|
556
|
+
? `已写(Run ${modelResolution.event.onRunId || '-'})`
|
|
557
|
+
: prevIsHistory && modelResolution.startRunModelConfig
|
|
558
|
+
? '等新 Run 起来后写在它自己的账本上 (上一条 Run 已收尾 = 历史, 不许改写)'
|
|
559
|
+
: `未写(${modelResolution.event.reason || '原因未明'})`}`);
|
|
560
|
+
}
|
|
503
561
|
// 2026-09-25 (飞轮接线 P4 / P2): 下一个 Run 必须真的读到**同一份**事实 ——
|
|
504
562
|
// ① 权威 continuation 的 nextAction (飞轮收尾写下的"下一步是什么");
|
|
505
563
|
// ② 已生效的新要求 (变更注入: 只影响后续 Run, 不改写历史);
|
|
@@ -525,7 +583,7 @@ export class ExecutionSupervisor {
|
|
|
525
583
|
if (!runner && this.resolver) {
|
|
526
584
|
let res;
|
|
527
585
|
try {
|
|
528
|
-
res = await this.resolver({ goal, kind, prevRunId, instruction, guards });
|
|
586
|
+
res = await this.resolver({ goal, kind, prevRunId, instruction, guards, ...(nextModelConfig ? { modelConfig: nextModelConfig } : {}) });
|
|
529
587
|
}
|
|
530
588
|
catch (err) {
|
|
531
589
|
res = { ok: false, kind: 'none', reason: `resolver 抛错: ${String(err?.message || err).slice(0, 140)}` };
|
|
@@ -600,7 +658,7 @@ export class ExecutionSupervisor {
|
|
|
600
658
|
let result;
|
|
601
659
|
const t0 = Date.now();
|
|
602
660
|
try {
|
|
603
|
-
result = await runner({ goal, kind, prevRunId, instruction, guards });
|
|
661
|
+
result = await runner({ goal, kind, prevRunId, instruction, guards, ...(nextModelConfig ? { modelConfig: nextModelConfig } : {}) });
|
|
604
662
|
}
|
|
605
663
|
catch (err) {
|
|
606
664
|
result = { error: err?.message || String(err) };
|
|
@@ -608,6 +666,30 @@ export class ExecutionSupervisor {
|
|
|
608
666
|
finally {
|
|
609
667
|
clearInterval(hb);
|
|
610
668
|
}
|
|
669
|
+
// ★ 2026-09-26 (P7 接线): 决定"只算不写"的那一档, 把同一条决定**补写进它真正生效的那条 Run**。
|
|
670
|
+
// 为什么不写在算决定的那一刻: 那时新 Run 还没有 id, 而唯一的现成落点是已经收尾的上一条 Run
|
|
671
|
+
// —— 那是历史 (`prevIsHistory` 分支就是为了不碰它)。落点等执行器报回 runId 再定, 事实不丢。
|
|
672
|
+
const runIdOfThisRun = result.runId && result.runId !== prevRunId ? result.runId : null;
|
|
673
|
+
if (modelResolution && nextModelConfig && runIdOfThisRun && !modelResolution.event.ok
|
|
674
|
+
&& prevIsHistory && (await readRun(runIdOfThisRun).catch(() => null))) {
|
|
675
|
+
const d = modelResolution.decision;
|
|
676
|
+
const from = prevRun?.modelConfig ?? null;
|
|
677
|
+
// 同口径 = 同一份 `configHash` 就是同一份 (P7 的 configHash 契约), 别自己另立一套
|
|
678
|
+
const outcome = (d.conflict && d.conflict.length) ? 'conflict'
|
|
679
|
+
: (from && from.configHash === nextModelConfig.configHash) ? 'frozen' : 'switched';
|
|
680
|
+
const w = await recordModelSwitch(runIdOfThisRun, {
|
|
681
|
+
to: nextModelConfig,
|
|
682
|
+
from,
|
|
683
|
+
outcome,
|
|
684
|
+
mode: (d.mode === 'auto' || d.mode === 'pinned' || d.mode === 'session' ? d.mode : 'unknown'),
|
|
685
|
+
source: d.source,
|
|
686
|
+
reason: `${d.reason}; 事件落在新 Run (上一条 Run 已收尾 = 历史, 不许改写)`.slice(0, 400),
|
|
687
|
+
guardsCarried: guards.length,
|
|
688
|
+
goalId: goal.goalId,
|
|
689
|
+
});
|
|
690
|
+
this.log(`[supervisor] goal=${goal.goalId} 模型决定补写在新 Run ${runIdOfThisRun} 的账本上: `
|
|
691
|
+
+ `${w.ok ? `outcome=${outcome}` : `写不进去 (${w.reason || '原因未明'})`}`);
|
|
692
|
+
}
|
|
611
693
|
// Run 结束 → Goal 决策 (确定性 reducer, 落盘)
|
|
612
694
|
const finalRunId = result.runId || prevRunId;
|
|
613
695
|
const finalRun = finalRunId ? await readRun(finalRunId) : null;
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
import * as os from 'os';
|
|
19
19
|
import * as path from 'path';
|
|
20
20
|
import { getCIDDatabase } from '../orbitdb/cid-database.js';
|
|
21
|
+
// 2026-09-28: 交流语言 (Efficode 是可选项, 不是默认项) —— 只有双方都声明才用
|
|
22
|
+
import { decodeFromPeer, encodeForPeer, recordLangDecision } from '../efficode/negotiate.js';
|
|
21
23
|
// ============ 依赖注入 (测试用, 避免单测起真实 OrbitDB 节点) ============
|
|
22
24
|
let _dbOverride = null;
|
|
23
25
|
/** 测试用: 注入 fake CIDDatabase */
|
|
@@ -281,7 +283,11 @@ export async function groupMessages(groupId, limit = 50) {
|
|
|
281
283
|
for (const entry of all) {
|
|
282
284
|
const v = entry.value;
|
|
283
285
|
if (v && typeof v.text === 'string' && typeof v.from === 'string') {
|
|
284
|
-
|
|
286
|
+
const m = { from: v.from, text: v.text, ts: typeof v.ts === 'number' ? v.ts : 0 };
|
|
287
|
+
// 只透传字符串形态的语言声明; 别的形状一律当"没声明"(不让脏值进到解码分派)
|
|
288
|
+
if (typeof v.lang === 'string' && v.lang.trim())
|
|
289
|
+
m.lang = v.lang.trim();
|
|
290
|
+
msgs.push(m);
|
|
285
291
|
}
|
|
286
292
|
}
|
|
287
293
|
msgs.sort((a, b) => a.ts - b.ts);
|
|
@@ -293,7 +299,7 @@ export async function groupMembers(groupId) {
|
|
|
293
299
|
return Array.from(new Set(msgs.map((m) => m.from)));
|
|
294
300
|
}
|
|
295
301
|
/** 发送群消息 (广播给所有成员) */
|
|
296
|
-
export async function groupSend(groupId, text, from) {
|
|
302
|
+
export async function groupSend(groupId, text, from, opts) {
|
|
297
303
|
const msg = String(text || '').trim();
|
|
298
304
|
if (!msg)
|
|
299
305
|
return { ok: false, error: '消息不能为空' };
|
|
@@ -309,13 +315,48 @@ export async function groupSend(groupId, text, from) {
|
|
|
309
315
|
}
|
|
310
316
|
}
|
|
311
317
|
try {
|
|
312
|
-
|
|
318
|
+
const record = { from: String(from || 'anonymous'), text: msg, ts: Date.now() };
|
|
319
|
+
// 只有显式给了合法声明才写字段 —— 不给就不写 (老读者读到的东西一字不变)
|
|
320
|
+
const lang = typeof opts?.lang === 'string' ? opts.lang.trim() : '';
|
|
321
|
+
if (lang)
|
|
322
|
+
record.lang = lang;
|
|
323
|
+
await store.add(record);
|
|
313
324
|
return { ok: true };
|
|
314
325
|
}
|
|
315
326
|
catch (e) {
|
|
316
327
|
return { ok: false, error: `发送失败: ${String(e?.message || e).slice(0, 160)}` };
|
|
317
328
|
}
|
|
318
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* 2026-09-28: 按语言裁决发群消息 —— 真可走的那条路。
|
|
332
|
+
*
|
|
333
|
+
* 双方都声明 efficode ⇒ 正文真编成 Efficode 包 (文本模式 Base64), `lang: 'efficode'` 一起落库;
|
|
334
|
+
* 否则**原文直发** + `lang: 'natural'` (或干脆不写字段, 由调用方定), 并记一笔回落原因。
|
|
335
|
+
* 裁决/编码/落库各自失败都有结构化结果, 不静默。
|
|
336
|
+
*/
|
|
337
|
+
export async function groupSendWithLang(groupId, text, from, opts = { mine: undefined, theirs: undefined }) {
|
|
338
|
+
const enc = encodeForPeer({ text, mine: opts.mine, theirs: opts.theirs, from, op: opts.op });
|
|
339
|
+
recordLangDecision(enc.decision, { channel: `group:${groupId}`, peer: from });
|
|
340
|
+
const wantLang = enc.lang === 'efficode' || opts.record === 'always' ? enc.lang : '';
|
|
341
|
+
const r = await groupSend(groupId, enc.text, from, wantLang ? { lang: wantLang } : undefined);
|
|
342
|
+
return {
|
|
343
|
+
ok: r.ok,
|
|
344
|
+
error: r.error,
|
|
345
|
+
lang: enc.lang,
|
|
346
|
+
bytes: enc.bytes,
|
|
347
|
+
fallback: enc.decision.fallback,
|
|
348
|
+
reason: enc.decision.reason,
|
|
349
|
+
symbolic: enc.symbolic,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
/**
|
|
353
|
+
* 2026-09-28: 读一条群消息 —— 严格按它自己声明的 `lang` 分派。
|
|
354
|
+
* 声明不是 efficode (含未知字符串 / 没声明) ⇒ 原样返回, **不进解码器**。
|
|
355
|
+
* 声明 efficode 且本机也声明支持 ⇒ 真解包 (畸形包抛, 由调用方如实报失败)。
|
|
356
|
+
*/
|
|
357
|
+
export function readGroupMessage(msg, mine) {
|
|
358
|
+
return decodeFromPeer({ payload: msg.text, declaredLang: msg.lang, mine });
|
|
359
|
+
}
|
|
319
360
|
/** 群组邀请链接 */
|
|
320
361
|
export async function groupLink(groupId) {
|
|
321
362
|
const groups = await loadGroups();
|