@tea-agent/loop-agent 0.20.1-beta.0 → 0.21.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/CHANGELOG.md +50 -0
- package/dist/application/dag/args.js +29 -0
- package/dist/application/dag/run-dag.js +3 -1
- package/dist/cli/command-definitions.js +15 -1
- package/dist/cli/program.js +11 -1
- package/dist/commands/dag-rerun-task.js +19 -0
- package/dist/commands/dag-rerun.js +111 -0
- package/dist/commands/init.js +7 -0
- package/dist/executors/dag-pi-executor.js +24 -0
- package/dist/executors/pi-executor.js +111 -36
- package/dist/executors/pi-sdk-executor.js +105 -29
- package/dist/executors/shell-executor.js +54 -11
- package/dist/shared/operator/capabilities.js +54 -0
- package/dist/worker/console/index.js +1 -1
- package/dist/worker/console/inspect-split.js +82 -0
- package/dist/worker/console/operation-runner.js +3 -1
- package/dist/worker/console/operation-store.js +1 -0
- package/dist/worker/console/operator-actions.js +153 -2
- package/dist/worker/console/operator-user-error.js +10 -0
- package/dist/worker/console/pi-readiness.js +4 -0
- package/dist/worker/console/recovery-cta.js +116 -5
- package/dist/worker/console/recovery-selection.js +107 -0
- package/dist/worker/console/resolve-dag-run-for-task.js +50 -11
- package/dist/worker/console/routes.js +20 -0
- package/dist/worker/console/sibling-controller.js +12 -7
- package/dist/worker/console/static/assets/index-CUDke82y.js +18 -0
- package/dist/worker/console/static/assets/index-wSEksVSO.css +1 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/loop-agent/loop-agent-client.js +43 -9
- package/dist/worker/observability/read-model.js +67 -1
- package/dist/worker/observe/static/constants.js +5 -0
- package/dist/worker/observe/static/format-pool.js +22 -3
- package/dist/worker/observe/static/index.html +1 -1
- package/dist/worker/observe/static/styles.css +32 -3
- package/dist/worker/observe/static/views/dag-inspector.js +2 -2
- package/dist/worker/run-task/run-task.js +23 -6
- package/dist/workflows/dag/backend-test-markdown-workflow.js +291 -97
- package/dist/workflows/dag/backend-test-result-contract.js +10 -4
- package/dist/workflows/dag/frontend-test-result-contract.js +64 -0
- package/dist/workflows/dag/init-hybrid.js +117 -64
- package/dist/workflows/dag/lifecycle.js +60 -4
- package/dist/workflows/dag/liveness-policy.js +250 -0
- package/dist/workflows/dag/node-execution.js +89 -6
- package/dist/workflows/dag/output-protocol.js +76 -0
- package/dist/workflows/dag/rerun-plan.js +611 -0
- package/dist/workflows/dag/rerun-run.js +497 -0
- package/dist/workflows/dag/rerun-task.js +284 -0
- package/dist/workflows/dag/retry-policy.js +20 -1
- package/dist/workflows/dag/runner.js +71 -1
- package/dist/workflows/dag/skill-snapshot.js +22 -3
- package/dist/workflows/dag/types.js +12 -0
- package/dist/workflows/dag/validate.js +11 -0
- package/dist/workflows/dag/workspace-checkpoint.js +163 -0
- package/docs/README.md +5 -5
- package/docs/architecture/dag-execution.md +11 -0
- package/docs/architecture/facts-and-state.md +1 -0
- package/docs/architecture/worker-and-feature.md +10 -0
- package/docs/templates/agent-dag.schema.json +17 -2
- package/docs/templates/backend-test-dag.generate-pytest.prompt.md +5 -2
- package/docs/templates/backend-test-dag.json +15 -15
- package/docs/templates/frontend-test-case-checklist.md +16 -1
- package/docs/templates/frontend-test-dag.generate-cases.prompt.md +26 -2
- package/docs/templates/frontend-test-dag.json +65 -6
- package/docs/templates/frontend-test-dag.retrieve-context.prompt.md +4 -1
- package/harness.json +1 -1
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +5 -0
- package/skills/loop-agent/references/hybrid-dag.md +2 -2
- package/skills/playwright-cli-case-generator/SKILL.md +35 -7
- package/dist/worker/console/static/assets/index-3vsjZJHq.js +0 -16
- package/dist/worker/console/static/assets/index-i1wV4LrY.css +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { appendFile, mkdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from './pi-executor.js';
|
|
3
|
+
import { BoundedTextPreview, classifyPiFailure, createPiJsonlStreamCollector, DEFAULT_ABORT_GRACE_MS, DEFAULT_STALL_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, extractAssistantTextFromPiJson, } from './pi-executor.js';
|
|
4
4
|
import { serializeSessionEvent } from './pi-event-serializer.js';
|
|
5
5
|
let sdkSessionFactoryOverride;
|
|
6
6
|
let sdkImportOverrideForTests;
|
|
@@ -169,6 +169,21 @@ export function shouldPersistSessionEvent(event) {
|
|
|
169
169
|
}
|
|
170
170
|
return true;
|
|
171
171
|
}
|
|
172
|
+
function classifySdkActivityKind(event) {
|
|
173
|
+
if (!event || typeof event !== 'object')
|
|
174
|
+
return 'provider';
|
|
175
|
+
const type = typeof event.type === 'string'
|
|
176
|
+
? String(event.type)
|
|
177
|
+
: '';
|
|
178
|
+
if (type === 'tool_start' || type === 'tool_end'
|
|
179
|
+
|| type === 'tool_execution_start' || type === 'tool_execution_end') {
|
|
180
|
+
return 'tool';
|
|
181
|
+
}
|
|
182
|
+
if (type === 'thinking_delta' || type === 'message_update') {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
return 'provider';
|
|
186
|
+
}
|
|
172
187
|
function createSessionEventAppender(filePath, onSessionEvent) {
|
|
173
188
|
let chain = Promise.resolve();
|
|
174
189
|
let dirEnsured = false;
|
|
@@ -181,6 +196,9 @@ function createSessionEventAppender(filePath, onSessionEvent) {
|
|
|
181
196
|
dirEnsured = true;
|
|
182
197
|
}
|
|
183
198
|
await appendFile(filePath, `${line}\n`, 'utf-8');
|
|
199
|
+
// Only after successful persistence: surface the persisted session event.
|
|
200
|
+
// Transport activity is reported synchronously by the subscription so
|
|
201
|
+
// filtered deltas and slow disk writes cannot trip the stall watchdog.
|
|
184
202
|
onSessionEvent?.(line, event);
|
|
185
203
|
}
|
|
186
204
|
catch {
|
|
@@ -271,6 +289,8 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
271
289
|
? `${modelConfig.provider}/${modelConfig.model}`
|
|
272
290
|
: modelConfig.model ?? 'default';
|
|
273
291
|
const timeoutMs = options.timeoutMs ?? modelConfig.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
292
|
+
const stallTimeoutMs = options.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
|
293
|
+
const abortGraceMs = options.abortGraceMs ?? DEFAULT_ABORT_GRACE_MS;
|
|
274
294
|
const piSdkArgs = [
|
|
275
295
|
'--provider', modelConfig.provider ?? '(default)',
|
|
276
296
|
'--model', modelConfig.model ?? '(default)',
|
|
@@ -282,15 +302,46 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
282
302
|
];
|
|
283
303
|
const startedAt = Date.now();
|
|
284
304
|
let timedOut = false;
|
|
305
|
+
let terminationConfirmed = true;
|
|
285
306
|
let stderr = '';
|
|
286
307
|
const stdoutPreview = new BoundedTextPreview('stdout');
|
|
287
308
|
const stdoutCollector = createPiJsonlStreamCollector();
|
|
288
309
|
const usageSamples = [];
|
|
289
310
|
let session;
|
|
311
|
+
let unsubscribe;
|
|
290
312
|
let timeoutHandle;
|
|
313
|
+
let stallHandle;
|
|
314
|
+
let disposeAttempted = false;
|
|
291
315
|
const sessionEventAppender = options.sessionEventsPath
|
|
292
316
|
? createSessionEventAppender(options.sessionEventsPath, options.onSessionEvent)
|
|
293
317
|
: undefined;
|
|
318
|
+
const appendStderr = (message) => {
|
|
319
|
+
stderr = stderr ? `${stderr}\n${message}` : message;
|
|
320
|
+
};
|
|
321
|
+
const runSessionActionWithGrace = async (label, action) => {
|
|
322
|
+
let graceHandle;
|
|
323
|
+
const outcome = await Promise.race([
|
|
324
|
+
Promise.resolve()
|
|
325
|
+
.then(action)
|
|
326
|
+
.then(() => ({ ok: true }))
|
|
327
|
+
.catch((error) => ({ ok: false, error })),
|
|
328
|
+
new Promise((resolve) => {
|
|
329
|
+
graceHandle = setTimeout(() => resolve({ ok: false, timedOut: true }), abortGraceMs);
|
|
330
|
+
}),
|
|
331
|
+
]);
|
|
332
|
+
if (graceHandle)
|
|
333
|
+
clearTimeout(graceHandle);
|
|
334
|
+
if (outcome.ok)
|
|
335
|
+
return true;
|
|
336
|
+
if ('timedOut' in outcome) {
|
|
337
|
+
appendStderr(`pi SDK ${label} was not confirmed within ${abortGraceMs}ms`);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
const message = outcome.error instanceof Error ? outcome.error.message : String(outcome.error);
|
|
341
|
+
appendStderr(`pi SDK ${label} failed: ${message}`);
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
};
|
|
294
345
|
try {
|
|
295
346
|
const createSession = await resolveSdkSessionFactory(options.reuseScope);
|
|
296
347
|
session = await createSession({
|
|
@@ -301,7 +352,32 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
301
352
|
model: modelConfig.model,
|
|
302
353
|
thinking: modelConfig.thinking,
|
|
303
354
|
});
|
|
304
|
-
|
|
355
|
+
let resolveStall;
|
|
356
|
+
const stallPromise = stallTimeoutMs > 0
|
|
357
|
+
? new Promise((resolve) => {
|
|
358
|
+
resolveStall = resolve;
|
|
359
|
+
})
|
|
360
|
+
: null;
|
|
361
|
+
const armStallWatchdog = () => {
|
|
362
|
+
if (!stallPromise || !resolveStall)
|
|
363
|
+
return;
|
|
364
|
+
if (stallHandle)
|
|
365
|
+
clearTimeout(stallHandle);
|
|
366
|
+
stallHandle = setTimeout(() => resolveStall?.('stall'), stallTimeoutMs);
|
|
367
|
+
};
|
|
368
|
+
unsubscribe = session.subscribe((event) => {
|
|
369
|
+
// Every real SDK event proves transport activity, including noisy deltas
|
|
370
|
+
// intentionally excluded from persisted JSONL and meaningful DAG progress.
|
|
371
|
+
armStallWatchdog();
|
|
372
|
+
const activityKind = classifySdkActivityKind(event);
|
|
373
|
+
if (activityKind) {
|
|
374
|
+
try {
|
|
375
|
+
options.onActivity?.({ kind: activityKind, at: new Date().toISOString() });
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
// best-effort: activity must never change Pi result
|
|
379
|
+
}
|
|
380
|
+
}
|
|
305
381
|
const usageSample = extractSdkUsageSample(event);
|
|
306
382
|
if (usageSample)
|
|
307
383
|
usageSamples.push(usageSample);
|
|
@@ -317,44 +393,44 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
317
393
|
? `${filePrefix}\n${options.userMessage}`
|
|
318
394
|
: options.userMessage;
|
|
319
395
|
const promptPromise = session.prompt(promptMessage);
|
|
396
|
+
armStallWatchdog();
|
|
320
397
|
const timeoutPromise = timeoutMs > 0
|
|
321
398
|
? new Promise((resolve) => {
|
|
322
399
|
timeoutHandle = setTimeout(() => {
|
|
323
|
-
|
|
324
|
-
void session?.abort();
|
|
325
|
-
resolve('timeout');
|
|
400
|
+
resolve('absolute-timeout');
|
|
326
401
|
}, timeoutMs);
|
|
327
402
|
})
|
|
328
403
|
: null;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
404
|
+
const raced = await Promise.race([
|
|
405
|
+
promptPromise.then(() => 'done'),
|
|
406
|
+
...(timeoutPromise ? [timeoutPromise] : []),
|
|
407
|
+
...(stallPromise ? [stallPromise] : []),
|
|
408
|
+
]);
|
|
409
|
+
if (raced !== 'done') {
|
|
410
|
+
timedOut = true;
|
|
411
|
+
appendStderr(raced === 'stall'
|
|
412
|
+
? `pi SDK step stalled after ${stallTimeoutMs}ms with no provider activity`
|
|
413
|
+
: `pi SDK step timed out after ${timeoutMs}ms`);
|
|
414
|
+
const abortConfirmed = await runSessionActionWithGrace('abort', () => session.abort());
|
|
415
|
+
disposeAttempted = true;
|
|
416
|
+
const disposeConfirmed = await runSessionActionWithGrace('dispose', () => session.dispose());
|
|
417
|
+
terminationConfirmed = abortConfirmed && disposeConfirmed;
|
|
337
418
|
}
|
|
338
|
-
else {
|
|
339
|
-
await promptPromise;
|
|
340
|
-
}
|
|
341
|
-
unsubscribe();
|
|
342
419
|
}
|
|
343
420
|
catch (error) {
|
|
344
421
|
const message = error instanceof Error ? error.message : String(error);
|
|
345
|
-
|
|
422
|
+
appendStderr(message);
|
|
346
423
|
}
|
|
347
424
|
finally {
|
|
348
425
|
if (timeoutHandle)
|
|
349
426
|
clearTimeout(timeoutHandle);
|
|
350
|
-
if (
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
}
|
|
427
|
+
if (stallHandle)
|
|
428
|
+
clearTimeout(stallHandle);
|
|
429
|
+
unsubscribe?.();
|
|
430
|
+
if (session && !disposeAttempted) {
|
|
431
|
+
disposeAttempted = true;
|
|
432
|
+
const disposeConfirmed = await runSessionActionWithGrace('dispose', () => session.dispose());
|
|
433
|
+
terminationConfirmed = terminationConfirmed && disposeConfirmed;
|
|
358
434
|
}
|
|
359
435
|
if (sessionEventAppender) {
|
|
360
436
|
try {
|
|
@@ -378,14 +454,14 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
378
454
|
? collected.parsedEvents
|
|
379
455
|
: fallbackParsed?.parsedEvents ?? 0;
|
|
380
456
|
const tokensUsed = aggregateSdkTokenUsage(usageSamples);
|
|
381
|
-
const failureCategory = classifyPiFailure({
|
|
457
|
+
const failureCategory = terminationConfirmed ? classifyPiFailure({
|
|
382
458
|
assistantText,
|
|
383
459
|
exitCode: timedOut ? 1 : stderr ? 1 : 0,
|
|
384
460
|
outputTooLarge: collected.outputTooLarge,
|
|
385
461
|
stderr,
|
|
386
462
|
stdout,
|
|
387
463
|
timedOut,
|
|
388
|
-
});
|
|
464
|
+
}) : 'termination-unconfirmed';
|
|
389
465
|
return {
|
|
390
466
|
assistantText,
|
|
391
467
|
backend: 'sdk',
|
|
@@ -394,7 +470,7 @@ export async function executeSingleSdkAttempt(options) {
|
|
|
394
470
|
exitCode: timedOut || stderr ? 1 : 0,
|
|
395
471
|
failureCategory,
|
|
396
472
|
modelDisplay,
|
|
397
|
-
ok: !timedOut && !stderr && assistantText.length > 0 && !collected.outputTooLarge,
|
|
473
|
+
ok: terminationConfirmed && !timedOut && !stderr && assistantText.length > 0 && !collected.outputTooLarge,
|
|
398
474
|
parsedEvents,
|
|
399
475
|
stderr,
|
|
400
476
|
stdout,
|
|
@@ -17,7 +17,7 @@ import { formatFrontendReviewContextStdout, runFrontendReviewContextGate, } from
|
|
|
17
17
|
import { formatTraceabilityGateStdout, materializeBackendTestCaseManifest, runBackendTestTraceabilityGate, } from "../workflows/dag/backend-test-case-manifest.js";
|
|
18
18
|
import { materializeBackendTestExecutionContract } from "../workflows/dag/backend-test-execution-contract.js";
|
|
19
19
|
import { materializeBackendTestResultFromRunDir, parseJunitXml } from "../workflows/dag/backend-test-result-contract.js";
|
|
20
|
-
import { collectBackendTestHumanCaseCatalog, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
20
|
+
import { collectBackendTestHumanCaseCatalog, collectBackendTestMappedPytestScripts, hasBlockingBackendMarkdownSafetyFindings, inspectBackendTestEnvironment, requiredBackendMarkdownCaseAcIds, renderBackendTestFacts, renderBackendTestHtml, redactBackendTestOutput, validateBackendMarkdownCases, validateBackendMarkdownTraceability, writeRunReport, } from "../workflows/dag/backend-test-markdown-workflow.js";
|
|
21
21
|
import { buildBackendTestCanonicalResultFromInitialShellSnippet, materializeBackendTestClassification, } from "../workflows/dag/backend-test-classification-contract.js";
|
|
22
22
|
import { backendTestSemanticReviewSchema, materializeBackendTestSemanticReview, } from "../workflows/dag/backend-test-semantic-review-contract.js";
|
|
23
23
|
import { pathsChangedDuringRun, readGitStatusPorcelain, snapshotGitStatusPorcelain, validateShellWriteGuard, } from "./shell-write-guard.js";
|
|
@@ -304,6 +304,14 @@ async function runShellWriteGuard(input) {
|
|
|
304
304
|
forbiddenPaths: input.task.forbiddenPaths,
|
|
305
305
|
});
|
|
306
306
|
}
|
|
307
|
+
async function readRequiredRunReport(reportsDir, filename) {
|
|
308
|
+
try {
|
|
309
|
+
return await readFile(path.join(reportsDir, filename), "utf8");
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
throw new Error(`missing required upstream report: reports/${filename}`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
307
315
|
async function executeBackendTestPipeline(input, meta) {
|
|
308
316
|
const pipeline = input.task.shell?.backendTestPipeline;
|
|
309
317
|
const started = Date.now();
|
|
@@ -332,22 +340,54 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
332
340
|
meta.spec.objective ?? "",
|
|
333
341
|
...(meta.spec.successCriteria ?? []),
|
|
334
342
|
]);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
343
|
+
let report;
|
|
344
|
+
try {
|
|
345
|
+
report = await validateBackendMarkdownCases({
|
|
346
|
+
workspaceRoot: input.cwd,
|
|
347
|
+
environmentMarkdown,
|
|
348
|
+
requiredRequirementIds,
|
|
349
|
+
sourceBinding: meta.spec.sourceBinding,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
report = `# Backend Markdown Case Validation\n\n## Status\n\nFAIL\n\n## Findings\n\n- Validator could not complete: ${error instanceof Error ? error.message : String(error)}\n`;
|
|
354
|
+
}
|
|
341
355
|
const reportPath = await writeRunReport(meta.runDir, "backend-md-case-validation.md", report);
|
|
342
356
|
outputs.push(`caseValidation=${reportPath}`, report);
|
|
357
|
+
if (hasBlockingBackendMarkdownSafetyFindings(report)) {
|
|
358
|
+
return {
|
|
359
|
+
ok: false,
|
|
360
|
+
stdout: outputs.join("\n"),
|
|
361
|
+
stderr: "backend Markdown safety gate blocked: secret-shaped values detected",
|
|
362
|
+
failureCategory: "invalid-output",
|
|
363
|
+
durationMs: Date.now() - started,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
343
366
|
}
|
|
344
367
|
else if (pipeline === "markdown-traceability") {
|
|
345
|
-
|
|
368
|
+
let report;
|
|
369
|
+
try {
|
|
370
|
+
report = await validateBackendMarkdownTraceability(input.cwd);
|
|
371
|
+
}
|
|
372
|
+
catch (error) {
|
|
373
|
+
report = `# Backend Test Traceability\n\n## Status\n\nFAIL\n\n## Findings\n\n- Traceability validation could not complete: ${error instanceof Error ? error.message : String(error)}\n`;
|
|
374
|
+
}
|
|
346
375
|
const reportPath = await writeRunReport(meta.runDir, "backend-test-traceability.md", report);
|
|
347
376
|
outputs.push(`traceability=${reportPath}`, report);
|
|
348
377
|
}
|
|
349
378
|
else if (pipeline === "markdown-execute-html") {
|
|
350
|
-
const
|
|
379
|
+
const mappedScripts = await collectBackendTestMappedPytestScripts(input.cwd);
|
|
380
|
+
const shellQuote = (value) => `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
381
|
+
const pytestTargets = mappedScripts.map(shellQuote).join(" ");
|
|
382
|
+
const pytestCommand = [
|
|
383
|
+
'mkdir -p "${HARNESS_DAG_RUN_DIR}/reports"',
|
|
384
|
+
`PYTHONUTF8=1 PYTHONIOENCODING=utf-8 PYTHONDONTWRITEBYTECODE=1 python -m pytest ${pytestTargets} -v -p no:cacheprovider -o junit_logging=all -o junit_log_passing_tests=true --junitxml="\${HARNESS_DAG_RUN_DIR}/reports/backend-test.junit.xml"`,
|
|
385
|
+
"STATUS=$?",
|
|
386
|
+
'printf "%s" "${STATUS}" > "${HARNESS_DAG_RUN_DIR}/reports/backend-test-pytest-exit.txt"',
|
|
387
|
+
'if { [ "${STATUS}" -eq 0 ] || [ "${STATUS}" -eq 1 ]; } && [ -s "${HARNESS_DAG_RUN_DIR}/reports/backend-test.junit.xml" ]; then exit 0; fi',
|
|
388
|
+
'exit "${STATUS}"',
|
|
389
|
+
].join("; ");
|
|
390
|
+
const results = await executePipelineCommands(input, meta, [pytestCommand]);
|
|
351
391
|
if (!results.every((result) => result.ok)) {
|
|
352
392
|
const failure = results.find((result) => !result.ok);
|
|
353
393
|
return { ok: false, stdout: results.map((result) => result.stdout).join("\n"), stderr: failure.stderr, failureCategory: failure.failureCategory, durationMs: Date.now() - started };
|
|
@@ -359,15 +399,18 @@ async function executeBackendTestPipeline(input, meta) {
|
|
|
359
399
|
throw new Error(`pytest did not complete with a reportable exit code: ${pytestExitCode}`);
|
|
360
400
|
const parsed = parseJunitXml(junitContent);
|
|
361
401
|
const cases = await collectBackendTestHumanCaseCatalog(input.cwd);
|
|
402
|
+
const caseValidationSummary = await readRequiredRunReport(reportsDir, "backend-md-case-validation.md");
|
|
403
|
+
const traceabilitySummary = await readRequiredRunReport(reportsDir, "backend-test-traceability.md");
|
|
362
404
|
const htmlContent = renderBackendTestHtml({
|
|
363
405
|
title: meta.spec.title,
|
|
364
406
|
parsed,
|
|
365
407
|
cases,
|
|
366
408
|
environmentSummary: await readFile(path.join(reportsDir, "backend-test-environment.md"), "utf8"),
|
|
367
|
-
|
|
409
|
+
caseValidationSummary,
|
|
410
|
+
traceabilitySummary,
|
|
368
411
|
});
|
|
369
412
|
const htmlPath = await writeRunReport(meta.runDir, "backend-test.html", htmlContent);
|
|
370
|
-
const facts = renderBackendTestFacts({ parsed, cases, pytestExitCode, junitRelativePath: "reports/backend-test.junit.xml", htmlRelativePath: "reports/backend-test.html", junitContent, htmlContent });
|
|
413
|
+
const facts = renderBackendTestFacts({ parsed, cases, pytestExitCode, junitRelativePath: "reports/backend-test.junit.xml", htmlRelativePath: "reports/backend-test.html", junitContent, htmlContent, caseValidationSummary, traceabilitySummary });
|
|
371
414
|
const factsPath = await writeRunReport(meta.runDir, "backend-test-facts.md", facts);
|
|
372
415
|
const sanitizedOutputs = results.map((result) => redactBackendTestOutput(result.stdout));
|
|
373
416
|
outputs.push(...sanitizedOutputs, `html=${htmlPath}`, `facts=${factsPath}`, facts);
|
|
@@ -167,6 +167,16 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
167
167
|
requiredErrorCodes: ["INVALID_INPUT", "OPERATION_NEEDS_RECONCILE"],
|
|
168
168
|
description: "Repo doctor (registry entry; envelope migration follow-up).",
|
|
169
169
|
},
|
|
170
|
+
{
|
|
171
|
+
action: "dagReport",
|
|
172
|
+
cli: "loop-agent dag report --run-id <runId> --json",
|
|
173
|
+
kind: "read",
|
|
174
|
+
inputSchemaVersion: 1,
|
|
175
|
+
resultSchemaVersion: 1,
|
|
176
|
+
envelopeSchemaVersion: 1,
|
|
177
|
+
requiredErrorCodes: ["NOT_FOUND", "INVALID_INPUT"],
|
|
178
|
+
description: "DAG run handoff report by run id.",
|
|
179
|
+
},
|
|
170
180
|
{
|
|
171
181
|
action: "status",
|
|
172
182
|
cli: "loop-agent status <taskId> --json",
|
|
@@ -226,6 +236,50 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
226
236
|
],
|
|
227
237
|
description: "Execute reviewed DAG; revalidate binding before writer start.",
|
|
228
238
|
},
|
|
239
|
+
{
|
|
240
|
+
action: "dagRerunPlan",
|
|
241
|
+
cli: "loop-agent dag rerun --run-id <id> --from-node <node> --plan --json",
|
|
242
|
+
kind: "read",
|
|
243
|
+
inputSchemaVersion: 1,
|
|
244
|
+
resultSchemaVersion: 1,
|
|
245
|
+
envelopeSchemaVersion: 1,
|
|
246
|
+
requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
|
|
247
|
+
description: "Read-only subgraph rerun plan for confirmation (no mutation).",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
action: "dagRerun",
|
|
251
|
+
cli: "loop-agent dag rerun --run-id <id> --from-node <node> --plan-hash <hash> --request-id <id> --reason <text> --json",
|
|
252
|
+
kind: "long-running",
|
|
253
|
+
inputSchemaVersion: 1,
|
|
254
|
+
resultSchemaVersion: 1,
|
|
255
|
+
envelopeSchemaVersion: 1,
|
|
256
|
+
requiredErrorCodes: [
|
|
257
|
+
...COMMON_MUTATION_ERRORS,
|
|
258
|
+
"BINDING_DRIFT",
|
|
259
|
+
"INVALID_INPUT",
|
|
260
|
+
],
|
|
261
|
+
description: "Execute continuation run from effective node with confirmed plan hash.",
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
action: "standaloneTaskRerun",
|
|
265
|
+
cli: "loop-agent dag rerun-task --run-id <id> --reason <text> --request-id <id> [--profile auto] [--task-id <id>] --json",
|
|
266
|
+
kind: "long-running",
|
|
267
|
+
inputSchemaVersion: 1,
|
|
268
|
+
resultSchemaVersion: 1,
|
|
269
|
+
envelopeSchemaVersion: 1,
|
|
270
|
+
requiredErrorCodes: [...COMMON_MUTATION_ERRORS, "INVALID_INPUT"],
|
|
271
|
+
description: "Full standalone task regenerate → validate → execute with parent lineage.",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
action: "workerTaskRetry",
|
|
275
|
+
cli: "agent-worker task retry <task-id> --feature-id <feature-id> --repo . --reason <text>",
|
|
276
|
+
kind: "mutation",
|
|
277
|
+
inputSchemaVersion: 1,
|
|
278
|
+
resultSchemaVersion: 1,
|
|
279
|
+
envelopeSchemaVersion: 1,
|
|
280
|
+
requiredErrorCodes: [...COMMON_MUTATION_ERRORS, "INVALID_INPUT"],
|
|
281
|
+
description: "Requeue failed Task Pool task (Failed → Ready) via in-package pool store.",
|
|
282
|
+
},
|
|
229
283
|
];
|
|
230
284
|
return {
|
|
231
285
|
schemaVersion: 1,
|
|
@@ -5,7 +5,7 @@ export { isLoopbackHost } from "./loopback.js";
|
|
|
5
5
|
export { repoFingerprintV1, normalizeWorktreeRealpath, } from "./repo-fingerprint.js";
|
|
6
6
|
export { buildObserveDeepLink } from "./observe-link.js";
|
|
7
7
|
export { classifyObserveHealth, probeAndClassifyObserve, capabilityForObserveTarget, OBSERVE_START_COMMAND, DEFAULT_OBSERVE_BASE_URL, } from "./observe-health-match.js";
|
|
8
|
-
export { allowedRecoveryCtas, classifyRecoveryFact, recoveryMatrixInvariants, } from "./recovery-cta.js";
|
|
8
|
+
export { allowedRecoveryCtas, classifyRecoveryFact, partitionRecoveryCtas, recoveryMatrixInvariants, } from "./recovery-cta.js";
|
|
9
9
|
export { createClosedResourceLoader, createInterviewResourceLoader, tryActivateTool, assertOfficialToolAllowed, listOfficialTools, OFFICIAL_DENIED_TOOL_IDS, INTERVIEW_ALLOWED_TOOLS, INTERVIEW_DENIED_TOOLS, authorizeInterviewTool, } from "./resource-loader.js";
|
|
10
10
|
export { resolveSiblingLoopAgentBin } from "./sibling-controller.js";
|
|
11
11
|
export { openConsoleAppData, defaultConsoleAppDataRoot } from "./app-data.js";
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
function asDisplayText(value) {
|
|
2
|
+
if (value == null)
|
|
3
|
+
return null;
|
|
4
|
+
if (typeof value === "string") {
|
|
5
|
+
const trimmed = value.trim();
|
|
6
|
+
return trimmed || null;
|
|
7
|
+
}
|
|
8
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
9
|
+
return String(value);
|
|
10
|
+
}
|
|
11
|
+
if (typeof value === "object") {
|
|
12
|
+
const record = value;
|
|
13
|
+
if (typeof record.text === "string" && record.text.trim()) {
|
|
14
|
+
return record.text.trim();
|
|
15
|
+
}
|
|
16
|
+
if (typeof record.errorMessage === "string" && record.errorMessage.trim()) {
|
|
17
|
+
return record.errorMessage.trim();
|
|
18
|
+
}
|
|
19
|
+
if (Array.isArray(record.content)) {
|
|
20
|
+
const parts = record.content
|
|
21
|
+
.map((part) => {
|
|
22
|
+
if (typeof part === "string")
|
|
23
|
+
return part;
|
|
24
|
+
if (part && typeof part === "object") {
|
|
25
|
+
const item = part;
|
|
26
|
+
if (typeof item.text === "string")
|
|
27
|
+
return item.text;
|
|
28
|
+
}
|
|
29
|
+
return "";
|
|
30
|
+
})
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
if (parts.length)
|
|
33
|
+
return parts.join("\n").slice(0, 240);
|
|
34
|
+
}
|
|
35
|
+
if (typeof record.role === "string") {
|
|
36
|
+
return `${record.role} 消息`;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
export function formatInspectTimelineEvent(event) {
|
|
42
|
+
const time = asDisplayText(event.at) ??
|
|
43
|
+
asDisplayText(event.timestamp) ??
|
|
44
|
+
asDisplayText(event.recordedAt) ??
|
|
45
|
+
"";
|
|
46
|
+
const text = asDisplayText(event.errorMessage) ??
|
|
47
|
+
asDisplayText(event.label) ??
|
|
48
|
+
asDisplayText(event.type) ??
|
|
49
|
+
asDisplayText(event.message) ??
|
|
50
|
+
"运行事件";
|
|
51
|
+
return time ? `${time} · ${text}` : text;
|
|
52
|
+
}
|
|
53
|
+
export function dagNodeKey(node) {
|
|
54
|
+
return String(node.nodeId ?? node.id ?? "").trim();
|
|
55
|
+
}
|
|
56
|
+
export function dagNodeLabel(node) {
|
|
57
|
+
const key = dagNodeKey(node);
|
|
58
|
+
return String(node.label ?? node.name ?? (key || "未命名节点"));
|
|
59
|
+
}
|
|
60
|
+
export function pickInspectNode(nodes, preferredNodeId) {
|
|
61
|
+
if (nodes.length === 0)
|
|
62
|
+
return undefined;
|
|
63
|
+
const preferred = preferredNodeId?.trim();
|
|
64
|
+
if (preferred) {
|
|
65
|
+
const match = nodes.find((node) => dagNodeKey(node) === preferred);
|
|
66
|
+
if (match)
|
|
67
|
+
return match;
|
|
68
|
+
}
|
|
69
|
+
const failed = nodes.find((node) => {
|
|
70
|
+
const status = String(node.status ?? "").toLowerCase();
|
|
71
|
+
return (status.includes("fail") ||
|
|
72
|
+
status.includes("error") ||
|
|
73
|
+
status === "blocked");
|
|
74
|
+
});
|
|
75
|
+
if (failed)
|
|
76
|
+
return failed;
|
|
77
|
+
const running = nodes.find((node) => {
|
|
78
|
+
const status = String(node.status ?? "").toLowerCase();
|
|
79
|
+
return status === "running" || status === "in_progress" || status === "active";
|
|
80
|
+
});
|
|
81
|
+
return running ?? nodes[0];
|
|
82
|
+
}
|
|
@@ -30,7 +30,9 @@ export async function runOperation(operationId, deps) {
|
|
|
30
30
|
});
|
|
31
31
|
stateEvent(deps.events, op, "starting", "spawning sibling loop-agent");
|
|
32
32
|
const run = deps.runCommand ??
|
|
33
|
-
((cliArgs, options) =>
|
|
33
|
+
((cliArgs, options) => op.externalCommand
|
|
34
|
+
? deps.client.runExternal(op.externalCommand, cliArgs, options)
|
|
35
|
+
: deps.client.run(cliArgs, options));
|
|
34
36
|
let finished;
|
|
35
37
|
try {
|
|
36
38
|
const result = await run(args, {
|
|
@@ -55,6 +55,7 @@ export class OperationStore {
|
|
|
55
55
|
createdAt: now,
|
|
56
56
|
taskId: input.taskId,
|
|
57
57
|
cliArgs: input.cliArgs,
|
|
58
|
+
...(input.externalCommand ? { externalCommand: input.externalCommand } : {}),
|
|
58
59
|
};
|
|
59
60
|
await writeSecureJson(operationFile(this.appData, operationId), operation);
|
|
60
61
|
await writeSecureJson(indexPath, { operationId, payloadHash });
|