@winmatrix/supervisor 1.0.5
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/package.json +24 -0
- package/src/blocked-args-filter.mjs +95 -0
- package/src/claude-session-lib.mjs +487 -0
- package/src/deliverable-tracker.mjs +119 -0
- package/src/engine-driver-config.mjs +131 -0
- package/src/env-sanitizer.mjs +112 -0
- package/src/resume-degradation.mjs +117 -0
- package/src/run-agent-engine.mjs +1580 -0
- package/src/run-engine.mjs +192 -0
- package/src/token-usage.mjs +147 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Engine executor — supervise-run worker 的统一子进程入口。
|
|
4
|
+
*
|
|
5
|
+
* 用法: node /opt/winmatrix/run-engine.mjs \
|
|
6
|
+
* --engine <claude-code|codex|hermes|openclaw> \
|
|
7
|
+
* [--model <model>] [--permission-mode <mode>] \
|
|
8
|
+
* [--allowed-tools <tool1,tool2>] [--resume <sessionId>|--fork <sessionId>] \
|
|
9
|
+
* [--timeout <seconds>] [--work-dir <dir>] \
|
|
10
|
+
* <prompt>
|
|
11
|
+
*
|
|
12
|
+
* 通过 @winmatrix/agent-sdk 的对应 Adapter 执行,将 AdapterEvent 序列化为 JSONL
|
|
13
|
+
* 输出到 stdout,供 run-agent-engine.mjs 解析并写入 events.jsonl。
|
|
14
|
+
*/
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
const SDK_PATH = process.env.WINMATRIX_AGENT_SDK_PATH
|
|
20
|
+
|| '/home/node/.local/lib/node_modules/@winmatrix/agent-sdk';
|
|
21
|
+
|
|
22
|
+
function parseArgs(argv) {
|
|
23
|
+
const options = {
|
|
24
|
+
engineId: undefined,
|
|
25
|
+
model: undefined,
|
|
26
|
+
permissionMode: undefined,
|
|
27
|
+
allowedTools: undefined,
|
|
28
|
+
resume: undefined,
|
|
29
|
+
fork: undefined,
|
|
30
|
+
timeoutSec: undefined,
|
|
31
|
+
workDir: undefined,
|
|
32
|
+
};
|
|
33
|
+
const positionals = [];
|
|
34
|
+
|
|
35
|
+
for (let i = 0; i < argv.length; i++) {
|
|
36
|
+
const arg = argv[i];
|
|
37
|
+
if (arg === '--engine') {
|
|
38
|
+
options.engineId = argv[++i];
|
|
39
|
+
} else if (arg === '--model') {
|
|
40
|
+
options.model = argv[++i];
|
|
41
|
+
} else if (arg === '--permission-mode') {
|
|
42
|
+
options.permissionMode = argv[++i];
|
|
43
|
+
} else if (arg === '--allowed-tools') {
|
|
44
|
+
options.allowedTools = argv[++i];
|
|
45
|
+
} else if (arg === '--resume') {
|
|
46
|
+
options.resume = argv[++i];
|
|
47
|
+
} else if (arg === '--fork') {
|
|
48
|
+
options.fork = argv[++i];
|
|
49
|
+
} else if (arg === '--timeout') {
|
|
50
|
+
options.timeoutSec = argv[++i];
|
|
51
|
+
} else if (arg === '--work-dir') {
|
|
52
|
+
options.workDir = argv[++i];
|
|
53
|
+
} else if (arg.startsWith('--')) {
|
|
54
|
+
// 忽略未来可能透传的 engine-specific 选项;后续可按 engineId 路由
|
|
55
|
+
console.error(`[run-engine] 忽略未知选项: ${arg}`);
|
|
56
|
+
} else {
|
|
57
|
+
positionals.push(arg);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return { options, positionals };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function writeEvent(event) {
|
|
65
|
+
process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function main() {
|
|
69
|
+
const { options, positionals } = parseArgs(process.argv.slice(2));
|
|
70
|
+
const prompt = positionals[0];
|
|
71
|
+
|
|
72
|
+
if (!options.engineId) {
|
|
73
|
+
console.error('[run-engine] 缺少 --engine');
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
if (!prompt) {
|
|
77
|
+
console.error('[run-engine] 缺少任务描述(第一个位置参数)');
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let sdk;
|
|
82
|
+
try {
|
|
83
|
+
sdk = require(SDK_PATH);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.error(`[run-engine] 无法加载 @winmatrix/agent-sdk: ${e.message}`);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const {
|
|
90
|
+
AdapterRegistry,
|
|
91
|
+
ClaudeAdapter,
|
|
92
|
+
CodexAdapter,
|
|
93
|
+
HermesAdapter,
|
|
94
|
+
OpenClawAdapter,
|
|
95
|
+
} = sdk;
|
|
96
|
+
|
|
97
|
+
const registry = new AdapterRegistry();
|
|
98
|
+
registry.register('claude-code', ClaudeAdapter);
|
|
99
|
+
registry.register('codex', CodexAdapter);
|
|
100
|
+
registry.register('hermes', HermesAdapter);
|
|
101
|
+
registry.register('openclaw', OpenClawAdapter);
|
|
102
|
+
|
|
103
|
+
if (!registry.has(options.engineId)) {
|
|
104
|
+
console.error(`[run-engine] 不支持的 engineId: ${options.engineId}`);
|
|
105
|
+
process.exit(1);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const adapterConfig = {};
|
|
109
|
+
const adapter = registry.create(options.engineId, adapterConfig);
|
|
110
|
+
|
|
111
|
+
/** @type {import('@winmatrix/agent-sdk').AgentTaskContext} */
|
|
112
|
+
const task = {
|
|
113
|
+
taskId: `run-engine-${process.pid}`,
|
|
114
|
+
input: prompt,
|
|
115
|
+
prompt,
|
|
116
|
+
engineId: options.engineId,
|
|
117
|
+
workDir: options.workDir,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (options.engineId === 'claude-code') {
|
|
121
|
+
task.claude = {};
|
|
122
|
+
if (options.permissionMode) task.claude.permissionMode = options.permissionMode;
|
|
123
|
+
if (options.allowedTools) task.claude.tools = options.allowedTools.split(',').map((s) => s.trim());
|
|
124
|
+
} else if (options.engineId === 'hermes') {
|
|
125
|
+
task.hermes = {};
|
|
126
|
+
if (options.model) task.hermes.model = options.model;
|
|
127
|
+
} else if (options.engineId === 'codex') {
|
|
128
|
+
task.codex = {};
|
|
129
|
+
if (options.model) task.codex.model = options.model;
|
|
130
|
+
} else if (options.engineId === 'openclaw') {
|
|
131
|
+
task.openclaw = {};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (options.resume) {
|
|
135
|
+
task.sessionId = options.resume;
|
|
136
|
+
task.resumeSession = true;
|
|
137
|
+
} else if (options.fork) {
|
|
138
|
+
task.sessionId = options.fork;
|
|
139
|
+
task.forkSession = true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let abortController;
|
|
143
|
+
let timeoutHandle;
|
|
144
|
+
if (options.timeoutSec) {
|
|
145
|
+
const timeoutMs = Number(options.timeoutSec) * 1000;
|
|
146
|
+
if (!Number.isNaN(timeoutMs) && timeoutMs > 0) {
|
|
147
|
+
abortController = new AbortController();
|
|
148
|
+
timeoutHandle = setTimeout(() => abortController.abort(), timeoutMs);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
for await (const event of adapter.execute(task, abortController?.signal)) {
|
|
154
|
+
switch (event.type) {
|
|
155
|
+
case 'delta':
|
|
156
|
+
writeEvent({ type: 'content_delta', text: event.content });
|
|
157
|
+
break;
|
|
158
|
+
case 'thinking':
|
|
159
|
+
writeEvent({ type: 'thinking_delta', text: event.content });
|
|
160
|
+
break;
|
|
161
|
+
case 'tool_call':
|
|
162
|
+
writeEvent({ type: 'tool_call', name: event.name, input: event.args });
|
|
163
|
+
break;
|
|
164
|
+
case 'result':
|
|
165
|
+
writeEvent({
|
|
166
|
+
type: 'result',
|
|
167
|
+
result: {
|
|
168
|
+
success: true,
|
|
169
|
+
text: event.content,
|
|
170
|
+
...(event.sessionId ? { sessionId: event.sessionId } : {}),
|
|
171
|
+
...(event.usage ? { usage: event.usage } : {}),
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
break;
|
|
175
|
+
case 'error':
|
|
176
|
+
writeEvent({ type: 'error', error: event.message, code: event.code });
|
|
177
|
+
break;
|
|
178
|
+
default:
|
|
179
|
+
// 其他事件忽略,不破坏 JSONL 解析
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
process.exit(0);
|
|
184
|
+
} catch (e) {
|
|
185
|
+
writeEvent({ type: 'error', error: e instanceof Error ? e.message : String(e) });
|
|
186
|
+
process.exit(1);
|
|
187
|
+
} finally {
|
|
188
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
main();
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D11.4: Token Usage 多路获取
|
|
3
|
+
*
|
|
4
|
+
* 不同 Engine 上报 token 使用量的方式不同,
|
|
5
|
+
* 按优先级尝试多条路径:
|
|
6
|
+
* Layer 1: 事件流中的 usage 事件(primary_event)
|
|
7
|
+
* Layer 2: 扫描 Engine session log 文件(session_log_scan)
|
|
8
|
+
* Fallback: usage=null,标记 unknown
|
|
9
|
+
*
|
|
10
|
+
* 参考 Multica 对 Codex 的三层 fallback 设计。
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 从事件数组中提取 token usage(Layer 1: primary_event)
|
|
18
|
+
*
|
|
19
|
+
* 优先使用最后一个 usage 事件(因为 Engine 可能在 Run 结束时才上报最终 usage)
|
|
20
|
+
*
|
|
21
|
+
* @param {Object[]} events - EngineProgressEvent envelope 数组
|
|
22
|
+
* @returns {Object|null}
|
|
23
|
+
*/
|
|
24
|
+
export function extractUsageFromEvents(events) {
|
|
25
|
+
if (!events || events.length === 0) return null;
|
|
26
|
+
|
|
27
|
+
// 从后往前找最后一个 usage 事件
|
|
28
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
29
|
+
const envelope = events[i];
|
|
30
|
+
const event = envelope.event || envelope;
|
|
31
|
+
|
|
32
|
+
if (event.type === 'usage' && event.usage) {
|
|
33
|
+
return {
|
|
34
|
+
...event.usage,
|
|
35
|
+
source: 'primary_event',
|
|
36
|
+
raw: event.usage,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Claude result 事件中也包含 usage
|
|
41
|
+
if (event.type === 'result' && event.result?.usage) {
|
|
42
|
+
return {
|
|
43
|
+
...event.result.usage,
|
|
44
|
+
source: 'primary_event',
|
|
45
|
+
raw: event.result.usage,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Layer 2: 从 Codex session log 中扫描 token 计数
|
|
55
|
+
*
|
|
56
|
+
* Codex 在 $CODEX_HOME/sessions/ 下写 JSONL rollout 文件,
|
|
57
|
+
* 其中包含 token_count 事件。
|
|
58
|
+
*
|
|
59
|
+
* @param {string} [codexHome] - CODEX_HOME 路径,默认 ~/.codex
|
|
60
|
+
* @param {string} [sessionId] - 指定 session 过滤,可选
|
|
61
|
+
* @returns {Object|null}
|
|
62
|
+
*/
|
|
63
|
+
export function extractUsageFromCodexSessionLog(codexHome, sessionId) {
|
|
64
|
+
const home = codexHome || process.env.CODEX_HOME || join(process.env.HOME || '', '.codex');
|
|
65
|
+
const sessionsDir = join(home, 'sessions');
|
|
66
|
+
|
|
67
|
+
if (!existsSync(sessionsDir)) return null;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const files = readdirSync(sessionsDir)
|
|
71
|
+
.filter(f => f.endsWith('.jsonl'))
|
|
72
|
+
.sort() // 按文件名排序,取最新的
|
|
73
|
+
.reverse();
|
|
74
|
+
|
|
75
|
+
for (const file of files.slice(0, 5)) { // 只扫描最近 5 个文件
|
|
76
|
+
const filePath = join(sessionsDir, file);
|
|
77
|
+
try {
|
|
78
|
+
const content = readFileSync(filePath, 'utf8');
|
|
79
|
+
const lines = content.split('\n').filter(Boolean);
|
|
80
|
+
|
|
81
|
+
// 如果指定了 sessionId,检查文件是否匹配
|
|
82
|
+
if (sessionId && !file.includes(sessionId) && !content.includes(sessionId)) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 从后往前找 token_count 事件
|
|
87
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
88
|
+
try {
|
|
89
|
+
const entry = JSON.parse(lines[i]);
|
|
90
|
+
if (entry.type === 'token_count' || entry.event_type === 'token_count') {
|
|
91
|
+
return {
|
|
92
|
+
inputTokens: entry.input_tokens || entry.inputTokens || 0,
|
|
93
|
+
outputTokens: entry.output_tokens || entry.outputTokens || 0,
|
|
94
|
+
totalTokens: (entry.input_tokens || entry.inputTokens || 0) +
|
|
95
|
+
(entry.output_tokens || entry.outputTokens || 0),
|
|
96
|
+
source: 'session_log_scan',
|
|
97
|
+
raw: entry,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// 跳过解析失败的行
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// 跳过无法读取的文件
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// sessions 目录不可读
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 多路获取 token usage 的统一入口
|
|
117
|
+
*
|
|
118
|
+
* 按优先级尝试:
|
|
119
|
+
* 1. primary_event: 从事件流中提取
|
|
120
|
+
* 2. session_log_scan: 从 Engine session log 文件扫描
|
|
121
|
+
* 3. unknown: 返回 null
|
|
122
|
+
*
|
|
123
|
+
* @param {Object[]} events - EngineProgressEvent envelope 数组
|
|
124
|
+
* @param {Object} options
|
|
125
|
+
* @param {string} options.engineId
|
|
126
|
+
* @param {string} [options.sessionId]
|
|
127
|
+
* @param {string} [options.codexHome]
|
|
128
|
+
* @returns {Object|null}
|
|
129
|
+
*/
|
|
130
|
+
export function extractTokenUsage(events, options = {}) {
|
|
131
|
+
const { engineId, sessionId, codexHome } = options;
|
|
132
|
+
|
|
133
|
+
// Layer 1: 从事件流提取
|
|
134
|
+
const fromEvents = extractUsageFromEvents(events);
|
|
135
|
+
if (fromEvents) return fromEvents;
|
|
136
|
+
|
|
137
|
+
// Layer 2: Engine 特定的 session log 扫描
|
|
138
|
+
if (engineId === 'codex') {
|
|
139
|
+
const fromLog = extractUsageFromCodexSessionLog(codexHome, sessionId);
|
|
140
|
+
if (fromLog) return fromLog;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 其他 Engine 的 Layer 2 可以在此扩展
|
|
144
|
+
|
|
145
|
+
// Fallback: unknown
|
|
146
|
+
return null;
|
|
147
|
+
}
|