@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/prompt.js
ADDED
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
import { hasChildren, getChildren, DEFLATE_THRESHOLD, HUMAN_PREVIEW_THRESHOLD, getParentStepId } from './ast-helpers.js';
|
|
2
|
+
import { serializeActBody } from './act-body-parser.js';
|
|
3
|
+
import { getWriteScope } from './ast-runtime.js';
|
|
4
|
+
import * as nodeFs from 'node:fs';
|
|
5
|
+
import * as nodePath from 'node:path';
|
|
6
|
+
const CHARS_PER_TOKEN = 4;
|
|
7
|
+
const TRUNCATE_MARKER = ' [TRUNCATED]';
|
|
8
|
+
const OUTPUT_VAL_MAX = 60; // 执行链里输出值预览的最大字符数
|
|
9
|
+
const OUTPUT_VAL_ELLIPSIS = '...'; // 超长时的省略号(截断长度 = MAX - 其长度,不再手算 57)
|
|
10
|
+
const AGGRESSIVE_INPUT_MAX = 400; // 预算裁尽仍超时,input 字符串激进截断的上限
|
|
11
|
+
const BUDGET_STANDARD = {
|
|
12
|
+
l1a: 800, l1b: 0, l2a: 600, l2b: 400, l3: 1200, l4: 600, l5: 400, total: 4000,
|
|
13
|
+
};
|
|
14
|
+
// L2c 重试反馈预算(带反馈重跑时才出现,单条最近失败说明)。见 prompt-assembler ^anc-exec-l2c-retry-feedback
|
|
15
|
+
const L2C_RETRY_FEEDBACK_BUDGET = 300;
|
|
16
|
+
const BUDGET_WITH_KNOWLEDGE = {
|
|
17
|
+
l1a: 600, l1b: 400, l2a: 600, l2b: 400, l3: 1200, l4: 600, l5: 400, total: 4400,
|
|
18
|
+
};
|
|
19
|
+
/** 上下文组装组件:按步骤类型把引擎运行时状态组成 6 层 AssembledContext(L1-L6)并做 token 预算裁剪,自身不发 API。见 [[prompt-assembler#^anc-exec-prompt-assembly]] */
|
|
20
|
+
export class PromptAssembler {
|
|
21
|
+
engine;
|
|
22
|
+
budget;
|
|
23
|
+
constructor(engine, hasKnowledgeProvider = false) {
|
|
24
|
+
this.engine = engine;
|
|
25
|
+
this.budget = hasKnowledgeProvider ? BUDGET_WITH_KNOWLEDGE : BUDGET_STANDARD;
|
|
26
|
+
}
|
|
27
|
+
assembleReasonContext(step) {
|
|
28
|
+
return this.assembleContext(step);
|
|
29
|
+
}
|
|
30
|
+
assembleCheckContext(step) {
|
|
31
|
+
return this.assembleContext(step);
|
|
32
|
+
}
|
|
33
|
+
assembleActContext(step) {
|
|
34
|
+
return this.assembleContext(step);
|
|
35
|
+
}
|
|
36
|
+
assembleAdaptiveContext(subtaskId) {
|
|
37
|
+
const spec = this.engine.getSpec();
|
|
38
|
+
const subtask = this.findStepById(subtaskId, spec);
|
|
39
|
+
if (!subtask) {
|
|
40
|
+
return {
|
|
41
|
+
task_context: '',
|
|
42
|
+
progress_summary: '',
|
|
43
|
+
inputs: {},
|
|
44
|
+
instruction: 'Replan required.',
|
|
45
|
+
output_schema: [],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const taskCtx = this.buildAdaptiveTaskContext(subtask);
|
|
49
|
+
const progress = this.buildSubtaskProgress(subtask);
|
|
50
|
+
const inputs = this.resolveInputs(subtask);
|
|
51
|
+
const instruction = `Replan required. The subtask "${subtask.summary}" failed. Provide new children steps that achieve the same outputs.`;
|
|
52
|
+
const outputSchema = subtask.outputs ?? [];
|
|
53
|
+
return {
|
|
54
|
+
task_context: truncateToChars(taskCtx, this.budget.l1a * CHARS_PER_TOKEN),
|
|
55
|
+
progress_summary: truncateToChars(progress, this.budget.l2a * CHARS_PER_TOKEN),
|
|
56
|
+
inputs,
|
|
57
|
+
instruction,
|
|
58
|
+
output_schema: outputSchema,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
reassembleAggressive(step) {
|
|
62
|
+
const spec = this.engine.getSpec();
|
|
63
|
+
const header = spec.header;
|
|
64
|
+
// L1a: Goal + direct parent only (1 level)
|
|
65
|
+
let taskCtx = `Goal: ${header.goal ?? ''}\n`;
|
|
66
|
+
const parentId = getParentStepId(step.step_id);
|
|
67
|
+
if (parentId) {
|
|
68
|
+
const parent = this.findStepById(parentId, spec);
|
|
69
|
+
if (parent) {
|
|
70
|
+
const outputs = parent.outputs?.map(o => o.name).join(', ') ?? '';
|
|
71
|
+
taskCtx += ` ${parent.step_id} [${parent.step_type}] ${parent.summary}${outputs ? ` | + → ${outputs}` : ''}\n`;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// L2a: dependency-driven progress (aggressive mode still uses same logic)
|
|
75
|
+
const progress = this.buildProgressSummary(step);
|
|
76
|
+
// L3: all values truncated to 400 chars (~100 tokens)
|
|
77
|
+
const inputs = this.resolveInputs(step, 400);
|
|
78
|
+
return {
|
|
79
|
+
task_context: taskCtx,
|
|
80
|
+
progress_summary: progress,
|
|
81
|
+
inputs,
|
|
82
|
+
instruction: step.instruction ?? step.summary,
|
|
83
|
+
output_schema: step.outputs ?? [],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
assembleContext(step) {
|
|
87
|
+
const budget = this.budget;
|
|
88
|
+
const taskCtx = this.buildTaskContext(step);
|
|
89
|
+
const progress = this.buildProgressSummary(step);
|
|
90
|
+
const iterHistory = this.buildIterationHistory(step);
|
|
91
|
+
const retryFeedback = this.buildRetryFeedback(step);
|
|
92
|
+
const inputs = this.resolveInputs(step);
|
|
93
|
+
const instruction = this.buildInstruction(step);
|
|
94
|
+
const outputSchema = step.outputs ?? [];
|
|
95
|
+
let ctx = {
|
|
96
|
+
task_context: truncateToChars(taskCtx, budget.l1a * CHARS_PER_TOKEN),
|
|
97
|
+
progress_summary: truncateToChars(progress, budget.l2a * CHARS_PER_TOKEN),
|
|
98
|
+
inputs,
|
|
99
|
+
instruction,
|
|
100
|
+
output_schema: outputSchema,
|
|
101
|
+
};
|
|
102
|
+
if (iterHistory) {
|
|
103
|
+
ctx.iteration_history = truncateToChars(iterHistory, budget.l2b * CHARS_PER_TOKEN);
|
|
104
|
+
}
|
|
105
|
+
if (retryFeedback) {
|
|
106
|
+
ctx.retry_feedback = truncateToChars(retryFeedback, L2C_RETRY_FEEDBACK_BUDGET * CHARS_PER_TOKEN);
|
|
107
|
+
}
|
|
108
|
+
ctx = this.applyBudgetTrimming(ctx);
|
|
109
|
+
return ctx;
|
|
110
|
+
}
|
|
111
|
+
// L2c 重试反馈:subtask 带反馈重跑时,注入上次失败说明(check 的 text 槽 / 失败原因),
|
|
112
|
+
// 让重跑执行者知道"上次错在哪"。首跑(无重试历史)返回 undefined 不渲染。
|
|
113
|
+
// 见 [[../../HopSpec V3核心规范#^anc-exec-retry-adaptive]] / prompt-assembler ^anc-exec-l2c-retry-feedback
|
|
114
|
+
// @a: anc-exec-l2c-retry-feedback
|
|
115
|
+
buildRetryFeedback(step) {
|
|
116
|
+
const fb = this.engine.getActiveRetryFeedback(step.step_id);
|
|
117
|
+
if (!fb)
|
|
118
|
+
return undefined;
|
|
119
|
+
return `⚠️ 上次尝试失败(第 ${fb.attempt} 次):${fb.reason}\n→ 本次重跑请针对此修正。`;
|
|
120
|
+
}
|
|
121
|
+
// L1 骨架:静态步骤树——step_id+[type]+summary+树结构+容器关键属性,
|
|
122
|
+
// 不含状态/数据/节点体(←/+→/>)。给 LLM 全局计划地图,与 L3 动态执行链互补。
|
|
123
|
+
// worker 子实例(subtreeRoot!=null)时裁到子树视图+父 parallel 身份头。
|
|
124
|
+
// 见 [[../../HopSpec V3核心规范#^anc-exec-l1-skeleton]]
|
|
125
|
+
/** 渲染骨架单节点(含容器 attr:loop max / subtask retry+adaptive / case condition)+ 递归 children。
|
|
126
|
+
* buildSpecSkeleton 与 buildSubtreeSkeleton 共用(原两处 render 逐字复制)。 */
|
|
127
|
+
renderSkeletonNode(node, depth, lines) {
|
|
128
|
+
const indent = ' '.repeat(depth);
|
|
129
|
+
let attr = '';
|
|
130
|
+
if (node.step_type === 'loop') {
|
|
131
|
+
const m = node.max_iterations;
|
|
132
|
+
if (m != null)
|
|
133
|
+
attr = ` max=${m}`;
|
|
134
|
+
}
|
|
135
|
+
else if (node.step_type === 'subtask') {
|
|
136
|
+
const st = node;
|
|
137
|
+
if (st.retry != null)
|
|
138
|
+
attr += ` retry=${st.retry}`;
|
|
139
|
+
if (st.adaptive)
|
|
140
|
+
attr += ` adaptive`;
|
|
141
|
+
}
|
|
142
|
+
else if (node.step_type === 'case') {
|
|
143
|
+
const c = node.condition;
|
|
144
|
+
if (c)
|
|
145
|
+
attr = `: ${c}`;
|
|
146
|
+
}
|
|
147
|
+
lines.push(`${indent}${node.step_id}. [${node.step_type}${attr}] ${node.summary}`);
|
|
148
|
+
if (hasChildren(node)) {
|
|
149
|
+
for (const child of getChildren(node))
|
|
150
|
+
this.renderSkeletonNode(child, depth + 1, lines);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
buildSpecSkeleton(spec) {
|
|
154
|
+
const subtreeRoot = this.engine.getSubtreeRoot();
|
|
155
|
+
if (subtreeRoot)
|
|
156
|
+
return this.buildSubtreeSkeleton(spec, subtreeRoot);
|
|
157
|
+
const lines = [];
|
|
158
|
+
for (const s of spec.steps ?? [])
|
|
159
|
+
this.renderSkeletonNode(s, 0, lines);
|
|
160
|
+
return lines.join('\n');
|
|
161
|
+
}
|
|
162
|
+
// worker 子树视图骨架:只渲染目标 child 子树 + 父 parallel 一行身份标注
|
|
163
|
+
// @a: anc-exec-parallel-worker-prompt, anc-exec-l1-skeleton
|
|
164
|
+
buildSubtreeSkeleton(spec, subtreeRootId) {
|
|
165
|
+
const lines = [];
|
|
166
|
+
const rootNode = this.findStepById(subtreeRootId, spec);
|
|
167
|
+
if (!rootNode)
|
|
168
|
+
return '';
|
|
169
|
+
// 父 parallel 身份标注
|
|
170
|
+
const parentId = getParentStepId(subtreeRootId);
|
|
171
|
+
if (parentId) {
|
|
172
|
+
const parent = this.findStepById(parentId, spec);
|
|
173
|
+
if (parent) {
|
|
174
|
+
lines.push(`${parent.step_id}. [${parent.step_type}] ${parent.summary} ← 你负责 ${subtreeRootId} 分支(与其它分支并行执行,无需关心兄弟)`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// 子树骨架
|
|
178
|
+
this.renderSkeletonNode(rootNode, 1, lines);
|
|
179
|
+
return lines.join('\n');
|
|
180
|
+
}
|
|
181
|
+
buildTaskContext(step) {
|
|
182
|
+
const spec = this.engine.getSpec();
|
|
183
|
+
const header = spec.header;
|
|
184
|
+
// minimal 模式非首步:砍跨步不变的定向段(Goal/Types/Outputs/骨架),保 Constraints(安全底线)+
|
|
185
|
+
// 祖先位置链(动态锚点)。首步(!hasAnyDone)或 full 模式 → 全量自包含。见 ^anc-exec-context-mode。
|
|
186
|
+
// @a: anc-exec-context-mode
|
|
187
|
+
const hasAnyDone = [...this.engine.getStepStates().values()].some(s => s === 'done' || s === 'failed');
|
|
188
|
+
const lean = this.engine.getContextMode() === 'minimal' && hasAnyDone;
|
|
189
|
+
let ctx = lean ? '' : `Goal: ${header.goal ?? ''}\n`;
|
|
190
|
+
if (header.constraints?.length) {
|
|
191
|
+
ctx += `Constraints: ${header.constraints.join('; ')}\n`; // 安全约束:minimal 也保
|
|
192
|
+
}
|
|
193
|
+
if (!lean && header.types?.length) {
|
|
194
|
+
ctx += `Types: ${header.types.map(t => t.name).join(', ')}\n`;
|
|
195
|
+
}
|
|
196
|
+
if (!lean && header.outputs?.length) {
|
|
197
|
+
ctx += `Outputs: ${header.outputs.map(o => `${o.name}: ${o.type}`).join(', ')}\n`;
|
|
198
|
+
}
|
|
199
|
+
// worker 子实例:显式承载子任务目标契约(让 worker 明白自己要交付什么)
|
|
200
|
+
// @a: anc-exec-parallel-worker-prompt
|
|
201
|
+
const subtreeRoot = this.engine.getSubtreeRoot();
|
|
202
|
+
if (subtreeRoot) {
|
|
203
|
+
const rootNode = this.findStepById(subtreeRoot, spec);
|
|
204
|
+
if (rootNode) {
|
|
205
|
+
const deliverables = rootNode.outputs?.map(o => `${o.name}: ${o.type}`).join(', ') ?? '';
|
|
206
|
+
ctx += `\n[你的子任务] ${rootNode.summary}\n`;
|
|
207
|
+
if (deliverables)
|
|
208
|
+
ctx += `交付输出: ${deliverables}\n`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// L1 骨架:静态步骤树,给 LLM 全局计划与自身位置(见 ^anc-exec-l1-skeleton)。minimal 非首步跳过。
|
|
212
|
+
if (!lean) {
|
|
213
|
+
const skeleton = this.buildSpecSkeleton(spec);
|
|
214
|
+
if (skeleton)
|
|
215
|
+
ctx += `Steps 骨架:\n${skeleton}\n`;
|
|
216
|
+
}
|
|
217
|
+
// 祖先链:worker 子实例跳过(子树骨架已含父 parallel 头,祖先链是冗余重复)
|
|
218
|
+
if (!subtreeRoot) {
|
|
219
|
+
const ancestors = this.getAncestorChain(step.step_id, spec);
|
|
220
|
+
const loopCounters = this.engine.getLoopCounters();
|
|
221
|
+
for (let i = 0; i < ancestors.length; i++) {
|
|
222
|
+
const a = ancestors[i];
|
|
223
|
+
const depth = i + 1;
|
|
224
|
+
const indent = ' '.repeat(depth);
|
|
225
|
+
const outputs = a.outputs?.map(o => o.name).join(', ') ?? '';
|
|
226
|
+
let loopTag = '';
|
|
227
|
+
if (a.step_type === 'loop') {
|
|
228
|
+
const iter = loopCounters.get(a.step_id);
|
|
229
|
+
const max = a.max_iterations;
|
|
230
|
+
if (iter != null)
|
|
231
|
+
loopTag = ` (iter ${iter}${max ? '/' + max : ''})`;
|
|
232
|
+
}
|
|
233
|
+
if (depth <= 3) {
|
|
234
|
+
ctx += `${indent}${a.step_id} [${a.step_type}]${loopTag} ${a.summary}${outputs ? ` | + → ${outputs}` : ''}\n`;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
ctx += `${indent}${a.step_id}: ${a.summary}${outputs ? ` → ${outputs}` : ''}\n`;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return ctx;
|
|
242
|
+
}
|
|
243
|
+
buildProgressSummary(currentStep) {
|
|
244
|
+
const spec = this.engine.getSpec();
|
|
245
|
+
const states = this.engine.getStepStates();
|
|
246
|
+
const hasAnyDone = [...states.values()].some(s => s === 'done' || s === 'failed');
|
|
247
|
+
if (!hasAnyDone)
|
|
248
|
+
return 'No steps completed yet.';
|
|
249
|
+
const fullDisplaySet = this.buildFullDisplaySet(currentStep, spec);
|
|
250
|
+
const lines = [];
|
|
251
|
+
for (const step of spec.steps ?? []) {
|
|
252
|
+
this.renderStepProgress(step, fullDisplaySet, 0, lines);
|
|
253
|
+
}
|
|
254
|
+
return lines.join('\n');
|
|
255
|
+
}
|
|
256
|
+
buildFullDisplaySet(currentStep, spec) {
|
|
257
|
+
const depVarNames = currentStep.inputs?.map(i => i.source) ?? [];
|
|
258
|
+
const depStepIds = this.findProducerSteps(depVarNames, spec);
|
|
259
|
+
const siblings = this.getSiblingStepIds(currentStep.step_id, spec);
|
|
260
|
+
const states = this.engine.getStepStates();
|
|
261
|
+
const doneSiblings = siblings.filter(id => states.get(id) === 'done' || states.get(id) === 'failed');
|
|
262
|
+
const recentSiblings = doneSiblings.slice(-5);
|
|
263
|
+
return new Set([...depStepIds, ...recentSiblings]);
|
|
264
|
+
}
|
|
265
|
+
renderStepProgress(step, fullDisplaySet, indent, lines) {
|
|
266
|
+
const states = this.engine.getStepStates();
|
|
267
|
+
const state = states.get(step.step_id);
|
|
268
|
+
if (!state || state === 'pending')
|
|
269
|
+
return;
|
|
270
|
+
const prefix = ' '.repeat(indent);
|
|
271
|
+
const isContainer = hasChildren(step);
|
|
272
|
+
// skipped:L3 是给后继提供可引用产出,未走路径无产出、对后继无价值 → 不显示
|
|
273
|
+
if (state === 'skipped')
|
|
274
|
+
return;
|
|
275
|
+
// branch:折叠为单行——标命中 case + 展开 branch 聚合输出,不展开 case 内部、不显示 skipped case。
|
|
276
|
+
// @a: anc-exec-l3-branch
|
|
277
|
+
if (step.step_type === 'branch') {
|
|
278
|
+
const taken = getChildren(step).find(c => {
|
|
279
|
+
const cs = states.get(c.step_id);
|
|
280
|
+
return cs === 'done' || cs === 'running' || cs === 'failed';
|
|
281
|
+
});
|
|
282
|
+
const outputVals = this.formatContainerOutputs(step);
|
|
283
|
+
const hitLabel = taken ? ` ◀ 命中 ${taken.step_id}` : '(无 case 命中,输出 None)';
|
|
284
|
+
lines.push(`${prefix}✓ ${step.step_id} [branch]: ${step.summary}${hitLabel}${outputVals}`);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (fullDisplaySet.has(step.step_id)) {
|
|
288
|
+
lines.push(`${prefix}${this.formatStepFull(step)}`);
|
|
289
|
+
}
|
|
290
|
+
else if (isContainer && state === 'done') {
|
|
291
|
+
const outputNames = step.outputs?.map(o => o.name).join(', ') ?? '';
|
|
292
|
+
lines.push(`${prefix}✓ ${step.step_id} [${step.step_type}]: ${step.summary}${outputNames ? ` → ${outputNames}` : ''}`);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
else if (state === 'done' || state === 'failed') {
|
|
296
|
+
const outputNames = step.outputs?.map(o => o.name).join(', ') ?? '';
|
|
297
|
+
const marker = state === 'done' ? '✓' : '✗';
|
|
298
|
+
lines.push(`${prefix}${marker} ${step.step_id}${outputNames ? `: ${outputNames}` : ''}`);
|
|
299
|
+
}
|
|
300
|
+
else if (state === 'running') {
|
|
301
|
+
lines.push(`${prefix}▶ ${step.step_id} [${step.step_type}]: ${step.summary}`);
|
|
302
|
+
}
|
|
303
|
+
if (isContainer) {
|
|
304
|
+
for (const child of getChildren(step)) {
|
|
305
|
+
this.renderStepProgress(child, fullDisplaySet, indent + 1, lines);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
formatStepFull(step) {
|
|
310
|
+
const state = this.engine.getStepStates().get(step.step_id);
|
|
311
|
+
const marker = state === 'done' ? '✓' : state === 'failed' ? '✗' : '▶';
|
|
312
|
+
const outputVals = (step.outputs && state === 'done') ? this.formatOutputVals(step) : [];
|
|
313
|
+
return `${marker} ${step.step_id} [${step.step_type}]: ${step.summary}${outputVals.length ? ` → ${outputVals.join(', ')}` : ''}`;
|
|
314
|
+
}
|
|
315
|
+
/** 把步骤输出格式化为 `name=值(截断) # desc` 列表——读 scope 变量、JSON.stringify、超长截断。
|
|
316
|
+
* formatStepFull / formatContainerOutputs 共用(原两处逐字复制)。 */
|
|
317
|
+
formatOutputVals(step) {
|
|
318
|
+
const spec = this.engine.getSpec();
|
|
319
|
+
const vars = this.engine.getVariableStore();
|
|
320
|
+
const scopeId = getWriteScope(step.step_id, spec);
|
|
321
|
+
return (step.outputs ?? []).map(o => {
|
|
322
|
+
const val = vars.read(o.name, scopeId);
|
|
323
|
+
const str = val != null ? JSON.stringify(val) : 'null';
|
|
324
|
+
const preview = str.length > OUTPUT_VAL_MAX
|
|
325
|
+
? str.slice(0, OUTPUT_VAL_MAX - OUTPUT_VAL_ELLIPSIS.length) + OUTPUT_VAL_ELLIPSIS
|
|
326
|
+
: str;
|
|
327
|
+
const desc = o.description ? ` # ${o.description}` : '';
|
|
328
|
+
return `${o.name}=${preview}${desc}`;
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
// 展开容器聚合输出值(branch 折叠展示用)——读容器透传到父 scope 的输出
|
|
332
|
+
formatContainerOutputs(step) {
|
|
333
|
+
if (!step.outputs || step.outputs.length === 0)
|
|
334
|
+
return '';
|
|
335
|
+
const parts = this.formatOutputVals(step);
|
|
336
|
+
return parts.length ? ` → ${parts.join(', ')}` : '';
|
|
337
|
+
}
|
|
338
|
+
findProducerSteps(varNames, spec) {
|
|
339
|
+
if (varNames.length === 0)
|
|
340
|
+
return [];
|
|
341
|
+
const nameSet = new Set(varNames);
|
|
342
|
+
const producers = [];
|
|
343
|
+
const walk = (steps) => {
|
|
344
|
+
for (const s of steps) {
|
|
345
|
+
if (s.outputs?.some(o => nameSet.has(o.name)))
|
|
346
|
+
producers.push(s.step_id);
|
|
347
|
+
if (hasChildren(s))
|
|
348
|
+
walk(getChildren(s));
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
walk(spec.steps ?? []);
|
|
352
|
+
return producers;
|
|
353
|
+
}
|
|
354
|
+
getSiblingStepIds(stepId, spec) {
|
|
355
|
+
const parentId = getParentStepId(stepId);
|
|
356
|
+
if (!parentId)
|
|
357
|
+
return (spec.steps ?? []).map(s => s.step_id);
|
|
358
|
+
const parent = this.findStepById(parentId, spec);
|
|
359
|
+
if (!parent || !hasChildren(parent))
|
|
360
|
+
return [];
|
|
361
|
+
return getChildren(parent).map(s => s.step_id);
|
|
362
|
+
}
|
|
363
|
+
buildIterationHistory(step) {
|
|
364
|
+
const spec = this.engine.getSpec();
|
|
365
|
+
const loopAncestor = this.findNearestLoopAncestor(step.step_id, spec);
|
|
366
|
+
if (!loopAncestor)
|
|
367
|
+
return undefined;
|
|
368
|
+
const loopCounters = this.engine.getLoopCounters();
|
|
369
|
+
const currentIter = loopCounters.get(loopAncestor.step_id) ?? 1;
|
|
370
|
+
if (currentIter <= 1)
|
|
371
|
+
return undefined;
|
|
372
|
+
const log = this.engine.getExecEvents();
|
|
373
|
+
const loopPrefix = loopAncestor.step_id + '.';
|
|
374
|
+
// Group log entries by iteration
|
|
375
|
+
const iterations = [];
|
|
376
|
+
let iterEntries = [];
|
|
377
|
+
let lastIter = 0;
|
|
378
|
+
for (const entry of log) {
|
|
379
|
+
if (!entry.step_id.startsWith(loopPrefix))
|
|
380
|
+
continue;
|
|
381
|
+
if (entry.event === 'step_start' && entry.step_id === loopAncestor.children[0]?.step_id) {
|
|
382
|
+
if (iterEntries.length > 0)
|
|
383
|
+
iterations.push(iterEntries);
|
|
384
|
+
iterEntries = [];
|
|
385
|
+
lastIter++;
|
|
386
|
+
}
|
|
387
|
+
iterEntries.push(entry);
|
|
388
|
+
}
|
|
389
|
+
if (iterEntries.length > 0)
|
|
390
|
+
iterations.push(iterEntries);
|
|
391
|
+
if (iterations.length <= 1)
|
|
392
|
+
return undefined;
|
|
393
|
+
const lines = [`Loop ${loopAncestor.step_id} — iteration ${currentIter}:`];
|
|
394
|
+
const recentStart = Math.max(0, iterations.length - 3);
|
|
395
|
+
for (let i = 0; i < iterations.length - 1; i++) {
|
|
396
|
+
const iter = iterations[i];
|
|
397
|
+
const iterNum = i + 1;
|
|
398
|
+
if (i < recentStart) {
|
|
399
|
+
const completed = iter.filter(e => e.event === 'step_done').length;
|
|
400
|
+
const failed = iter.filter(e => e.event === 'step_failed').length;
|
|
401
|
+
lines.push(` iter ${iterNum}: ${completed} done, ${failed} failed`);
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
lines.push(` iter ${iterNum}:`);
|
|
405
|
+
for (const e of iter) {
|
|
406
|
+
if (e.event === 'step_done' || e.event === 'step_failed') {
|
|
407
|
+
lines.push(` ${e.event === 'step_done' ? '✓' : '✗'} ${e.step_id}${e.detail ? ': ' + e.detail : ''}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return lines.join('\n');
|
|
413
|
+
}
|
|
414
|
+
// 解析 step 的 ← inputs 取值。两种模式:
|
|
415
|
+
// - 默认(forceTruncate=undefined):走 agent 通道 deflate——小值原样;大值(>DEFLATE_THRESHOLD=4096)写
|
|
416
|
+
// work_zone/vars/<name>.json,返 {$file:abs_path} 指针。LLM 看到指针决定要不要 Read 拿真值。
|
|
417
|
+
// 见 design/prompt-assembler.md 决策5 ^anc-exec-inputs-deflate / shared-types ^anc-exec-deflate。
|
|
418
|
+
// - forceTruncate=N(reassembleAggressive 救命模式):用 [TRUNCATED] 强截断到 N 字符,接受信息损失。
|
|
419
|
+
// 独立模式 workZone 空串 → fallback 原值(无截断无 deflate)。// @a: anc-exec-inputs-deflate
|
|
420
|
+
resolveInputs(step, forceTruncate) {
|
|
421
|
+
const spec = this.engine.getSpec();
|
|
422
|
+
const vars = this.engine.getVariableStore();
|
|
423
|
+
const scopeId = getWriteScope(step.step_id, spec);
|
|
424
|
+
const workZone = this.engine.getWorkZone();
|
|
425
|
+
const inputs = {};
|
|
426
|
+
if (!step.inputs)
|
|
427
|
+
return inputs;
|
|
428
|
+
for (const binding of step.inputs) {
|
|
429
|
+
const val = vars.read(binding.source, scopeId);
|
|
430
|
+
if (val === null) {
|
|
431
|
+
inputs[binding.name] = null;
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (val === undefined) {
|
|
435
|
+
inputs[binding.name] = undefined;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
// 激进救命模式:强截断
|
|
439
|
+
if (forceTruncate !== undefined) {
|
|
440
|
+
const str = typeof val === 'string' ? val : (JSON.stringify(val) ?? '');
|
|
441
|
+
if (str.length > forceTruncate) {
|
|
442
|
+
inputs[binding.name] = str.slice(0, forceTruncate - TRUNCATE_MARKER.length) + TRUNCATE_MARKER;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
inputs[binding.name] = val;
|
|
446
|
+
}
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
// 默认 agent 通道:小值原样,大值 deflate 到 $file
|
|
450
|
+
const serialized = JSON.stringify(val);
|
|
451
|
+
if (workZone && serialized && serialized.length > DEFLATE_THRESHOLD) {
|
|
452
|
+
const filePath = nodePath.join(workZone, 'vars', `${binding.name}.json`);
|
|
453
|
+
nodeFs.writeFileSync(filePath, serialized, { mode: 0o600 });
|
|
454
|
+
inputs[binding.name] = { $file: filePath };
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
inputs[binding.name] = val;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return inputs;
|
|
461
|
+
}
|
|
462
|
+
buildInstruction(step) {
|
|
463
|
+
let base = step.instruction ? `${step.summary}\n${step.instruction}` : step.summary;
|
|
464
|
+
// call 步骤:附加 callee + 双向映射信息,供 CC 驱动子 spec 调用 // @a: anc-step-call
|
|
465
|
+
if (step.step_type === 'call') {
|
|
466
|
+
const call = step;
|
|
467
|
+
const lines = [`[CALL] callee_spec_id: ${call.callee_spec_id ?? '(unspecified)'}`];
|
|
468
|
+
if (call.param_mapping?.length) {
|
|
469
|
+
lines.push('inputs (子参数 ← 父变量): ' + call.param_mapping.map(m => `${m.to} ← ${m.from}`).join(', '));
|
|
470
|
+
}
|
|
471
|
+
if (call.output_mapping?.length) {
|
|
472
|
+
lines.push('outputs (父变量 ← 子输出): ' + call.output_mapping.map(m => `${m.to} ← ${m.from}`).join(', '));
|
|
473
|
+
}
|
|
474
|
+
base += '\n' + lines.join('\n');
|
|
475
|
+
}
|
|
476
|
+
// act/commit 有结构化 body:复用模式交付 body 文本给 caller(CC)严格按此执行。
|
|
477
|
+
// body 含工具调用(caller 按 ToolProvider 清单注册的同名工具执行)。// @a: anc-exec-act-body-interp
|
|
478
|
+
if ((step.step_type === 'act' || step.step_type === 'commit') && step.body) {
|
|
479
|
+
const bodyText = serializeActBody(step.body);
|
|
480
|
+
base += '\n\n[ACT BODY — 严格按此 hop_python 逻辑执行,不要重新规划;工具调用对应你按 ToolProvider 清单注册的同名工具]\n```hop_python\n'
|
|
481
|
+
+ bodyText + '\n```';
|
|
482
|
+
}
|
|
483
|
+
return base;
|
|
484
|
+
}
|
|
485
|
+
buildAdaptiveTaskContext(subtask) {
|
|
486
|
+
const spec = this.engine.getSpec();
|
|
487
|
+
const header = spec.header;
|
|
488
|
+
let ctx = `Goal: ${header.goal ?? ''}\n`;
|
|
489
|
+
ctx += `Subtask: ${subtask.summary}\n`;
|
|
490
|
+
if (subtask.outputs?.length) {
|
|
491
|
+
ctx += `Required outputs: ${subtask.outputs.map(o => `${o.name}: ${o.type}`).join(', ')}\n`;
|
|
492
|
+
}
|
|
493
|
+
return ctx;
|
|
494
|
+
}
|
|
495
|
+
buildSubtaskProgress(subtask) {
|
|
496
|
+
const states = this.engine.getStepStates();
|
|
497
|
+
const log = this.engine.getExecEvents();
|
|
498
|
+
const lines = [];
|
|
499
|
+
for (const child of subtask.children) {
|
|
500
|
+
const state = states.get(child.step_id);
|
|
501
|
+
if (state === 'done') {
|
|
502
|
+
lines.push(`✓ ${child.step_id} [${child.step_type}]: ${child.summary}`);
|
|
503
|
+
}
|
|
504
|
+
else if (state === 'failed') {
|
|
505
|
+
const failEntry = [...log].reverse().find(e => e.step_id === child.step_id && e.event === 'step_failed');
|
|
506
|
+
lines.push(`✗ ${child.step_id} [${child.step_type}]: ${child.summary} — FAILED: ${failEntry?.detail ?? 'unknown'}`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
return lines.length > 0 ? lines.join('\n') : 'No steps executed in this subtask yet.';
|
|
510
|
+
}
|
|
511
|
+
applyBudgetTrimming(ctx) {
|
|
512
|
+
const total = this.estimateContextTokens(ctx);
|
|
513
|
+
if (total <= this.budget.total)
|
|
514
|
+
return ctx;
|
|
515
|
+
// 按价值从低到高裁剪:L3b iteration_history → L3 progress_summary → L2 knowledge
|
|
516
|
+
// → L2d doc_ref → L1 task_context → L2c retry_feedback → L4 inputs(L5/L6 不裁)
|
|
517
|
+
let excess = total - this.budget.total;
|
|
518
|
+
if (ctx.iteration_history && excess > 0) {
|
|
519
|
+
const saved = this.estimateTokens(ctx.iteration_history);
|
|
520
|
+
ctx.iteration_history = undefined;
|
|
521
|
+
excess -= saved;
|
|
522
|
+
}
|
|
523
|
+
if (excess > 0) {
|
|
524
|
+
const current = this.estimateTokens(ctx.progress_summary);
|
|
525
|
+
const target = Math.max(100, current - excess);
|
|
526
|
+
ctx.progress_summary = truncateToChars(ctx.progress_summary, target * CHARS_PER_TOKEN);
|
|
527
|
+
excess -= (current - target);
|
|
528
|
+
}
|
|
529
|
+
if (ctx.knowledge_context && excess > 0) {
|
|
530
|
+
const saved = this.estimateTokens(ctx.knowledge_context);
|
|
531
|
+
ctx.knowledge_context = undefined;
|
|
532
|
+
excess -= saved;
|
|
533
|
+
}
|
|
534
|
+
// L2d doc-ref:作者显式引用,优先级高于 knowledge(故在其后裁剪),仅在 knowledge 裁尽仍超才砍
|
|
535
|
+
// @a: anc-exec-doc-ref-injection
|
|
536
|
+
if (ctx.doc_ref_context && excess > 0) {
|
|
537
|
+
const saved = this.estimateTokens(ctx.doc_ref_context);
|
|
538
|
+
ctx.doc_ref_context = undefined;
|
|
539
|
+
excess -= saved;
|
|
540
|
+
}
|
|
541
|
+
if (excess > 0) {
|
|
542
|
+
const current = this.estimateTokens(ctx.task_context);
|
|
543
|
+
const target = Math.max(100, current - excess);
|
|
544
|
+
ctx.task_context = truncateToChars(ctx.task_context, target * CHARS_PER_TOKEN);
|
|
545
|
+
excess -= (current - target);
|
|
546
|
+
}
|
|
547
|
+
// L2c 重试反馈靠后裁剪(价值高,直接指导重跑修正),仅 L1/L2/L3 裁尽仍超才裁
|
|
548
|
+
if (ctx.retry_feedback && excess > 0) {
|
|
549
|
+
const saved = this.estimateTokens(ctx.retry_feedback);
|
|
550
|
+
ctx.retry_feedback = undefined;
|
|
551
|
+
excess -= saved;
|
|
552
|
+
}
|
|
553
|
+
if (excess > 0) {
|
|
554
|
+
// Aggressive input truncation
|
|
555
|
+
for (const key of Object.keys(ctx.inputs)) {
|
|
556
|
+
const val = ctx.inputs[key];
|
|
557
|
+
if (typeof val === 'string' && val.length > AGGRESSIVE_INPUT_MAX) {
|
|
558
|
+
ctx.inputs[key] = val.slice(0, AGGRESSIVE_INPUT_MAX - TRUNCATE_MARKER.length) + TRUNCATE_MARKER;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
return ctx;
|
|
563
|
+
}
|
|
564
|
+
estimateContextTokens(ctx) {
|
|
565
|
+
let total = this.estimateTokens(ctx.task_context);
|
|
566
|
+
total += this.estimateTokens(ctx.progress_summary);
|
|
567
|
+
total += this.estimateTokens(ctx.instruction);
|
|
568
|
+
total += this.estimateTokens(JSON.stringify(ctx.output_schema));
|
|
569
|
+
total += this.estimateTokens(JSON.stringify(ctx.inputs));
|
|
570
|
+
if (ctx.knowledge_context)
|
|
571
|
+
total += this.estimateTokens(ctx.knowledge_context);
|
|
572
|
+
if (ctx.doc_ref_context)
|
|
573
|
+
total += this.estimateTokens(ctx.doc_ref_context);
|
|
574
|
+
if (ctx.iteration_history)
|
|
575
|
+
total += this.estimateTokens(ctx.iteration_history);
|
|
576
|
+
if (ctx.retry_feedback)
|
|
577
|
+
total += this.estimateTokens(ctx.retry_feedback);
|
|
578
|
+
return total;
|
|
579
|
+
}
|
|
580
|
+
estimateTokens(text) {
|
|
581
|
+
return Math.ceil(text.length / CHARS_PER_TOKEN);
|
|
582
|
+
}
|
|
583
|
+
getAncestorChain(stepId, spec) {
|
|
584
|
+
const ancestors = [];
|
|
585
|
+
let currentId = getParentStepId(stepId);
|
|
586
|
+
while (currentId) {
|
|
587
|
+
const step = this.findStepById(currentId, spec);
|
|
588
|
+
if (step)
|
|
589
|
+
ancestors.unshift(step);
|
|
590
|
+
currentId = getParentStepId(currentId);
|
|
591
|
+
}
|
|
592
|
+
return ancestors;
|
|
593
|
+
}
|
|
594
|
+
findNearestLoopAncestor(stepId, spec) {
|
|
595
|
+
let currentId = getParentStepId(stepId);
|
|
596
|
+
while (currentId) {
|
|
597
|
+
const step = this.findStepById(currentId, spec);
|
|
598
|
+
if (step && step.step_type === 'loop')
|
|
599
|
+
return step;
|
|
600
|
+
currentId = getParentStepId(currentId);
|
|
601
|
+
}
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
findStepById(stepId, spec) {
|
|
605
|
+
const search = (steps) => {
|
|
606
|
+
for (const step of steps) {
|
|
607
|
+
if (step.step_id === stepId)
|
|
608
|
+
return step;
|
|
609
|
+
if (hasChildren(step)) {
|
|
610
|
+
const found = search(getChildren(step));
|
|
611
|
+
if (found)
|
|
612
|
+
return found;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return null;
|
|
616
|
+
};
|
|
617
|
+
return search(spec.steps ?? []);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
function truncateToChars(text, maxChars) {
|
|
621
|
+
if (text.length <= maxChars)
|
|
622
|
+
return text;
|
|
623
|
+
return text.slice(0, maxChars - TRUNCATE_MARKER.length) + TRUNCATE_MARKER;
|
|
624
|
+
}
|
|
625
|
+
// ============================================================
|
|
626
|
+
// L2 Knowledge Retrieval // @a: anc-exec-knowledge-retrieval
|
|
627
|
+
// ============================================================
|
|
628
|
+
const RE_KNOWLEDGE_HINT = /@knowledge\s+([^\n@]+)/g;
|
|
629
|
+
const L2_BUDGET_CHARS = 8000 * CHARS_PER_TOKEN; // 8000 tokens
|
|
630
|
+
/**
|
|
631
|
+
* Extract @knowledge hints from text (constraints, instruction, etc.)
|
|
632
|
+
* Returns unique keywords/phrases declared via `@knowledge <term>`.
|
|
633
|
+
*/
|
|
634
|
+
export function extractKnowledgeHints(text) {
|
|
635
|
+
if (!text)
|
|
636
|
+
return [];
|
|
637
|
+
const hints = [];
|
|
638
|
+
let match;
|
|
639
|
+
const re = new RegExp(RE_KNOWLEDGE_HINT.source, 'g');
|
|
640
|
+
while ((match = re.exec(text)) !== null) {
|
|
641
|
+
const hint = match[1].trim();
|
|
642
|
+
if (hint && !hints.includes(hint)) {
|
|
643
|
+
hints.push(hint);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return hints;
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Collects all @knowledge hints from spec constraints and a step's instruction.
|
|
650
|
+
*/
|
|
651
|
+
export function collectKnowledgeHints(spec, step) {
|
|
652
|
+
// Spec-level: scan constraints
|
|
653
|
+
const constraintsText = (spec.header.constraints ?? []).join('\n');
|
|
654
|
+
const specHints = extractKnowledgeHints(constraintsText);
|
|
655
|
+
// Step-level: scan instruction
|
|
656
|
+
const stepHints = extractKnowledgeHints(step.instruction ?? '');
|
|
657
|
+
return { specHints, stepHints };
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Retrieve knowledge from a KnowledgeProvider based on @knowledge hints.
|
|
661
|
+
* This is an async operation — call from the dispatcher after assembling context.
|
|
662
|
+
*
|
|
663
|
+
* @param provider - The KnowledgeProvider to query
|
|
664
|
+
* @param specHints - Global @knowledge hints from spec constraints
|
|
665
|
+
* @param stepHints - Step-scoped @knowledge hints from instruction
|
|
666
|
+
* @param supplementaryQuery - Optional query from lack_of_info (passive path)
|
|
667
|
+
* @returns KnowledgeRetrievalResult with fragments from each source
|
|
668
|
+
*/
|
|
669
|
+
/** 对一组 query 逐个检索并追加到 target,provider 抛错则静默降级(跳过该 query)。
|
|
670
|
+
* 四个知识来源共用(原各写一份 try/catch)。 */
|
|
671
|
+
async function retrieveInto(provider, target, queries, topK) {
|
|
672
|
+
for (const q of queries) {
|
|
673
|
+
if (!q || !q.trim())
|
|
674
|
+
continue;
|
|
675
|
+
try {
|
|
676
|
+
target.push(...await provider.retrieve(q.trim(), topK));
|
|
677
|
+
}
|
|
678
|
+
catch {
|
|
679
|
+
// Degrade gracefully: skip this query
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
export async function retrieveKnowledge(provider, specHints, stepHints, supplementaryQuery, dynamicQuery) {
|
|
684
|
+
const result = {
|
|
685
|
+
specKnowledge: [],
|
|
686
|
+
stepKnowledge: [],
|
|
687
|
+
supplementary: [],
|
|
688
|
+
dynamic: [],
|
|
689
|
+
};
|
|
690
|
+
await retrieveInto(provider, result.specKnowledge, specHints, 3); // 来源1: Spec @knowledge
|
|
691
|
+
await retrieveInto(provider, result.stepKnowledge, stepHints, 3); // 来源3: Step @knowledge
|
|
692
|
+
await retrieveInto(provider, result.supplementary, supplementaryQuery ? [supplementaryQuery] : [], 3); // 来源2: lack_of_info 被动路径
|
|
693
|
+
await retrieveInto(provider, result.dynamic, dynamicQuery ? [dynamicQuery] : [], 5); // 来源4: summary+instruction 整段 query
|
|
694
|
+
return result;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* Format knowledge retrieval results into the L2 knowledge_context string.
|
|
698
|
+
* Deduplicates by source_id and applies budget trimming.
|
|
699
|
+
*/
|
|
700
|
+
export function formatKnowledgeContext(result, triggerStepId, budgetChars = L2_BUDGET_CHARS) {
|
|
701
|
+
// Deduplicate across all sources by source_id
|
|
702
|
+
const seen = new Set();
|
|
703
|
+
const dedup = (fragments) => {
|
|
704
|
+
const unique = [];
|
|
705
|
+
for (const f of fragments) {
|
|
706
|
+
if (!seen.has(f.source_id)) {
|
|
707
|
+
seen.add(f.source_id);
|
|
708
|
+
unique.push(f);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return unique;
|
|
712
|
+
};
|
|
713
|
+
// Sort each source by relevance (high > medium > low)
|
|
714
|
+
const relevanceOrder = { high: 0, medium: 1, low: 2 };
|
|
715
|
+
const sortByRelevance = (a, b) => (relevanceOrder[a.relevance] ?? 2) - (relevanceOrder[b.relevance] ?? 2);
|
|
716
|
+
const specFragments = dedup(result.specKnowledge).sort(sortByRelevance);
|
|
717
|
+
const stepFragments = dedup(result.stepKnowledge).sort(sortByRelevance);
|
|
718
|
+
const suppFragments = dedup(result.supplementary).sort(sortByRelevance);
|
|
719
|
+
const dynamicFragments = dedup(result.dynamic ?? []).sort(sortByRelevance); // 来源4,优先级最低,最后 dedup
|
|
720
|
+
if (specFragments.length === 0 && stepFragments.length === 0 && suppFragments.length === 0 && dynamicFragments.length === 0) {
|
|
721
|
+
return '';
|
|
722
|
+
}
|
|
723
|
+
const sections = [];
|
|
724
|
+
if (specFragments.length > 0) {
|
|
725
|
+
const lines = specFragments.map(f => `${f.source_id}: ${f.content}`);
|
|
726
|
+
sections.push(`### Spec @knowledge(全程可用)\n${lines.join('\n')}`);
|
|
727
|
+
}
|
|
728
|
+
if (suppFragments.length > 0) {
|
|
729
|
+
const triggerNote = triggerStepId
|
|
730
|
+
? `(由步骤 ${triggerStepId} 的 lack_of_info 触发,前序步骤未参考此知识)`
|
|
731
|
+
: '';
|
|
732
|
+
const lines = suppFragments.map(f => `${f.source_id}: ${f.content}`);
|
|
733
|
+
sections.push(`### 补充检索${triggerNote}\n${lines.join('\n')}`);
|
|
734
|
+
}
|
|
735
|
+
if (stepFragments.length > 0) {
|
|
736
|
+
const lines = stepFragments.map(f => `${f.source_id}: ${f.content}`);
|
|
737
|
+
sections.push(`### 步骤 @knowledge\n${lines.join('\n')}`);
|
|
738
|
+
}
|
|
739
|
+
if (dynamicFragments.length > 0) {
|
|
740
|
+
const lines = dynamicFragments.map(f => `${f.source_id}: ${f.content}`);
|
|
741
|
+
sections.push(`### 动态检索\n${lines.join('\n')}`);
|
|
742
|
+
}
|
|
743
|
+
let formatted = `[知识上下文]\n${sections.join('\n---\n')}`;
|
|
744
|
+
// Budget trimming: remove from lowest priority source first (step > supplementary > spec)
|
|
745
|
+
if (formatted.length > budgetChars) {
|
|
746
|
+
formatted = truncateToChars(formatted, budgetChars);
|
|
747
|
+
}
|
|
748
|
+
return formatted;
|
|
749
|
+
}
|
|
750
|
+
/**
|
|
751
|
+
* Inject L2 knowledge context into an assembled context.
|
|
752
|
+
* This is the main async entry point for the dispatcher to enrich context with knowledge.
|
|
753
|
+
*
|
|
754
|
+
* @param ctx - The assembled context (modified in place and returned)
|
|
755
|
+
* @param provider - KnowledgeProvider from HostConfig
|
|
756
|
+
* @param spec - The spec AST (for extracting constraints)
|
|
757
|
+
* @param step - The current step node (for extracting step-level hints)
|
|
758
|
+
* @param supplementaryQuery - Optional query from lack_of_info (passive path)
|
|
759
|
+
* @param triggerStepId - Step ID that triggered supplementary retrieval
|
|
760
|
+
* @returns The enriched AssembledContext
|
|
761
|
+
*/
|
|
762
|
+
export async function injectKnowledgeContext(ctx, provider, spec, step, supplementaryQuery, triggerStepId) {
|
|
763
|
+
// 返回检索到的 source_id 列表(供 HopLog knowledge 流控字段记录),空 = 无检索
|
|
764
|
+
const { specHints, stepHints } = collectKnowledgeHints(spec, step);
|
|
765
|
+
// 来源4 动态检索:整段 summary + instruction 作 query(v1 无 NLP,靠 Provider 语义匹配)
|
|
766
|
+
const dynamicQuery = [step.summary, step.instruction].filter(Boolean).join(' ');
|
|
767
|
+
// If no hints, no supplementary, no dynamic query, nothing to retrieve
|
|
768
|
+
if (specHints.length === 0 && stepHints.length === 0 && !supplementaryQuery && !dynamicQuery.trim()) {
|
|
769
|
+
return [];
|
|
770
|
+
}
|
|
771
|
+
try {
|
|
772
|
+
const result = await retrieveKnowledge(provider, specHints, stepHints, supplementaryQuery, dynamicQuery);
|
|
773
|
+
const knowledgeText = formatKnowledgeContext(result, triggerStepId);
|
|
774
|
+
if (knowledgeText) {
|
|
775
|
+
ctx.knowledge_context = knowledgeText;
|
|
776
|
+
}
|
|
777
|
+
const sources = [...result.specKnowledge, ...result.stepKnowledge, ...result.supplementary, ...result.dynamic]
|
|
778
|
+
.map(f => f.source_id);
|
|
779
|
+
return [...new Set(sources)];
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
// Knowledge injection degradation: if provider throws, L2 is empty
|
|
783
|
+
// Record warning would happen at dispatcher level
|
|
784
|
+
return [];
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* 把 AssembledContext 格式化为完整自包含 prompt 文本。
|
|
789
|
+
* 包含:角色说明(让任何 LLM 拿到就能正确执行)+ 6 层结构化内容。
|
|
790
|
+
* 复用模式下记录到 hoplog llm.prompt(引擎交给 CC 推理的完整 context 渲染——CC 即 llm,
|
|
791
|
+
* 见 [[spec-observability#^anc-obs-mode-boundary]])。独立模式的真实 API prompt 由 dispatcher
|
|
792
|
+
* 的 buildSystemPrompt+buildMessages 另行组装(结构不同:system + 多条 user message),
|
|
793
|
+
* 本函数不代表独立模式实际发送内容(独立模式 llm.prompt 记录属 v2)。
|
|
794
|
+
* 覆盖守护见 tests/prompt.test.ts(@v: anc-obs-debug-context)——新增 AssembledContext 字段须同步本函数。
|
|
795
|
+
*/
|
|
796
|
+
export function formatPromptText(ctx, stepType) {
|
|
797
|
+
const sections = [];
|
|
798
|
+
// 角色前缀:自包含执行指引,按步骤类型给出行为要求
|
|
799
|
+
const rolePrefix = buildRolePrefix(stepType);
|
|
800
|
+
sections.push(rolePrefix);
|
|
801
|
+
sections.push('═══ L1. Spec 契约 ═══');
|
|
802
|
+
sections.push(ctx.task_context);
|
|
803
|
+
if (ctx.knowledge_context) {
|
|
804
|
+
sections.push('═══ L2. 知识上下文 ═══');
|
|
805
|
+
sections.push(ctx.knowledge_context);
|
|
806
|
+
}
|
|
807
|
+
if (ctx.doc_ref_context) { // L2d doc-ref // @a: anc-exec-doc-ref-injection
|
|
808
|
+
sections.push('═══ L2d. 引用文档 ═══');
|
|
809
|
+
sections.push(ctx.doc_ref_context);
|
|
810
|
+
}
|
|
811
|
+
if (ctx.retry_feedback) {
|
|
812
|
+
sections.push('═══ L2c. 重试反馈 ═══');
|
|
813
|
+
sections.push(ctx.retry_feedback);
|
|
814
|
+
}
|
|
815
|
+
sections.push('═══ L3. 执行链上下文 ═══');
|
|
816
|
+
sections.push(ctx.progress_summary);
|
|
817
|
+
if (ctx.iteration_history) {
|
|
818
|
+
sections.push('═══ L3b. 迭代历史 ═══');
|
|
819
|
+
sections.push(ctx.iteration_history);
|
|
820
|
+
}
|
|
821
|
+
sections.push('═══ L4. 步骤输入 ═══');
|
|
822
|
+
const inputLines = Object.entries(ctx.inputs)
|
|
823
|
+
.map(([k, v]) => `${k}: ${typeof v === 'string' ? v : JSON.stringify(v)}`)
|
|
824
|
+
.join('\n');
|
|
825
|
+
sections.push(inputLines || '(无输入)');
|
|
826
|
+
sections.push('═══ L5. 步骤指令 ═══');
|
|
827
|
+
sections.push(ctx.instruction);
|
|
828
|
+
sections.push('═══ L6. 输出约束 ═══');
|
|
829
|
+
const schemaLines = ctx.output_schema
|
|
830
|
+
.map((o) => `+ → ${o.name}: ${o.type}${o.description ? ` # ${o.description}` : ''}`)
|
|
831
|
+
.join('\n');
|
|
832
|
+
sections.push(schemaLines || '(无输出)');
|
|
833
|
+
return sections.join('\n\n');
|
|
834
|
+
}
|
|
835
|
+
function buildRolePrefix(stepType) {
|
|
836
|
+
const common = `你正在执行一个 HopSpec 规约中的步骤。严格按以下规则操作。
|
|
837
|
+
|
|
838
|
+
⚠️ 硬约束(违反即错误):
|
|
839
|
+
- 输出字段名、类型、数量必须与 L6 输出约束完全一致——不得多也不得少
|
|
840
|
+
- L1 Constraints 不可违反
|
|
841
|
+
- 不得编造数据——信息不足时必须输出 lack_of_info
|
|
842
|
+
- 每条 Bash 命令只做一件事,禁止管道(|)、链式(&&)
|
|
843
|
+
|
|
844
|
+
下面是按 6 层组织的结构化上下文:
|
|
845
|
+
- L1 Spec 契约+骨架:任务目标、约束、全局步骤计划——不可违反
|
|
846
|
+
- L3 执行链:已完成步骤的产出(你可以引用)、当前位置(◀ 标记)
|
|
847
|
+
- L4 步骤输入:你的输入数据
|
|
848
|
+
- L5 步骤指令:你具体要做什么
|
|
849
|
+
- L6 输出约束:你的输出必须严格符合此结构`;
|
|
850
|
+
const typeGuide = {
|
|
851
|
+
reason: `你的角色:推理分析。
|
|
852
|
+
【必须】基于 L5 指令和 L4 输入数据进行推理,产出 JSON 对象。
|
|
853
|
+
【必须】JSON 的 key 严格使用 L6 output_schema 中每个变量的 name,数量完全一致。
|
|
854
|
+
【必须】信息不足时输出 {"lack_of_info":"说明缺少什么信息"},绝不编造。
|
|
855
|
+
【禁止】输出 L6 未声明的额外字段。`,
|
|
856
|
+
check: `你的角色:验证判定。
|
|
857
|
+
【必须】固定产出恰好两个值:一个 bool(判定槽:通过=true / 失败=false)+ 一个 text(说明槽:失败原因)。
|
|
858
|
+
【必须】看 L6 output_schema 哪个是 bool 哪个是 text,按各自的 name 填入。
|
|
859
|
+
【禁止】自行决定重试或修改之前步骤的产出——你只负责如实判定,引擎自动处理后续。`,
|
|
860
|
+
act: `你的角色:确定性执行(禁止推理)。
|
|
861
|
+
【如果】L5 包含 \`\`\`hop_python 代码块 → 严格逐行执行(赋值、内置函数、工具调用),不得改动逻辑、不得跳行、不得添加额外操作。
|
|
862
|
+
【如果】没有代码块 → 按 L5 自然语言指令使用 Bash/Read/Write 工具执行。
|
|
863
|
+
【必须】产出 JSON,key 严格对应 L6 output_schema 的变量名。
|
|
864
|
+
【禁止】在执行期自由发挥或加入推理。`,
|
|
865
|
+
commit: `你的角色:不可逆执行(与 act 同源)。
|
|
866
|
+
执行方式与 act 完全相同,唯一区别是操作不可逆(如写入生产数据库、发送邮件、git push)。
|
|
867
|
+
【必须】确认操作内容无误后再执行。产出 JSON 格式同 act。
|
|
868
|
+
【禁止】在执行期自由发挥。`,
|
|
869
|
+
confirm: `你的角色:纯审批闸门。
|
|
870
|
+
【必须】展示待审批动作(presented_data.question + context),等 caller 答 approve/reject。
|
|
871
|
+
【禁止】自行替人做决策。approve→bool true / reject→全局中止。不收集业务数据值(那是 ask)。`,
|
|
872
|
+
ask: `你的角色:数据收集——向 caller 请求业务数据值。
|
|
873
|
+
【必须】展示 question(要 caller 提供什么)+ output_schema(要填的变量/类型)+ default_value(前序推断默认值)+ options(候选,如有)。
|
|
874
|
+
【必须】caller 答"值是什么"(具体数据值,或采用 default 快捷,或选 option),值落到 +→ 声明的变量名。
|
|
875
|
+
【禁止】把 approve/reject 当数据值——ask 不是审批闸门,要中止用 confirm。`,
|
|
876
|
+
};
|
|
877
|
+
const guide = (stepType && typeGuide[stepType]) || `【必须】输出 JSON 格式,key 严格对应 L6 output_schema 的变量名。`;
|
|
878
|
+
return `${common}\n\n${guide}`;
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* 组装 parallel 容器的完整自包含 fan-out 指令文本。
|
|
882
|
+
* driver agent 拿到这段文本就能完成 fan-out→驱动→join 全流程。
|
|
883
|
+
*/
|
|
884
|
+
export function formatParallelPromptText(p) {
|
|
885
|
+
const childList = p.children
|
|
886
|
+
.map(c => `- ${c.child_step_id} ${c.summary} params: ${JSON.stringify(c.params_for_child)}`)
|
|
887
|
+
.join('\n');
|
|
888
|
+
const cli = p.cliPath;
|
|
889
|
+
const sd = p.stateDir;
|
|
890
|
+
const workerPrompts = p.children
|
|
891
|
+
.map(c => {
|
|
892
|
+
const paramsJson = JSON.stringify(c.params_for_child);
|
|
893
|
+
return `--- child ${c.child_step_id}(${c.summary})---
|
|
894
|
+
你是 HopSpec parallel worker,负责驱动 child ${c.child_step_id}(${c.summary})到终态。
|
|
895
|
+
严格按以下步骤操作,不得跳步、不得自由发挥。
|
|
896
|
+
|
|
897
|
+
⚠️ 硬约束(违反即错误):
|
|
898
|
+
- 输出的 JSON 字段名、类型、数量必须与 output_schema 完全一致——不得多、不得少、不得改名
|
|
899
|
+
- 不得编造数据——如果基于输入数据和指令无法完成任务,必须输出 {"lack_of_info":"说明缺少什么信息"}
|
|
900
|
+
- check 步骤只做判定(true/false),不得自行决定重试或修改之前步骤的产出——引擎自动处理重试
|
|
901
|
+
- act/commit 步骤如果有 hop_python body 代码块,必须逐行严格执行代码逻辑,不得改动、不得跳行、不得添加额外操作
|
|
902
|
+
- 每条 Bash 命令必须独立完整——禁止管道(|)、禁止链式(&&)、禁止 shell 变量赋值
|
|
903
|
+
- 所有命令直接写完整路径执行,不需要 cd
|
|
904
|
+
|
|
905
|
+
第 1 步:执行启动命令(直接复制执行,不要修改)
|
|
906
|
+
node ${cli} run ${p.specPath} --parallel-parent ${p.instanceId} --parallel-child ${c.child_step_id} --state-dir ${sd} --log-dir ${p.parentLogDir}/parallel/${c.child_step_id} --log-level debug --params '${paramsJson}'
|
|
907
|
+
|
|
908
|
+
这条命令返回一个 JSON 对象。
|
|
909
|
+
|
|
910
|
+
第 2 步:读返回 JSON 的 "status" 字段,严格按值分派
|
|
911
|
+
- "completed" → child 执行完毕。报告终态和产出的变量值摘要,然后结束。
|
|
912
|
+
- "failed" → 执行失败。报告失败原因,然后结束。
|
|
913
|
+
- "step_ready" → 引擎给你一个步骤要执行,继续第 3 步。
|
|
914
|
+
- 其他值 → 报告异常状态,然后结束。
|
|
915
|
+
|
|
916
|
+
第 3 步:从返回的 JSON 中提取以下字段(后续步骤都会用到)
|
|
917
|
+
- step_id:当前步骤 ID(如 "${c.child_step_id}.1"),提交结果时必须填入这个确切的值
|
|
918
|
+
- step_type:步骤类型,决定你怎么执行(reason / check / act / commit)
|
|
919
|
+
- context.instruction:你的任务指令——告诉你具体做什么
|
|
920
|
+
- context.inputs:你的输入数据——变量名到值的映射
|
|
921
|
+
- context.output_schema:你必须产出的变量列表,每个有 name(变量名)和 type(类型)。这是你输出 JSON 的 key 的唯一来源
|
|
922
|
+
|
|
923
|
+
第 4 步:按 step_type 严格执行
|
|
924
|
+
|
|
925
|
+
(a) reason 步骤——推理分析:
|
|
926
|
+
根据 context.instruction 的指示,基于 context.inputs 的数据进行推理分析。
|
|
927
|
+
【必须】产出 JSON 对象,key 严格使用 output_schema 中每个变量的 name,value 是你的推理结果。
|
|
928
|
+
【必须】字段数量与 output_schema 完全一致——不多不少。
|
|
929
|
+
【必须】信息不足时输出 {"lack_of_info":"说明缺少什么信息"},绝不编造。
|
|
930
|
+
示例:output_schema = [{name:"points_a", type:"text"}]
|
|
931
|
+
→ 你必须产出恰好一个字段:{"points_a":"你的分析结果文本"}
|
|
932
|
+
|
|
933
|
+
(b) check 步骤——验证判定:
|
|
934
|
+
【必须】固定产出恰好两个值:一个 bool(判定槽:通过=true / 失败=false)+ 一个 text(说明槽)。
|
|
935
|
+
【必须】看 output_schema 哪个是 bool 类型、哪个是 text 类型,按各自的 name 填入。
|
|
936
|
+
【禁止】自行决定重试或修改之前步骤的产出——你只负责如实判定,引擎自动处理后续。
|
|
937
|
+
示例:output_schema = [{name:"a_ok", type:"bool"}, {name:"a_note", type:"text"}]
|
|
938
|
+
→ 你必须产出恰好两个字段:{"a_ok":true,"a_note":"校验通过,摘要包含所有要点"}
|
|
939
|
+
|
|
940
|
+
(c) act 步骤——确定性执行(禁止推理):
|
|
941
|
+
【如果】context.instruction 包含 \`\`\`hop_python 代码块 → 严格按代码逐行执行(赋值、内置函数、工具调用),不得改动逻辑、不得跳行、不得添加额外操作。
|
|
942
|
+
【如果】没有代码块 → 按 instruction 的自然语言描述,使用 Bash/Read/Write 工具执行操作。
|
|
943
|
+
【必须】产出 JSON,key 严格对应 output_schema 的变量名。
|
|
944
|
+
|
|
945
|
+
(d) commit 步骤——不可逆执行:
|
|
946
|
+
执行方式与 act 完全相同,唯一区别是操作不可逆(如写入生产数据库、发送邮件)。
|
|
947
|
+
【必须】确认操作内容无误后再执行。产出 JSON 格式同 act。
|
|
948
|
+
|
|
949
|
+
第 5 步:提交结果(直接复制模板,替换两个占位符)
|
|
950
|
+
node ${cli} submit_and_fetch_next <STEP_ID> --output '<OUTPUT_JSON>' --state-dir ${sd}
|
|
951
|
+
|
|
952
|
+
【必须】替换规则:
|
|
953
|
+
- <STEP_ID> → 第 3 步提取的 step_id 值(如 ${c.child_step_id}.1),不要留尖括号
|
|
954
|
+
- <OUTPUT_JSON> → 你在第 4 步产出的完整 JSON 字符串,用单引号包裹
|
|
955
|
+
|
|
956
|
+
完整示例:
|
|
957
|
+
node ${cli} submit_and_fetch_next ${c.child_step_id}.1 --output '{"points_a":"分析结果..."}' --state-dir ${sd}
|
|
958
|
+
|
|
959
|
+
第 6 步:回到第 2 步
|
|
960
|
+
submit_and_fetch_next 返回新的 JSON。回到第 2 步读 status 继续分派。
|
|
961
|
+
循环直到 status 为 "completed" 或 "failed",然后报告终态并结束。`;
|
|
962
|
+
})
|
|
963
|
+
.join('\n\n');
|
|
964
|
+
return `你是 HopSpec parallel driver,负责并行调度以下 child 容器到终态后 join 合并。
|
|
965
|
+
|
|
966
|
+
═══ 任务概览 ═══
|
|
967
|
+
Goal: ${p.goal}
|
|
968
|
+
Outputs: ${p.outputs}
|
|
969
|
+
Parallel step ${p.parallelStepId} 分发 ${p.children.length} 个 child(max_concurrent=${p.maxConcurrent}):
|
|
970
|
+
${childList}
|
|
971
|
+
|
|
972
|
+
═══ Fan-out:对每个 child 起一个 subagent ═══
|
|
973
|
+
用你所在 runtime 的子 agent 机制(如 CC 的 Agent 工具 / Codex 的 subagent)起一个 subagent 驱动一个 child 子实例。
|
|
974
|
+
并发数 ≤ ${p.maxConcurrent};child 数超出则分批,前批全部返回后起下批。
|
|
975
|
+
|
|
976
|
+
每个 subagent prompt(已填入实际参数):
|
|
977
|
+
|
|
978
|
+
${workerPrompts}
|
|
979
|
+
|
|
980
|
+
═══ Join:全部 subagent 返回后 ═══
|
|
981
|
+
node ${p.cliPath} join_parallel ${p.parallelStepId} --state-dir ${p.stateDir} --instance ${p.instanceId}
|
|
982
|
+
引擎 merge 后返回下一步 JSON,继续主循环。
|
|
983
|
+
|
|
984
|
+
═══ 错误处理 ═══
|
|
985
|
+
- 某 subagent 返回 failed → 不阻塞其余,join 时该 child 输出写 None
|
|
986
|
+
- 某 subagent 异常退出 → 视为 failed
|
|
987
|
+
- 含 confirm 的 child 不进并行,主 driver 串行驱动`;
|
|
988
|
+
}
|
|
989
|
+
// 人通道格式化:把 record 中每个值按 HUMAN_PREVIEW_THRESHOLD(5K) 处理——
|
|
990
|
+
// 小值原样;大值写 work_zone/vars/<name>.json,返 {$file, preview} 复合格式,
|
|
991
|
+
// preview = 头 5K 字符 + "...[完整内容见文件]"。driver 完整 dump preview 给 user 看 +
|
|
992
|
+
// 提示完整在哪。见 design/shared-types.md ^anc-exec-deflate / 概念 ^anc-exec-audience-routing。
|
|
993
|
+
// workZone 空串(独立模式)时跳过 deflate,原值返回(信息不丢,只是没文件链接)。
|
|
994
|
+
// @a: anc-exec-deflate, anc-exec-audience-routing
|
|
995
|
+
export function formatHumanContext(record, workZone) {
|
|
996
|
+
const result = {};
|
|
997
|
+
for (const [key, value] of Object.entries(record)) {
|
|
998
|
+
if (value === null || value === undefined) {
|
|
999
|
+
result[key] = value;
|
|
1000
|
+
continue;
|
|
1001
|
+
}
|
|
1002
|
+
const str = typeof value === 'string' ? value : (JSON.stringify(value) ?? '');
|
|
1003
|
+
if (!workZone || str.length <= HUMAN_PREVIEW_THRESHOLD) {
|
|
1004
|
+
result[key] = value;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
// 大值卸到文件 + 返 preview
|
|
1008
|
+
const filePath = nodePath.join(workZone, 'vars', `${key}.json`);
|
|
1009
|
+
nodeFs.writeFileSync(filePath, JSON.stringify(value), { mode: 0o600 });
|
|
1010
|
+
const preview = str.slice(0, HUMAN_PREVIEW_THRESHOLD) + '\n...[完整内容见文件]';
|
|
1011
|
+
result[key] = { $file: filePath, preview };
|
|
1012
|
+
}
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|