@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.
@@ -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
+ }