@tea-agent/loop-agent 0.44.0-next.10 → 0.44.0-next.12
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/CHANGELOG.md +11 -0
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi/sessions/index.js +6 -0
- package/dist/executors/dag-pi/sessions/plan-batches.js +280 -0
- package/dist/executors/dag-pi/sessions/plan-prompts.js +367 -0
- package/dist/executors/dag-pi/sessions/scout-parallel.js +197 -0
- package/dist/executors/dag-pi/sessions/segmented-plan.js +1184 -0
- package/dist/executors/dag-pi/sessions/writer-evidence.js +60 -0
- package/dist/executors/dag-pi-executor.js +12 -2075
- package/dist/executors/shell-executor.js +39 -11
- package/dist/worker/console/routes.js +5 -0
- package/dist/worker/console/static/app-icon.svg +39 -0
- package/dist/worker/console/static/index.html +3 -0
- package/dist/worker/console/static/manifest.webmanifest +19 -0
- package/dist/worker/observe/static/console-theme.js +38 -0
- package/dist/workflows/dag/checkpoint.js +686 -0
- package/dist/workflows/dag/frontend-repair.js +24 -0
- package/dist/workflows/dag/frontend-review-scopes.js +10 -2
- package/dist/workflows/dag/frontend-test-execution-evidence.js +163 -67
- package/dist/workflows/dag/frontend-test-framework-adapters.js +309 -0
- package/dist/workflows/dag/hybrid/sources.js +1812 -0
- package/dist/workflows/dag/hybrid/templates/backend-test.js +1807 -0
- package/dist/workflows/dag/hybrid/templates/frontend-test.js +702 -0
- package/dist/workflows/dag/hybrid/templates/frontend.js +1529 -0
- package/dist/workflows/dag/hybrid/templates/index.js +8 -0
- package/dist/workflows/dag/hybrid/templates/kg-bootstrap.js +449 -0
- package/dist/workflows/dag/hybrid/templates/knowledge-sync.js +495 -0
- package/dist/workflows/dag/hybrid/templates/shared.js +101 -0
- package/dist/workflows/dag/hybrid/templates/standard.js +265 -0
- package/dist/workflows/dag/hybrid/types.js +32 -0
- package/dist/workflows/dag/init-hybrid.js +42 -7162
- package/dist/workflows/dag/runner.js +11 -1178
- package/dist/workflows/dag/terminal-status.js +502 -0
- package/docs/architecture/dag-execution.md +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
### 新增
|
|
6
6
|
|
|
7
|
+
- 前端 DAG 的行为验证不再只认 Vitest:测试框架改为可插拔适配层,首批支持 Vitest、Jest 与 Playwright Test。三类项目都能用**真实报告**证明契约绑定的测试文件确实执行且通过,`npm`/`pnpm`/`yarn` 的常用测试入口走同一套识别逻辑,直接命令与 `package.json` 脚本不会再得出不同结论。
|
|
8
|
+
- 未适配的测试框架(Cypress、`node --test`、Mocha、自定义脚本等)现在明确报「缺少适配能力」,并附上具体命令与原因;此前这类命令会被判为不支持,错误信息还暗示你「必须改用 Vitest」,把工具的能力缺口显示成了用户配置错误。
|
|
9
|
+
- 行为验证保留「测试确实执行过」的校验:零测试、全部跳过、报告缺失、报告过期、契约绑定文件未执行都判失败,不会退回只看退出码(全部跳过的运行退出码是 0)。
|
|
10
|
+
|
|
7
11
|
- main 上的 npm `next` 预发布改为跑默认测试(fast + integration),不再在每次发布时重复跑约 6 分钟的 I/O-heavy 池;完整三池仍由 PR、正式 release 线、本地推送前检查与发布前钩子承担。同时把该预发布任务的 I/O 并发从 1 调到 2,并把 npm 包可见性确认的等待窗口从约 3 分钟拉长到约 10 分钟,避免包已发出却因仓库异步处理未完成而被判失败。
|
|
8
12
|
|
|
9
13
|
- 修复 main 分支的 next 发布链在类型检查阶段内存溢出失败的问题:完整源码与测试的类型图已超过 Node 默认约 2 GiB 堆上限,类型检查进程因此中止(exit 134),导致 `next` 发布无法完成。此前该上限只在部分验证入口生效,其余入口直接运行类型检查就会因默认堆不足而失败;现把上限固化到类型检查命令本身,各验证入口与本地开发统一生效,不再依赖各入口自行声明。
|
|
@@ -137,6 +141,7 @@
|
|
|
137
141
|
|
|
138
142
|
### 改进
|
|
139
143
|
|
|
144
|
+
- DAG 运行时内部按职责拆开:模型配置、领域工具、分段会话、模板族、终态判定与检查点各自独立维护,测试也按行为分成可单独调度的文件。命令、任务流程与用户可见结果不变。本机默认测试并行下整池耗时未证实变快。
|
|
140
145
|
- Operator Chat 输入框下方的会话统计对齐 DSH:长串静态文字收敛为两个可点击摘要,可分别查看模型/工具用时、TTFT、TPS 与 Token 分项;详情卡互斥并支持外部点击或 Escape 关闭,窄屏打开时不会推动输入框或消息流。
|
|
141
146
|
- Operator Chat 长会话新增完整轮次轨道与按需历史定位,可从任意用户消息精确新建会话;历史图片刷新后仍可预览,仓库文件链接可直接定位到行,最终回答显示单轮 token/耗时,并支持可持久化的紧凑/完整阅读模式。图片文件与消息记录分离保存,删除、分叉和失败回滚会同步清理,附件读取重新校验类型与引用。
|
|
142
147
|
- 本地分支合并默认只执行与影响面匹配的定向测试、类型检查、构建和治理检查;完整测试推迟到 push 或完整交付前,并通过 receipt、`verify:tree` 或 pre-push 在最终 tree 上只执行一次。发布与推送门禁仍包含 I/O-heavy 测试。
|
|
@@ -149,6 +154,12 @@
|
|
|
149
154
|
|
|
150
155
|
### 修复
|
|
151
156
|
|
|
157
|
+
- 前端 DAG 的验证失败不再把配置问题算到业务代码头上:报告缺失或过期、报告协议错误、命令或脚本哈希漂移、框架未适配都归为验证配置问题,不再触发一轮无意义的实现重写;测试断言失败仍归实现问题。
|
|
158
|
+
- 已经自带 `--reporter`/`--outputFile` 的测试命令或脚本不会被悄悄覆盖;无法安全注入报告参数时在生成阶段直接报错,要求改用可注入的入口。
|
|
159
|
+
- 修复 Playwright 项目在 Review 阶段被误判失败的问题:审查重读报告时没有带上协议,仍按 Vitest 解析,导致 Playwright 报告因缺少 `testResults` 报错,即使执行与 trace 已经通过。report 引用现在记录协议,review 按同一协议解析并校验一致性。
|
|
160
|
+
- 修复 Playwright 报告路径按 `testDir` 解析错误的问题:报告中的文件路径相对 Playwright 自身的 `config.rootDir`,此前按进程 cwd 解析,配了 `testDir: "./tests"` 的项目会得到 `orders.spec.js` 而非 `tests/orders.spec.js`,契约目标永远无法匹配。
|
|
161
|
+
- 修复新建 Jest/Playwright 项目被误报“绑定漂移”的问题:`deferred` 观测的协议只是占位值,此前无条件参与比较,writer 一补上合法测试脚本就触发 `FRONTEND_TEST_BINDING_DRIFT`;现在只有已解析出 adapter 的观测才比较协议,真实脚本改动仍会被拦截。
|
|
162
|
+
|
|
152
163
|
- 修复 macOS/Linux 开发路径在 Windows 暴露的三类兼容问题:前端组合验证夹具不再依赖 POSIX 单引号;无 `.gitmodules` 的仓库不再把 Git 空结果误判为启动失败;定时任务用 Git 规范化内容生成稳定树指纹,CRLF checkout 不再造成虚假 evidence drift。托管任务路径与嵌套 submodule 测试也统一使用跨平台路径语义。
|
|
153
164
|
- 切换会话后,旧会话尚未确认的输入不会再出现在新会话中。
|
|
154
165
|
- 修复 Windows 上 Pi 节点在没有提交改动时误报“文件在打开期间被替换”的问题,同时保留原有文件安全检查。
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.44.0-next.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-09-
|
|
3
|
+
"version": "0.44.0-next.12",
|
|
4
|
+
"gitSha": "91181001a8cbed75a00a8a6d8e76ae5fab889b36",
|
|
5
|
+
"builtAt": "2026-09-17T10:04:11.141Z"
|
|
6
6
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** DAG Pi 会话族兼容转发:既有测试从 entry 导入的符号统一经此转发,逐步迁移后移除。 */
|
|
2
|
+
export { estimateFrontendPlanRequirementRecordCalls, batchFrontendPlanRequirements, FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY, FRONTEND_PLAN_BATCH_MAX_SESSIONS, mapWithConcurrency, buildFrontendPlanWorkload, collectFrontendPlanRequirementIds, loadOrCreateFrontendPlanCoverageLayout, collectFrontendVerificationCommandFiles } from "./plan-batches.js";
|
|
3
|
+
export { compactFrontendPlanLedgerContext, compactFrontendPlanPromptForRequirementSlice } from "./plan-prompts.js";
|
|
4
|
+
export { isPlannerThinkingExhausted, isWriterThinkingExhausted, readWriterThinkingExhaustionEvidence, WRITER_BUDGET_EXHAUSTED_CATEGORY, WRITER_TOKEN_BUDGET, WRITER_THINKING_EXHAUSTED_CATEGORY, PLANNER_THINKING_EXHAUSTED_CATEGORY } from "./writer-evidence.js";
|
|
5
|
+
export { aggregateParallelPiResults, combineSequentialPiResults, runFrontendScoutParallelSessions } from "./scout-parallel.js";
|
|
6
|
+
export { runFrontendContractSegmentedSessions, runFrontendPlanSegmentedSessions, runFrontendReviewSegmentedSessions, runFrontendScoutSegmentedSessions } from "./segmented-plan.js";
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/** DAG Pi 分段会话的批次规划:work 装配、批次上限、覆盖布局与并发映射。覆盖布局会读写 coverage-layout.json;禁止反向依赖执行器入口。 */
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readFile, stat } from "node:fs/promises";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { FRONTEND_SCOPE_TARGET_BYTES, packFrontendInputUnits, parseFrontendInputBlock } from "../../../workflows/dag/frontend-input-projection.js";
|
|
6
|
+
import { sha256OfCanonicalJson } from "../../../task/contract/hash.js";
|
|
7
|
+
import { writeDagNodeJsonArtifact } from "../../../infrastructure/harness/artifact-store.js";
|
|
8
|
+
import { isRecordObject } from "../guards.js";
|
|
9
|
+
export const FRONTEND_PLAN_SEGMENTS = [
|
|
10
|
+
{
|
|
11
|
+
id: "coverage",
|
|
12
|
+
toolNames: new Set([
|
|
13
|
+
"record_plan_requirement",
|
|
14
|
+
"record_plan_group_coverage",
|
|
15
|
+
"record_plan_verification_target",
|
|
16
|
+
"record_plan_evidence_gap",
|
|
17
|
+
"adopt_staged_fact",
|
|
18
|
+
]),
|
|
19
|
+
instruction: [
|
|
20
|
+
"PLAN PHASE — requirement coverage only.",
|
|
21
|
+
"Your ONLY job: for every frozen requirement, emit record_plan_requirement (requirement → implementation files) and record_plan_verification_target facts (verification target bound to requirement ids and files). Group related requirements under one non-static behavior target when one observable test behavior proves them together; do not mechanically create one target per requirement. Target ids identify contract entries, not test-title markers. Reuse affected existing test files and their names; do not add tests or rename titles just to carry generated ids. Never submit prose as a symbol. Do NOT record components, UI states, mock, dependency, or routes — a follow-up session owns those.",
|
|
22
|
+
"A coverage session is complete only when EVERY requirement assigned to this session (the full inventory, or the exact COVERAGE BATCH / shard list when present) has committed coverage facts: a record_plan_requirement entry plus verification targets, or a committed evidence gap. Keep committing in batches of up to 4 record_* calls per assistant message until then; do not write a concluding summary while any assigned requirement is still uncommitted — an early stop strands the remainder into a MISSING-FACT repair session and doubles the sessions needed.",
|
|
23
|
+
"If a requirement genuinely cannot have a verification target, record a non-empty record_plan_evidence_gap. Do not call finalize_plan; it is not available in this phase.",
|
|
24
|
+
].join(" "),
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "ux-registry",
|
|
28
|
+
toolNames: new Set(["record_state_registry", "adopt_staged_fact"]),
|
|
29
|
+
instruction: [
|
|
30
|
+
"PLAN PHASE — global UX vocabulary.",
|
|
31
|
+
"Bootstrap the global UX vocabulary from the execution-group index and authoritative declared states. This is navigation, not permission to decide unseen behavior. Detailed complete scopes may extend the registry with replace:true while preserving live names. UI states use declaredUiStates ids when present. Interaction names are stable kebab-case behavior domains; merge requirements that describe the same behavior instead of renaming it per AC slice. Empty arrays explicitly declare that no UX vocabulary applies. Do not record component choices or state-flow details in this phase.",
|
|
32
|
+
"Do not call finalize_plan; it is not available in this phase.",
|
|
33
|
+
].join(" "),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "ux-local",
|
|
37
|
+
toolNames: new Set([
|
|
38
|
+
"record_state_registry",
|
|
39
|
+
"record_component_choice",
|
|
40
|
+
"record_state_flow",
|
|
41
|
+
"record_plan_verification_target",
|
|
42
|
+
"adopt_staged_fact",
|
|
43
|
+
]),
|
|
44
|
+
instruction: [
|
|
45
|
+
"PLAN PHASE — global UX decisions.",
|
|
46
|
+
"Requirements and verification targets are already committed in the ledger; do not re-record unchanged facts. Review the current complete execution-group scope and the committed global UX registry together, then record each component choice, UI state and interaction exactly once. Bind each applicable state to its verificationTargetIds; the runtime derives the reverse VT.uiStates relation. If a VT requires correction, record_plan_verification_target with replace:true is available after declaring its states; preserve its requirement coverage. Multiple requirements describing one behavior share one registry name and state-flow entry; never repeat or rename it per AC. Cross-cutting data flow belongs to the global Mock/data phase. Do not record routes, Mock/API policy, dependencies, or design deviations here.",
|
|
47
|
+
"Do not call finalize_plan; it is not available in this phase.",
|
|
48
|
+
].join(" "),
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: "global-route",
|
|
52
|
+
toolNames: new Set(["record_route_selection", "adopt_staged_fact"]),
|
|
53
|
+
instruction: [
|
|
54
|
+
"PLAN PHASE — global route decision.",
|
|
55
|
+
"Use the Scout target surface and record only the selected route(s). Do not record requirement-local UX, Mock/data, dependency, or deviation facts. Do not call finalize_plan.",
|
|
56
|
+
].join(" "),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: "global-mock-data",
|
|
60
|
+
toolNames: new Set(["record_data_flow", "record_mock_api", "record_mock_endpoint", "adopt_staged_fact"]),
|
|
61
|
+
instruction: [
|
|
62
|
+
"PLAN PHASE — global Mock/API and data policy.",
|
|
63
|
+
"Record the cross-cutting interaction-to-endpoint data flow and Mock/API strategy only. Keep this decision set separate from route, component, state, dependency, and deviation facts. Do not call finalize_plan.",
|
|
64
|
+
].join(" "),
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: "global-dependency-deviation",
|
|
68
|
+
toolNames: new Set(["record_dependency", "record_design_deviation", "adopt_staged_fact"]),
|
|
69
|
+
instruction: [
|
|
70
|
+
"PLAN PHASE — global dependency and design-deviation policy.",
|
|
71
|
+
"Record only dependency policy and design-evidence conflicts. Do not record route, Mock/data, or requirement-local UX facts. Do not call finalize_plan.",
|
|
72
|
+
].join(" "),
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
export function estimateFrontendPlanRequirementRecordCalls(fact) {
|
|
76
|
+
const record = fact && typeof fact === "object" && !Array.isArray(fact)
|
|
77
|
+
? fact
|
|
78
|
+
: {};
|
|
79
|
+
const declaredTargetCount = Array.isArray(record.verificationTargetIds)
|
|
80
|
+
? record.verificationTargetIds.filter((value) => typeof value === "string" && value.trim()).length
|
|
81
|
+
: Array.isArray(record.verificationTargets)
|
|
82
|
+
? record.verificationTargets.length
|
|
83
|
+
: 0;
|
|
84
|
+
const evidence = record.evidence && typeof record.evidence === "object"
|
|
85
|
+
? record.evidence
|
|
86
|
+
: undefined;
|
|
87
|
+
const targetCount = Math.max(declaredTargetCount, evidence?.behavior === "required" ? 2 : 1);
|
|
88
|
+
return 1 + targetCount;
|
|
89
|
+
}
|
|
90
|
+
export function batchFrontendPlanRequirements(input) {
|
|
91
|
+
const batches = [];
|
|
92
|
+
let current = [];
|
|
93
|
+
let currentCost = 0;
|
|
94
|
+
for (const id of input.requirementIds) {
|
|
95
|
+
const cost = Math.max(1, input.requirementCosts?.get(id) ?? 2);
|
|
96
|
+
if (current.length > 0 &&
|
|
97
|
+
(currentCost + cost > input.maxEstimatedRecordCalls ||
|
|
98
|
+
(input.maxRequirements !== undefined &&
|
|
99
|
+
current.length >= input.maxRequirements))) {
|
|
100
|
+
batches.push(current);
|
|
101
|
+
current = [];
|
|
102
|
+
currentCost = 0;
|
|
103
|
+
}
|
|
104
|
+
current.push(id);
|
|
105
|
+
currentCost += cost;
|
|
106
|
+
}
|
|
107
|
+
if (current.length > 0)
|
|
108
|
+
batches.push(current);
|
|
109
|
+
return batches;
|
|
110
|
+
}
|
|
111
|
+
export const FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS = 10;
|
|
112
|
+
export const FRONTEND_PLAN_COVERAGE_MAX_CONCURRENCY = 4;
|
|
113
|
+
export const FRONTEND_PLAN_BATCH_MAX_SESSIONS = 128;
|
|
114
|
+
export const FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS = 8;
|
|
115
|
+
export const FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS = 24;
|
|
116
|
+
export async function mapWithConcurrency(items, limit, worker, shouldReduceConcurrency) {
|
|
117
|
+
const results = new Array(items.length);
|
|
118
|
+
let nextIndex = 0;
|
|
119
|
+
let concurrency = Math.max(1, limit);
|
|
120
|
+
const running = new Map();
|
|
121
|
+
try {
|
|
122
|
+
while (nextIndex < items.length || running.size > 0) {
|
|
123
|
+
while (nextIndex < items.length && running.size < concurrency) {
|
|
124
|
+
const index = nextIndex++;
|
|
125
|
+
running.set(index, worker(items[index], index).then(result => ({ index, result })));
|
|
126
|
+
}
|
|
127
|
+
const { index, result } = await Promise.race(running.values());
|
|
128
|
+
running.delete(index);
|
|
129
|
+
results[index] = result;
|
|
130
|
+
// Drain existing work; only pending shards use the reduced cap.
|
|
131
|
+
// Failed shards remain failed and receive no extra retry allowance.
|
|
132
|
+
if (shouldReduceConcurrency(result))
|
|
133
|
+
concurrency = Math.max(1, Math.floor(concurrency / 2));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
await Promise.allSettled(running.values());
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
return results;
|
|
141
|
+
}
|
|
142
|
+
export function collectFrontendVerificationCommandFiles(basePrompt) {
|
|
143
|
+
const files = new Set();
|
|
144
|
+
const configRe = /--config\s+([\w@./-]+\.(?:js|mjs|cjs|ts|json))/g;
|
|
145
|
+
const checkRe = /node\s+--check\s+([\w@./-]+\.(?:js|mjs|cjs))/g;
|
|
146
|
+
for (const re of [configRe, checkRe]) {
|
|
147
|
+
for (const match of basePrompt.matchAll(re)) {
|
|
148
|
+
const file = match[1];
|
|
149
|
+
if (file && file.includes("/"))
|
|
150
|
+
files.add(file);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return [...files].sort();
|
|
154
|
+
}
|
|
155
|
+
export function countFrontendPlanTargetSurfaces(basePrompt) {
|
|
156
|
+
const match = /<frontend_plan_input>[\s\S]*?<\/frontend_plan_input>/.exec(basePrompt);
|
|
157
|
+
if (!match)
|
|
158
|
+
return undefined;
|
|
159
|
+
for (const line of match[0].split(/\r?\n/)) {
|
|
160
|
+
try {
|
|
161
|
+
const payload = JSON.parse(line);
|
|
162
|
+
if (Array.isArray(payload.targetSurface))
|
|
163
|
+
return payload.targetSurface.length;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// surrounding lines are prose
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
export function buildFrontendPlanWorkload(input) {
|
|
172
|
+
const allRequirementIds = input.requirementIds;
|
|
173
|
+
const compiledInput = parseFrontendInputBlock(input.basePrompt, "plan")?.payload;
|
|
174
|
+
const fullUnits = new Map(compiledInput?.requirements.map(r => [r.id, r]) ?? []);
|
|
175
|
+
const declaredGroups = Array.isArray(compiledInput?.executionGroups)
|
|
176
|
+
? compiledInput.executionGroups.filter(isRecordObject) : [];
|
|
177
|
+
const workGroups = declaredGroups.map(g => ({
|
|
178
|
+
id: String(g.id), kind: String(g.kind),
|
|
179
|
+
requirementIds: Array.isArray(g.requirementIds)
|
|
180
|
+
? g.requirementIds.filter((id) => typeof id === "string" && allRequirementIds.includes(id)) : [],
|
|
181
|
+
})).filter(g => g.requirementIds.length);
|
|
182
|
+
const groupedIds = new Set(workGroups.flatMap(g => g.requirementIds));
|
|
183
|
+
for (const id of allRequirementIds) {
|
|
184
|
+
if (!groupedIds.has(id))
|
|
185
|
+
workGroups.push({ id, kind: "unclassified", requirementIds: [id] });
|
|
186
|
+
}
|
|
187
|
+
const workCost = (ids) => 1 + ids.reduce((total, id) => total + Math.max(1, (input.requirementCosts?.get(id) ?? 2) - 1), 0);
|
|
188
|
+
const policy = input.sessionOptions.frontendExecutionPolicy;
|
|
189
|
+
const targetBytes = policy?.scopeTargetBytes ?? FRONTEND_SCOPE_TARGET_BYTES;
|
|
190
|
+
const buildWorkBatches = (ids) => {
|
|
191
|
+
const work = workGroups.flatMap((g, index) => {
|
|
192
|
+
const members = g.requirementIds.filter(id => ids.includes(id));
|
|
193
|
+
return members.length ? [{ id: `${index}:${g.id}`, requirementIds: members,
|
|
194
|
+
requirements: members.map(id => fullUnits.get(id) ?? { id }), estimatedCalls: workCost(members) }] : [];
|
|
195
|
+
});
|
|
196
|
+
// Pack complete work units, not the repeated request scaffold. Deducting
|
|
197
|
+
// fixed context/tools can leave a one-byte budget and force one AC per
|
|
198
|
+
// session without reducing that overhead. The SDK checks the actual
|
|
199
|
+
// request against model capacity; capacity recovery splits unfinished work.
|
|
200
|
+
return packFrontendInputUnits(work, {
|
|
201
|
+
targetBytes,
|
|
202
|
+
maxUnits: policy?.maxScopeUnits ?? 4,
|
|
203
|
+
maxCost: FRONTEND_PLAN_COVERAGE_MAX_RECORD_CALLS, cost: g => g.estimatedCalls,
|
|
204
|
+
}).map(batch => batch.flatMap(g => g.requirementIds));
|
|
205
|
+
};
|
|
206
|
+
// This renderer also carries the Plan's evolving UX checkpoint on resume.
|
|
207
|
+
// Only the Contract/Scout-derived input participates in ownership binding.
|
|
208
|
+
const { committedUx: _committedUx, ...frozenInput } = compiledInput ?? {};
|
|
209
|
+
return {
|
|
210
|
+
frozenInput, workGroups, buildWorkBatches,
|
|
211
|
+
compactEligible: input.requirementCosts !== undefined &&
|
|
212
|
+
workGroups.length <= FRONTEND_PLAN_SMALL_MAX_REQUIREMENTS &&
|
|
213
|
+
workGroups.reduce((total, group) => total + workCost(group.requirementIds), 0) <= FRONTEND_PLAN_SMALL_MAX_ESTIMATED_CALLS &&
|
|
214
|
+
countFrontendPlanTargetSurfaces(input.basePrompt) === 1,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
export function collectFrontendPlanRequirementIds(contractFacts) {
|
|
218
|
+
return [
|
|
219
|
+
...new Set(contractFacts
|
|
220
|
+
.filter((record) => record.fact?.kind ===
|
|
221
|
+
"requirement")
|
|
222
|
+
.map((record) => record.fact?.id)
|
|
223
|
+
.filter((id) => typeof id === "string")),
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
export const frontendPlanCoverageLayoutSchema = z.object({
|
|
227
|
+
schemaVersion: z.literal(1),
|
|
228
|
+
bindingSha256: z.string(),
|
|
229
|
+
coverageBatches: z.array(z.array(z.string().min(1)).min(1)).min(1),
|
|
230
|
+
layoutSha256: z.string(),
|
|
231
|
+
}).strict();
|
|
232
|
+
export async function loadOrCreateFrontendPlanCoverageLayout(input) {
|
|
233
|
+
const fileName = "coverage-layout.json";
|
|
234
|
+
const file = path.join(input.runDir, input.nodeId, fileName);
|
|
235
|
+
const bindingSha256 = sha256OfCanonicalJson(input.binding);
|
|
236
|
+
let raw;
|
|
237
|
+
try {
|
|
238
|
+
raw = await readFile(file, "utf8");
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error.code !== "ENOENT")
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
let layout;
|
|
245
|
+
if (raw !== undefined) {
|
|
246
|
+
layout = frontendPlanCoverageLayoutSchema.parse(JSON.parse(raw));
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
// Losing the ownership receipt must never repartition acknowledged facts.
|
|
250
|
+
for (const relative of ["plan-typed-facts.jsonl", "parallel"]) {
|
|
251
|
+
const existing = await stat(path.join(input.runDir, input.nodeId, relative)).catch(error => {
|
|
252
|
+
if (error.code !== "ENOENT")
|
|
253
|
+
throw error;
|
|
254
|
+
return undefined;
|
|
255
|
+
});
|
|
256
|
+
if (existing && (existing.isDirectory() || existing.size > 0)) {
|
|
257
|
+
throw Error("FRONTEND_PLAN_LAYOUT_MISSING: preserve the existing ledger and restart its owning phase");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const descriptor = {
|
|
261
|
+
schemaVersion: 1, bindingSha256,
|
|
262
|
+
coverageBatches: input.workload.compactEligible ? [input.requirementIds] : input.workload.buildWorkBatches(input.requirementIds),
|
|
263
|
+
};
|
|
264
|
+
layout = { ...descriptor, layoutSha256: sha256OfCanonicalJson(descriptor) };
|
|
265
|
+
}
|
|
266
|
+
const { layoutSha256, ...descriptor } = layout;
|
|
267
|
+
if (layout.bindingSha256 !== bindingSha256 || layoutSha256 !== sha256OfCanonicalJson(descriptor)) {
|
|
268
|
+
throw Error("FRONTEND_PLAN_LAYOUT_MISMATCH: frozen coverage ownership or input binding changed");
|
|
269
|
+
}
|
|
270
|
+
const members = layout.coverageBatches.flat();
|
|
271
|
+
const owners = new Map(layout.coverageBatches.flatMap((batch, index) => batch.map(id => [id, index])));
|
|
272
|
+
if (members.length !== owners.size || members.length !== input.requirementIds.length ||
|
|
273
|
+
input.requirementIds.some(id => !owners.has(id)) ||
|
|
274
|
+
input.workload.workGroups.some(group => new Set(group.requirementIds.map(id => owners.get(id))).size !== 1)) {
|
|
275
|
+
throw Error("FRONTEND_PLAN_LAYOUT_INVALID: coverage must partition complete execution groups exactly once");
|
|
276
|
+
}
|
|
277
|
+
if (raw === undefined)
|
|
278
|
+
await writeDagNodeJsonArtifact(input.runDir, input.nodeId, fileName, layout);
|
|
279
|
+
return layout.coverageBatches;
|
|
280
|
+
}
|