@hoplogic/hopjit 0.1.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 +21 -0
- package/README.md +99 -0
- package/dist/act-body-interpreter.d.ts +24 -0
- package/dist/act-body-interpreter.js +159 -0
- package/dist/act-body-parser.d.ts +10 -0
- package/dist/act-body-parser.js +577 -0
- package/dist/act-builtins.d.ts +12 -0
- package/dist/act-builtins.js +104 -0
- package/dist/ast-helpers.d.ts +26 -0
- package/dist/ast-helpers.js +58 -0
- package/dist/ast-runtime.d.ts +49 -0
- package/dist/ast-runtime.js +224 -0
- package/dist/ast-types.d.ts +260 -0
- package/dist/ast-types.js +4 -0
- package/dist/cli-types.d.ts +190 -0
- package/dist/cli-types.js +4 -0
- package/dist/cli.d.ts +26 -0
- package/dist/cli.js +623 -0
- package/dist/dispatcher.d.ts +92 -0
- package/dist/dispatcher.js +692 -0
- package/dist/doc-ref.d.ts +41 -0
- package/dist/doc-ref.js +214 -0
- package/dist/engine-traverse.d.ts +37 -0
- package/dist/engine-traverse.js +385 -0
- package/dist/engine.d.ts +141 -0
- package/dist/engine.js +1792 -0
- package/dist/errors.d.ts +47 -0
- package/dist/errors.js +34 -0
- package/dist/hoplog.d.ts +120 -0
- package/dist/hoplog.js +501 -0
- package/dist/parser.d.ts +7 -0
- package/dist/parser.js +802 -0
- package/dist/persistence.d.ts +60 -0
- package/dist/persistence.js +208 -0
- package/dist/prompt.d.ts +130 -0
- package/dist/prompt.js +1014 -0
- package/dist/provider-types.d.ts +134 -0
- package/dist/provider-types.js +3 -0
- package/dist/runtime-types.d.ts +84 -0
- package/dist/runtime-types.js +4 -0
- package/dist/tools.d.ts +16 -0
- package/dist/tools.js +141 -0
- package/dist/validator.d.ts +23 -0
- package/dist/validator.js +959 -0
- package/driver/codex/AGENTS.md +54 -0
- package/driver/hopskill-build/SKILL.md +137 -0
- package/driver/hopskill-build/references/spec-skeleton.md +132 -0
- package/driver/hopskill-build/references/step-type-cheatsheet.md +56 -0
- package/driver/hopskill-build/references/three-focus-rules.md +148 -0
- package/driver/hopspec-skill.md +187 -0
- package/driver/references/cli-discovery.md +36 -0
- package/driver/references/discovery.md +45 -0
- package/driver/references/driver-subagent.md +50 -0
- package/driver/references/parallel-worker.md +50 -0
- package/driver/references/step-execution-rules.md +47 -0
- package/package.json +52 -0
package/dist/hoplog.js
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
// @module: hoplog ^anc-obs-hoplog
|
|
2
|
+
import { mkdirSync, appendFileSync, readFileSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { randomBytes } from 'node:crypto';
|
|
5
|
+
const LEVEL_PRIORITY = {
|
|
6
|
+
debug: 0, info: 1, warn: 2,
|
|
7
|
+
};
|
|
8
|
+
// 审计性字段 key 集合——写入时无视日志级别
|
|
9
|
+
const AUDIT_FIELDS = new Set(['tool', 'commit_audit', 'hitl', 'callee_spec_id']);
|
|
10
|
+
// 流控轨迹字段——info 级写入
|
|
11
|
+
const FLOW_FIELDS = new Set(['knowledge', 'doc_refs', 'retry', 'taken', 'condition', 'llm', 'tokens_used']);
|
|
12
|
+
const _metaFieldsCovered = true;
|
|
13
|
+
void _metaFieldsCovered;
|
|
14
|
+
/**
|
|
15
|
+
* HopLog 执行轨迹日志——唯一信息源是 main.yaml 文件本身。
|
|
16
|
+
* 每个 record* 方法实时 appendFileSync,无内存镜像(查询已记录内容请读文件)。
|
|
17
|
+
*/
|
|
18
|
+
export class HopLog {
|
|
19
|
+
level;
|
|
20
|
+
runDir;
|
|
21
|
+
filePath;
|
|
22
|
+
// 已 start 的步骤集合——守卫孤儿字段(步骤未 start 时 done/meta/warn 不写入)
|
|
23
|
+
startedSteps = new Set();
|
|
24
|
+
// 延迟 resume marker:resume() 只暂存时间戳,由下一次写入按正确层级落盘(三出口之一:emitResumed
|
|
25
|
+
// 续未完成 step / recordStepStart 内联接新 step / flushTopLevelResume 顶层收尾)。
|
|
26
|
+
// 避免旧实现把裸注释砸进未闭合 prompt 块标量中间破坏 YAML。见 design ^anc-obs-hoplog-resume。
|
|
27
|
+
pendingResume = null;
|
|
28
|
+
// loop 多轮:hoplog 展示态轮次计数。同一 loop 子步 step-id 每轮重复(4.1 每轮同名),
|
|
29
|
+
// 嵌套后成同层重复 key(YAML 非法)——检测"已 done 过的 step 又 start"=新轮次、+1,
|
|
30
|
+
// hoplog key 写 `4.1#2`(#iter 仅展示 key,不碰 step-id 铁钉、depth 仍按点号算)。
|
|
31
|
+
// 与 startedSteps 同性质的展示态计数,不违反"无内存镜像"。见 design ^anc-obs-nested-tree。
|
|
32
|
+
iterCounts = new Map();
|
|
33
|
+
// 已 done 的 step(判 loop 新轮:已 done 又 start = 进入下一轮)。
|
|
34
|
+
doneSteps = new Set();
|
|
35
|
+
// 已写过 resumed_at 的 step——同一 step 只落一个 resumed_at(多次 resume 续同一未完成 step 会各触发
|
|
36
|
+
// emitResumed,重复写语义冗余)。首次恢复的 marker 足以标记"该 step 曾被恢复",后续恢复不再重复写。
|
|
37
|
+
// resume 重建时从文件扫已有 resumed_at 行填入(跨进程去重)。顶层事件(close/replan)用空串 '' 记。
|
|
38
|
+
resumedWritten = new Set();
|
|
39
|
+
constructor(options) {
|
|
40
|
+
this.level = options.level ?? 'info';
|
|
41
|
+
const runId = generateRunId();
|
|
42
|
+
this.runDir = join(options.logDir, `${options.specId}-${runId}`); // @a: anc-obs-file-layout
|
|
43
|
+
mkdirSync(this.runDir, { recursive: true });
|
|
44
|
+
this.filePath = join(this.runDir, 'main.yaml');
|
|
45
|
+
// Header — written immediately。spec_id(用户在 spec 写的 Id:)/trace_id(调用方传入)走 toYaml 转义
|
|
46
|
+
// ——外部来源值含空格/冒号/特殊字符会破 YAML header;run_id(内部生成固定格式)/level(枚举)无需。
|
|
47
|
+
this.append(`spec_id: ${toYaml(options.specId)}\n`);
|
|
48
|
+
this.append(`run_id: ${runId}\n`);
|
|
49
|
+
this.append(`trace_id: ${toYaml(options.traceId ?? runId)}\n`); // @a: anc-obs-trace-inherit
|
|
50
|
+
this.append(`level: ${this.level}\n`);
|
|
51
|
+
this.append(`started_at: ${toYaml(timestamp())}\n`);
|
|
52
|
+
// title/goal 在 spec: 下的 2 空格层级;多行值走 block scalar,内容须比键更深缩进(indent=1 → 内容 4 空格 > 键 2 空格)。
|
|
53
|
+
// 不传 indent 时多行 goal 的 block scalar 内容仅 2 空格 = 键缩进 → YAML 非法(block 判空 + 内容行无冒号)。
|
|
54
|
+
this.append(`spec:\n title: ${toYaml(options.title, 1)}\n goal: ${toYaml(options.goal, 1)}\n`);
|
|
55
|
+
const filteredInputs = this.filterValues(options.inputs);
|
|
56
|
+
if (filteredInputs && Object.keys(filteredInputs).length > 0) {
|
|
57
|
+
this.append(`inputs:\n${toYaml(filteredInputs, 1)}\n`);
|
|
58
|
+
}
|
|
59
|
+
this.append(`execution:\n`);
|
|
60
|
+
}
|
|
61
|
+
static resume(runDir, level) {
|
|
62
|
+
const log = Object.create(HopLog.prototype);
|
|
63
|
+
log.runDir = runDir;
|
|
64
|
+
log.filePath = join(runDir, 'main.yaml');
|
|
65
|
+
// 从已有 main.yaml 头部读取 level(跨进程恢复时保持原 run 的日志级别)
|
|
66
|
+
if (!level && existsSync(log.filePath)) {
|
|
67
|
+
const head = readFileSync(log.filePath, 'utf-8').slice(0, 500);
|
|
68
|
+
const m = head.match(/^level:\s*(debug|info|warn)/m);
|
|
69
|
+
if (m)
|
|
70
|
+
level = m[1];
|
|
71
|
+
}
|
|
72
|
+
log.level = level ?? 'info';
|
|
73
|
+
// 从已有 main.yaml 重建 startedSteps——跨进程恢复时守卫不能丢已记录的步骤,
|
|
74
|
+
// 否则下一进程的 recordStepDone 会误判为孤儿。步骤记录格式:` "<id>":` 或 ` <id>:`(二级缩进)
|
|
75
|
+
log.startedSteps = new Set();
|
|
76
|
+
log.iterCounts = new Map();
|
|
77
|
+
log.doneSteps = new Set();
|
|
78
|
+
log.resumedWritten = new Set();
|
|
79
|
+
if (existsSync(log.filePath)) {
|
|
80
|
+
const text = readFileSync(log.filePath, 'utf-8');
|
|
81
|
+
// 嵌套树后 step key 缩进为任意偶数(2/4/6…)、值为 step-id 形态(可带 #iter 后缀)、冒号后换行(块头无 inline 值)。
|
|
82
|
+
// id 形态约束(`[\d.]+` 可带 `#\d+`)+ 冒号后换行,避免误匹配 llm/inputs/outputs 等 body 字段键。
|
|
83
|
+
const re = /^ +(?:"([\d.]+(?:#\d+)?)"|([\d.]+(?:#\d+)?)):\s*$/gm;
|
|
84
|
+
let m;
|
|
85
|
+
while ((m = re.exec(text)) !== null) {
|
|
86
|
+
const key = m[1] ?? m[2];
|
|
87
|
+
const hash = key.indexOf('#');
|
|
88
|
+
if (hash >= 0) {
|
|
89
|
+
// loop 多轮 key `4.1#2`:还原 step-id + 重建轮次计数(取最大 N)
|
|
90
|
+
const base = key.slice(0, hash);
|
|
91
|
+
const iter = Number(key.slice(hash + 1));
|
|
92
|
+
log.startedSteps.add(base);
|
|
93
|
+
if (Number.isFinite(iter))
|
|
94
|
+
log.iterCounts.set(base, Math.max(log.iterCounts.get(base) ?? 1, iter));
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
log.startedSteps.add(key);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 重建 doneSteps:loop 新轮的判据是"已 **done** 又 start",**不是**"已 started 又 start"——
|
|
101
|
+
// resume 续一个**未完成**的 step(如 subtask 内某 act 中断、resume 重发其 step_ready 再走
|
|
102
|
+
// recordStepStart)不是新轮,误当新轮会生成错误的 #iter key(实测 v3:4.2 中断 resume → "4#2"/"4.2#2")。
|
|
103
|
+
// 故 doneSteps 只收文件中**真正终态**的 step:栈式单遍——step key 入栈,遇 status: completed/failed
|
|
104
|
+
// 标记其归属 step(缩进比 status 浅 2 的最近 step key)为 done。
|
|
105
|
+
const lines = text.split('\n');
|
|
106
|
+
const stepStack = [];
|
|
107
|
+
for (const line of lines) {
|
|
108
|
+
if (line.trim() === '')
|
|
109
|
+
continue;
|
|
110
|
+
const indent = line.length - line.trimStart().length;
|
|
111
|
+
const keyMatch = line.match(/^ +(?:"([\d.]+(?:#\d+)?)"|([\d.]+(?:#\d+)?)):\s*$/);
|
|
112
|
+
if (keyMatch) {
|
|
113
|
+
const key = keyMatch[1] ?? keyMatch[2];
|
|
114
|
+
const base = key.indexOf('#') >= 0 ? key.slice(0, key.indexOf('#')) : key;
|
|
115
|
+
while (stepStack.length && stepStack[stepStack.length - 1].indent >= indent)
|
|
116
|
+
stepStack.pop();
|
|
117
|
+
stepStack.push({ id: base, indent });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const sm = line.match(/^( +)status: (completed|failed)\s*$/);
|
|
121
|
+
if (sm) {
|
|
122
|
+
const stInd = sm[1].length;
|
|
123
|
+
// status 归属:缩进恰为 stInd-2 的最近 step(栈顶应是它)
|
|
124
|
+
for (let i = stepStack.length - 1; i >= 0; i--) {
|
|
125
|
+
if (stepStack[i].indent === stInd - 2) {
|
|
126
|
+
log.doneSteps.add(stepStack[i].id);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// resumed_at 归属:填 resumedWritten 去重(跨进程——本进程 resume 续同 step 不再重写第二个 resumed_at)。
|
|
132
|
+
const rm = line.match(/^( *)resumed_at:/);
|
|
133
|
+
if (rm) {
|
|
134
|
+
const rInd = rm[1].length;
|
|
135
|
+
if (rInd === 0) {
|
|
136
|
+
log.resumedWritten.add('');
|
|
137
|
+
} // 顶层 resumed_at(close/replan)
|
|
138
|
+
else
|
|
139
|
+
for (let i = stepStack.length - 1; i >= 0; i--) {
|
|
140
|
+
if (stepStack[i].indent === rInd - 2) {
|
|
141
|
+
log.resumedWritten.add(stepStack[i].id);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// startedSteps 已含文件中所有写过骨架的 step(含未 done 的)——recordStepStart 幂等判据直接用它:
|
|
148
|
+
// startedSteps 有且未 done = resume 续/ recover 重发,跳过骨架只补 resumed_at(见 recordStepStart)。
|
|
149
|
+
}
|
|
150
|
+
mkdirSync(runDir, { recursive: true });
|
|
151
|
+
// resume marker 延迟写入:不在此 append(可能砸进上一步未闭合的 prompt 块标量中间破坏
|
|
152
|
+
// YAML),仅暂存时间戳,由下一次写入按正确层级落成结构化字段 resumed_at(三出口见 pendingResume 声明处)。
|
|
153
|
+
// 见 design ^anc-obs-hoplog-resume。
|
|
154
|
+
log.pendingResume = timestamp();
|
|
155
|
+
return log;
|
|
156
|
+
}
|
|
157
|
+
// resumed_at 的**顶层出口**(close/replan 场景):resume 后未再写任何 step 就直接终态/重规划时,
|
|
158
|
+
// 把暂存的 marker 落成顶层字段(0 缩进,stepKey 用空串 '' 去重)。另两个出口按落点分工:
|
|
159
|
+
// ① emitResumed(stepId)——resume 续**未完成** step(recordStepStart 幂等分支);
|
|
160
|
+
// ② recordStepStart 内联——resume 后首个**新** step,作该 step 块字段。
|
|
161
|
+
// 三出口对应三种真实落点语义,不合一(旧设计企图用单个 flushResume 从 stepId 反推缩进覆盖所有场景,
|
|
162
|
+
// 嵌套 resume/多次 flush 时归属错乱、resumed_at 落错层截断 YAML,实测崩——故按落点拆分)。
|
|
163
|
+
// pendingResume 非空且顶层未写过才落,写后清空——天然幂等,多次调用仅首次落、余次 no-op。
|
|
164
|
+
// 见 design ^anc-obs-hoplog-resume / ^anc-obs-nested-tree。// @a: anc-obs-hoplog-resume
|
|
165
|
+
flushTopLevelResume() {
|
|
166
|
+
if (this.pendingResume === null)
|
|
167
|
+
return;
|
|
168
|
+
// 顶层 resumed_at 已写过(stepKey='')→ 不重复写;仍清 pendingResume 避免它漏到别处。
|
|
169
|
+
if (this.resumedWritten.has('')) {
|
|
170
|
+
this.pendingResume = null;
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
this.append(`resumed_at: ${toYaml(this.pendingResume)}\n`);
|
|
174
|
+
this.resumedWritten.add('');
|
|
175
|
+
this.pendingResume = null;
|
|
176
|
+
}
|
|
177
|
+
// step-id 点号深度 = 树层级(顶层=1)。缩进纯函数:key 前导 2d、body 字段 2d+2、子块值基准 d+2
|
|
178
|
+
// (toYaml 的 indentLevel 单位是 2 空格,d+2 对应 2d+4 空格)。#iter 后缀不影响深度。见 design ^anc-obs-nested-tree。
|
|
179
|
+
stepDepth(stepId) {
|
|
180
|
+
const base = stepId.split('#')[0];
|
|
181
|
+
return base.split('.').length;
|
|
182
|
+
}
|
|
183
|
+
ind(spaces) { return ' '.repeat(spaces); }
|
|
184
|
+
// hoplog 展示 key:loop 多轮子步加 `#iter` 区分(iterCounts 记当前轮次)。首轮及非重复 step 无后缀。
|
|
185
|
+
hoplogKey(stepId) {
|
|
186
|
+
const iter = this.iterCounts.get(stepId);
|
|
187
|
+
return iter && iter > 1 ? `${stepId}#${iter}` : stepId;
|
|
188
|
+
}
|
|
189
|
+
// YAMLL 块头:写 `<2d>"<step-id>":`(缩进 = step 深度,供折叠)。每个 record 事件自成一块。
|
|
190
|
+
// 见 design/spec-observability.md ^anc-obs-nested-tree(v3 YAMLL)。// @a: anc-obs-nested-tree
|
|
191
|
+
blockHeader(stepId) {
|
|
192
|
+
this.append(`${this.ind(2 * this.stepDepth(stepId))}${toYaml(this.hoplogKey(stepId))}:\n`);
|
|
193
|
+
}
|
|
194
|
+
recordStepStart(stepId, type, summary, inputs, promptText) {
|
|
195
|
+
// loop 新轮:已 done 过的 step 又 start = 下一轮迭代 → 轮次 +1(hoplog key 加 #iter)。
|
|
196
|
+
if (this.doneSteps.has(stepId)) {
|
|
197
|
+
this.iterCounts.set(stepId, (this.iterCounts.get(stepId) ?? 1) + 1);
|
|
198
|
+
this.doneSteps.delete(stepId);
|
|
199
|
+
}
|
|
200
|
+
// resume 续未完成 / recover 重发(可能多次):start 块已写过 → 不重写,只写一个 resumed 块(去重)。
|
|
201
|
+
// YAMLL 下这不是"避免重复 key"(块间本就允许重复 step-id),而是语义去重:一次恢复标一次。
|
|
202
|
+
if (this.startedSteps.has(stepId)) {
|
|
203
|
+
this.emitResumed(stepId);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
this.startedSteps.add(stepId);
|
|
207
|
+
const d = this.stepDepth(stepId);
|
|
208
|
+
const f = 2 * d + 2;
|
|
209
|
+
this.blockHeader(stepId); // 独立块:块头 `<2d>id:`
|
|
210
|
+
this.append(`${this.ind(f)}type: ${type}\n`);
|
|
211
|
+
this.append(`${this.ind(f)}summary: ${toYaml(summary)}\n`);
|
|
212
|
+
this.append(`${this.ind(f)}at: ${toYaml(timestamp())}\n`);
|
|
213
|
+
// resume 后首个新 step:进程在此恢复——resumed_at 作本块字段落盘(本块的 f,天然对齐),一次即清。
|
|
214
|
+
if (this.pendingResume !== null && !this.resumedWritten.has(stepId)) {
|
|
215
|
+
this.append(`${this.ind(f)}resumed_at: ${toYaml(this.pendingResume)}\n`);
|
|
216
|
+
this.resumedWritten.add(stepId);
|
|
217
|
+
this.pendingResume = null;
|
|
218
|
+
}
|
|
219
|
+
const filtered = this.filterValues(inputs);
|
|
220
|
+
if (filtered && Object.keys(filtered).length > 0) {
|
|
221
|
+
this.append(`${this.ind(f)}inputs:\n${toYaml(filtered, d + 2)}\n`);
|
|
222
|
+
}
|
|
223
|
+
// debug 级记录完整 prompt(给 LLM 的完整输入——6 层格式化文本)
|
|
224
|
+
if (promptText && this.level === 'debug') {
|
|
225
|
+
this.append(`${this.ind(f)}llm:\n`);
|
|
226
|
+
this.append(`${this.ind(f + 2)}prompt: ${toYaml(promptText, d + 2)}\n`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// resume marker:自成一块 `<2d>id:` + `<2d+2>resumed_at:`。resume 时就知道续的是哪个 step
|
|
230
|
+
// (engine 从 state.json running 传入),块头即用它,缩进自算——不再延迟到"下一次调用"。
|
|
231
|
+
// 同 step 已写过 resumed 块则跳过(多次 resume 只标一次)。// @a: anc-obs-hoplog-resume
|
|
232
|
+
emitResumed(stepId) {
|
|
233
|
+
if (this.pendingResume === null)
|
|
234
|
+
return;
|
|
235
|
+
if (this.resumedWritten.has(stepId)) {
|
|
236
|
+
this.pendingResume = null;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.blockHeader(stepId);
|
|
240
|
+
this.append(`${this.ind(2 * this.stepDepth(stepId) + 2)}resumed_at: ${toYaml(this.pendingResume)}\n`);
|
|
241
|
+
this.resumedWritten.add(stepId);
|
|
242
|
+
this.pendingResume = null;
|
|
243
|
+
}
|
|
244
|
+
// 孤儿写入 = Engine 调用序列 bug 的信号——写入错误标记暴露问题,但不抛异常打断执行
|
|
245
|
+
guardOrphan(stepId, method) {
|
|
246
|
+
if (this.startedSteps.has(stepId))
|
|
247
|
+
return true;
|
|
248
|
+
this.append(`# ERROR: orphan ${method}("${stepId}") — step never started (caller sequence bug)\n`);
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
recordStepDone(stepId, outputs) {
|
|
252
|
+
if (!this.guardOrphan(stepId, 'recordStepDone'))
|
|
253
|
+
return;
|
|
254
|
+
const d = this.stepDepth(stepId);
|
|
255
|
+
const f = 2 * d + 2;
|
|
256
|
+
this.blockHeader(stepId); // YAMLL:done 自成一块(块头 `<2d>id:`,与 start 块同 step-id、独立)
|
|
257
|
+
const filtered = this.filterValues(outputs);
|
|
258
|
+
if (filtered && Object.keys(filtered).length > 0) {
|
|
259
|
+
if (this.level === 'debug') {
|
|
260
|
+
this.append(`${this.ind(f)}response:\n${toYaml(filtered, d + 2)}\n`);
|
|
261
|
+
}
|
|
262
|
+
this.append(`${this.ind(f)}outputs:\n${toYaml(filtered, d + 2)}\n`);
|
|
263
|
+
}
|
|
264
|
+
this.append(`${this.ind(f)}status: completed\n`);
|
|
265
|
+
this.append(`${this.ind(f)}completed_at: ${toYaml(timestamp())}\n`);
|
|
266
|
+
this.doneSteps.add(stepId); // 标记已 done(loop 再次 start 同 id → 新轮次)
|
|
267
|
+
}
|
|
268
|
+
recordStepFailed(stepId, reason, failKind = 'error') {
|
|
269
|
+
if (!this.guardOrphan(stepId, 'recordStepFailed'))
|
|
270
|
+
return;
|
|
271
|
+
const d = this.stepDepth(stepId);
|
|
272
|
+
const f = 2 * d + 2;
|
|
273
|
+
this.blockHeader(stepId); // YAMLL:failed 自成一块
|
|
274
|
+
this.append(`${this.ind(f)}status: failed\n`);
|
|
275
|
+
this.append(`${this.ind(f)}failed_at: ${toYaml(timestamp())}\n`);
|
|
276
|
+
this.append(`${this.ind(f)}fail_kind: ${toYaml(failKind)}\n`);
|
|
277
|
+
this.append(`${this.ind(f)}reason: ${toYaml(sanitize(reason))}\n`);
|
|
278
|
+
this.doneSteps.add(stepId); // 标记已终态(loop 再次 start 同 id → 新轮次)
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* 记录步骤特有字段。审计性字段(tool/commit_audit/hitl/callee_spec_id)
|
|
282
|
+
* 无视日志级别始终写入;流控轨迹字段(knowledge/retry/taken/condition/llm/tokens_used)info 级写入。
|
|
283
|
+
*/
|
|
284
|
+
recordStepMeta(stepId, meta) {
|
|
285
|
+
if (!this.guardOrphan(stepId, 'recordStepMeta'))
|
|
286
|
+
return;
|
|
287
|
+
const d = this.stepDepth(stepId);
|
|
288
|
+
const f = 2 * d + 2;
|
|
289
|
+
// 先算出实际要写的字段(按级别过滤后),非空才写块头——避免空 meta 留一个孤块头。
|
|
290
|
+
const entries = Object.entries(meta).filter(([key, value]) => {
|
|
291
|
+
if (value === undefined)
|
|
292
|
+
return false;
|
|
293
|
+
const isAudit = AUDIT_FIELDS.has(key);
|
|
294
|
+
const isFlow = FLOW_FIELDS.has(key);
|
|
295
|
+
return isAudit || !isFlow || this.shouldRecord('info');
|
|
296
|
+
});
|
|
297
|
+
if (entries.length === 0)
|
|
298
|
+
return;
|
|
299
|
+
this.blockHeader(stepId); // YAMLL:meta 自成一块
|
|
300
|
+
for (const [key, value] of entries) {
|
|
301
|
+
const valStr = toYaml(value, d + 2);
|
|
302
|
+
if (typeof value === 'object' && value !== null) {
|
|
303
|
+
this.append(`${this.ind(f)}${key}:\n${valStr}\n`);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
this.append(`${this.ind(f)}${key}: ${valStr}\n`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// stepId 非空 → step 级告警(warn 作该 step 块字段,过 guardOrphan 守卫)。
|
|
311
|
+
// stepId 为空 '' → 文档级告警(如"Output 声明了却从未赋值",不归属任何 step):
|
|
312
|
+
// 落顶层 `warn:` 独立块,不过 guardOrphan、不写 blockHeader(空 id 无 step 归属,
|
|
313
|
+
// 旧实现误让它走 guardOrphan 被当孤儿写成 `# ERROR: orphan` 而正文丢失)。
|
|
314
|
+
recordWarn(stepId, message) {
|
|
315
|
+
if (!this.shouldRecord('warn'))
|
|
316
|
+
return;
|
|
317
|
+
if (stepId === '') { // 文档级:顶层独立块
|
|
318
|
+
this.append(`warn: ${toYaml(sanitize(message))}\n`);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (!this.guardOrphan(stepId, 'recordWarn'))
|
|
322
|
+
return;
|
|
323
|
+
const f = 2 * this.stepDepth(stepId) + 2;
|
|
324
|
+
this.blockHeader(stepId); // YAMLL:warn 自成一块
|
|
325
|
+
this.append(`${this.ind(f)}warn: ${toYaml(sanitize(message))}\n`);
|
|
326
|
+
}
|
|
327
|
+
// parallel fan-out/join 审计:作为 parallel 步骤(parallelStepId)的 body 子字段写入,缩进按步骤深度
|
|
328
|
+
// (fanout:/join: 键在 2d+2,与 type/summary 并列;子内容更深)。始终写入(无视级别)。
|
|
329
|
+
// ⚠️ 曾误实现为顶格 0 缩进——在 execution 深缩进流中途 append 顶格键会提前闭合 execution mapping,
|
|
330
|
+
// 令其后步骤错位、YAML 不可解析。改为随步骤深度缩进后自然嵌在该 parallel 子树内。
|
|
331
|
+
// 见 design/spec-observability.md ^anc-obs-nested-tree。// @a: anc-exec-parallel-batch, anc-exec-parallel-join-merge, anc-obs-nested-tree
|
|
332
|
+
recordParallelFanout(parallelStepId, childIds, maxConcurrent) {
|
|
333
|
+
const f = 2 * this.stepDepth(parallelStepId) + 2;
|
|
334
|
+
this.blockHeader(parallelStepId); // YAMLL:fanout 自成一块(块头 = parallel step-id)
|
|
335
|
+
this.append(`${this.ind(f)}fanout:\n`);
|
|
336
|
+
// 字段序:max_concurrent(窗口容量,先看到)→ children(全集)→ started_at(开始持续派发的锚点)。
|
|
337
|
+
// fanout 不做二元起止——parallel 阶段"结束"由 join 的 ended_at 承载。见 design ^anc-obs-nested-tree。
|
|
338
|
+
this.append(`${this.ind(f + 2)}max_concurrent: ${maxConcurrent}\n`);
|
|
339
|
+
this.append(`${this.ind(f + 2)}children:\n${childIds.map(id => `${this.ind(f + 4)}- ${toYaml(id)}`).join('\n')}\n`);
|
|
340
|
+
this.append(`${this.ind(f + 2)}started_at: ${toYaml(timestamp())}\n`);
|
|
341
|
+
}
|
|
342
|
+
// 每派发一个 child 一次(fanout-plan/fanout-next 顾问进程在 toLaunch 循环内调):流式 append 一条 dispatch 块。
|
|
343
|
+
// dispatched_at = 引擎决定派发的时刻("引擎动作时刻",如实反映滑动窗口的持续派发,非 fanout 一瞬塌缩);
|
|
344
|
+
// log = 该 child 子日志目录(确定性算出)。顾问进程 load 后 HopLog.resume 重建父句柄故能 append。
|
|
345
|
+
// 见 design/spec-observability.md ^anc-obs-parallel-dispatch。// @a: anc-obs-parallel-dispatch
|
|
346
|
+
recordParallelDispatch(parallelStepId, childId, logDir) {
|
|
347
|
+
const f = 2 * this.stepDepth(parallelStepId) + 2;
|
|
348
|
+
this.blockHeader(parallelStepId); // YAMLL:dispatch 自成一块(块头 = parallel step-id,块间重复合法)
|
|
349
|
+
this.append(`${this.ind(f)}dispatch:\n`);
|
|
350
|
+
this.append(`${this.ind(f + 2)}child: ${toYaml(childId)}\n`);
|
|
351
|
+
this.append(`${this.ind(f + 2)}dispatched_at: ${toYaml(timestamp())}\n`);
|
|
352
|
+
this.append(`${this.ind(f + 2)}log: ${toYaml(logDir)}\n`);
|
|
353
|
+
}
|
|
354
|
+
// join 完成时记录。childResults 每项带 failed + 可选 log(子实例日志路径,join 时逐个读子实例顺手带出)。
|
|
355
|
+
// startedAt = engine 进入 join 的时刻(外部捕获传入);ended_at = 本方法 merge 完的 timestamp()。
|
|
356
|
+
// 见 design/spec-observability.md ^anc-obs-nested-tree。// @a: anc-obs-parallel-dispatch
|
|
357
|
+
recordParallelJoin(parallelStepId, childResults, startedAt) {
|
|
358
|
+
const f = 2 * this.stepDepth(parallelStepId) + 2;
|
|
359
|
+
this.blockHeader(parallelStepId); // YAMLL:join 自成一块
|
|
360
|
+
this.append(`${this.ind(f)}join:\n`);
|
|
361
|
+
this.append(`${this.ind(f + 2)}children:\n`);
|
|
362
|
+
for (const [id, res] of Object.entries(childResults)) {
|
|
363
|
+
const status = res.failed ? 'failed' : 'completed';
|
|
364
|
+
// child 值写成 inline mapping:status + 可选 log(子日志定位线索)
|
|
365
|
+
const logPart = res.log ? `, log: ${toYaml(res.log)}` : '';
|
|
366
|
+
this.append(`${this.ind(f + 4)}${toYaml(id)}: {status: ${status}${logPart}}\n`);
|
|
367
|
+
}
|
|
368
|
+
if (startedAt)
|
|
369
|
+
this.append(`${this.ind(f + 2)}started_at: ${toYaml(startedAt)}\n`);
|
|
370
|
+
this.append(`${this.ind(f + 2)}ended_at: ${toYaml(timestamp())}\n`);
|
|
371
|
+
}
|
|
372
|
+
// replan 提报记录:降级阶梯第3/4档运行时生成的审计事件,供离线沉淀为预声明备用链路。
|
|
373
|
+
// 顶层事件、无视日志级别始终记录、不受 guardOrphan(replan 是 subtask 级跨进程事件,
|
|
374
|
+
// 不是叶子步 meta——subtask 容器本就不 recordStepStart,且跨进程 startedSteps 重置)。
|
|
375
|
+
// 见 design/spec-observability.md ^anc-obs-replan-audit。// @a: anc-obs-replan-audit
|
|
376
|
+
recordReplanAudit(audit) {
|
|
377
|
+
this.flushTopLevelResume(); // 顶层事件:resumed_at 落顶层(0 缩进)
|
|
378
|
+
this.append(`replan_audit:\n${toYaml(audit, 1)}\n`);
|
|
379
|
+
}
|
|
380
|
+
close(status) {
|
|
381
|
+
this.flushTopLevelResume(); // resume 后直接终态也不丢 marker——close 前落顶层 resumed_at(0 缩进)
|
|
382
|
+
this.append(`status: ${status}\n`);
|
|
383
|
+
this.append(`ended_at: ${toYaml(timestamp())}\n`);
|
|
384
|
+
}
|
|
385
|
+
getRunDir() {
|
|
386
|
+
return this.runDir;
|
|
387
|
+
}
|
|
388
|
+
getFilePath() {
|
|
389
|
+
return this.filePath;
|
|
390
|
+
}
|
|
391
|
+
flush() {
|
|
392
|
+
// All writes are immediate via appendFileSync — flush is a no-op
|
|
393
|
+
}
|
|
394
|
+
append(text) {
|
|
395
|
+
appendFileSync(this.filePath, text);
|
|
396
|
+
}
|
|
397
|
+
shouldRecord(level) {
|
|
398
|
+
return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[this.level];
|
|
399
|
+
}
|
|
400
|
+
filterValues(values) {
|
|
401
|
+
if (!values)
|
|
402
|
+
return undefined;
|
|
403
|
+
// warn: 不记录变量值(只看步骤骨架和警告)
|
|
404
|
+
if (this.level === 'warn')
|
|
405
|
+
return undefined;
|
|
406
|
+
// debug: 完整记录(仅脱敏 API key,不截断——debug 的核心价值是完整还原 LLM 输入)
|
|
407
|
+
// info: 脱敏 + 截断(摘要足够,控制文件大小)
|
|
408
|
+
return this.level === 'debug' ? sanitizeRecordFull(values) : sanitizeRecordTruncated(values);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
/** 日志脱敏 helper:抹除 API key 类敏感 pattern 并截断超长文本,保证密钥不落 HopLog。见 [[spec-observability#^anc-obs-log-levels]] */
|
|
412
|
+
export function sanitize(text) {
|
|
413
|
+
let s = text.replace(/\b(sk-|key-)[a-zA-Z0-9]{20,}/g, '$1***');
|
|
414
|
+
if (s.length > 200)
|
|
415
|
+
s = s.substring(0, 200) + ' [TRUNCATED]';
|
|
416
|
+
return s;
|
|
417
|
+
}
|
|
418
|
+
function sanitizeFull(text) {
|
|
419
|
+
return text.replace(/\b(sk-|key-)[a-zA-Z0-9]{20,}/g, '$1***');
|
|
420
|
+
}
|
|
421
|
+
/** 对 record 的字符串值逐个应用 fn(非字符串原样)。full/truncated 两个 sanitize 变体共用骨架。 */
|
|
422
|
+
function mapStringValues(rec, fn) {
|
|
423
|
+
const result = {};
|
|
424
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
425
|
+
result[k] = typeof v === 'string' ? fn(v) : v;
|
|
426
|
+
}
|
|
427
|
+
return result;
|
|
428
|
+
}
|
|
429
|
+
function sanitizeRecordFull(rec) {
|
|
430
|
+
return mapStringValues(rec, sanitizeFull);
|
|
431
|
+
}
|
|
432
|
+
function sanitizeRecordTruncated(rec) {
|
|
433
|
+
return mapStringValues(rec, sanitize);
|
|
434
|
+
}
|
|
435
|
+
function generateRunId() {
|
|
436
|
+
const now = new Date();
|
|
437
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
438
|
+
const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
|
|
439
|
+
const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
|
|
440
|
+
const hex = randomBytes(2).toString('hex');
|
|
441
|
+
return `${date}T${time}-${hex}`;
|
|
442
|
+
}
|
|
443
|
+
export function timestamp() {
|
|
444
|
+
const now = new Date();
|
|
445
|
+
const off = -now.getTimezoneOffset();
|
|
446
|
+
const sign = off >= 0 ? '+' : '-';
|
|
447
|
+
const h = String(Math.floor(Math.abs(off) / 60)).padStart(2, '0');
|
|
448
|
+
const m = String(Math.abs(off) % 60).padStart(2, '0');
|
|
449
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
450
|
+
// 毫秒级精度(.mmm 三位零填充):亚秒事件可分辨——同批 dispatch 落同一秒、join 起止单进程内间隔<1s。
|
|
451
|
+
// 见 design ^anc-obs-timestamp / ^anc-obs-parallel-dispatch。
|
|
452
|
+
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
453
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${ms}${sign}${h}:${m}`;
|
|
454
|
+
}
|
|
455
|
+
/** 把日志条目对象序列化为 block-style YAML 文本,供 record* 方法实时 appendFileSync 写入 main.yaml。见 [[spec-observability#^anc-obs-hoplog-flush]] */
|
|
456
|
+
export function toYaml(obj, indent = 0) {
|
|
457
|
+
const prefix = ' '.repeat(indent);
|
|
458
|
+
if (obj === null || obj === undefined)
|
|
459
|
+
return 'null';
|
|
460
|
+
if (typeof obj === 'boolean')
|
|
461
|
+
return String(obj);
|
|
462
|
+
if (typeof obj === 'number')
|
|
463
|
+
return String(obj);
|
|
464
|
+
if (typeof obj === 'string') {
|
|
465
|
+
if (obj.includes('\n')) {
|
|
466
|
+
const lines = obj.split('\n');
|
|
467
|
+
return '|\n' + lines.map(l => prefix + ' ' + l).join('\n');
|
|
468
|
+
}
|
|
469
|
+
if (obj === '' || /[:{}\[\],&*?|>!'"%@`#]/.test(obj) || /^\s/.test(obj) || /\s$/.test(obj)
|
|
470
|
+
|| /^-?\d+(\.\d+)?$/.test(obj) || obj === 'true' || obj === 'false' || obj === 'null') {
|
|
471
|
+
return JSON.stringify(obj);
|
|
472
|
+
}
|
|
473
|
+
return obj;
|
|
474
|
+
}
|
|
475
|
+
if (Array.isArray(obj)) {
|
|
476
|
+
if (obj.length === 0)
|
|
477
|
+
return '[]';
|
|
478
|
+
return obj.map(item => {
|
|
479
|
+
const val = toYaml(item, indent + 1);
|
|
480
|
+
if (typeof item === 'object' && item !== null && !Array.isArray(item)) {
|
|
481
|
+
return prefix + '- ' + val.trimStart();
|
|
482
|
+
}
|
|
483
|
+
return prefix + '- ' + val;
|
|
484
|
+
}).join('\n');
|
|
485
|
+
}
|
|
486
|
+
if (typeof obj === 'object') {
|
|
487
|
+
const entries = Object.entries(obj);
|
|
488
|
+
if (entries.length === 0)
|
|
489
|
+
return '{}';
|
|
490
|
+
return entries.map(([key, value]) => {
|
|
491
|
+
if (value === undefined)
|
|
492
|
+
return null;
|
|
493
|
+
const valStr = toYaml(value, indent + 1);
|
|
494
|
+
if (typeof value === 'object' && value !== null && (Array.isArray(value) ? value.length > 0 : Object.keys(value).length > 0)) {
|
|
495
|
+
return prefix + key + ':\n' + valStr;
|
|
496
|
+
}
|
|
497
|
+
return prefix + key + ': ' + valStr;
|
|
498
|
+
}).filter(Boolean).join('\n');
|
|
499
|
+
}
|
|
500
|
+
return String(obj);
|
|
501
|
+
}
|
package/dist/parser.d.ts
ADDED