@tea-agent/loop-agent 0.30.0 → 0.31.1

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/dist/executors/dag-pi-executor.js +72 -4
  3. package/dist/executors/pi-sdk-executor.js +61 -0
  4. package/dist/executors/shell-executor.js +136 -79
  5. package/dist/worker/observability/dag-execution-trajectory.js +591 -0
  6. package/dist/worker/observability/read-model.js +258 -31
  7. package/dist/worker/observe/dag-node-execution-output.js +180 -0
  8. package/dist/worker/observe/routes.js +53 -6
  9. package/dist/worker/observe/static/dag-edge-routing.js +368 -0
  10. package/dist/worker/observe/static/dag-history-labels.js +95 -0
  11. package/dist/worker/observe/static/dag-layout.d.ts +12 -7
  12. package/dist/worker/observe/static/dag-layout.js +101 -21
  13. package/dist/worker/observe/static/favicon.svg +37 -0
  14. package/dist/worker/observe/static/format.js +31 -1
  15. package/dist/worker/observe/static/index.html +1 -1
  16. package/dist/worker/observe/static/state.js +102 -0
  17. package/dist/worker/observe/static/styles.css +267 -7
  18. package/dist/worker/observe/static/views/dag-graph.js +414 -154
  19. package/dist/worker/observe/static/views/dag-inspector.js +478 -27
  20. package/dist/worker/observe/static/views/dag-trajectory.js +313 -0
  21. package/dist/worker/observe/static/views/dag.js +20 -3
  22. package/dist/workflows/dag/backend-test-pytest-collection.js +162 -7
  23. package/dist/workflows/dag/backend-test-result-contract.js +105 -67
  24. package/dist/workflows/dag/backend-test-scenario-param.js +92 -30
  25. package/dist/workflows/dag/backend-test-writer-completeness.js +55 -0
  26. package/dist/workflows/dag/init-hybrid.js +46 -49
  27. package/dist/workflows/dag/rerun-task.js +86 -0
  28. package/docs/architecture/README.md +4 -0
  29. package/docs/architecture/dag-execution.md +1 -1
  30. package/docs/architecture/worker-and-feature.md +1 -1
  31. package/docs/governance/README.md +3 -0
  32. package/docs/operations/README.md +1 -0
  33. package/docs/templates/backend-test-dag.json +40 -60
  34. package/docs/templates/frontend-test-dag.json +1 -1
  35. package/harness.json +3 -3
  36. package/package.json +3 -2
  37. package/scripts/kb-bootstrap-init-skeleton.sh +1 -1
@@ -299,6 +299,37 @@ export function truncateText(text, maxLen) {
299
299
  return `${normalized.slice(0, maxLen)}…`;
300
300
  }
301
301
 
302
+ /** Approximate display columns for CJK-heavy UI strings (SVG node cards). */
303
+ export function displayWidth(text) {
304
+ let width = 0;
305
+ for (const char of String(text ?? "")) {
306
+ const code = char.codePointAt(0) ?? 0;
307
+ // Treat non-ASCII (CJK / fullwidth / most symbols) as double-width.
308
+ width += code > 0x00ff ? 2 : 1;
309
+ }
310
+ return width;
311
+ }
312
+
313
+ /** Truncate by display width so CJK status lines fit fixed SVG node cards. */
314
+ export function truncateDisplayText(text, maxWidth) {
315
+ if (!text) return "—";
316
+ const normalized = String(text).replace(/\s+/g, " ").trim();
317
+ const limit = Math.max(1, Number(maxWidth) || 0);
318
+ if (displayWidth(normalized) <= limit) return normalized;
319
+ let out = "";
320
+ let width = 0;
321
+ // Reserve 1 column for the ellipsis when possible.
322
+ const budget = Math.max(1, limit - 1);
323
+ for (const char of normalized) {
324
+ const code = char.codePointAt(0) ?? 0;
325
+ const w = code > 0x00ff ? 2 : 1;
326
+ if (width + w > budget) break;
327
+ out += char;
328
+ width += w;
329
+ }
330
+ return `${out}…`;
331
+ }
332
+
302
333
  export function repoBaseName(repoRoot) {
303
334
  if (!repoRoot) return "未绑定仓库";
304
335
  const trimmed = String(repoRoot).replace(/[\\/]+$/, "");
@@ -314,4 +345,3 @@ export function formatRepoPathDisplay(repoRoot) {
314
345
  .replace(/^\/Users\/[^/]+/, "~")
315
346
  .replace(/^\/home\/[^/]+/, "~");
316
347
  }
317
-
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>Loop Agent</title>
7
- <link rel="icon" href="data:," />
7
+ <link rel="icon" type="image/svg+xml" href="favicon.svg" />
8
8
  <link rel="preconnect" href="https://cdn.jsdelivr.net" />
9
9
  <link
10
10
  rel="stylesheet"
@@ -41,6 +41,21 @@ export const uiState = {
41
41
  dagNodeInputData: null,
42
42
  dagNodeInputError: null,
43
43
  dagNodeInputLoading: false,
44
+ /**
45
+ * Execution-output request identity ("<dagRunId>:<nodeId>:<key>#<seq>").
46
+ * Stale async responses are rejected when this no longer matches.
47
+ */
48
+ dagNodeExecutionOutputIdentity: null,
49
+ dagNodeExecutionOutputRequestSeq: 0,
50
+ /**
51
+ * Cache of loaded execution bodies keyed by dagRunId\0nodeId\0key.
52
+ * Survives poll re-renders; cleared on node switch.
53
+ */
54
+ dagNodeExecutionOutputCache: null,
55
+ /** Loading/error for the currently requested execution key (not global wipe). */
56
+ dagNodeExecutionOutputLoading: false,
57
+ dagNodeExecutionOutputError: null,
58
+ dagNodeExecutionOutputKey: null,
44
59
  /** When true,「执行过程」展开 message_start/end 原始协议事件。 */
45
60
  sessionTimelineShowProtocol: false,
46
61
  /** Open <details> keys restored across timeline re-renders. */
@@ -48,8 +63,19 @@ export const uiState = {
48
63
  lastSnapshot: null,
49
64
  dagInspectorOpen: false,
50
65
  dagInspectorTab: "output",
66
+ /**
67
+ * Inspector attempt selection (identity: dagRunId + nodeId + attempt key).
68
+ * null = latest attempt; reset on node switch; preserved across poll refreshes.
69
+ */
70
+ dagInspectorAttempt: null,
71
+ /**
72
+ * DAG 图区域当前视图模式镜像(真实持久在 dagGraphViewModes Map)。
73
+ * 同会话内重新进入同一 dagRunId 时保持,不切回逻辑拓扑(AC-001)。
74
+ */
75
+ dagGraphViewMode: "topology",
51
76
  dagInspectorWidth: null,
52
77
  dagGraphViewportState: null,
78
+ dagTrajectoryViewportState: null,
53
79
  dagTimelineViewportState: null,
54
80
  dagNodeOutputViewportState: null,
55
81
  dagNodeInputViewportState: null,
@@ -69,6 +95,42 @@ export const uiState = {
69
95
  /** Per-dagRunId graph viewport restore map (stable Map instance). */
70
96
  export const dagGraphViewportStates = new Map();
71
97
 
98
+ /**
99
+ * Per-dagRunId graph view mode ("topology" | "trajectory"). Session-only
100
+ * persistence: poll refreshes and re-entering the same dagRunId keep the
101
+ * current mode; a full page reload falls back to 逻辑拓扑 (AC-001 evidence gap).
102
+ */
103
+ export const dagGraphViewModes = new Map();
104
+
105
+ /** Per-dagRunId execution-trajectory viewport restore map. */
106
+ export const dagTrajectoryViewportStates = new Map();
107
+
108
+ /** Set the graph view mode for a dagRunId (topology | trajectory). */
109
+ export function setDagGraphViewMode(dagRunId, mode) {
110
+ const normalized = mode === "trajectory" ? "trajectory" : "topology";
111
+ dagGraphViewModes.set(dagRunId, normalized);
112
+ uiState.dagGraphViewMode = normalized;
113
+ return normalized;
114
+ }
115
+
116
+ /** Resolve the effective graph view mode for a dagRunId (default topology). */
117
+ export function currentDagGraphViewMode(dagRunId) {
118
+ return dagGraphViewModes.get(dagRunId) ?? "topology";
119
+ }
120
+
121
+ export function updateDagTrajectoryViewportState(
122
+ _previous,
123
+ dagRunId,
124
+ scrollLeft,
125
+ scrollTop,
126
+ ) {
127
+ return {
128
+ dagRunId,
129
+ scrollLeft: Math.max(0, Number(scrollLeft) || 0),
130
+ scrollTop: Math.max(0, Number(scrollTop) || 0),
131
+ };
132
+ }
133
+
72
134
  /**
73
135
  * Per-context inspector content scroll map.
74
136
  * Key format: dag:<dagRunId>:<nodeId>:<tab>
@@ -140,6 +202,46 @@ export function makeDagNodeInputIdentity(dagRunId, nodeId) {
140
202
  return `${dagRunId}:${nodeId}#${uiState.dagNodeInputRequestSeq}`;
141
203
  }
142
204
 
205
+ /** Build a fresh execution-output identity token (includes key). */
206
+ export function makeDagNodeExecutionOutputIdentity(dagRunId, nodeId, key) {
207
+ uiState.dagNodeExecutionOutputRequestSeq += 1;
208
+ return `${dagRunId}:${nodeId}:${key ?? "latest"}#${uiState.dagNodeExecutionOutputRequestSeq}`;
209
+ }
210
+
211
+ /** Stable cache key for a node execution body. */
212
+ export function dagNodeExecutionOutputCacheKey(dagRunId, nodeId, key) {
213
+ return `${dagRunId ?? ""}\0${nodeId ?? ""}\0${key ?? "latest"}`;
214
+ }
215
+
216
+ /** Read a cached execution body entry (or null). */
217
+ export function getDagNodeExecutionOutputCache(dagRunId, nodeId, key) {
218
+ const map = uiState.dagNodeExecutionOutputCache;
219
+ if (!(map instanceof Map)) return null;
220
+ return map.get(dagNodeExecutionOutputCacheKey(dagRunId, nodeId, key)) ?? null;
221
+ }
222
+
223
+ /** Write a successful body into the cache without clearing other keys. */
224
+ export function setDagNodeExecutionOutputCache(dagRunId, nodeId, key, entry) {
225
+ if (!(uiState.dagNodeExecutionOutputCache instanceof Map)) {
226
+ uiState.dagNodeExecutionOutputCache = new Map();
227
+ }
228
+ uiState.dagNodeExecutionOutputCache.set(
229
+ dagNodeExecutionOutputCacheKey(dagRunId, nodeId, key),
230
+ entry,
231
+ );
232
+ return entry;
233
+ }
234
+
235
+ /** Drop all cached execution bodies (node switch / leave). */
236
+ export function clearDagNodeExecutionOutputCache() {
237
+ uiState.dagNodeExecutionOutputCache = new Map();
238
+ uiState.dagNodeExecutionOutputError = null;
239
+ uiState.dagNodeExecutionOutputLoading = false;
240
+ uiState.dagNodeExecutionOutputKey = null;
241
+ uiState.dagNodeExecutionOutputIdentity = null;
242
+ uiState.dagNodeExecutionOutputRequestSeq += 1;
243
+ }
244
+
143
245
  export function isPageVisible() {
144
246
  return (
145
247
  typeof document === "undefined" || document.visibilityState !== "hidden"
@@ -1453,6 +1453,43 @@ body.is-resizing-dag-graph {
1453
1453
  overflow: auto;
1454
1454
  overscroll-behavior: contain;
1455
1455
  }
1456
+ .dag-inspector-attempt-bar {
1457
+ position: sticky;
1458
+ top: calc(-1 * var(--space-4));
1459
+ z-index: 2;
1460
+ display: flex;
1461
+ flex-wrap: wrap;
1462
+ align-items: center;
1463
+ gap: var(--space-2);
1464
+ padding-bottom: var(--space-3);
1465
+ margin-bottom: var(--space-3);
1466
+ border-bottom: 1px solid var(--hairline);
1467
+ background: var(--surface);
1468
+ }
1469
+ .dag-inspector-attempt-label {
1470
+ font-size: 12px;
1471
+ color: var(--muted);
1472
+ }
1473
+ .dag-inspector-attempt-select {
1474
+ font-size: 12px;
1475
+ padding: 2px 6px;
1476
+ border: 1px solid var(--hairline);
1477
+ border-radius: 6px;
1478
+ background: var(--surface);
1479
+ color: var(--ink);
1480
+ }
1481
+ .dag-inspector-attempt-preview {
1482
+ display: flex;
1483
+ flex-wrap: wrap;
1484
+ align-items: center;
1485
+ gap: var(--space-2);
1486
+ font-size: 12px;
1487
+ color: var(--body);
1488
+ }
1489
+ .dag-inspector-attempt-artifact-note {
1490
+ color: var(--muted);
1491
+ font-size: 12px;
1492
+ }
1456
1493
  .panel-graph {
1457
1494
  min-height: 0;
1458
1495
  }
@@ -1513,22 +1550,35 @@ body.is-resizing-dag-graph {
1513
1550
  }
1514
1551
  .dag-edge {
1515
1552
  fill: none;
1516
- stroke: color-mix(in srgb, var(--hairline-strong) 80%, var(--muted));
1517
- stroke-width: 1.75;
1553
+ /* P1 ordinary edges: stronger contrast / slightly thicker for readability. */
1554
+ stroke: color-mix(in srgb, var(--hairline-strong) 78%, var(--muted));
1555
+ stroke-width: 1.6;
1556
+ stroke-linecap: round;
1557
+ stroke-linejoin: round;
1518
1558
  transition:
1519
1559
  opacity var(--transition-fast),
1520
1560
  stroke var(--transition-fast);
1521
1561
  }
1522
1562
  #dag-arrow path {
1523
- fill: color-mix(in srgb, var(--hairline-strong) 80%, var(--muted));
1563
+ fill: color-mix(in srgb, var(--hairline-strong) 62%, var(--muted));
1524
1564
  }
1565
+ /* Control loops stay visible but visually lighter than the main chain. */
1525
1566
  .dag-edge-control {
1526
- stroke: var(--orange);
1527
- stroke-width: 2;
1528
- stroke-dasharray: 6 4;
1567
+ stroke: color-mix(in srgb, var(--orange) 58%, var(--muted));
1568
+ stroke-width: 1.25;
1569
+ stroke-dasharray: 5 5;
1570
+ opacity: 0.72;
1529
1571
  }
1530
1572
  #dag-arrow-control path {
1531
- fill: var(--orange);
1573
+ fill: color-mix(in srgb, var(--orange) 58%, var(--muted));
1574
+ }
1575
+ .dag-edge-control-label {
1576
+ fill: color-mix(in srgb, var(--orange) 70%, var(--muted));
1577
+ font-size: 10px;
1578
+ font-weight: 500;
1579
+ opacity: 0.85;
1580
+ pointer-events: none;
1581
+ user-select: none;
1532
1582
  }
1533
1583
  .dag-graph-node {
1534
1584
  cursor: pointer;
@@ -1597,6 +1647,10 @@ body.is-resizing-dag-graph {
1597
1647
  fill: var(--body);
1598
1648
  font-size: 11px;
1599
1649
  }
1650
+ .dag-node-history {
1651
+ fill: var(--muted);
1652
+ font-size: 10px;
1653
+ }
1600
1654
  .dag-dimmed {
1601
1655
  opacity: 0.28;
1602
1656
  }
@@ -1609,6 +1663,212 @@ body.is-resizing-dag-graph {
1609
1663
  }
1610
1664
  }
1611
1665
 
1666
+ /* ---- 双视图 segmented control(逻辑拓扑 | 执行轨迹,AC-001) ---- */
1667
+ .dag-graph-toolbar-left {
1668
+ display: flex;
1669
+ align-items: center;
1670
+ flex-wrap: wrap;
1671
+ gap: var(--space-3);
1672
+ }
1673
+ .dag-graph-mode-switch {
1674
+ display: inline-flex;
1675
+ align-items: center;
1676
+ gap: 2px;
1677
+ padding: 3px;
1678
+ border: 1px solid var(--hairline);
1679
+ border-radius: var(--radius-pill);
1680
+ background: var(--surface);
1681
+ box-shadow: var(--shadow-panel);
1682
+ }
1683
+ .dag-graph-mode-tab {
1684
+ min-height: 26px;
1685
+ padding: 0 12px;
1686
+ border: 1px solid transparent;
1687
+ border-radius: var(--radius-pill);
1688
+ background: transparent;
1689
+ color: var(--muted);
1690
+ font-size: 12px;
1691
+ font-weight: 700;
1692
+ cursor: pointer;
1693
+ }
1694
+ .dag-graph-mode-tab:hover {
1695
+ color: var(--ink);
1696
+ }
1697
+ .dag-graph-mode-tab[aria-selected="true"] {
1698
+ background: var(--ink);
1699
+ color: #fff;
1700
+ }
1701
+ .dag-graph-mode-tab:focus-visible {
1702
+ outline: 2px solid var(--orange);
1703
+ outline-offset: 1px;
1704
+ color: var(--ink);
1705
+ }
1706
+ .dag-graph-mode-tab[aria-selected="true"]:focus-visible {
1707
+ outline-color: var(--orange-active);
1708
+ color: #fff;
1709
+ }
1710
+
1711
+ /* ---- 执行轨迹 storyboard(AC-002..AC-006) ---- */
1712
+ .dag-trajectory {
1713
+ display: flex;
1714
+ flex-direction: column;
1715
+ gap: var(--space-3);
1716
+ padding: var(--space-4);
1717
+ min-width: max-content;
1718
+ }
1719
+ .dag-trajectory-lane {
1720
+ display: flex;
1721
+ flex-direction: column;
1722
+ gap: var(--space-2);
1723
+ }
1724
+ .dag-trajectory-lane-head {
1725
+ display: flex;
1726
+ align-items: baseline;
1727
+ gap: var(--space-2);
1728
+ }
1729
+ .dag-trajectory-lane-title {
1730
+ font-size: 13px;
1731
+ font-weight: 750;
1732
+ color: var(--ink);
1733
+ }
1734
+ .dag-trajectory-lane-meta {
1735
+ font-size: 11px;
1736
+ color: var(--muted);
1737
+ }
1738
+ .dag-trajectory-lane-cards {
1739
+ display: flex;
1740
+ flex-wrap: wrap;
1741
+ align-items: stretch;
1742
+ gap: var(--space-2);
1743
+ }
1744
+ .dag-trajectory-occurrence {
1745
+ display: flex;
1746
+ flex-direction: column;
1747
+ align-items: flex-start;
1748
+ gap: 6px;
1749
+ min-width: 176px;
1750
+ max-width: 240px;
1751
+ padding: 10px 12px;
1752
+ border: 1px solid var(--hairline);
1753
+ border-radius: var(--radius-lg);
1754
+ background: var(--surface);
1755
+ color: var(--ink);
1756
+ text-align: left;
1757
+ cursor: pointer;
1758
+ box-shadow: var(--shadow-panel);
1759
+ }
1760
+ .dag-trajectory-occurrence:hover,
1761
+ .dag-trajectory-occurrence:focus-visible {
1762
+ border-color: var(--orange);
1763
+ outline: none;
1764
+ }
1765
+ .dag-trajectory-occurrence-node {
1766
+ font-size: 12px;
1767
+ font-weight: 700;
1768
+ word-break: break-all;
1769
+ }
1770
+ .dag-trajectory-occurrence-status {
1771
+ align-self: flex-start;
1772
+ }
1773
+ .dag-trajectory-occurrence-meta {
1774
+ font-size: 11px;
1775
+ color: var(--muted);
1776
+ }
1777
+ .dag-trajectory-occurrence-verdict {
1778
+ font-size: 11px;
1779
+ color: var(--body);
1780
+ font-family: "JetBrains Mono", "SFMono-Regular", Consolas, monospace;
1781
+ }
1782
+ .dag-trajectory-attempt-strip {
1783
+ font-size: 11px;
1784
+ color: var(--body);
1785
+ }
1786
+ /* 历史失败已恢复/已覆盖:独立中性样式(不占用当前终态失败红色,不洗白事实)。 */
1787
+ .dag-trajectory-occurrence.is-recovered {
1788
+ border-color: color-mix(in srgb, var(--amber, #b58900) 55%, var(--hairline));
1789
+ background: color-mix(in srgb, var(--amber-soft, #fff7d6) 55%, var(--surface));
1790
+ }
1791
+ .dag-trajectory-recovered {
1792
+ font-size: 11px;
1793
+ font-weight: 700;
1794
+ color: var(--amber, #b58900);
1795
+ }
1796
+ .dag-trajectory-partial {
1797
+ font-size: 11px;
1798
+ color: var(--muted);
1799
+ border: 1px dashed var(--hairline-strong);
1800
+ border-radius: 6px;
1801
+ padding: 2px 6px;
1802
+ }
1803
+ .dag-trajectory-transition {
1804
+ display: flex;
1805
+ align-items: center;
1806
+ gap: var(--space-2);
1807
+ padding-left: 4px;
1808
+ }
1809
+ .dag-trajectory-transition-arrow {
1810
+ color: var(--muted);
1811
+ font-size: 14px;
1812
+ }
1813
+ .dag-trajectory-transition-reason {
1814
+ font-size: 11px;
1815
+ color: var(--muted);
1816
+ }
1817
+ .dag-trajectory-banner {
1818
+ padding: 8px 12px;
1819
+ border: 1px solid color-mix(in srgb, var(--amber, #b58900) 50%, var(--hairline));
1820
+ border-radius: var(--radius-lg);
1821
+ background: color-mix(in srgb, var(--amber-soft, #fff7d6) 60%, var(--surface));
1822
+ color: var(--body);
1823
+ font-size: 12px;
1824
+ }
1825
+ .dag-trajectory-banner.is-error {
1826
+ border-color: color-mix(in srgb, var(--red) 50%, var(--hairline));
1827
+ background: color-mix(in srgb, var(--red) 6%, var(--surface));
1828
+ color: var(--red);
1829
+ }
1830
+ .dag-trajectory-empty {
1831
+ display: flex;
1832
+ flex-direction: column;
1833
+ align-items: center;
1834
+ gap: var(--space-3);
1835
+ padding: var(--space-8) var(--space-4);
1836
+ color: var(--muted);
1837
+ }
1838
+ .dag-trajectory-empty-text {
1839
+ margin: 0;
1840
+ font-size: 13px;
1841
+ }
1842
+ .dag-trajectory-back {
1843
+ min-height: 32px;
1844
+ padding: 0 12px;
1845
+ border: 1px solid var(--hairline);
1846
+ border-radius: var(--radius-pill);
1847
+ background: var(--surface);
1848
+ color: var(--ink);
1849
+ font-size: 12px;
1850
+ font-weight: 650;
1851
+ cursor: pointer;
1852
+ }
1853
+ .dag-trajectory-back:hover,
1854
+ .dag-trajectory-back:focus-visible {
1855
+ border-color: var(--orange);
1856
+ color: var(--orange-active);
1857
+ outline: none;
1858
+ }
1859
+ .dag-trajectory-fallback {
1860
+ margin: 0;
1861
+ padding: var(--space-4) var(--space-5);
1862
+ list-style: none;
1863
+ display: flex;
1864
+ flex-direction: column;
1865
+ gap: var(--space-2);
1866
+ }
1867
+ .dag-trajectory-fallback-item {
1868
+ font-size: 12px;
1869
+ color: var(--body);
1870
+ }
1871
+
1612
1872
  .dag-section-heading {
1613
1873
  display: flex;
1614
1874
  align-items: baseline;