@tea-agent/loop-agent 0.36.0 → 0.36.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 (33) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/build-stamp.json +3 -3
  3. package/dist/cli/command-definitions.js +7 -0
  4. package/dist/cli/program.js +6 -1
  5. package/dist/commands/dag-request-interrupt.js +20 -0
  6. package/dist/executors/pi-sdk-executor.js +34 -0
  7. package/dist/shared/operator/capabilities.js +76 -3
  8. package/dist/task/source-prepare/semantic-intake.js +144 -23
  9. package/dist/worker/console/chat/semantic-activity.js +6 -0
  10. package/dist/worker/console/chat/workspace-landing.js +1 -0
  11. package/dist/worker/console/operation-runner.js +48 -0
  12. package/dist/worker/console/operation-wait.js +41 -0
  13. package/dist/worker/console/operator-actions.js +235 -26
  14. package/dist/worker/console/operator-user-error.js +4 -0
  15. package/dist/worker/console/recovery-cta.js +50 -3
  16. package/dist/worker/console/recovery-error-copy.js +198 -0
  17. package/dist/worker/console/static/assets/index-D83DYAFG.css +1 -0
  18. package/dist/worker/console/static/assets/index-IXm7oYjL.js +59 -0
  19. package/dist/worker/console/static/index.html +2 -2
  20. package/dist/worker/console/static-src/app/console-types.js +13 -9
  21. package/dist/worker/console/static-src/app/useOperatorActions.js +4 -2
  22. package/dist/worker/console/static-src/app/useRecoveryActions.js +65 -56
  23. package/dist/worker/console/static-src/app/useRecoveryConsole.js +68 -1
  24. package/dist/worker/observability/interrupt-eligibility.js +264 -0
  25. package/dist/worker/observe/static/operator-chrome.d.ts +1 -0
  26. package/dist/worker/observe/static/operator-chrome.js +5 -0
  27. package/dist/worker/observe/static/views/dag.js +12 -0
  28. package/dist/workflows/dag/interrupt-request.js +559 -0
  29. package/dist/workflows/dag/runner.js +72 -4
  30. package/package.json +1 -1
  31. package/skills/loop-agent/references/command-reference.md +1 -0
  32. package/dist/worker/console/static/assets/index-2OeZODxk.js +0 -57
  33. package/dist/worker/console/static/assets/index-DVJlUL8X.css +0 -1
@@ -8,6 +8,7 @@ import { readCandidateRecord } from "../../infrastructure/evaluation/candidate-s
8
8
  import { CANONICAL_TASK_ID_PATTERN, formatLocalCompactDate, } from "../../task/runtime.js";
9
9
  import { assertFrozenBudget, initRunBudgetLedger, preflightBudgetOrBreach, recordFinishedNodeBudget, writeBudgetLedgerArtifacts, } from "./budget-enforcement.js";
10
10
  import { getDagRunDir, isTerminalDagRunStatus, locateDagRun, readDagRunSpec, readDagRunState, readHumanApprovalArtifact, requireActiveDagRun, } from "./lifecycle.js";
11
+ import { markInterruptNeedsReconcile, mergeAbortSignals, readInterruptRequest, runnerIdentityFromState, settleDagInterrupt, startInterruptWatcher, } from "./interrupt-request.js";
11
12
  import { moveToCompletedRunDir, moveToPausedRunDir, prepareRunDir, writeRunSpec, writeRunState, } from "./run-store.js";
12
13
  import { evaluateNodeLiveness, resolveLivenessPolicy, } from "./liveness-policy.js";
13
14
  import { createDagNodeExecutor } from "./executor-registry.js";
@@ -450,6 +451,18 @@ async function executeDagCheckpoint(input) {
450
451
  void persistState().catch(() => { });
451
452
  }, runnerLivenessPolicy.heartbeatIntervalMs);
452
453
  heartbeatTimer.unref();
454
+ await persistState();
455
+ const interruptController = new AbortController();
456
+ const expectedRunnerIdentity = runnerIdentityFromState(state);
457
+ const interruptWatcher = expectedRunnerIdentity
458
+ ? startInterruptWatcher({
459
+ runDir,
460
+ runId: state.runId,
461
+ expectedRunnerIdentity,
462
+ controller: interruptController,
463
+ })
464
+ : undefined;
465
+ const abortSignal = mergeAbortSignals(input.abortSignal, interruptController.signal);
453
466
  // Graceful terminal persistence: an outer SIGTERM/SIGINT (operator hard
454
467
  // timeout, supervision layer, or shell wall-clock) must not leave the run
455
468
  // orphaned in RUNNING with a dead heartbeat. Persist a terminal failed
@@ -549,7 +562,7 @@ async function executeDagCheckpoint(input) {
549
562
  meta: { runDir, runId: state.runId, spec },
550
563
  }),
551
564
  executeScheduledNode: async (nodeId, executeNode, onPause) => {
552
- if (input.abortSignal?.aborted)
565
+ if (abortSignal?.aborted)
553
566
  return;
554
567
  if (isHardBudgetBreached(state.budgetLedger))
555
568
  return;
@@ -616,6 +629,7 @@ async function executeDagCheckpoint(input) {
616
629
  // still producing a truthful failure handoff for max-passes/non-retry.
617
630
  pausedByNodeId = await executeRanks(terminalCloseoutRanks);
618
631
  }
632
+ interruptWatcher?.stop();
619
633
  // Stop heartbeats before terminal archive. A late heartbeat writing
620
634
  // state.json under completed/ trips the completed-facts write guard and
621
635
  // makes run-dag exit non-zero after every node already finished.
@@ -626,12 +640,24 @@ async function executeDagCheckpoint(input) {
626
640
  await writeBudgetLedgerArtifacts(runDir, state.budgetLedger);
627
641
  if (pausedByNodeId) {
628
642
  state.status = "paused";
643
+ await finalizeRunOwnedInterrupt({
644
+ runDir,
645
+ state,
646
+ paused: true,
647
+ interruptAborted: interruptController.signal.aborted,
648
+ });
629
649
  await persistState();
630
650
  await notifyRunObserver(input.observer, "onRunFinish", state);
631
651
  runDir = await moveToPausedRunDir(runDir, pausedRunDir);
632
652
  }
633
653
  else {
634
654
  const { recoveryPending } = await finalizeTerminalRunStatus(state, spec.tasks.length, runDir, cwd);
655
+ await finalizeRunOwnedInterrupt({
656
+ runDir,
657
+ state,
658
+ paused: false,
659
+ interruptAborted: interruptController.signal.aborted,
660
+ });
635
661
  await persistState();
636
662
  if (recoveryPending) {
637
663
  // Recovery coordinator (phase 3c AC-2/AC-3): materialize the reserved
@@ -664,7 +690,7 @@ async function executeDagCheckpoint(input) {
664
690
  maxConcurrent,
665
691
  executeNode: input.executeNode,
666
692
  observer: input.observer,
667
- abortSignal: input.abortSignal,
693
+ abortSignal,
668
694
  });
669
695
  }
670
696
  }
@@ -701,10 +727,51 @@ async function executeDagCheckpoint(input) {
701
727
  }
702
728
  finally {
703
729
  clearInterval(heartbeatTimer);
730
+ interruptWatcher?.stop();
704
731
  process.removeListener("SIGTERM", onSigTerm);
705
732
  process.removeListener("SIGINT", onSigInt);
706
733
  }
707
734
  }
735
+ async function finalizeRunOwnedInterrupt(input) {
736
+ const request = await readInterruptRequest(input.runDir);
737
+ if (!request)
738
+ return;
739
+ if (request.status === "settled" || request.status === "needs-reconcile") {
740
+ return;
741
+ }
742
+ const hasUnconfirmed = Object.values(input.state.nodes).some((node) => node.failureCategory === "termination-unconfirmed");
743
+ if (hasUnconfirmed || input.paused) {
744
+ await markInterruptNeedsReconcile({
745
+ runDir: input.runDir,
746
+ outcome: hasUnconfirmed
747
+ ? "termination-unconfirmed"
748
+ : "paused-before-settle",
749
+ });
750
+ return;
751
+ }
752
+ if (input.interruptAborted &&
753
+ request.status === "acknowledged" &&
754
+ (input.state.status === "failed" ||
755
+ input.state.status === "partial_failed" ||
756
+ input.state.failureCategory === "controller-interrupted")) {
757
+ await settleDagInterrupt({
758
+ runDir: input.runDir,
759
+ outcome: "controller-interrupted",
760
+ });
761
+ return;
762
+ }
763
+ if (request.status === "requested") {
764
+ await markInterruptNeedsReconcile({
765
+ runDir: input.runDir,
766
+ outcome: "runner-did-not-acknowledge",
767
+ });
768
+ return;
769
+ }
770
+ await markInterruptNeedsReconcile({
771
+ runDir: input.runDir,
772
+ outcome: "run-exited-before-interrupt-settled",
773
+ });
774
+ }
708
775
  async function notifyRunObserver(observer, event, state) {
709
776
  try {
710
777
  await observer?.[event]?.(state);
@@ -859,8 +926,9 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
859
926
  // mechanical partial_failed just because prewrite FINISHED while the writer
860
927
  // was SKIPPED. Fail closed on retryable-invalid/blocked before aggregation.
861
928
  let recoveryPending = false;
929
+ const skipFrontendRecovery = state.failureCategory === "controller-interrupted";
862
930
  const hasFrontendWriter = Object.keys(state.nodes).some((id) => FRONTEND_WRITER_NODE_IDS.includes(id));
863
- if (hasFrontendWriter) {
931
+ if (hasFrontendWriter && !skipFrontendRecovery) {
864
932
  const admission = await readFrontendPrewriteResult(runDir);
865
933
  if (admission.ok) {
866
934
  if (admission.result.classification === "blocked") {
@@ -888,7 +956,7 @@ export async function finalizeTerminalRunStatus(state, taskCount, runDir, cwd) {
888
956
  // for frontend-implementation writers with a remaining root continuation, and
889
957
  // only when the rollback journal (captured before the provider call) restores
890
958
  // cleanly; otherwise the run stays failed with auto-recovery-blocked.
891
- if (hasFrontendWriter && !recoveryPending) {
959
+ if (hasFrontendWriter && !recoveryPending && !skipFrontendRecovery) {
892
960
  const writerNodeId = FRONTEND_WRITER_NODE_IDS.find((id) => {
893
961
  const node = state.nodes[id];
894
962
  return (node &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tea-agent/loop-agent",
3
- "version": "0.36.0",
3
+ "version": "0.36.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "loop-agent": "bin/loop-agent.js",
@@ -321,6 +321,7 @@ loop-agent dag report [--run-id <run-id>] [--lifecycle active|paused|completed|a
321
321
  loop-agent dag reconcile-run --run-id <run-id> # 只读检查 effectiveStatus 与恢复/收口资格
322
322
  loop-agent dag reconcile-run --run-id <run-id> --action supersede --reason "..." # 显式保留证据并标记为任务已另行完成
323
323
  loop-agent dag reconcile-run --run-id <run-id> --action abandon --reason "..." # 显式保留证据并收口为已放弃
324
+ loop-agent dag request-interrupt --run-id <run-id> --request-id <id> --target-operation-id <id> --reason-code <code> --reason <detail> [--json] # 协作中止:写入 interrupt.json(identity CAS);需配 --expected-runner-pid/--expected-runner-hostname 校验 runner 身份
324
325
  loop-agent dag rerun --run-id <run-id> --from-node <node-id> --plan [--json] # 从节点重跑资格预检(不执行)
325
326
  loop-agent dag rerun --run-id <run-id> --from-node <node-id> --plan-hash <sha256> --request-id <key> --reason "..." [--json] # 安全子图 continuation
326
327
  loop-agent dag rerun-task --run-id <run-id> --reason "..." --request-id <key> [--profile auto] [--task-id <id>] [--json] # standalone 完整任务重跑