@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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { collectExistingFrontendVerificationPaths } from "../workflows/dag/frontend-implementation-contract.js";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
import { inspectFrontendTestCommand, compareFrontendTestObservation, parseFrontendTestExecutionReport } from "../workflows/dag/frontend-test-execution-evidence.js";
|
|
4
|
+
import { FRONTEND_TEST_OBSERVATION_FAILURES, inspectFrontendTestCommand, compareFrontendTestObservation, parseFrontendTestExecutionReport } from "../workflows/dag/frontend-test-execution-evidence.js";
|
|
5
5
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
7
7
|
import path from "node:path";
|
|
@@ -135,6 +135,13 @@ function shouldReuseSuccessfulShellCommands(shell) {
|
|
|
135
135
|
return shell.verifyEvidence?.phase === "intermediate" || shell.verifyEvidence?.phase === "final";
|
|
136
136
|
}
|
|
137
137
|
const DEFAULT_SHELL_TIMEOUT_MS = 300_000;
|
|
138
|
+
/**
|
|
139
|
+
* Tolerance for filesystem mtime granularity when rejecting a test report as
|
|
140
|
+
* stale. Reports are written by the command we just ran, so the only legitimate
|
|
141
|
+
* skew is coarse mtime resolution (1s on some filesystems) — not a real clock
|
|
142
|
+
* difference.
|
|
143
|
+
*/
|
|
144
|
+
const REPORT_FRESHNESS_SKEW_MS = 2_000;
|
|
138
145
|
const SUMMARY_STDOUT_MAX = 4_000;
|
|
139
146
|
const SUMMARY_STDERR_MAX = 2_000;
|
|
140
147
|
const TEST_TIMEOUT_CLASSIFY_MAX_CHARS = 8_000;
|
|
@@ -312,7 +319,10 @@ export async function executeShellCommand(input) {
|
|
|
312
319
|
: undefined;
|
|
313
320
|
const child = spawn(resolveBashExecutable(), ["-c", input.command], {
|
|
314
321
|
cwd: input.cwd,
|
|
315
|
-
env: buildShellProcessEnv(input.envAllowlist,
|
|
322
|
+
env: buildShellProcessEnv(input.envAllowlist, {
|
|
323
|
+
...injectedEnv,
|
|
324
|
+
...input.extraEnv,
|
|
325
|
+
}),
|
|
316
326
|
...processTreeSpawnOptions(),
|
|
317
327
|
stdio: ["ignore", "pipe", "pipe"],
|
|
318
328
|
});
|
|
@@ -2140,16 +2150,20 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
2140
2150
|
let result = cached ? reuseCommandResult(cached, command) : undefined;
|
|
2141
2151
|
if (!result) {
|
|
2142
2152
|
const commandNumber = results.length + 1;
|
|
2143
|
-
const reportPath = path.join(meta.runDir, input.task.id, "commands", `${commandNumber}-${randomUUID()}.vitest.json`);
|
|
2144
2153
|
const observedTest = testPreflight.inspected.get(command);
|
|
2145
|
-
|
|
2146
|
-
|
|
2154
|
+
// Name the report after the resolved protocol so an operator can
|
|
2155
|
+
// tell at a glance which runner produced it.
|
|
2156
|
+
const reportScheme = observedTest?.observation.scheme ?? "vitest-json-v1";
|
|
2157
|
+
const reportPath = path.join(meta.runDir, input.task.id, "commands", `${commandNumber}-${randomUUID()}.${reportScheme}.json`);
|
|
2158
|
+
const observedInvocation = observedTest?.withReporter?.(reportPath);
|
|
2159
|
+
if (observedInvocation)
|
|
2147
2160
|
await mkdir(path.dirname(reportPath), { recursive: true });
|
|
2148
2161
|
result = await executeShellCommand({
|
|
2149
|
-
command:
|
|
2162
|
+
command: observedInvocation?.command ?? command,
|
|
2150
2163
|
cwd,
|
|
2151
2164
|
timeoutMs: shell.timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS,
|
|
2152
2165
|
envAllowlist: shell.envAllowlist,
|
|
2166
|
+
extraEnv: observedInvocation?.env,
|
|
2153
2167
|
dagRunMeta: { runDir: meta.runDir, runId: meta.runId },
|
|
2154
2168
|
reportActivity: input.reportActivity,
|
|
2155
2169
|
outputArtifacts: {
|
|
@@ -2158,21 +2172,35 @@ async function executeFrontendVerificationBundle(input, meta) {
|
|
|
2158
2172
|
},
|
|
2159
2173
|
});
|
|
2160
2174
|
let reportError;
|
|
2161
|
-
if (
|
|
2175
|
+
if (observedInvocation && result.ok) {
|
|
2162
2176
|
try {
|
|
2163
|
-
//
|
|
2177
|
+
// Runners report physical paths (e.g. /private/var on macOS).
|
|
2164
2178
|
// Match those in workspace coordinates even when shell.cwd is
|
|
2165
2179
|
// a subdirectory or the caller used a directory alias.
|
|
2180
|
+
const reportStat = await stat(reportPath);
|
|
2181
|
+
// A stale report from an earlier run would otherwise let a
|
|
2182
|
+
// command that wrote nothing at all look verified.
|
|
2183
|
+
if (reportStat.mtimeMs < started - REPORT_FRESHNESS_SKEW_MS) {
|
|
2184
|
+
throw new Error(`stale test execution report (${FRONTEND_TEST_OBSERVATION_FAILURES.reportProtocol}): report predates this command`);
|
|
2185
|
+
}
|
|
2166
2186
|
const rawReport = await readFile(reportPath, "utf8");
|
|
2167
2187
|
const reportSha256 = createHash("sha256").update(rawReport).digest("hex");
|
|
2168
|
-
|
|
2188
|
+
const parsedExecutions = parseFrontendTestExecutionReport(JSON.parse(rawReport), await realpath(observedTest?.effectiveCwd ?? cwd), await realpath(input.cwd), observedTest.observation.scheme);
|
|
2189
|
+
// Zero tests is not evidence: the runner may have matched no
|
|
2190
|
+
// file at all. The trace gate also rejects a target file with
|
|
2191
|
+
// no matching record, but failing here keeps the report itself
|
|
2192
|
+
// from being recorded as a successful observation.
|
|
2193
|
+
if (parsedExecutions.length === 0) {
|
|
2194
|
+
throw new Error(`test execution report contains no test cases (${FRONTEND_TEST_OBSERVATION_FAILURES.reportProtocol})`);
|
|
2195
|
+
}
|
|
2196
|
+
commandExecutionEvidence.set(key, parsedExecutions);
|
|
2169
2197
|
const actual = commandExecutionEvidence.get(key);
|
|
2170
2198
|
for (let offset = 0; offset < actual.length; offset += 24) {
|
|
2171
2199
|
const relative = `${input.task.id}/commands/${commandNumber}-execution-${offset / 24}.json`;
|
|
2172
|
-
const excerpt = { schemaVersion: 1, commandSha256: observedTest.observation.commandSha256, cwd: observedTest.observation.cwd, reportSha256, offset, total: actual.length, behaviorCoverage: "unconfirmed", tests: actual.slice(offset, offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) })) };
|
|
2200
|
+
const excerpt = { schemaVersion: 1, scheme: observedTest.observation.scheme, commandSha256: observedTest.observation.commandSha256, cwd: observedTest.observation.cwd, reportSha256, offset, total: actual.length, behaviorCoverage: "unconfirmed", tests: actual.slice(offset, offset + 24).map(test => ({ file: test.file, status: test.status, title: test.title.slice(0, 500), ...(test.title.length > 500 ? { titleTruncated: true } : {}) })) };
|
|
2173
2201
|
await writeDagRunJsonArtifact(meta.runDir, relative, excerpt);
|
|
2174
2202
|
const bytes = await readFile(path.join(meta.runDir, relative));
|
|
2175
|
-
testExecutionEvidence.push({ path: relative, sha256: createHash("sha256").update(bytes).digest("hex"), reportPath: path.relative(meta.runDir, reportPath).replace(/\\/g, "/"), reportSha256 });
|
|
2203
|
+
testExecutionEvidence.push({ path: relative, sha256: createHash("sha256").update(bytes).digest("hex"), reportPath: path.relative(meta.runDir, reportPath).replace(/\\/g, "/"), reportSha256, scheme: observedTest.observation.scheme });
|
|
2176
2204
|
}
|
|
2177
2205
|
}
|
|
2178
2206
|
catch (error) {
|
|
@@ -38,6 +38,11 @@ function contentTypeFor(pathname) {
|
|
|
38
38
|
return "text/css; charset=utf-8";
|
|
39
39
|
if (lower.endsWith(".json"))
|
|
40
40
|
return "application/json; charset=utf-8";
|
|
41
|
+
// A web app manifest must carry this exact type: browsers ignore the
|
|
42
|
+
// document (and therefore offer no install entry) when it arrives as
|
|
43
|
+
// application/octet-stream.
|
|
44
|
+
if (lower.endsWith(".webmanifest"))
|
|
45
|
+
return "application/manifest+json";
|
|
41
46
|
if (lower.endsWith(".svg"))
|
|
42
47
|
return "image/svg+xml";
|
|
43
48
|
if (lower.endsWith(".png"))
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
2
|
+
<title>Loop Agent App Icon</title>
|
|
3
|
+
<style>
|
|
4
|
+
.loop-ink { stroke: #26251e; }
|
|
5
|
+
@media (prefers-color-scheme: dark) {
|
|
6
|
+
.loop-ink { stroke: #f7f7f4; }
|
|
7
|
+
}
|
|
8
|
+
</style>
|
|
9
|
+
<defs>
|
|
10
|
+
<mask id="square-handoff" maskUnits="userSpaceOnUse" x="0" y="6" width="64" height="52">
|
|
11
|
+
<rect x="0" y="6" width="64" height="52" fill="#ffffff" />
|
|
12
|
+
<path
|
|
13
|
+
d="m31.15 32.28 2.83 3.11"
|
|
14
|
+
fill="none"
|
|
15
|
+
stroke="#000000"
|
|
16
|
+
stroke-linecap="round"
|
|
17
|
+
stroke-width="6.23"
|
|
18
|
+
/>
|
|
19
|
+
</mask>
|
|
20
|
+
</defs>
|
|
21
|
+
<g transform="translate(4.8485 13.899) scale(0.56566)">
|
|
22
|
+
<path
|
|
23
|
+
class="loop-ink"
|
|
24
|
+
d="M12 32c0-16 16-20 28-6l17 18c11 11 27 5 27-12S68 9 57 20L39 39C28 51 12 48 12 32Z"
|
|
25
|
+
fill="none"
|
|
26
|
+
stroke-linecap="round"
|
|
27
|
+
stroke-linejoin="round"
|
|
28
|
+
stroke-width="11"
|
|
29
|
+
mask="url(#square-handoff)"
|
|
30
|
+
/>
|
|
31
|
+
</g>
|
|
32
|
+
<path
|
|
33
|
+
d="m31.15 32.28 2.83 3.11"
|
|
34
|
+
fill="none"
|
|
35
|
+
stroke="#f54e00"
|
|
36
|
+
stroke-linecap="round"
|
|
37
|
+
stroke-width="5.37"
|
|
38
|
+
/>
|
|
39
|
+
</svg>
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<meta name="theme-color" content="#f4f2ee" />
|
|
7
|
+
<link rel="manifest" href="/manifest.webmanifest" />
|
|
6
8
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
9
|
+
<link rel="apple-touch-icon" href="/app-icon.svg" />
|
|
7
10
|
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
|
|
8
11
|
<link
|
|
9
12
|
rel="stylesheet"
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "/",
|
|
3
|
+
"name": "Loop Operator Console",
|
|
4
|
+
"short_name": "Loop Console",
|
|
5
|
+
"description": "Loop Agent 操作台:任务、Agent DAG 运行与交付证据的只读观察与受控操作界面",
|
|
6
|
+
"start_url": "/",
|
|
7
|
+
"scope": "/",
|
|
8
|
+
"display": "standalone",
|
|
9
|
+
"background_color": "#f4f2ee",
|
|
10
|
+
"theme_color": "#f4f2ee",
|
|
11
|
+
"icons": [
|
|
12
|
+
{
|
|
13
|
+
"src": "/app-icon.svg",
|
|
14
|
+
"sizes": "any",
|
|
15
|
+
"type": "image/svg+xml",
|
|
16
|
+
"purpose": "any"
|
|
17
|
+
}
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -7,10 +7,46 @@ export const CONSOLE_THEMES = Object.freeze([
|
|
|
7
7
|
|
|
8
8
|
const STORAGE_KEY = "loop-console.theme.v1";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Titlebar colour per theme, matching each palette's --canvas. An installed PWA
|
|
12
|
+
* paints the OS window frame with the theme-color meta value, so it must track
|
|
13
|
+
* the active theme: a fixed brand colour leaves an orange titlebar above the
|
|
14
|
+
* dark midnight palette. Values mirror console-theme.css --canvas.
|
|
15
|
+
*/
|
|
16
|
+
const TITLEBAR_COLOR = Object.freeze({
|
|
17
|
+
default: "#f4f2ee",
|
|
18
|
+
midnight: "#1c222a",
|
|
19
|
+
daylight: "#eef0f2",
|
|
20
|
+
});
|
|
21
|
+
|
|
10
22
|
function normalizeTheme(value) {
|
|
11
23
|
return CONSOLE_THEMES.some((theme) => theme.id === value) ? value : "default";
|
|
12
24
|
}
|
|
13
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Project the theme onto the document's theme-color meta tag. Browsers read
|
|
28
|
+
* this tag live (the manifest theme_color only applies at install time), and
|
|
29
|
+
* an installed window repaints its frame when it changes.
|
|
30
|
+
*
|
|
31
|
+
* Strictly best-effort: the titlebar is cosmetic, so a missing or partial DOM
|
|
32
|
+
* (SSR, a test stub, an exotic embedder) must never break theme switching.
|
|
33
|
+
*/
|
|
34
|
+
function syncTitlebarColor(theme) {
|
|
35
|
+
try {
|
|
36
|
+
const head = document?.head;
|
|
37
|
+
if (!head || typeof document.createElement !== "function") return;
|
|
38
|
+
let meta = document.querySelector?.('meta[name="theme-color"]');
|
|
39
|
+
if (!meta) {
|
|
40
|
+
meta = document.createElement("meta");
|
|
41
|
+
meta.setAttribute("name", "theme-color");
|
|
42
|
+
head.appendChild(meta);
|
|
43
|
+
}
|
|
44
|
+
meta.setAttribute("content", TITLEBAR_COLOR[theme] ?? TITLEBAR_COLOR.default);
|
|
45
|
+
} catch {
|
|
46
|
+
// The palette switch above already succeeded; the titlebar is optional.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
14
50
|
export function applyStoredConsoleTheme() {
|
|
15
51
|
let value;
|
|
16
52
|
try {
|
|
@@ -20,6 +56,7 @@ export function applyStoredConsoleTheme() {
|
|
|
20
56
|
}
|
|
21
57
|
const theme = normalizeTheme(value);
|
|
22
58
|
document.documentElement.dataset.consoleTheme = theme;
|
|
59
|
+
syncTitlebarColor(theme);
|
|
23
60
|
return theme;
|
|
24
61
|
}
|
|
25
62
|
|
|
@@ -27,6 +64,7 @@ export function setConsoleTheme(value) {
|
|
|
27
64
|
const theme = normalizeTheme(value);
|
|
28
65
|
// Apply before persistence: a storage failure must not disable the picker.
|
|
29
66
|
document.documentElement.dataset.consoleTheme = theme;
|
|
67
|
+
syncTitlebarColor(theme);
|
|
30
68
|
try {
|
|
31
69
|
window.localStorage.setItem(STORAGE_KEY, theme);
|
|
32
70
|
} catch {
|