cotutor 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +9 -7
- package/NOTICE +3 -3
- package/README.md +36 -13
- package/avatars/chinese.webp +0 -0
- package/avatars/english.webp +0 -0
- package/avatars/math.webp +0 -0
- package/bin/cotutor.js +13 -3
- package/cards/photo/card.css +10 -3
- package/cards/photo/card.md +2 -2
- package/cards/tutor/card.css +13 -0
- package/cards/tutor/card.md +2 -1
- package/cards//346/235/277/344/271/246/346/200/216/344/271/210/345/206/231.md +1 -1
- package/dist/app/activity-eval.js +3 -2
- package/dist/app/cli/activities.d.ts +8 -1
- package/dist/app/cli/activities.js +42 -4
- package/dist/app/cli/cert.d.ts +5 -0
- package/dist/app/cli/cert.js +20 -7
- package/dist/app/cli/chalk/browser.d.ts +2 -2
- package/dist/app/cli/chalk/browser.js +22 -7
- package/dist/app/cli/chalk/main.js +3 -2
- package/dist/app/cli/doctor.d.ts +6 -5
- package/dist/app/cli/doctor.js +107 -171
- package/dist/app/cli/firewall.d.ts +24 -0
- package/dist/app/cli/firewall.js +79 -0
- package/dist/app/cli/init.js +8 -5
- package/dist/app/cli/main.js +347 -24
- package/dist/app/cli/migrate.d.ts +2 -11
- package/dist/app/cli/migrate.js +1 -59
- package/dist/app/cli/port-holder.d.ts +16 -0
- package/dist/app/cli/port-holder.js +34 -0
- package/dist/app/cli/port.d.ts +37 -0
- package/dist/app/cli/port.js +116 -0
- package/dist/app/cli/sandbox.d.ts +25 -0
- package/dist/app/cli/sandbox.js +166 -0
- package/dist/app/cli/serve-log.d.ts +13 -0
- package/dist/app/cli/serve-log.js +90 -0
- package/dist/app/cli/serve.d.ts +18 -4
- package/dist/app/cli/serve.js +78 -8
- package/dist/app/cli/service-windows.d.ts +50 -0
- package/dist/app/cli/service-windows.js +117 -0
- package/dist/app/cli/service.d.ts +138 -0
- package/dist/app/cli/service.js +429 -0
- package/dist/app/cli/skeleton.d.ts +1 -1
- package/dist/app/cli/skeleton.js +13 -13
- package/dist/app/cli/skills.d.ts +5 -1
- package/dist/app/cli/skills.js +14 -4
- package/dist/app/cli/tray-icons.d.ts +11 -0
- package/dist/app/cli/tray-icons.js +87 -0
- package/dist/app/cli/tray-ids.d.ts +6 -0
- package/dist/app/cli/tray-ids.js +6 -0
- package/dist/app/cli/tray-mac.d.ts +8 -0
- package/dist/app/cli/tray-mac.js +140 -0
- package/dist/app/cli/tray-run.d.ts +1 -0
- package/dist/app/cli/tray-run.js +33 -0
- package/dist/app/cli/tray-windows.d.ts +5 -0
- package/dist/app/cli/tray-windows.js +166 -0
- package/dist/app/cli/tray.d.ts +205 -0
- package/dist/app/cli/tray.js +618 -0
- package/dist/app/cli/usage.d.ts +1 -1
- package/dist/app/cli/usage.js +11 -6
- package/dist/app/dictation/route.js +3 -2
- package/dist/app/http/app.d.ts +5 -10
- package/dist/app/http/app.js +45 -98
- package/dist/app/http/avatars.d.ts +17 -0
- package/dist/app/http/avatars.js +35 -0
- package/dist/app/http/dev-tutor-files.js +1 -1
- package/dist/app/http/parent-home.d.ts +14 -0
- package/dist/app/http/parent-home.js +69 -6
- package/dist/app/http/parent-look.d.ts +2 -0
- package/dist/app/http/parent-look.js +151 -0
- package/dist/app/http/raw-view.js +3 -3
- package/dist/app/http/voice-preview.d.ts +4 -0
- package/dist/app/http/voice-preview.js +34 -0
- package/dist/app/machine/bailian.d.ts +0 -2
- package/dist/app/machine/bailian.js +5 -6
- package/dist/app/mock/mock-live.js +1 -1
- package/dist/app/mock/mock.d.ts +4 -2
- package/dist/app/mock/mock.js +20 -12
- package/dist/app/replay.js +4 -4
- package/dist/app/skills/home-doc.js +1 -1
- package/dist/app/workspace.d.ts +9 -4
- package/dist/app/workspace.js +33 -10
- package/dist/app/ws/route.d.ts +8 -2
- package/dist/app/ws/route.js +7 -3
- package/dist/board/parse.d.ts +0 -2
- package/dist/board/parse.js +0 -6
- package/dist/board/presentation.d.ts +9 -15
- package/dist/board/presentation.js +8 -10
- package/dist/brain/api-tools.js +2 -1
- package/dist/brain/api.d.ts +12 -17
- package/dist/brain/api.js +16 -33
- package/dist/brain/index.d.ts +23 -71
- package/dist/brain/index.js +6 -69
- package/dist/brain/log.d.ts +5 -0
- package/dist/brain/log.js +22 -0
- package/dist/brain/runtime.d.ts +30 -0
- package/dist/brain/runtime.js +23 -0
- package/dist/brain/{cli/stream-json.d.ts → stream-json.d.ts} +2 -4
- package/dist/brain/{cli/stream-json.js → stream-json.js} +0 -4
- package/dist/brain/stream.d.ts +5 -5
- package/dist/brain/stream.js +7 -7
- package/dist/brain/transcript.d.ts +3 -3
- package/dist/brain/transcript.js +6 -6
- package/dist/brain/{cli/types.d.ts → types.d.ts} +3 -48
- package/dist/brain/types.js +14 -0
- package/dist/chalk/glyphs/skeleton.d.ts +3 -3
- package/dist/chalk/glyphs/skeleton.js +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.js +1 -1
- package/dist/memory/diary.js +3 -3
- package/dist/realtime/bailian.d.ts +1 -1
- package/dist/realtime/bailian.js +3 -2
- package/dist/realtime/preview.js +1 -1
- package/dist/realtime/voices.d.ts +2 -0
- package/dist/realtime/voices.js +57 -57
- package/dist/runtime/activities.d.ts +13 -1
- package/dist/runtime/activities.js +28 -2
- package/dist/runtime/activity-state.js +2 -2
- package/dist/runtime/agent-file.d.ts +3 -3
- package/dist/runtime/agent-file.js +3 -3
- package/dist/runtime/context/tutor-rules.d.ts +2 -2
- package/dist/runtime/context/tutor-rules.js +3 -3
- package/dist/runtime/home/home.d.ts +12 -1
- package/dist/runtime/home/home.js +8 -1
- package/dist/runtime/home/store.d.ts +1 -1
- package/dist/runtime/home/store.js +36 -21
- package/dist/runtime/journal/mix.d.ts +15 -6
- package/dist/runtime/journal/mix.js +60 -19
- package/dist/runtime/journal/recorder.d.ts +58 -13
- package/dist/runtime/journal/recorder.js +106 -16
- package/dist/runtime/journal/store.d.ts +4 -4
- package/dist/runtime/journal/store.js +6 -6
- package/dist/runtime/kid-input.js +2 -3
- package/dist/runtime/live/call.d.ts +12 -0
- package/dist/runtime/live/call.js +51 -18
- package/dist/runtime/live/turns.js +1 -1
- package/dist/runtime/runner.d.ts +9 -55
- package/dist/runtime/runner.js +52 -173
- package/dist/scenes/paint/painter.js +4 -3
- package/dist/schema/config.d.ts +43 -22
- package/dist/schema/config.js +45 -47
- package/dist/schema/conversation.d.ts +6 -0
- package/dist/schema/home.d.ts +11 -1
- package/dist/schema/home.js +7 -2
- package/dist/schema/workspace.d.ts +27 -0
- package/dist/schema/workspace.js +28 -0
- package/dist/shared/default-port.d.ts +2 -0
- package/dist/shared/default-port.js +2 -0
- package/dist/shared/qianwen.d.ts +14 -10
- package/dist/shared/qianwen.js +26 -22
- package/dist/shared/site-url.d.ts +2 -0
- package/dist/shared/site-url.js +5 -0
- package/dist/ui/dev/dev-page.js +25 -252
- package/dist/ui/dev/dev-theme.d.ts +1 -1
- package/dist/ui/dev/dev-theme.js +1 -1
- package/dist/ui/dev/voice-gallery-page.js +1 -1
- package/dist/ui/kid/board-mark.d.ts +2 -2
- package/dist/ui/kid/board-mark.js +2 -2
- package/dist/ui/kid/home-tiles.d.ts +14 -0
- package/dist/ui/kid/home-tiles.js +44 -0
- package/dist/ui/kid/kid-page.js +68 -103
- package/dist/ui/kid/voice-ui.d.ts +1 -1
- package/dist/ui/kid/voice-ui.js +1 -1
- package/dist/ui/kid/word-emoji.js +5 -3
- package/dist/ui/parent/parent-home-page.js +315 -7
- package/dist/ui/parent/parent-page.js +6 -2
- package/dist/ui/parent/parent-settings-page.d.ts +1 -0
- package/dist/ui/parent/parent-settings-page.js +133 -0
- package/dist/ui/{dev → parent}/qr-page.d.ts +2 -2
- package/dist/ui/{dev → parent}/qr-page.js +19 -14
- package/dist/ui/shared/brand.d.ts +3 -1
- package/dist/ui/shared/brand.js +3 -1
- package/grades/grade-2/README.md +1 -1
- package/grades/grade-2/VALIDATION.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/chinese-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/chinese-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/english-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/english-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//345/244/215/344/271/240.md +1 -1
- package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271.md → 243/200/346/237/245/344/275/234/344/270/232.md} +4 -4
- package/grades/grade-2/agents/math-tutor/activities//346/{211/271/346/224/271. → 243/200/346/237/245/344/275/234/344/270/232.}/344/276/213/345/255/220.md +2 -2
- package/grades/grade-2/agents/math-tutor/activities//351/242/204/344/271/240.md +1 -1
- package/package.json +2 -1
- package/skills/cotutor-analyze/references//345/221/275/344/273/244/344/270/216/346/226/207/344/273/266.md +2 -2
- package/skills/cotutor-board/references/photo.md +2 -2
- package/skills/cotutor-home/SKILL.md +1 -0
- package/skills/cotutor-home/references/tutor.md +2 -1
- package/skills/cotutor-home/references//351/246/226/351/241/265/350/257/255/346/263/225.md +2 -1
- package/skills/cotutor-tune/references//345/255/227/346/256/265.md +6 -3
- package/skills/cotutor-tutor/SKILL.md +5 -13
- package/dist/brain/cli/adapters.d.ts +0 -60
- package/dist/brain/cli/adapters.js +0 -91
- package/dist/brain/cli/qwen.d.ts +0 -11
- package/dist/brain/cli/qwen.js +0 -141
- package/dist/brain/cli/run-plan.d.ts +0 -59
- package/dist/brain/cli/run-plan.js +0 -71
- package/dist/brain/cli/run.d.ts +0 -35
- package/dist/brain/cli/run.js +0 -256
- package/dist/brain/cli/types.js +0 -3
- package/dist/brain/cli/warm.d.ts +0 -53
- package/dist/brain/cli/warm.js +0 -103
package/dist/brain/cli/run.js
DELETED
|
@@ -1,256 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 脑执行的 CLI 实现:按 cotutor.json 的运行时起老师进程(qwen 之类,读输出、关工具、环境都在同目录的适配器 adapters.ts 与各家的文件),
|
|
3
|
-
* stream-json 落 <日期>.<job>.log、stderr 落 .err.log;每行读成统一事件,一行只解析一次,同一批事件给拼正文、工具、会话 id 用。
|
|
4
|
-
* - 预热(./warm.ts):消息走 stdin 的运行时,下一轮的进程提前起好,对得上就接过来
|
|
5
|
-
* - 断流看门狗:stall.ms 没吐一个字节、也没有工具在跑 → 杀掉,resume 同一个会话接着写(最多 stall.retries 次)。
|
|
6
|
-
* CLI 自己的断流重试可能要等很久(2026-09-21 真跑一轮连断两次,等了 6 分钟)
|
|
7
|
-
* - 回复停在写成正文的工具调用上(这轮没给它那个工具,qwen 照自己的格式写出来):resume 同一个会话告诉它不会有结果、接着回孩子,只一次
|
|
8
|
-
* - 收尾:从 .log 读整轮转录;断流接着跑的,断在半截的那段拼回最后一段前面
|
|
9
|
-
*/
|
|
10
|
-
import { spawn } from 'node:child_process';
|
|
11
|
-
import { closeSync, createWriteStream, openSync, writeSync } from 'node:fs';
|
|
12
|
-
import { appendFile, mkdir, readFile } from 'node:fs/promises';
|
|
13
|
-
import { basename } from 'node:path';
|
|
14
|
-
import { parserOf, prepareCli, processEnv, toolsOf, unexpectedTools } from "./adapters.js";
|
|
15
|
-
import { planRun, stallPrompt, toolLeakPrompt } from "./run-plan.js";
|
|
16
|
-
import { createPartialReader } from "../stream.js";
|
|
17
|
-
import { parseTranscript, toolCalls, toolSummary } from "../transcript.js";
|
|
18
|
-
import { WarmPool } from "./warm.js";
|
|
19
|
-
const cliName = (p) => p.argv[0].slice(p.argv[0].lastIndexOf('/') + 1);
|
|
20
|
-
export class CliBrain {
|
|
21
|
-
spares;
|
|
22
|
-
workspace;
|
|
23
|
-
/** 起子进程用的环境(测试注入);缺省 process.env */
|
|
24
|
-
env;
|
|
25
|
-
constructor(opts) {
|
|
26
|
-
this.workspace = opts.workspace;
|
|
27
|
-
this.env = opts.env;
|
|
28
|
-
this.spares = new WarmPool({ idleMs: opts.warmIdleMs });
|
|
29
|
-
}
|
|
30
|
-
plan(req) {
|
|
31
|
-
const { config, root } = this.workspace();
|
|
32
|
-
return planRun(config, { session: req.session }, { agent: req.agent, prompt: req.prompt, agentBody: req.agentBody, systemBody: req.systemBody, boardFile: req.boardFile, runtime: req.runtime, effort: req.effort, tools: req.tools, root, readDirs: req.readDirs });
|
|
33
|
-
}
|
|
34
|
-
prepare(plan) {
|
|
35
|
-
return prepareCli(plan.cli, this.workspace().root).catch(() => { });
|
|
36
|
-
}
|
|
37
|
-
canWarm(runtime) {
|
|
38
|
-
return Boolean(runtime.stdin);
|
|
39
|
-
}
|
|
40
|
-
warm(tutor, slot, plan, key) {
|
|
41
|
-
this.spares.warm(tutor, slot, { argv: plan.argv, env: plan.env, ...key }, this.processEnv(plan));
|
|
42
|
-
}
|
|
43
|
-
warmed(tutor, slot) {
|
|
44
|
-
return this.spares.has(tutor, slot);
|
|
45
|
-
}
|
|
46
|
-
dropWarm(tutor, slot) {
|
|
47
|
-
this.spares.drop(tutor, slot);
|
|
48
|
-
}
|
|
49
|
-
close() {
|
|
50
|
-
this.spares.dropAll();
|
|
51
|
-
}
|
|
52
|
-
processEnv(p) {
|
|
53
|
-
return processEnv(p, { ...(this.env ?? process.env), COTUTOR_WORKSPACE: this.workspace().root });
|
|
54
|
-
}
|
|
55
|
-
start(req, plan, ctx, hooks) {
|
|
56
|
-
// 预热的进程:resume 的接那个位置(对得上 = 这个话题从它起来之后没人写过),新开的接 fresh;对不上(或这轮不走 stdin)就杀掉
|
|
57
|
-
const spare = this.spares.claim(ctx.tutor, plan.resume ? 'resume' : 'fresh', { argv: plan.argv, env: plan.env, ...ctx.key });
|
|
58
|
-
// 断流接着跑:carried = 被杀那次已经拼出的正文(接着跑的那次换新读取器,正文 = carried + 新的);
|
|
59
|
-
// open = 断在半截、没进会话的那段(递回给老师,收尾时拼在最后一段前面)
|
|
60
|
-
let reader = createPartialReader();
|
|
61
|
-
let carried = '';
|
|
62
|
-
let open = '';
|
|
63
|
-
const liveText = () => { const t = reader.text(); return !carried ? t : !t ? carried : open ? carried + t : `${carried}\n\n${t}`; };
|
|
64
|
-
const resumePlan = (id, prompt) => this.plan({ ...req, runtime: plan.runtime, session: { id, runtime: plan.runtime }, prompt });
|
|
65
|
-
const stall = ctx.stall;
|
|
66
|
-
const files = ctx.files;
|
|
67
|
-
const cwd = ctx.key.cwd;
|
|
68
|
-
const go = async () => {
|
|
69
|
-
await mkdir(cwd, { recursive: true });
|
|
70
|
-
const out = createWriteStream(files.log);
|
|
71
|
-
const err = openSync(files.err, 'w');
|
|
72
|
-
hooks.onStart({ cli: cliName(plan), runtime: plan.runtime, resume: plan.resume, ...(spare ? { warmBornAt: spare.bornAt } : {}) });
|
|
73
|
-
// 工具调用回报一条(子代理的标 sub);顺手记哪些顶层工具还没回结果:工具在跑时进程不吐字是正常的,断流看门狗不算这段
|
|
74
|
-
const parse = parserOf(plan.cli);
|
|
75
|
-
let lineBuf = '';
|
|
76
|
-
const toolsRunning = new Set();
|
|
77
|
-
// 消息走 stdin 的运行时:看到收尾就关 stdin,进程自己退(每次 runOnce 换一个)
|
|
78
|
-
let onResult = null;
|
|
79
|
-
// 进程起来报的工具表里不该有的(适配器核对;不带工具那轮却有工具 = 关工具的旗标失效了,CLI 升版本最先坏的地方)
|
|
80
|
-
let unexpected = [];
|
|
81
|
-
let session = plan.session;
|
|
82
|
-
// 新会话要等模型开始回了(开始回复、吐字、在想、整条回复)才在盘上、才 resume 得了
|
|
83
|
-
// (2026-09-22 真跑:init 吐了会话 id 就被杀,resume 报 No conversation found;吐过三个字再杀,盘上有这条用户消息)
|
|
84
|
-
let persisted = plan.resume;
|
|
85
|
-
let stalls = 0;
|
|
86
|
-
// warm:预热好的进程(只给第一次;断流接着跑的那次照旧冷起)
|
|
87
|
-
const runOnce = (p, warm = null) => new Promise((resolveExit) => {
|
|
88
|
-
// 被杀的那次没收完的半行不要了
|
|
89
|
-
lineBuf = '';
|
|
90
|
-
const child = warm?.child ?? spawn(p.argv[0], p.argv.slice(1), {
|
|
91
|
-
cwd,
|
|
92
|
-
env: this.processEnv(p),
|
|
93
|
-
stdio: [p.stdin ? 'pipe' : 'ignore', 'pipe', err],
|
|
94
|
-
});
|
|
95
|
-
if (warm) {
|
|
96
|
-
// 预热的进程 stderr 是管道(起它时还没有这轮的 err.log):攒下的和之后的都写进来(池子交出来时暂停着,这里 resume)
|
|
97
|
-
const toErr = (c) => { try {
|
|
98
|
-
writeSync(err, c);
|
|
99
|
-
}
|
|
100
|
-
catch { /* err.log 已关 */ } };
|
|
101
|
-
warm.stderr.forEach(toErr);
|
|
102
|
-
child.stderr?.on('data', toErr);
|
|
103
|
-
child.stderr?.resume();
|
|
104
|
-
}
|
|
105
|
-
if (p.stdin) {
|
|
106
|
-
child.stdin?.on('error', () => { });
|
|
107
|
-
child.stdin?.write(p.stdin);
|
|
108
|
-
onResult = () => { onResult = null; child.stdin?.end(); };
|
|
109
|
-
}
|
|
110
|
-
else
|
|
111
|
-
onResult = null;
|
|
112
|
-
let stalled = false;
|
|
113
|
-
let idle = null;
|
|
114
|
-
let lastByte = Date.now();
|
|
115
|
-
const arm = () => {
|
|
116
|
-
if (idle)
|
|
117
|
-
clearTimeout(idle);
|
|
118
|
-
if (!stall.ms)
|
|
119
|
-
return;
|
|
120
|
-
idle = setTimeout(() => {
|
|
121
|
-
idle = null;
|
|
122
|
-
if (toolsRunning.size)
|
|
123
|
-
return arm();
|
|
124
|
-
stalled = true;
|
|
125
|
-
child.kill('SIGTERM');
|
|
126
|
-
setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
127
|
-
child.kill('SIGKILL'); }, 2000).unref();
|
|
128
|
-
}, stall.ms);
|
|
129
|
-
};
|
|
130
|
-
arm();
|
|
131
|
-
child.stdout?.on('data', (chunk) => {
|
|
132
|
-
lastByte = Date.now();
|
|
133
|
-
arm();
|
|
134
|
-
out.write(chunk);
|
|
135
|
-
lineBuf += chunk.toString('utf8');
|
|
136
|
-
const lines = lineBuf.split('\n');
|
|
137
|
-
lineBuf = lines.pop() ?? '';
|
|
138
|
-
let changed = false;
|
|
139
|
-
for (const line of lines) {
|
|
140
|
-
const events = parse(line);
|
|
141
|
-
for (const e of events) {
|
|
142
|
-
if (e.kind === 'session') {
|
|
143
|
-
if (!session)
|
|
144
|
-
session = e.id;
|
|
145
|
-
}
|
|
146
|
-
else if (e.kind === 'init')
|
|
147
|
-
unexpected = unexpectedTools(plan.cli, plan.tools, e.tools);
|
|
148
|
-
else if (e.kind === 'result')
|
|
149
|
-
onResult?.();
|
|
150
|
-
else if (e.kind === 'results') {
|
|
151
|
-
if (!e.sub)
|
|
152
|
-
for (const r of e.results)
|
|
153
|
-
toolsRunning.delete(r.id);
|
|
154
|
-
}
|
|
155
|
-
else {
|
|
156
|
-
if (e.kind === 'start' || e.kind === 'delta' || e.kind === 'thinking' || e.kind === 'assistant')
|
|
157
|
-
persisted = true;
|
|
158
|
-
if (e.kind === 'assistant')
|
|
159
|
-
for (const tool of toolsOf(e.parts)) {
|
|
160
|
-
if (!e.sub && tool.id)
|
|
161
|
-
toolsRunning.add(tool.id);
|
|
162
|
-
hooks.onTool({ name: toolSummary(tool.name, tool.input), sub: e.sub });
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
if (reader.apply(events))
|
|
167
|
-
changed = true;
|
|
168
|
-
}
|
|
169
|
-
if (changed)
|
|
170
|
-
hooks.onText();
|
|
171
|
-
});
|
|
172
|
-
const done = (r) => {
|
|
173
|
-
if (idle)
|
|
174
|
-
clearTimeout(idle);
|
|
175
|
-
resolveExit({ ...r, ...(stalled ? { stalled: true } : {}) });
|
|
176
|
-
};
|
|
177
|
-
child.once('error', (e) => done({ code: null, spawnError: e }));
|
|
178
|
-
child.once('close', (code) => { if (stalled)
|
|
179
|
-
void appendFile(files.err, `cotutor: ${Date.now() - lastByte}ms 没有输出,当断流杀掉\n`).catch(() => { }); done({ code }); });
|
|
180
|
-
if (warm) {
|
|
181
|
-
// 开口前吐的按顺序补喂,再放开暂停;接手之前就退了的,close 已经错过,直接收尾
|
|
182
|
-
for (const c of warm.stdout)
|
|
183
|
-
child.stdout?.emit('data', c);
|
|
184
|
-
child.stdout?.resume();
|
|
185
|
-
if (warm.closed())
|
|
186
|
-
done({ code: child.exitCode });
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
let exit = await runOnce(plan, spare);
|
|
190
|
-
while (exit.stalled) {
|
|
191
|
-
stalls++;
|
|
192
|
-
if (persisted)
|
|
193
|
-
open += reader.current();
|
|
194
|
-
else {
|
|
195
|
-
session = plan.session;
|
|
196
|
-
open = '';
|
|
197
|
-
}
|
|
198
|
-
const retry = stalls <= stall.retries ? stalls : 0;
|
|
199
|
-
hooks.onStall({ idleMs: stall.ms, retry, kept: Array.from(open).length });
|
|
200
|
-
if (!retry)
|
|
201
|
-
break;
|
|
202
|
-
carried = persisted ? liveText() : '';
|
|
203
|
-
reader = createPartialReader();
|
|
204
|
-
toolsRunning.clear();
|
|
205
|
-
// 会话还没落盘(进程起来就卡住、一条整的回复都没有):原样从头再起一次;落了盘就 resume 接着写
|
|
206
|
-
const next = persisted && session ? resumePlan(session, stallPrompt(open)) : plan;
|
|
207
|
-
hooks.onStart({ cli: cliName(next), runtime: next.runtime, resume: next.resume });
|
|
208
|
-
exit = await runOnce(next);
|
|
209
|
-
}
|
|
210
|
-
// 回复停在写成正文的工具调用上(板书解析器已剥掉,孩子没看到):老师在等结果,resume 同一个会话告诉它不会有结果、接着回孩子,只一次。
|
|
211
|
-
// 这次的正文接在前面那段后面
|
|
212
|
-
let leaked = '';
|
|
213
|
-
if (!exit.stalled && exit.code === 0 && session && req.stuckOnTool?.(liveText())) {
|
|
214
|
-
leaked = liveText();
|
|
215
|
-
carried = leaked;
|
|
216
|
-
open = '';
|
|
217
|
-
reader = createPartialReader();
|
|
218
|
-
toolsRunning.clear();
|
|
219
|
-
const next = resumePlan(session, toolLeakPrompt());
|
|
220
|
-
hooks.onStart({ cli: cliName(next), runtime: next.runtime, resume: next.resume });
|
|
221
|
-
exit = await runOnce(next);
|
|
222
|
-
}
|
|
223
|
-
hooks.onExit(Date.now());
|
|
224
|
-
await new Promise((r) => out.end(r));
|
|
225
|
-
closeSync(err);
|
|
226
|
-
if (exit.spawnError)
|
|
227
|
-
await appendFile(files.err, `cotutor: 起不来 ${plan.argv[0]}:${exit.spawnError.message}\n`);
|
|
228
|
-
const logText = await readFile(files.log, 'utf8').catch(() => '');
|
|
229
|
-
const transcript = parseTranscript(logText, parse);
|
|
230
|
-
// 断流接着跑的:断在半截的那段没进会话、也不在最后的 result 里,拼回最后一段前面(老师是从断处接着写的)
|
|
231
|
-
// (result 的正文被 trim 过,断在句末 / 围栏上的补回换行,免得两句粘成一行)
|
|
232
|
-
if (open && transcript.final?.text)
|
|
233
|
-
transcript.final.text = open + (/[。!!??;;`\n]\s*$/.test(open) ? '\n' : '') + transcript.final.text;
|
|
234
|
-
if (leaked && transcript.final?.text)
|
|
235
|
-
transcript.final.text = `${leaked}\n\n${transcript.final.text}`;
|
|
236
|
-
if (exit.stalled && !transcript.final)
|
|
237
|
-
transcript.final = { text: null, ok: false, reason: `stalled:${stalls} 次断流(每次 ${stall.ms}ms 没输出),接着跑的次数用完了` };
|
|
238
|
-
// 这轮用了哪些工具、读了什么:从 .log 抽出来物化(家长端「看原文」一站、cotutor show)
|
|
239
|
-
const tools = toolCalls(logText, parse).slice(0, 200);
|
|
240
|
-
if (!transcript.final) {
|
|
241
|
-
// 进程退了但没有 result 事件:起不来、被杀、或 CLI 崩了;标 error,原因指向 err.log
|
|
242
|
-
transcript.final = { text: null, ok: false, reason: exit.spawnError ? `spawn:${exit.spawnError.message}` : `exit:${exit.code ?? 'signal'}` };
|
|
243
|
-
transcript.items.push({ kind: 'done', text: `本轮没有收尾(${transcript.final.reason}),看 ${basename(files.err)}` });
|
|
244
|
-
}
|
|
245
|
-
const warnings = [];
|
|
246
|
-
if (unexpected.length)
|
|
247
|
-
warnings.push(`${plan.cli} 起来时带着这轮不该有的工具:${unexpected.join('、')}(${plan.tools === 'off' ? '这轮应该一个工具都不带' : '这轮只该带只读的几样'});关工具的旗标可能随 CLI 升级失效了,看 src/brain/cli/${plan.cli}.ts`);
|
|
248
|
-
if (leaked)
|
|
249
|
-
warnings.push('老师把工具调用写成了正文、停在那里等结果(板上没出、嘴没念),应用 resume 同一个会话叫它直接回孩子');
|
|
250
|
-
if (stalls)
|
|
251
|
-
warnings.push(`API 流断了 ${stalls} 次(每次 ${stall.ms / 1000} 秒没动静),应用杀掉进程、resume 会话接着写的${exit.stalled ? ';次数用完还断,这轮没收尾' : ''}`);
|
|
252
|
-
return { transcript: { ...transcript, final: transcript.final }, tools, warnings };
|
|
253
|
-
};
|
|
254
|
-
return { warm: Boolean(spare), text: liveText, done: go() };
|
|
255
|
-
}
|
|
256
|
-
}
|
package/dist/brain/cli/types.js
DELETED
package/dist/brain/cli/warm.d.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 预热:消息走 stdin 的运行时,下一轮的老师进程提前起好、在 stdin 上等着;
|
|
3
|
-
* 孩子开口时对得上就接过来写消息,省掉起进程与读会话(真跑冷起中位 1.8 秒,预热约 0.07 秒)。
|
|
4
|
-
* 一位老师两个位置:resume(接某个话题的会话)与 fresh(新开会话:新话题、接着上次)。
|
|
5
|
-
* 对得上 = argv、适配器定的环境变量、cwd、日期、**这个话题**末条 job 都和它起来时一样(fresh 没有会话,末条记 null):
|
|
6
|
-
* argv 管换话题 / 会话 / 老师正文 / effort / cotutor.json,末条 job 管「会话被别的进程写过」(它起来时读的会话就旧了,拿它写会分叉)。
|
|
7
|
-
* 对不上、闲置超时、自己退了,都杀掉,调用方走冷起。开口前吐的字节先攒着,接过来时交出去。
|
|
8
|
-
*/
|
|
9
|
-
import { type ChildProcess } from 'node:child_process';
|
|
10
|
-
/** 备用进程闲置多久杀掉(闲着不花钱,只占内存,真跑一个约 64MB) */
|
|
11
|
-
export declare const WARM_IDLE_MS: number;
|
|
12
|
-
/** resume:接某个话题的会话;fresh:新开会话 */
|
|
13
|
-
export type Slot = 'resume' | 'fresh';
|
|
14
|
-
export declare const SLOTS: readonly Slot[];
|
|
15
|
-
/** 对得上才用:argv、适配器定的环境变量、cwd、日期、这个话题末条 job(fresh 为 null) */
|
|
16
|
-
export interface SpareKey {
|
|
17
|
-
argv: readonly string[];
|
|
18
|
-
/** 计划里适配器定的环境变量(qwen 按 effort 选的配置之类);不给当空 */
|
|
19
|
-
env?: Readonly<Record<string, string>>;
|
|
20
|
-
cwd: string;
|
|
21
|
-
date: string;
|
|
22
|
-
lastJob: string | null;
|
|
23
|
-
}
|
|
24
|
-
export interface Spare {
|
|
25
|
-
child: ChildProcess;
|
|
26
|
-
/** 起来的时刻(Date.now()) */
|
|
27
|
-
bornAt: number;
|
|
28
|
-
/** 接过来之前 stdout / stderr 吐的 */
|
|
29
|
-
stdout: Buffer[];
|
|
30
|
-
stderr: Buffer[];
|
|
31
|
-
/** 进程已经 close(或起不来)了:接手之前就发生的 close 接手的等不到,看这个 */
|
|
32
|
-
closed: () => boolean;
|
|
33
|
-
}
|
|
34
|
-
export declare class WarmPool {
|
|
35
|
-
private readonly held;
|
|
36
|
-
private readonly idleMs;
|
|
37
|
-
constructor(opts?: {
|
|
38
|
-
idleMs?: number;
|
|
39
|
-
});
|
|
40
|
-
/** 这位老师这个位置(不给 = 任一位置)有没有活着的备用进程 */
|
|
41
|
-
has(tutor: string, slot?: Slot): boolean;
|
|
42
|
-
/** 在这个位置起一个备用进程;已有一个对得上的就不动,对不上的先杀 */
|
|
43
|
-
warm(tutor: string, slot: Slot, key: SpareKey, env: NodeJS.ProcessEnv): void;
|
|
44
|
-
/**
|
|
45
|
-
* 接过这个位置的备用进程:对得上就交出去(不再归池子管);对不上 / 已经退了就杀掉,返回 null。没有也返回 null。另一个位置不动。
|
|
46
|
-
* 交出去时 stdout / stderr 摘掉池子的监听并**暂停**:接手的挂好监听、先喂攒下的,再 resume(中间一个字节不丢)。
|
|
47
|
-
* 池子挂的 error / close 监听留着(接手之前进程出错不至于成了没人接的 error 事件);进程可能在接手之前就退了,接手的看 closed()。
|
|
48
|
-
*/
|
|
49
|
-
claim(tutor: string, slot: Slot, key: SpareKey): Spare | null;
|
|
50
|
-
/** 杀掉这位老师这个位置(不给 = 两个位置)的备用进程 */
|
|
51
|
-
drop(tutor: string, slot?: Slot): void;
|
|
52
|
-
dropAll(): void;
|
|
53
|
-
}
|
package/dist/brain/cli/warm.js
DELETED
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 预热:消息走 stdin 的运行时,下一轮的老师进程提前起好、在 stdin 上等着;
|
|
3
|
-
* 孩子开口时对得上就接过来写消息,省掉起进程与读会话(真跑冷起中位 1.8 秒,预热约 0.07 秒)。
|
|
4
|
-
* 一位老师两个位置:resume(接某个话题的会话)与 fresh(新开会话:新话题、接着上次)。
|
|
5
|
-
* 对得上 = argv、适配器定的环境变量、cwd、日期、**这个话题**末条 job 都和它起来时一样(fresh 没有会话,末条记 null):
|
|
6
|
-
* argv 管换话题 / 会话 / 老师正文 / effort / cotutor.json,末条 job 管「会话被别的进程写过」(它起来时读的会话就旧了,拿它写会分叉)。
|
|
7
|
-
* 对不上、闲置超时、自己退了,都杀掉,调用方走冷起。开口前吐的字节先攒着,接过来时交出去。
|
|
8
|
-
*/
|
|
9
|
-
import { spawn } from 'node:child_process';
|
|
10
|
-
/** 备用进程闲置多久杀掉(闲着不花钱,只占内存,真跑一个约 64MB) */
|
|
11
|
-
export const WARM_IDLE_MS = 60 * 60_000;
|
|
12
|
-
export const SLOTS = ['resume', 'fresh'];
|
|
13
|
-
const keyOf = (k) => JSON.stringify([k.argv, Object.entries(k.env ?? {}).sort(), k.cwd, k.date, k.lastJob]);
|
|
14
|
-
const at = (tutor, slot) => `${tutor}\n${slot}`;
|
|
15
|
-
export class WarmPool {
|
|
16
|
-
held = new Map();
|
|
17
|
-
idleMs;
|
|
18
|
-
constructor(opts = {}) {
|
|
19
|
-
this.idleMs = opts.idleMs ?? WARM_IDLE_MS;
|
|
20
|
-
}
|
|
21
|
-
/** 这位老师这个位置(不给 = 任一位置)有没有活着的备用进程 */
|
|
22
|
-
has(tutor, slot) {
|
|
23
|
-
return (slot ? [slot] : SLOTS).some((s) => {
|
|
24
|
-
const h = this.held.get(at(tutor, s));
|
|
25
|
-
return Boolean(h && !h.exited);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
/** 在这个位置起一个备用进程;已有一个对得上的就不动,对不上的先杀 */
|
|
29
|
-
warm(tutor, slot, key, env) {
|
|
30
|
-
const id = at(tutor, slot);
|
|
31
|
-
const k = keyOf(key);
|
|
32
|
-
const cur = this.held.get(id);
|
|
33
|
-
if (cur && !cur.exited && cur.key === k)
|
|
34
|
-
return;
|
|
35
|
-
this.drop(tutor, slot);
|
|
36
|
-
const child = spawn(key.argv[0], key.argv.slice(1), { cwd: key.cwd, env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
37
|
-
const h = { child, bornAt: Date.now(), stdout: [], stderr: [], key: k, exited: false, idle: setTimeout(() => this.drop(tutor, slot), this.idleMs) };
|
|
38
|
-
h.idle.unref();
|
|
39
|
-
child.stdout?.on('data', (c) => h.stdout.push(c));
|
|
40
|
-
child.stderr?.on('data', (c) => h.stderr.push(c));
|
|
41
|
-
// 写 stdin 时进程已经退了(EPIPE)不该把服务带倒
|
|
42
|
-
child.stdin?.on('error', () => { });
|
|
43
|
-
const gone = () => {
|
|
44
|
-
h.exited = true;
|
|
45
|
-
clearTimeout(h.idle);
|
|
46
|
-
if (this.held.get(id) === h)
|
|
47
|
-
this.held.delete(id);
|
|
48
|
-
};
|
|
49
|
-
child.once('error', gone);
|
|
50
|
-
child.once('close', gone);
|
|
51
|
-
this.held.set(id, h);
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* 接过这个位置的备用进程:对得上就交出去(不再归池子管);对不上 / 已经退了就杀掉,返回 null。没有也返回 null。另一个位置不动。
|
|
55
|
-
* 交出去时 stdout / stderr 摘掉池子的监听并**暂停**:接手的挂好监听、先喂攒下的,再 resume(中间一个字节不丢)。
|
|
56
|
-
* 池子挂的 error / close 监听留着(接手之前进程出错不至于成了没人接的 error 事件);进程可能在接手之前就退了,接手的看 closed()。
|
|
57
|
-
*/
|
|
58
|
-
claim(tutor, slot, key) {
|
|
59
|
-
const id = at(tutor, slot);
|
|
60
|
-
const h = this.held.get(id);
|
|
61
|
-
if (!h)
|
|
62
|
-
return null;
|
|
63
|
-
this.held.delete(id);
|
|
64
|
-
clearTimeout(h.idle);
|
|
65
|
-
if (h.exited || h.key !== keyOf(key)) {
|
|
66
|
-
kill(h.child);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
for (const s of [h.child.stdout, h.child.stderr]) {
|
|
70
|
-
s?.removeAllListeners('data');
|
|
71
|
-
s?.pause();
|
|
72
|
-
}
|
|
73
|
-
return { child: h.child, bornAt: h.bornAt, stdout: h.stdout, stderr: h.stderr, closed: () => h.exited };
|
|
74
|
-
}
|
|
75
|
-
/** 杀掉这位老师这个位置(不给 = 两个位置)的备用进程 */
|
|
76
|
-
drop(tutor, slot) {
|
|
77
|
-
for (const s of slot ? [slot] : SLOTS) {
|
|
78
|
-
const id = at(tutor, s);
|
|
79
|
-
const h = this.held.get(id);
|
|
80
|
-
if (!h)
|
|
81
|
-
continue;
|
|
82
|
-
this.held.delete(id);
|
|
83
|
-
clearTimeout(h.idle);
|
|
84
|
-
kill(h.child);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
dropAll() {
|
|
88
|
-
for (const h of this.held.values()) {
|
|
89
|
-
clearTimeout(h.idle);
|
|
90
|
-
kill(h.child);
|
|
91
|
-
}
|
|
92
|
-
this.held.clear();
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
/** 关 stdin 让它自己退;2 秒还在就 SIGKILL */
|
|
96
|
-
function kill(child) {
|
|
97
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
98
|
-
return;
|
|
99
|
-
child.stdin?.end();
|
|
100
|
-
child.kill('SIGTERM');
|
|
101
|
-
setTimeout(() => { if (child.exitCode === null && child.signalCode === null)
|
|
102
|
-
child.kill('SIGKILL'); }, 2000).unref();
|
|
103
|
-
}
|