@tea-agent/loop-agent 0.26.1 → 0.26.3
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 +47 -0
- package/dist/application/dag/generate-task-dag.js +33 -0
- package/dist/commands/task-source-prepare.js +6 -0
- package/dist/executors/dag-pi-executor.js +156 -9
- package/dist/executors/shell-executor.js +111 -0
- package/dist/executors/shell-presets.js +12 -4
- package/dist/executors/shell-write-guard.js +145 -12
- package/dist/task/config-types.js +6 -0
- package/dist/task/contract/constants.js +1 -0
- package/dist/task/contract/project.js +8 -0
- package/dist/task/contract/schema.js +1 -0
- package/dist/task/frontend-preflight.js +131 -0
- package/dist/task/runtime.js +2 -4
- package/dist/task/source-prepare/build-draft.js +9 -0
- package/dist/task/source-prepare/completeness.js +1 -1
- package/dist/worker/observability/read-model.js +134 -0
- package/dist/worker/observe/static/state.js +61 -0
- package/dist/worker/observe/static/styles.css +8 -0
- package/dist/worker/observe/static/views/dag-graph.js +107 -31
- package/dist/worker/observe/static/views/dag-inspector.js +374 -157
- package/dist/worker/observe/static/views/dag.js +4 -11
- package/dist/workflows/dag/backend-test-pytest-collection.js +277 -0
- package/dist/workflows/dag/convergence/controller.js +110 -21
- package/dist/workflows/dag/frontend-implementation-contract.js +218 -17
- package/dist/workflows/dag/frontend-repair.js +29 -29
- package/dist/workflows/dag/frontend-review-context.js +7 -1
- package/dist/workflows/dag/frontend-verification-trace.js +26 -5
- package/dist/workflows/dag/frontend-worktree-diff.js +14 -3
- package/dist/workflows/dag/governance-profile.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +115 -39
- package/dist/workflows/dag/output-protocol.js +180 -7
- package/dist/workflows/dag/runner.js +141 -52
- package/dist/workflows/dag/types.js +5 -1
- package/dist/workflows/dag/validate.js +3 -2
- package/docs/templates/backend-test-dag.json +100 -8
- package/package.json +1 -1
- package/skills/loop-agent/references/hybrid-dag.md +1 -1
|
@@ -53,6 +53,8 @@ export const uiState = {
|
|
|
53
53
|
dagTimelineViewportState: null,
|
|
54
54
|
dagNodeOutputViewportState: null,
|
|
55
55
|
dagNodeInputViewportState: null,
|
|
56
|
+
/** Spec-evidence inspector content scroll (single active region snapshot). */
|
|
57
|
+
dagNodeSpecEvidenceViewportState: null,
|
|
56
58
|
runOutputViewportState: null,
|
|
57
59
|
/**
|
|
58
60
|
* Spec-evidence file preview drill-down. null = list view; otherwise
|
|
@@ -67,6 +69,65 @@ export const uiState = {
|
|
|
67
69
|
/** Per-dagRunId graph viewport restore map (stable Map instance). */
|
|
68
70
|
export const dagGraphViewportStates = new Map();
|
|
69
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Per-context inspector content scroll map.
|
|
74
|
+
* Key format: dag:<dagRunId>:<nodeId>:<tab>
|
|
75
|
+
*/
|
|
76
|
+
export const dagInspectorViewportStates = new Map();
|
|
77
|
+
|
|
78
|
+
/** Build a stable inspector scroll region key (AC-005 context isolation). */
|
|
79
|
+
export function inspectorViewportRegionKey(dagRunId, nodeId, tab) {
|
|
80
|
+
return `dag:${dagRunId ?? ""}:${nodeId ?? ""}:${tab ?? "output"}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function updateDagInspectorViewportState(
|
|
84
|
+
_previous,
|
|
85
|
+
regionKey,
|
|
86
|
+
scrollLeft,
|
|
87
|
+
scrollTop,
|
|
88
|
+
) {
|
|
89
|
+
return {
|
|
90
|
+
regionKey,
|
|
91
|
+
scrollLeft: Math.max(0, Number(scrollLeft) || 0),
|
|
92
|
+
scrollTop: Math.max(0, Number(scrollTop) || 0),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Persist inspector scroll for a context. When `preserveIntent` is true and the
|
|
98
|
+
* incoming position is a zero clamp on short (loading) content while a non-zero
|
|
99
|
+
* intent already exists, keep the previous intent instead of overwriting to 0.
|
|
100
|
+
*/
|
|
101
|
+
export function saveDagInspectorViewportState(
|
|
102
|
+
regionKey,
|
|
103
|
+
scrollLeft,
|
|
104
|
+
scrollTop,
|
|
105
|
+
options = {},
|
|
106
|
+
) {
|
|
107
|
+
if (!regionKey) return null;
|
|
108
|
+
const left = Math.max(0, Number(scrollLeft) || 0);
|
|
109
|
+
const top = Math.max(0, Number(scrollTop) || 0);
|
|
110
|
+
if (options.preserveIntent) {
|
|
111
|
+
const existing = dagInspectorViewportStates.get(regionKey);
|
|
112
|
+
if (
|
|
113
|
+
existing &&
|
|
114
|
+
left === 0 &&
|
|
115
|
+
top === 0 &&
|
|
116
|
+
(existing.scrollLeft > 0 || existing.scrollTop > 0)
|
|
117
|
+
) {
|
|
118
|
+
return existing;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const next = updateDagInspectorViewportState(null, regionKey, left, top);
|
|
122
|
+
dagInspectorViewportStates.set(regionKey, next);
|
|
123
|
+
return next;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function getDagInspectorViewportState(regionKey) {
|
|
127
|
+
if (!regionKey) return null;
|
|
128
|
+
return dagInspectorViewportStates.get(regionKey) ?? null;
|
|
129
|
+
}
|
|
130
|
+
|
|
70
131
|
/** Build a fresh session-event identity token and bump the global sequence. */
|
|
71
132
|
export function makeSessionEventIdentity(dagRunId, nodeId) {
|
|
72
133
|
uiState.sessionEventIdentitySeq += 1;
|
|
@@ -1522,6 +1522,14 @@ body.is-resizing-dag-graph {
|
|
|
1522
1522
|
#dag-arrow path {
|
|
1523
1523
|
fill: color-mix(in srgb, var(--hairline-strong) 80%, var(--muted));
|
|
1524
1524
|
}
|
|
1525
|
+
.dag-edge-control {
|
|
1526
|
+
stroke: var(--orange);
|
|
1527
|
+
stroke-width: 2;
|
|
1528
|
+
stroke-dasharray: 6 4;
|
|
1529
|
+
}
|
|
1530
|
+
#dag-arrow-control path {
|
|
1531
|
+
fill: var(--orange);
|
|
1532
|
+
}
|
|
1525
1533
|
.dag-graph-node {
|
|
1526
1534
|
cursor: pointer;
|
|
1527
1535
|
transition: opacity var(--transition-fast);
|
|
@@ -26,30 +26,52 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
26
26
|
const edges = dag.edges ?? [];
|
|
27
27
|
const layout = layoutDag(nodes, edges, dag.ranks ?? []);
|
|
28
28
|
const wrap = existingWrap ?? el("div", "dag-graph-wrap");
|
|
29
|
+
// Preserve .dag-graph-viewport DOM identity across poll refreshes so the
|
|
30
|
+
// user's active scroll/pointer target is not destroyed mid-gesture.
|
|
31
|
+
let viewport =
|
|
32
|
+
existingWrap?.querySelector?.(":scope > .dag-graph-viewport") ??
|
|
33
|
+
existingWrap?.querySelector?.(".dag-graph-viewport") ??
|
|
34
|
+
null;
|
|
29
35
|
if (existingWrap) {
|
|
30
|
-
//
|
|
31
|
-
|
|
36
|
+
// Remove only non-interactive siblings. Keeping the viewport connected is
|
|
37
|
+
// important: detaching and re-inserting the same node still cancels a native
|
|
38
|
+
// scrollbar drag/pointer target even though object identity is unchanged.
|
|
39
|
+
for (const child of [...wrap.children]) {
|
|
40
|
+
if (child !== viewport) wrap.removeChild(child);
|
|
41
|
+
}
|
|
42
|
+
if (viewport) {
|
|
43
|
+
while (viewport.firstChild) viewport.removeChild(viewport.firstChild);
|
|
44
|
+
}
|
|
32
45
|
}
|
|
33
46
|
const toolbar = el("div", "dag-graph-toolbar");
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
47
|
+
let summaryText = `${nodes.length} 节点 · ${layout.edges.length} 条依赖`;
|
|
48
|
+
const convergence = dag.convergence;
|
|
49
|
+
if (convergence?.enabled) {
|
|
50
|
+
summaryText += ` · pass ${convergence.currentPass}/${convergence.maxPasses}`;
|
|
51
|
+
if (convergence.terminalReason) {
|
|
52
|
+
summaryText += ` · ${convergence.terminalReason}`;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
toolbar.appendChild(el("span", "dag-graph-summary", summaryText));
|
|
41
56
|
const reset = el("button", "dag-graph-reset", "重置视图");
|
|
42
57
|
toolbar.appendChild(reset);
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
wrap.appendChild(
|
|
58
|
+
const appendBeforeViewport = (child) => {
|
|
59
|
+
if (viewport?.parentNode === wrap) wrap.insertBefore(child, viewport);
|
|
60
|
+
else wrap.appendChild(child);
|
|
61
|
+
};
|
|
62
|
+
appendBeforeViewport(toolbar);
|
|
63
|
+
if (edges.length === 0) {
|
|
64
|
+
appendBeforeViewport(
|
|
46
65
|
el(
|
|
47
66
|
"p",
|
|
48
67
|
"dag-graph-fallback",
|
|
49
68
|
"该运行没有可用依赖数据;图按并行层级展示,节点表格仍保留完整数据。",
|
|
50
69
|
),
|
|
51
70
|
);
|
|
52
|
-
|
|
71
|
+
}
|
|
72
|
+
if (!viewport) {
|
|
73
|
+
viewport = el("div", "dag-graph-viewport");
|
|
74
|
+
}
|
|
53
75
|
viewport.dataset.dagRunId = dagRunId;
|
|
54
76
|
const svg = svgEl("svg", {
|
|
55
77
|
class: "dag-graph",
|
|
@@ -71,6 +93,17 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
71
93
|
});
|
|
72
94
|
marker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
|
|
73
95
|
defs.appendChild(marker);
|
|
96
|
+
const controlMarker = svgEl("marker", {
|
|
97
|
+
id: "dag-arrow-control",
|
|
98
|
+
viewBox: "0 0 10 10",
|
|
99
|
+
refX: 9,
|
|
100
|
+
refY: 5,
|
|
101
|
+
markerWidth: 7,
|
|
102
|
+
markerHeight: 7,
|
|
103
|
+
orient: "auto-start-reverse",
|
|
104
|
+
});
|
|
105
|
+
controlMarker.appendChild(svgEl("path", { d: "M 0 0 L 10 5 L 0 10 z" }));
|
|
106
|
+
defs.appendChild(controlMarker);
|
|
74
107
|
svg.appendChild(defs);
|
|
75
108
|
const related = new Set([uiState.selectedDagNodeId]);
|
|
76
109
|
for (const edge of layout.edges)
|
|
@@ -91,6 +124,28 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
91
124
|
path.firstChild.textContent = `${edge.from} → ${edge.to}`;
|
|
92
125
|
svg.appendChild(path);
|
|
93
126
|
}
|
|
127
|
+
// 控制回环:仅由 read model 派生(dag.controlEdges),不进 layout、不参与拓扑。
|
|
128
|
+
// 路径从 review-gate-shell 左侧绕回 repair-pi 右侧,避开节点矩形。
|
|
129
|
+
const layoutByNode = new Map(layout.nodes.map((n) => [n.nodeId, n]));
|
|
130
|
+
for (const ctrl of dag.controlEdges ?? []) {
|
|
131
|
+
const from = layoutByNode.get(ctrl.from);
|
|
132
|
+
const to = layoutByNode.get(ctrl.to);
|
|
133
|
+
if (!from || !to) continue;
|
|
134
|
+
const x1 = from.x;
|
|
135
|
+
const y1 = from.y + from.height / 2;
|
|
136
|
+
const x2 = to.x + to.width;
|
|
137
|
+
const y2 = to.y + to.height / 2;
|
|
138
|
+
const lift = Math.max(48, (from.x - to.x) / 2);
|
|
139
|
+
const rise = Math.max(40, Math.abs(from.y - to.y) / 2 + 16);
|
|
140
|
+
const path = svgEl("path", {
|
|
141
|
+
d: `M ${x1} ${y1} C ${x1 - lift} ${y1 - rise}, ${x2 + lift} ${y2 - rise}, ${x2} ${y2}`,
|
|
142
|
+
class: "dag-edge dag-edge-control",
|
|
143
|
+
"marker-end": "url(#dag-arrow-control)",
|
|
144
|
+
});
|
|
145
|
+
path.appendChild(svgEl("title"));
|
|
146
|
+
path.firstChild.textContent = `${ctrl.from} → ${ctrl.to} (control loop)`;
|
|
147
|
+
svg.appendChild(path);
|
|
148
|
+
}
|
|
94
149
|
const byId = Object.fromEntries(nodes.map((node) => [node.nodeId, node]));
|
|
95
150
|
for (const position of layout.nodes) {
|
|
96
151
|
const node = byId[position.nodeId] ?? { nodeId: position.nodeId };
|
|
@@ -115,7 +170,14 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
115
170
|
executor.textContent = `◈ ${node.executor ?? "unknown"}`;
|
|
116
171
|
group.appendChild(executor);
|
|
117
172
|
const state = svgEl("text", { x: 14, y: 74, class: "dag-node-status" });
|
|
118
|
-
|
|
173
|
+
let stateText = `${statusSymbol(status)} ${statusLabel(node.status)} · ${formatMs(node.durationMs)}`;
|
|
174
|
+
if (node.passes?.length) {
|
|
175
|
+
stateText += ` · P${node.passes.join(",")}`;
|
|
176
|
+
}
|
|
177
|
+
if (node.attemptCount > 0) {
|
|
178
|
+
stateText += ` · attempt ${node.attemptCount}`;
|
|
179
|
+
}
|
|
180
|
+
state.textContent = stateText;
|
|
119
181
|
group.appendChild(state);
|
|
120
182
|
const activate = () => selectDagNode(dagRunId, node.nodeId);
|
|
121
183
|
group.addEventListener("click", activate);
|
|
@@ -128,36 +190,45 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
128
190
|
svg.appendChild(group);
|
|
129
191
|
}
|
|
130
192
|
viewport.appendChild(svg);
|
|
131
|
-
wrap.appendChild(viewport);
|
|
193
|
+
if (viewport.parentNode !== wrap) wrap.appendChild(viewport);
|
|
132
194
|
const graphViewportState =
|
|
133
195
|
dagGraphViewportStates.get(dagRunId) ??
|
|
134
196
|
(uiState.dagGraphViewportState?.dagRunId === dagRunId
|
|
135
197
|
? uiState.dagGraphViewportState
|
|
136
198
|
: null);
|
|
137
|
-
viewport.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
viewport.
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
199
|
+
// Bind scroll capture once per viewport instance so reuse does not stack listeners.
|
|
200
|
+
if (viewport.dataset.scrollBound !== "1") {
|
|
201
|
+
viewport.dataset.scrollBound = "1";
|
|
202
|
+
viewport.addEventListener(
|
|
203
|
+
"scroll",
|
|
204
|
+
() => {
|
|
205
|
+
const id = viewport.dataset.dagRunId;
|
|
206
|
+
if (!id) return;
|
|
207
|
+
uiState.dagGraphViewportState = updateDagGraphViewportState(
|
|
208
|
+
uiState.dagGraphViewportState,
|
|
209
|
+
id,
|
|
210
|
+
viewport.scrollLeft,
|
|
211
|
+
viewport.scrollTop,
|
|
212
|
+
);
|
|
213
|
+
dagGraphViewportStates.set(id, uiState.dagGraphViewportState);
|
|
214
|
+
},
|
|
215
|
+
{ passive: true },
|
|
216
|
+
);
|
|
217
|
+
}
|
|
150
218
|
if (graphViewportState) {
|
|
151
219
|
const restoreViewport = () => {
|
|
220
|
+
if (viewport.dataset.dagRunId !== dagRunId) return;
|
|
152
221
|
viewport.scrollLeft = graphViewportState.scrollLeft;
|
|
153
222
|
viewport.scrollTop = graphViewportState.scrollTop;
|
|
154
223
|
};
|
|
224
|
+
// Apply immediately so identity reuse keeps position even before paint,
|
|
225
|
+
// then again on rAF after SVG layout settles.
|
|
226
|
+
restoreViewport();
|
|
155
227
|
if (typeof requestAnimationFrame === "function") {
|
|
156
228
|
requestAnimationFrame(restoreViewport);
|
|
157
|
-
} else {
|
|
158
|
-
restoreViewport();
|
|
159
229
|
}
|
|
160
230
|
}
|
|
231
|
+
// Toolbar is rebuilt each paint; bind reset to the current viewport/dagRunId.
|
|
161
232
|
reset.addEventListener("click", () => {
|
|
162
233
|
uiState.dagGraphViewportState = updateDagGraphViewportState(
|
|
163
234
|
uiState.dagGraphViewportState,
|
|
@@ -166,7 +237,12 @@ export function renderDagGraph(dag, dagRunId, existingWrap = null) {
|
|
|
166
237
|
0,
|
|
167
238
|
);
|
|
168
239
|
dagGraphViewportStates.set(dagRunId, uiState.dagGraphViewportState);
|
|
169
|
-
viewport.scrollTo
|
|
240
|
+
if (typeof viewport.scrollTo === "function") {
|
|
241
|
+
viewport.scrollTo({ left: 0, top: 0, behavior: "smooth" });
|
|
242
|
+
} else {
|
|
243
|
+
viewport.scrollLeft = 0;
|
|
244
|
+
viewport.scrollTop = 0;
|
|
245
|
+
}
|
|
170
246
|
});
|
|
171
247
|
return wrap;
|
|
172
248
|
}
|