@reconcrap/boss-recommend-mcp 1.3.38 → 2.0.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/README.md +53 -33
- package/package.json +61 -9
- package/skills/boss-recommend-pipeline/SKILL.md +4 -0
- package/src/chat-mcp.js +1333 -0
- package/src/chat-runtime-config.js +559 -0
- package/src/cli.js +1095 -196
- package/src/core/browser/index.js +378 -0
- package/src/core/capture/index.js +298 -0
- package/src/core/cv-acquisition/index.js +219 -0
- package/src/core/greet-quota/index.js +54 -0
- package/src/core/infinite-list/index.js +459 -0
- package/src/core/reporting/legacy-csv.js +332 -0
- package/src/core/run/index.js +286 -0
- package/src/core/screening/index.js +1166 -0
- package/src/core/self-heal/index.js +848 -0
- package/src/domains/chat/cards.js +129 -0
- package/src/domains/chat/constants.js +183 -0
- package/src/domains/chat/detail.js +1369 -0
- package/src/domains/chat/index.js +7 -0
- package/src/domains/chat/jobs.js +334 -0
- package/src/domains/chat/page-guard.js +88 -0
- package/src/domains/chat/roots.js +56 -0
- package/src/domains/chat/run-service.js +1101 -0
- package/src/domains/recommend/actions.js +457 -0
- package/src/domains/recommend/cards.js +228 -0
- package/src/domains/recommend/constants.js +141 -0
- package/src/domains/recommend/detail.js +341 -0
- package/src/domains/recommend/filters.js +581 -0
- package/src/domains/recommend/index.js +10 -0
- package/src/domains/recommend/jobs.js +232 -0
- package/src/domains/recommend/refresh.js +204 -0
- package/src/domains/recommend/roots.js +78 -0
- package/src/domains/recommend/run-service.js +903 -0
- package/src/domains/recommend/scopes.js +245 -0
- package/src/domains/recruit/actions.js +277 -0
- package/src/domains/recruit/cards.js +67 -0
- package/src/domains/recruit/constants.js +130 -0
- package/src/domains/recruit/detail.js +414 -0
- package/src/domains/recruit/index.js +9 -0
- package/src/domains/recruit/instruction-parser.js +451 -0
- package/src/domains/recruit/refresh.js +40 -0
- package/src/domains/recruit/roots.js +68 -0
- package/src/domains/recruit/run-service.js +580 -0
- package/src/domains/recruit/search.js +1149 -0
- package/src/index.js +578 -419
- package/src/recommend-mcp.js +1257 -0
- package/src/recruit-mcp.js +1035 -0
- package/src/adapters.js +0 -3079
- package/src/boss-chat.js +0 -1037
- package/src/pipeline.js +0 -2249
- package/src/recommend-healing-config.js +0 -131
- package/src/recommend-healing-rules.json +0 -261
- package/src/self-heal.js +0 -2237
- package/src/test-adapters-runtime.js +0 -628
- package/src/test-boss-chat.js +0 -3196
- package/src/test-index-async.js +0 -498
- package/src/test-parser.js +0 -742
- package/src/test-pipeline.js +0 -2703
- package/src/test-run-state.js +0 -152
- package/src/test-self-heal.js +0 -224
- package/vendor/boss-chat-cli/README.md +0 -134
- package/vendor/boss-chat-cli/package.json +0 -53
- package/vendor/boss-chat-cli/src/app.js +0 -1501
- package/vendor/boss-chat-cli/src/browser/chat-page.js +0 -3562
- package/vendor/boss-chat-cli/src/cli.js +0 -1713
- package/vendor/boss-chat-cli/src/mcp/server.js +0 -149
- package/vendor/boss-chat-cli/src/mcp/tool-runtime.js +0 -193
- package/vendor/boss-chat-cli/src/runtime/async-run-state.js +0 -260
- package/vendor/boss-chat-cli/src/runtime/interaction.js +0 -102
- package/vendor/boss-chat-cli/src/runtime/run-control.js +0 -102
- package/vendor/boss-chat-cli/src/services/chrome-client.js +0 -107
- package/vendor/boss-chat-cli/src/services/llm.js +0 -1292
- package/vendor/boss-chat-cli/src/services/llm.test.js +0 -326
- package/vendor/boss-chat-cli/src/services/profile-store.js +0 -173
- package/vendor/boss-chat-cli/src/services/report-store.js +0 -317
- package/vendor/boss-chat-cli/src/services/resume-capture.js +0 -469
- package/vendor/boss-chat-cli/src/services/resume-network.js +0 -727
- package/vendor/boss-chat-cli/src/services/state-store.js +0 -90
- package/vendor/boss-chat-cli/src/utils/customer-key.js +0 -82
- package/vendor/boss-recommend-screen-cli/boss-recommend-screen-cli.cjs +0 -6927
- package/vendor/boss-recommend-screen-cli/scripts/capture-full-resume-canvas.cjs +0 -817
- package/vendor/boss-recommend-screen-cli/scripts/stitch_resume_chunks.py +0 -141
- package/vendor/boss-recommend-screen-cli/test-recoverable-resume-failures.cjs +0 -2294
- package/vendor/boss-recommend-search-cli/src/cli.js +0 -1698
- package/vendor/boss-recommend-search-cli/src/test-job-selection.js +0 -211
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const LEGACY_INPUT_HEADER = ["运行输入字段", "运行输入值"];
|
|
5
|
+
|
|
6
|
+
export const LEGACY_RESULT_HEADER = [
|
|
7
|
+
"姓名",
|
|
8
|
+
"最高学历学校",
|
|
9
|
+
"最高学历专业",
|
|
10
|
+
"最近工作公司",
|
|
11
|
+
"最近工作职位",
|
|
12
|
+
"评估通过详细原因",
|
|
13
|
+
"处理结果",
|
|
14
|
+
"判断依据(CoT)",
|
|
15
|
+
"动作执行结果",
|
|
16
|
+
"简历来源",
|
|
17
|
+
"原始判定通过",
|
|
18
|
+
"最终判定通过",
|
|
19
|
+
"证据总数",
|
|
20
|
+
"证据命中数",
|
|
21
|
+
"证据门控降级",
|
|
22
|
+
"错误码",
|
|
23
|
+
"错误信息",
|
|
24
|
+
"候选人ID",
|
|
25
|
+
"总耗时ms",
|
|
26
|
+
"候选卡片读取ms",
|
|
27
|
+
"点击候选人ms",
|
|
28
|
+
"详情打开ms",
|
|
29
|
+
"network简历等待ms",
|
|
30
|
+
"文本模型ms",
|
|
31
|
+
"截图获取ms",
|
|
32
|
+
"视觉模型ms",
|
|
33
|
+
"late network retry ms",
|
|
34
|
+
"DOM fallback ms",
|
|
35
|
+
"通过后动作ms",
|
|
36
|
+
"关闭详情ms",
|
|
37
|
+
"休息ms",
|
|
38
|
+
"checkpoint保存ms"
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
const SEARCH_PARAM_ORDER = [
|
|
42
|
+
"school_tag",
|
|
43
|
+
"degree",
|
|
44
|
+
"degrees",
|
|
45
|
+
"gender",
|
|
46
|
+
"recent_not_view",
|
|
47
|
+
"city",
|
|
48
|
+
"schools",
|
|
49
|
+
"keyword",
|
|
50
|
+
"filter_recent_viewed",
|
|
51
|
+
"job",
|
|
52
|
+
"start_from",
|
|
53
|
+
"target_count",
|
|
54
|
+
"detail_source"
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
const SCREEN_PARAM_ORDER = [
|
|
58
|
+
"criteria",
|
|
59
|
+
"target_count",
|
|
60
|
+
"post_action",
|
|
61
|
+
"max_greet_count"
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
function normalizeText(value) {
|
|
65
|
+
return String(value || "").replace(/\s+/g, " ").trim();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeBlockText(value) {
|
|
69
|
+
return String(value ?? "").trim();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function csvCell(value) {
|
|
73
|
+
const text = String(value ?? "");
|
|
74
|
+
return `"${text.replace(/"/g, '""')}"`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function ensureDirectory(dirPath) {
|
|
78
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function cloneJson(value, fallback = null) {
|
|
82
|
+
try {
|
|
83
|
+
return value === undefined ? fallback : JSON.parse(JSON.stringify(value));
|
|
84
|
+
} catch {
|
|
85
|
+
return fallback;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatInputValue(value) {
|
|
90
|
+
if (value === undefined) return "";
|
|
91
|
+
if (value === null) return "null";
|
|
92
|
+
if (typeof value === "string") return value;
|
|
93
|
+
return JSON.stringify(value);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function appendInputRow(rows, field, value) {
|
|
97
|
+
if (!field || value === undefined) return;
|
|
98
|
+
rows.push({
|
|
99
|
+
field,
|
|
100
|
+
value: formatInputValue(value)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function appendPrefixedRows(rows, prefix, values = {}, order = []) {
|
|
105
|
+
const source = values && typeof values === "object" && !Array.isArray(values) ? values : {};
|
|
106
|
+
const emitted = new Set();
|
|
107
|
+
for (const key of order) {
|
|
108
|
+
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
109
|
+
appendInputRow(rows, `${prefix}.${key}`, source[key]);
|
|
110
|
+
emitted.add(key);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (const key of Object.keys(source).sort()) {
|
|
114
|
+
if (emitted.has(key)) continue;
|
|
115
|
+
appendInputRow(rows, `${prefix}.${key}`, source[key]);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function buildLegacyScreenInputRows({
|
|
120
|
+
instruction = "",
|
|
121
|
+
selectedPage = "",
|
|
122
|
+
selectedJob = null,
|
|
123
|
+
userSearchParams = {},
|
|
124
|
+
effectiveSearchParams = {},
|
|
125
|
+
screenParams = {},
|
|
126
|
+
followUp = null,
|
|
127
|
+
extraRows = []
|
|
128
|
+
} = {}) {
|
|
129
|
+
const rows = [];
|
|
130
|
+
appendInputRow(rows, "instruction", instruction);
|
|
131
|
+
appendInputRow(rows, "selected_page", selectedPage);
|
|
132
|
+
|
|
133
|
+
if (selectedJob && typeof selectedJob === "object") {
|
|
134
|
+
appendInputRow(rows, "selected_job.value", selectedJob.value);
|
|
135
|
+
appendInputRow(rows, "selected_job.title", selectedJob.title);
|
|
136
|
+
appendInputRow(rows, "selected_job.label", selectedJob.label);
|
|
137
|
+
} else if (selectedJob) {
|
|
138
|
+
appendInputRow(rows, "selected_job.label", selectedJob);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
appendPrefixedRows(rows, "user_search_params", userSearchParams, SEARCH_PARAM_ORDER);
|
|
142
|
+
appendPrefixedRows(rows, "effective_search_params", effectiveSearchParams, SEARCH_PARAM_ORDER);
|
|
143
|
+
appendPrefixedRows(rows, "screen_params", screenParams, SCREEN_PARAM_ORDER);
|
|
144
|
+
appendInputRow(rows, "follow_up", followUp);
|
|
145
|
+
|
|
146
|
+
for (const row of extraRows || []) {
|
|
147
|
+
if (Array.isArray(row)) appendInputRow(rows, row[0], row[1]);
|
|
148
|
+
else appendInputRow(rows, row?.field, row?.value);
|
|
149
|
+
}
|
|
150
|
+
return rows;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function defaultLegacyCsvPathForReport(reportPath) {
|
|
154
|
+
const resolved = path.resolve(reportPath);
|
|
155
|
+
const parsed = path.parse(resolved);
|
|
156
|
+
return path.join(parsed.dir, `${parsed.name}.csv`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function firstDefined(...values) {
|
|
160
|
+
for (const value of values) {
|
|
161
|
+
if (value !== undefined && value !== null) return value;
|
|
162
|
+
}
|
|
163
|
+
return "";
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function firstText(...values) {
|
|
167
|
+
for (const value of values) {
|
|
168
|
+
const text = normalizeBlockText(value);
|
|
169
|
+
if (text) return text;
|
|
170
|
+
}
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function firstBoolean(...values) {
|
|
175
|
+
for (const value of values) {
|
|
176
|
+
if (typeof value === "boolean") return value;
|
|
177
|
+
if (typeof value === "number") return value !== 0;
|
|
178
|
+
const text = normalizeText(value).toLowerCase();
|
|
179
|
+
if (["true", "pass", "passed", "yes", "是", "通过", "符合"].includes(text)) return true;
|
|
180
|
+
if (["false", "fail", "failed", "no", "否", "不通过", "不符合"].includes(text)) return false;
|
|
181
|
+
}
|
|
182
|
+
return "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function evidenceCount(llm = {}) {
|
|
186
|
+
if (Number.isFinite(llm.evidence_count)) return llm.evidence_count;
|
|
187
|
+
if (Array.isArray(llm.evidence)) return llm.evidence.length;
|
|
188
|
+
return "";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function actionResultText(row = {}) {
|
|
192
|
+
const action = row.post_action || row.action || {};
|
|
193
|
+
if (action.requested === true && !action.skipped) {
|
|
194
|
+
return firstText(action.reason, action.kind, action.type, "requested");
|
|
195
|
+
}
|
|
196
|
+
if (action.skipped) {
|
|
197
|
+
return firstText(action.reason, action.kind, action.type, "skipped");
|
|
198
|
+
}
|
|
199
|
+
if (action.action_clicked || action.clicked) {
|
|
200
|
+
return firstText(action.effective, action.requested, action.kind, action.type, "clicked");
|
|
201
|
+
}
|
|
202
|
+
if (action.action_attempted || action.attempted) return "failed";
|
|
203
|
+
if (action.requested && action.requested !== "none") return "not_attempted";
|
|
204
|
+
return "";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function pickLlm(row = {}) {
|
|
208
|
+
return row.llm
|
|
209
|
+
|| row.llm_screening
|
|
210
|
+
|| row.detail?.llm_screening
|
|
211
|
+
|| row.screening?.llm
|
|
212
|
+
|| {};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function pickCandidate(row = {}) {
|
|
216
|
+
const screeningCandidate = row.screening?.candidate || {};
|
|
217
|
+
const candidate = row.candidate || row.card_candidate || {};
|
|
218
|
+
return {
|
|
219
|
+
...screeningCandidate,
|
|
220
|
+
...candidate,
|
|
221
|
+
identity: {
|
|
222
|
+
...(screeningCandidate.identity || {}),
|
|
223
|
+
...(candidate.identity || {})
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function timingValue(row = {}, ...keys) {
|
|
229
|
+
const timings = row.timings || row.timing || {};
|
|
230
|
+
for (const key of keys) {
|
|
231
|
+
const value = firstDefined(row[key], timings[key]);
|
|
232
|
+
if (value !== "") return value;
|
|
233
|
+
}
|
|
234
|
+
return "";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function legacyScreenResultRow(row = {}) {
|
|
238
|
+
const candidate = pickCandidate(row);
|
|
239
|
+
const identity = candidate.identity || {};
|
|
240
|
+
const detail = row.detail || {};
|
|
241
|
+
const screening = row.screening || {};
|
|
242
|
+
const llm = pickLlm(row);
|
|
243
|
+
const rawPassed = firstBoolean(llm.passed, screening.passed, row.raw_passed, row.passed);
|
|
244
|
+
const finalPassed = firstBoolean(row.final_passed, row.finalPassed, rawPassed);
|
|
245
|
+
const hasError = Boolean(row.error || row.error_code || row.error_message);
|
|
246
|
+
const processResult = hasError
|
|
247
|
+
? "error"
|
|
248
|
+
: finalPassed === true
|
|
249
|
+
? "passed"
|
|
250
|
+
: "skipped";
|
|
251
|
+
const cot = firstText(
|
|
252
|
+
llm.reasoning_content,
|
|
253
|
+
llm.raw_reasoning_content,
|
|
254
|
+
llm.decision_cot,
|
|
255
|
+
llm.cot,
|
|
256
|
+
llm.raw_model_output,
|
|
257
|
+
llm.raw_content,
|
|
258
|
+
row.decision_cot,
|
|
259
|
+
row.cot,
|
|
260
|
+
screening.decision_cot,
|
|
261
|
+
screening.cot
|
|
262
|
+
);
|
|
263
|
+
const error = row.error || {};
|
|
264
|
+
const cvSource = firstText(
|
|
265
|
+
detail.cv_acquisition?.source,
|
|
266
|
+
row.cv_source,
|
|
267
|
+
candidate.source,
|
|
268
|
+
screening.candidate?.source
|
|
269
|
+
);
|
|
270
|
+
const totalEvidence = evidenceCount(llm);
|
|
271
|
+
return [
|
|
272
|
+
identity.name,
|
|
273
|
+
identity.school,
|
|
274
|
+
identity.major,
|
|
275
|
+
identity.current_company,
|
|
276
|
+
identity.current_position,
|
|
277
|
+
"",
|
|
278
|
+
processResult,
|
|
279
|
+
cot,
|
|
280
|
+
actionResultText(row),
|
|
281
|
+
cvSource,
|
|
282
|
+
rawPassed,
|
|
283
|
+
finalPassed,
|
|
284
|
+
totalEvidence,
|
|
285
|
+
totalEvidence,
|
|
286
|
+
"",
|
|
287
|
+
row.error_code || error.code || error.name || "",
|
|
288
|
+
row.error_message || error.message || "",
|
|
289
|
+
candidate.id || row.candidate_id || "",
|
|
290
|
+
timingValue(row, "total_ms"),
|
|
291
|
+
timingValue(row, "card_read_ms"),
|
|
292
|
+
timingValue(row, "candidate_click_ms"),
|
|
293
|
+
timingValue(row, "detail_open_ms"),
|
|
294
|
+
timingValue(row, "network_cv_wait_ms"),
|
|
295
|
+
timingValue(row, "text_model_ms"),
|
|
296
|
+
timingValue(row, "screenshot_capture_ms"),
|
|
297
|
+
timingValue(row, "vision_model_ms"),
|
|
298
|
+
timingValue(row, "late_network_retry_ms"),
|
|
299
|
+
timingValue(row, "dom_fallback_ms"),
|
|
300
|
+
timingValue(row, "post_action_ms"),
|
|
301
|
+
timingValue(row, "close_detail_ms"),
|
|
302
|
+
timingValue(row, "sleep_ms"),
|
|
303
|
+
timingValue(row, "checkpoint_save_ms")
|
|
304
|
+
];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function writeLegacyScreenCsv(filePath, {
|
|
308
|
+
inputRows = [],
|
|
309
|
+
results = []
|
|
310
|
+
} = {}) {
|
|
311
|
+
const resolved = path.resolve(filePath);
|
|
312
|
+
ensureDirectory(path.dirname(resolved));
|
|
313
|
+
const normalizedInputRows = (inputRows || []).map((row) => ({
|
|
314
|
+
field: row?.field ?? row?.[0] ?? "",
|
|
315
|
+
value: row?.value ?? row?.[1] ?? ""
|
|
316
|
+
}));
|
|
317
|
+
const lines = [
|
|
318
|
+
LEGACY_INPUT_HEADER.map(csvCell).join(","),
|
|
319
|
+
...normalizedInputRows.map((row) => [row.field, row.value].map(csvCell).join(",")),
|
|
320
|
+
"",
|
|
321
|
+
LEGACY_RESULT_HEADER.map(csvCell).join(","),
|
|
322
|
+
...(results || []).map((row) => legacyScreenResultRow(row).map(csvCell).join(","))
|
|
323
|
+
];
|
|
324
|
+
const tempPath = `${resolved}.tmp`;
|
|
325
|
+
fs.writeFileSync(tempPath, `\uFEFF${lines.join("\n")}\n`, "utf8");
|
|
326
|
+
fs.renameSync(tempPath, resolved);
|
|
327
|
+
return resolved;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function cloneReportInput(value, fallback = {}) {
|
|
331
|
+
return cloneJson(value, fallback);
|
|
332
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
export const RUN_STATUS_QUEUED = "queued";
|
|
2
|
+
export const RUN_STATUS_RUNNING = "running";
|
|
3
|
+
export const RUN_STATUS_PAUSED = "paused";
|
|
4
|
+
export const RUN_STATUS_COMPLETED = "completed";
|
|
5
|
+
export const RUN_STATUS_CANCELING = "canceling";
|
|
6
|
+
export const RUN_STATUS_CANCELED = "canceled";
|
|
7
|
+
export const RUN_STATUS_FAILED = "failed";
|
|
8
|
+
|
|
9
|
+
const TERMINAL_STATUSES = new Set([
|
|
10
|
+
RUN_STATUS_COMPLETED,
|
|
11
|
+
RUN_STATUS_CANCELED,
|
|
12
|
+
RUN_STATUS_FAILED
|
|
13
|
+
]);
|
|
14
|
+
|
|
15
|
+
export class RunCanceledError extends Error {
|
|
16
|
+
constructor(message = "Run canceled") {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "RunCanceledError";
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function nowIso() {
|
|
23
|
+
return new Date().toISOString();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createRunId(prefix = "run") {
|
|
27
|
+
const random = Math.random().toString(36).slice(2, 10);
|
|
28
|
+
return `${prefix}_${Date.now().toString(36)}_${random}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clone(value) {
|
|
32
|
+
return JSON.parse(JSON.stringify(value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function createDeferred() {
|
|
36
|
+
let resolve;
|
|
37
|
+
let reject;
|
|
38
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
39
|
+
resolve = promiseResolve;
|
|
40
|
+
reject = promiseReject;
|
|
41
|
+
});
|
|
42
|
+
return { promise, resolve, reject };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function snapshotFromEntry(entry) {
|
|
46
|
+
const run = entry.run;
|
|
47
|
+
return clone({
|
|
48
|
+
runId: run.runId,
|
|
49
|
+
name: run.name,
|
|
50
|
+
status: run.status,
|
|
51
|
+
phase: run.phase,
|
|
52
|
+
progress: run.progress,
|
|
53
|
+
context: run.context,
|
|
54
|
+
checkpoint: run.checkpoint,
|
|
55
|
+
startedAt: run.startedAt,
|
|
56
|
+
updatedAt: run.updatedAt,
|
|
57
|
+
completedAt: run.completedAt,
|
|
58
|
+
canPause: run.status === RUN_STATUS_RUNNING,
|
|
59
|
+
canResume: run.status === RUN_STATUS_PAUSED,
|
|
60
|
+
canCancel: !TERMINAL_STATUSES.has(run.status),
|
|
61
|
+
error: run.error,
|
|
62
|
+
summary: run.summary
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createRunLifecycleManager({
|
|
67
|
+
idPrefix = "run",
|
|
68
|
+
now = nowIso
|
|
69
|
+
} = {}) {
|
|
70
|
+
const runs = new Map();
|
|
71
|
+
|
|
72
|
+
function getEntry(runId) {
|
|
73
|
+
const entry = runs.get(runId);
|
|
74
|
+
if (!entry) throw new Error(`Unknown runId: ${runId}`);
|
|
75
|
+
return entry;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function touch(entry) {
|
|
79
|
+
entry.run.updatedAt = now();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function setStatus(entry, status, patch = {}) {
|
|
83
|
+
entry.run.status = status;
|
|
84
|
+
Object.assign(entry.run, patch);
|
|
85
|
+
touch(entry);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function createControls(entry) {
|
|
89
|
+
return {
|
|
90
|
+
signal: entry.controller.signal,
|
|
91
|
+
get runId() {
|
|
92
|
+
return entry.run.runId;
|
|
93
|
+
},
|
|
94
|
+
get status() {
|
|
95
|
+
return entry.run.status;
|
|
96
|
+
},
|
|
97
|
+
setPhase(phase) {
|
|
98
|
+
entry.run.phase = phase;
|
|
99
|
+
touch(entry);
|
|
100
|
+
},
|
|
101
|
+
updateProgress(progressPatch = {}) {
|
|
102
|
+
entry.run.progress = {
|
|
103
|
+
...entry.run.progress,
|
|
104
|
+
...progressPatch
|
|
105
|
+
};
|
|
106
|
+
touch(entry);
|
|
107
|
+
return snapshotFromEntry(entry);
|
|
108
|
+
},
|
|
109
|
+
checkpoint(checkpointPatch = {}) {
|
|
110
|
+
entry.run.checkpoint = {
|
|
111
|
+
...entry.run.checkpoint,
|
|
112
|
+
...checkpointPatch,
|
|
113
|
+
updatedAt: now()
|
|
114
|
+
};
|
|
115
|
+
touch(entry);
|
|
116
|
+
return snapshotFromEntry(entry);
|
|
117
|
+
},
|
|
118
|
+
async waitIfPaused() {
|
|
119
|
+
if (entry.controller.signal.aborted) {
|
|
120
|
+
throw new RunCanceledError();
|
|
121
|
+
}
|
|
122
|
+
if (!entry.pauseRequested) return;
|
|
123
|
+
setStatus(entry, RUN_STATUS_PAUSED);
|
|
124
|
+
while (entry.pauseRequested) {
|
|
125
|
+
const deferred = createDeferred();
|
|
126
|
+
entry.pauseWaiters.add(deferred);
|
|
127
|
+
try {
|
|
128
|
+
await deferred.promise;
|
|
129
|
+
} finally {
|
|
130
|
+
entry.pauseWaiters.delete(deferred);
|
|
131
|
+
}
|
|
132
|
+
if (entry.controller.signal.aborted) {
|
|
133
|
+
throw new RunCanceledError();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
setStatus(entry, RUN_STATUS_RUNNING);
|
|
137
|
+
},
|
|
138
|
+
async sleep(ms) {
|
|
139
|
+
if (entry.controller.signal.aborted) {
|
|
140
|
+
throw new RunCanceledError();
|
|
141
|
+
}
|
|
142
|
+
await new Promise((resolve, reject) => {
|
|
143
|
+
const timer = setTimeout(resolve, ms);
|
|
144
|
+
const onAbort = () => {
|
|
145
|
+
clearTimeout(timer);
|
|
146
|
+
reject(new RunCanceledError());
|
|
147
|
+
};
|
|
148
|
+
entry.controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
throwIfCanceled() {
|
|
152
|
+
if (entry.controller.signal.aborted) {
|
|
153
|
+
throw new RunCanceledError();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function settle(entry, task) {
|
|
160
|
+
try {
|
|
161
|
+
const summary = await task(entry.controls);
|
|
162
|
+
if (entry.controller.signal.aborted || entry.cancelRequested) {
|
|
163
|
+
setStatus(entry, RUN_STATUS_CANCELED, {
|
|
164
|
+
completedAt: now(),
|
|
165
|
+
summary: summary || entry.run.summary
|
|
166
|
+
});
|
|
167
|
+
} else {
|
|
168
|
+
setStatus(entry, RUN_STATUS_COMPLETED, {
|
|
169
|
+
completedAt: now(),
|
|
170
|
+
summary: summary || entry.run.summary
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (error instanceof RunCanceledError || entry.controller.signal.aborted || entry.cancelRequested) {
|
|
175
|
+
setStatus(entry, RUN_STATUS_CANCELED, {
|
|
176
|
+
completedAt: now(),
|
|
177
|
+
error: null
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
setStatus(entry, RUN_STATUS_FAILED, {
|
|
182
|
+
completedAt: now(),
|
|
183
|
+
error: {
|
|
184
|
+
name: error?.name || "Error",
|
|
185
|
+
message: error?.message || String(error)
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function startRun({ name, context = {}, progress = {}, checkpoint = {}, task }) {
|
|
192
|
+
if (typeof task !== "function") {
|
|
193
|
+
throw new Error("startRun requires a task function");
|
|
194
|
+
}
|
|
195
|
+
const runId = createRunId(idPrefix);
|
|
196
|
+
const startedAt = now();
|
|
197
|
+
const entry = {
|
|
198
|
+
controller: new AbortController(),
|
|
199
|
+
pauseRequested: false,
|
|
200
|
+
cancelRequested: false,
|
|
201
|
+
pauseWaiters: new Set(),
|
|
202
|
+
run: {
|
|
203
|
+
runId,
|
|
204
|
+
name: name || runId,
|
|
205
|
+
status: RUN_STATUS_QUEUED,
|
|
206
|
+
phase: "queued",
|
|
207
|
+
progress,
|
|
208
|
+
context,
|
|
209
|
+
checkpoint,
|
|
210
|
+
startedAt,
|
|
211
|
+
updatedAt: startedAt,
|
|
212
|
+
completedAt: null,
|
|
213
|
+
error: null,
|
|
214
|
+
summary: null
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
entry.controls = createControls(entry);
|
|
218
|
+
runs.set(runId, entry);
|
|
219
|
+
setStatus(entry, RUN_STATUS_RUNNING, { phase: "running" });
|
|
220
|
+
entry.promise = settle(entry, task);
|
|
221
|
+
return snapshotFromEntry(entry);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function getRun(runId) {
|
|
225
|
+
return snapshotFromEntry(getEntry(runId));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function pauseRun(runId) {
|
|
229
|
+
const entry = getEntry(runId);
|
|
230
|
+
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
231
|
+
entry.pauseRequested = true;
|
|
232
|
+
if (entry.run.status === RUN_STATUS_RUNNING) {
|
|
233
|
+
touch(entry);
|
|
234
|
+
}
|
|
235
|
+
return snapshotFromEntry(entry);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function resumeRun(runId) {
|
|
239
|
+
const entry = getEntry(runId);
|
|
240
|
+
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
241
|
+
entry.pauseRequested = false;
|
|
242
|
+
for (const waiter of entry.pauseWaiters) {
|
|
243
|
+
waiter.resolve();
|
|
244
|
+
}
|
|
245
|
+
if (entry.run.status === RUN_STATUS_PAUSED) {
|
|
246
|
+
setStatus(entry, RUN_STATUS_RUNNING);
|
|
247
|
+
} else {
|
|
248
|
+
touch(entry);
|
|
249
|
+
}
|
|
250
|
+
return snapshotFromEntry(entry);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function cancelRun(runId) {
|
|
254
|
+
const entry = getEntry(runId);
|
|
255
|
+
if (TERMINAL_STATUSES.has(entry.run.status)) return snapshotFromEntry(entry);
|
|
256
|
+
entry.cancelRequested = true;
|
|
257
|
+
setStatus(entry, RUN_STATUS_CANCELING);
|
|
258
|
+
entry.controller.abort();
|
|
259
|
+
entry.pauseRequested = false;
|
|
260
|
+
for (const waiter of entry.pauseWaiters) {
|
|
261
|
+
waiter.resolve();
|
|
262
|
+
}
|
|
263
|
+
return snapshotFromEntry(entry);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function waitForRun(runId, { timeoutMs = 10000 } = {}) {
|
|
267
|
+
const entry = getEntry(runId);
|
|
268
|
+
const timeout = new Promise((_, reject) => {
|
|
269
|
+
setTimeout(() => reject(new Error(`Timed out waiting for run ${runId}`)), timeoutMs);
|
|
270
|
+
});
|
|
271
|
+
await Promise.race([entry.promise, timeout]);
|
|
272
|
+
return snapshotFromEntry(entry);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
startRun,
|
|
277
|
+
getRun,
|
|
278
|
+
pauseRun,
|
|
279
|
+
resumeRun,
|
|
280
|
+
cancelRun,
|
|
281
|
+
waitForRun,
|
|
282
|
+
listRuns() {
|
|
283
|
+
return Array.from(runs.values()).map(snapshotFromEntry);
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|