@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.
- package/CHANGELOG.md +11 -0
- package/dist/build-stamp.json +3 -3
- package/dist/cli/command-definitions.js +7 -0
- package/dist/cli/program.js +6 -1
- package/dist/commands/dag-request-interrupt.js +20 -0
- package/dist/executors/pi-sdk-executor.js +34 -0
- package/dist/shared/operator/capabilities.js +76 -3
- package/dist/task/source-prepare/semantic-intake.js +144 -23
- package/dist/worker/console/chat/semantic-activity.js +6 -0
- package/dist/worker/console/chat/workspace-landing.js +1 -0
- package/dist/worker/console/operation-runner.js +48 -0
- package/dist/worker/console/operation-wait.js +41 -0
- package/dist/worker/console/operator-actions.js +235 -26
- package/dist/worker/console/operator-user-error.js +4 -0
- package/dist/worker/console/recovery-cta.js +50 -3
- package/dist/worker/console/recovery-error-copy.js +198 -0
- package/dist/worker/console/static/assets/index-D83DYAFG.css +1 -0
- package/dist/worker/console/static/assets/index-IXm7oYjL.js +59 -0
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/app/console-types.js +13 -9
- package/dist/worker/console/static-src/app/useOperatorActions.js +4 -2
- package/dist/worker/console/static-src/app/useRecoveryActions.js +65 -56
- package/dist/worker/console/static-src/app/useRecoveryConsole.js +68 -1
- package/dist/worker/observability/interrupt-eligibility.js +264 -0
- package/dist/worker/observe/static/operator-chrome.d.ts +1 -0
- package/dist/worker/observe/static/operator-chrome.js +5 -0
- package/dist/worker/observe/static/views/dag.js +12 -0
- package/dist/workflows/dag/interrupt-request.js +559 -0
- package/dist/workflows/dag/runner.js +72 -4
- package/package.json +1 -1
- package/skills/loop-agent/references/command-reference.md +1 -0
- package/dist/worker/console/static/assets/index-2OeZODxk.js +0 -57
- package/dist/worker/console/static/assets/index-DVJlUL8X.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.36.1] - 2026-08-16
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
|
|
9
|
+
- cooperative live DAG interrupt and recovery UX
|
|
10
|
+
|
|
11
|
+
### 修复
|
|
12
|
+
|
|
13
|
+
- move interrupt eligibility to observability to honor kernel import boundary
|
|
14
|
+
- retry semantic intake across Pi tiers with 300s timeout
|
|
15
|
+
|
|
5
16
|
## [0.36.0] - 2026-08-15
|
|
6
17
|
|
|
7
18
|
### 新增
|
package/dist/build-stamp.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.36.
|
|
4
|
-
"gitSha": "
|
|
5
|
-
"builtAt": "2026-08-
|
|
3
|
+
"version": "0.36.1",
|
|
4
|
+
"gitSha": "e023c82feae11ad8a6f5f9e50e8553cc38512f5e",
|
|
5
|
+
"builtAt": "2026-08-16T09:24:03.417Z"
|
|
6
6
|
}
|
|
@@ -32,6 +32,7 @@ import { runDagApprove } from "../commands/dag-approve.js";
|
|
|
32
32
|
import { runDagReject } from "../commands/dag-reject.js";
|
|
33
33
|
import { runDagReconcileTasks } from "../commands/dag-reconcile-tasks.js";
|
|
34
34
|
import { runDagReconcileRun } from "../commands/dag-reconcile-run.js";
|
|
35
|
+
import { runDagRequestInterrupt } from "../commands/dag-request-interrupt.js";
|
|
35
36
|
import { runDagRerunTask } from "../commands/dag-rerun-task.js";
|
|
36
37
|
import { runDagRerun } from "../commands/dag-rerun.js";
|
|
37
38
|
import { runDagResume } from "../commands/dag-resume.js";
|
|
@@ -129,6 +130,7 @@ const DAG_SUBCOMMANDS = [
|
|
|
129
130
|
"report",
|
|
130
131
|
"closeout-draft",
|
|
131
132
|
"reconcile-run",
|
|
133
|
+
"request-interrupt",
|
|
132
134
|
"rerun",
|
|
133
135
|
"rerun-task",
|
|
134
136
|
"reconcile-tasks",
|
|
@@ -549,6 +551,11 @@ export const COMMAND_DEFINITIONS = [
|
|
|
549
551
|
await runDagReconcileRun(repoRoot, reconcileRunArgs);
|
|
550
552
|
return;
|
|
551
553
|
}
|
|
554
|
+
if (subcommand === "request-interrupt") {
|
|
555
|
+
const interruptArgs = rest.filter((arg) => Boolean(arg));
|
|
556
|
+
await runDagRequestInterrupt(repoRoot, interruptArgs);
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
552
559
|
if (subcommand === "rerun") {
|
|
553
560
|
const rerunArgs = rest.filter((arg) => Boolean(arg));
|
|
554
561
|
await runDagRerun(repoRoot, rerunArgs);
|
package/dist/cli/program.js
CHANGED
|
@@ -11,6 +11,7 @@ import { runDagFinalVerification } from "../commands/dag-final-verification.js";
|
|
|
11
11
|
import { runDagInitHybrid } from "../commands/dag-init-hybrid.js";
|
|
12
12
|
import { runDagReconcileTasks } from "../commands/dag-reconcile-tasks.js";
|
|
13
13
|
import { runDagReconcileRun } from "../commands/dag-reconcile-run.js";
|
|
14
|
+
import { runDagRequestInterrupt } from "../commands/dag-request-interrupt.js";
|
|
14
15
|
import { runDagRerunTask } from "../commands/dag-rerun-task.js";
|
|
15
16
|
import { runDagRerun } from "../commands/dag-rerun.js";
|
|
16
17
|
import { runDagReject } from "../commands/dag-reject.js";
|
|
@@ -331,6 +332,9 @@ async function runDagAction(repoRoot, subcommand, rest) {
|
|
|
331
332
|
case "reconcile-run":
|
|
332
333
|
await runDagReconcileRun(repoRoot, args);
|
|
333
334
|
return;
|
|
335
|
+
case "request-interrupt":
|
|
336
|
+
await runDagRequestInterrupt(repoRoot, args);
|
|
337
|
+
return;
|
|
334
338
|
case "rerun":
|
|
335
339
|
await runDagRerun(repoRoot, args);
|
|
336
340
|
return;
|
|
@@ -356,7 +360,7 @@ async function runDagAction(repoRoot, subcommand, rest) {
|
|
|
356
360
|
throw new Error("usage: dag decision <inspect|validate> --run-id <id> [--node-id <node-id>]");
|
|
357
361
|
}
|
|
358
362
|
default:
|
|
359
|
-
throw new Error("usage: dag <init-hybrid|execute|validate|workflow-plan|workflow-validate|workflow-compile|approve|reject|resume|status|doctor|report|closeout-draft|reconcile-run|rerun|rerun-task|reconcile-tasks|final-verification|decision> ...");
|
|
363
|
+
throw new Error("usage: dag <init-hybrid|execute|validate|workflow-plan|workflow-validate|workflow-compile|approve|reject|resume|status|doctor|report|closeout-draft|reconcile-run|request-interrupt|rerun|rerun-task|reconcile-tasks|final-verification|decision> ...");
|
|
360
364
|
}
|
|
361
365
|
}
|
|
362
366
|
function compactArgs(args) {
|
|
@@ -493,6 +497,7 @@ function addDagCommand(program, defaultRepoRoot) {
|
|
|
493
497
|
"report",
|
|
494
498
|
"closeout-draft",
|
|
495
499
|
"reconcile-run",
|
|
500
|
+
"request-interrupt",
|
|
496
501
|
"rerun",
|
|
497
502
|
"rerun-task",
|
|
498
503
|
"reconcile-tasks",
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DagInterruptError, executeDagRequestInterrupt, parseDagRequestInterruptArgs, } from "../workflows/dag/interrupt-request.js";
|
|
2
|
+
export { parseDagRequestInterruptArgs, executeDagRequestInterrupt };
|
|
3
|
+
export async function runDagRequestInterrupt(repoRoot, rawArgs) {
|
|
4
|
+
try {
|
|
5
|
+
const result = await executeDagRequestInterrupt(repoRoot, rawArgs);
|
|
6
|
+
console.log(JSON.stringify(result, null, 2));
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error instanceof DagInterruptError) {
|
|
10
|
+
console.log(JSON.stringify({
|
|
11
|
+
ok: false,
|
|
12
|
+
code: error.code,
|
|
13
|
+
message: error.message,
|
|
14
|
+
}, null, 2));
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -78,6 +78,40 @@ export async function checkPiSdkAvailability(_repoRoot) {
|
|
|
78
78
|
return { ok: false, detail: `pi SDK not available: ${message}` };
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Best-effort Pi SDK model catalog (credentialed providers first).
|
|
83
|
+
* Empty on SDK/auth/runtime failure — callers must fall back to harness tiers.
|
|
84
|
+
*/
|
|
85
|
+
export async function listPiSdkAvailableModels() {
|
|
86
|
+
try {
|
|
87
|
+
const imported = sdkImportOverrideForTests
|
|
88
|
+
? await sdkImportOverrideForTests()
|
|
89
|
+
: await loadPiSdkModule();
|
|
90
|
+
const sdk = imported;
|
|
91
|
+
const getAgentDir = sdk.getAgentDir;
|
|
92
|
+
const ModelRuntime = sdk.ModelRuntime;
|
|
93
|
+
if (typeof getAgentDir !== "function" || typeof ModelRuntime?.create !== "function") {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
const agentDir = getAgentDir();
|
|
97
|
+
const runtime = await ModelRuntime.create({
|
|
98
|
+
authPath: path.join(agentDir, "auth.json"),
|
|
99
|
+
modelsPath: path.join(agentDir, "models.json"),
|
|
100
|
+
});
|
|
101
|
+
const snapshot = runtime.snapshot ?? {};
|
|
102
|
+
const configured = snapshot.configuredProviders;
|
|
103
|
+
const isConfigured = (provider) => configured instanceof Set ? configured.has(provider) : false;
|
|
104
|
+
return (snapshot.available ?? []).map((entry) => ({
|
|
105
|
+
provider: entry.provider,
|
|
106
|
+
id: entry.id,
|
|
107
|
+
name: entry.name,
|
|
108
|
+
hasCredentials: isConfigured(entry.provider),
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return [];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
81
115
|
/** Fail-closed probe for the SDK-only structured custom-tool surface. */
|
|
82
116
|
export async function checkPiSdkCustomToolCapability(_repoRoot) {
|
|
83
117
|
if (sdkSessionFactoryOverride) {
|
|
@@ -1102,7 +1102,7 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
1102
1102
|
resultSchemaVersion: 1,
|
|
1103
1103
|
envelopeSchemaVersion: 1,
|
|
1104
1104
|
requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
|
|
1105
|
-
description: "Read-only R1 subgraph plan (no mutation). Call before dagRerun. Prefer primaryFailure.nodeId from dagReport. If eligible=false (writer/decision/fingerprint,
|
|
1105
|
+
description: "Read-only R1 subgraph plan (no mutation). Call before dagRerun. Prefer primaryFailure.nodeId from dagReport. If eligible=false (writer/decision/fingerprint, unsafe shell, worker-managed, workspace/controller drift), do not force execute — Worker-owned runs use workerTaskRetry; standalone runs use standaloneTaskRerun or same-task advance after fixing the root cause (contract changes must be adopted first). Returns planHash required by execute.",
|
|
1106
1106
|
inputParams: [
|
|
1107
1107
|
{
|
|
1108
1108
|
name: "runId",
|
|
@@ -1165,6 +1165,73 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
1165
1165
|
modelCallable: "always",
|
|
1166
1166
|
humanConfirmation: "none",
|
|
1167
1167
|
},
|
|
1168
|
+
{
|
|
1169
|
+
action: "dagInterruptEligibility",
|
|
1170
|
+
cli: "console dag-interrupt-eligibility",
|
|
1171
|
+
kind: "read",
|
|
1172
|
+
inputSchemaVersion: 1,
|
|
1173
|
+
resultSchemaVersion: 1,
|
|
1174
|
+
envelopeSchemaVersion: 1,
|
|
1175
|
+
requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
|
|
1176
|
+
description: "Read-only eligibility for cooperative live-DAG interrupt. v1 only Console-owned active running operations. Worker-managed / unknown owner / orphaned / terminal runs return eligible=false with stable reasonCodes. Does not create a Human Gate receipt or mutate the run.",
|
|
1177
|
+
inputParams: [
|
|
1178
|
+
{
|
|
1179
|
+
name: "runId",
|
|
1180
|
+
type: "string",
|
|
1181
|
+
required: true,
|
|
1182
|
+
description: "active dag run id to inspect for interrupt eligibility",
|
|
1183
|
+
},
|
|
1184
|
+
],
|
|
1185
|
+
modelCallable: "always",
|
|
1186
|
+
humanConfirmation: "none",
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
action: "dagRequestInterrupt",
|
|
1190
|
+
cli: "loop-agent dag request-interrupt --run-id <id> --request-id <id> --target-operation-id <id> --reason-code <code> --reason <detail> --json",
|
|
1191
|
+
kind: "mutation",
|
|
1192
|
+
inputSchemaVersion: 1,
|
|
1193
|
+
resultSchemaVersion: 1,
|
|
1194
|
+
envelopeSchemaVersion: 1,
|
|
1195
|
+
requiredErrorCodes: [
|
|
1196
|
+
...COMMON_MUTATION_ERRORS,
|
|
1197
|
+
"HUMAN_CONFIRMATION_REQUIRED",
|
|
1198
|
+
"RUN_NOT_ACTIVE",
|
|
1199
|
+
"RUN_NOT_CONSOLE_OWNED",
|
|
1200
|
+
"RUNNER_IDENTITY_MISMATCH",
|
|
1201
|
+
"INTERRUPT_ALREADY_PENDING",
|
|
1202
|
+
"RUN_ALREADY_TERMINAL",
|
|
1203
|
+
"INTERRUPT_NEEDS_RECONCILE",
|
|
1204
|
+
],
|
|
1205
|
+
description: "Request cooperative abort of a Console-owned running DAG. Success means interrupt.json is requested, not that the run has already stopped. Browser Human Gate only; model must not call this action.",
|
|
1206
|
+
inputParams: [
|
|
1207
|
+
{
|
|
1208
|
+
name: "runId",
|
|
1209
|
+
type: "string",
|
|
1210
|
+
required: true,
|
|
1211
|
+
description: "active dag run id",
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
name: "reasonCode",
|
|
1215
|
+
type: "string",
|
|
1216
|
+
required: true,
|
|
1217
|
+
description: "requirements-changed | suspected-runaway | resource-protection | operator-request | other",
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
name: "reasonDetail",
|
|
1221
|
+
type: "string",
|
|
1222
|
+
required: true,
|
|
1223
|
+
description: "10–500 character operator reason; do not auto-fill placeholders",
|
|
1224
|
+
},
|
|
1225
|
+
{
|
|
1226
|
+
name: "confirmationId",
|
|
1227
|
+
type: "string",
|
|
1228
|
+
required: true,
|
|
1229
|
+
description: "human confirmation receipt id from prepareMutationGate",
|
|
1230
|
+
},
|
|
1231
|
+
],
|
|
1232
|
+
modelCallable: "prepare-only",
|
|
1233
|
+
humanConfirmation: "required",
|
|
1234
|
+
},
|
|
1168
1235
|
{
|
|
1169
1236
|
action: "prepareMutationGate",
|
|
1170
1237
|
cli: "console prepare-mutation-gate",
|
|
@@ -1173,13 +1240,13 @@ export function buildOperatorCapabilitiesDocument() {
|
|
|
1173
1240
|
resultSchemaVersion: 1,
|
|
1174
1241
|
envelopeSchemaVersion: 1,
|
|
1175
1242
|
requiredErrorCodes: ["INVALID_INPUT", "NOT_FOUND"],
|
|
1176
|
-
description: "Prepare a one-shot Human Gate receipt for Night Scheduler mutations. Model may prepare; only the browser mutation gate may consume.",
|
|
1243
|
+
description: "Prepare a one-shot Human Gate receipt for destructive reconcile, live DAG interrupt, and Night Scheduler mutations. Model may prepare; only the browser mutation gate may consume. Do not use for dagRerun / standaloneTaskRerun / workerTaskRetry.",
|
|
1177
1244
|
inputParams: [
|
|
1178
1245
|
{
|
|
1179
1246
|
name: "action",
|
|
1180
1247
|
type: "string",
|
|
1181
1248
|
required: true,
|
|
1182
|
-
description: "target action: workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard",
|
|
1249
|
+
description: "target action: dagReconcileRun | dagRequestInterrupt | workerAdmissionPrepare | workerSchedulerAdd | workerSchedulerCancel | workerSchedulerHarvest | workerSchedulerDiscard. Not for standaloneTaskRerun / workerTaskRetry / dagRerun (those are autonomous).",
|
|
1183
1250
|
},
|
|
1184
1251
|
{
|
|
1185
1252
|
name: "actionParams",
|
|
@@ -1992,6 +2059,12 @@ export const OPERATOR_COMMAND_COVERAGE = Object.freeze([
|
|
|
1992
2059
|
action: "dagReconcileRun",
|
|
1993
2060
|
source: "loop-agent",
|
|
1994
2061
|
},
|
|
2062
|
+
{
|
|
2063
|
+
command: "loop-agent dag request-interrupt",
|
|
2064
|
+
coverage: "human-gated-required",
|
|
2065
|
+
action: "dagRequestInterrupt",
|
|
2066
|
+
source: "loop-agent",
|
|
2067
|
+
},
|
|
1995
2068
|
{
|
|
1996
2069
|
command: "loop-agent dag rerun",
|
|
1997
2070
|
coverage: "model-callable",
|
|
@@ -13,6 +13,7 @@ import { z } from "zod";
|
|
|
13
13
|
import { executePiStep } from "../../executors/pi-executor.js";
|
|
14
14
|
import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
|
|
15
15
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
16
|
+
import { listPiSdkAvailableModels } from "../../executors/pi-sdk-executor.js";
|
|
16
17
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
17
18
|
import { TASK_CONTRACT_DRAFT_SCHEMA_VERSION } from "../contract/constants.js";
|
|
18
19
|
import { getTaskPaths } from "../runtime.js";
|
|
@@ -20,6 +21,14 @@ import { detectProductArtifactMeta } from "./artifact-meta.js";
|
|
|
20
21
|
import { filterPlaceholderPaths } from "./placeholder-paths.js";
|
|
21
22
|
export const SEMANTIC_INTAKE_ARTIFACT_REL = "artifacts/intake/semantic-draft.json";
|
|
22
23
|
export const SEMANTIC_INTAKE_ATTEMPT_MARKER = "artifacts/intake/semantic-intake-attempted.json";
|
|
24
|
+
/** Per-model wall clock for one semantic-intake Pi attempt. */
|
|
25
|
+
export const SEMANTIC_INTAKE_ATTEMPT_TIMEOUT_MS = 300_000;
|
|
26
|
+
/** Extra Pi-catalog models after MED → HIGH → LOW. */
|
|
27
|
+
export const SEMANTIC_INTAKE_MAX_EXTRA_MODELS = 5;
|
|
28
|
+
/** Parent CLI budget covering harness tiers plus extra catalog models. */
|
|
29
|
+
export const SEMANTIC_INTAKE_CLI_TIMEOUT_MS = SEMANTIC_INTAKE_ATTEMPT_TIMEOUT_MS *
|
|
30
|
+
(3 + SEMANTIC_INTAKE_MAX_EXTRA_MODELS);
|
|
31
|
+
const NON_CHAT_MODEL = /embed|whisper|tts|dall-e|dalle|image|rerank|moderation/i;
|
|
23
32
|
const productArtifactBlockers = new Set([
|
|
24
33
|
"PRODUCT_ANALYSIS_NOT_EXECUTABLE",
|
|
25
34
|
"PRODUCT_CLARIFICATION_NOT_EXECUTABLE",
|
|
@@ -126,6 +135,58 @@ export function evaluateSemanticIntakeEligibility(input) {
|
|
|
126
135
|
reason: "structural or engineering-boundary gaps with imported sources",
|
|
127
136
|
};
|
|
128
137
|
}
|
|
138
|
+
function intakeModelKey(ref) {
|
|
139
|
+
return ref.trim().replace(/\\/g, "/").toLowerCase();
|
|
140
|
+
}
|
|
141
|
+
function intakeModelRef(provider, id) {
|
|
142
|
+
const trimmedId = id.trim();
|
|
143
|
+
if (trimmedId.includes("/"))
|
|
144
|
+
return trimmedId.replace(/\\/g, "/");
|
|
145
|
+
return `${provider.trim()}/${trimmedId}`;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* MED → HIGH → LOW from harness, then Pi-supported catalog models
|
|
149
|
+
* (credentialed first). Duplicates and non-chat ids are skipped.
|
|
150
|
+
*/
|
|
151
|
+
export function buildSemanticIntakeModelQueue(input) {
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
const queue = [];
|
|
154
|
+
const push = (raw) => {
|
|
155
|
+
const ref = raw?.trim().replace(/\\/g, "/");
|
|
156
|
+
if (!ref)
|
|
157
|
+
return;
|
|
158
|
+
const key = intakeModelKey(ref);
|
|
159
|
+
if (seen.has(key))
|
|
160
|
+
return;
|
|
161
|
+
seen.add(key);
|
|
162
|
+
queue.push(ref);
|
|
163
|
+
};
|
|
164
|
+
push(input.harness.MED);
|
|
165
|
+
push(input.harness.HIGH);
|
|
166
|
+
push(input.harness.LOW);
|
|
167
|
+
const extras = [...(input.available ?? [])]
|
|
168
|
+
.filter((entry) => {
|
|
169
|
+
const hay = `${entry.id} ${entry.name ?? ""} ${entry.provider}`;
|
|
170
|
+
return !NON_CHAT_MODEL.test(hay);
|
|
171
|
+
})
|
|
172
|
+
.sort((left, right) => {
|
|
173
|
+
const leftCred = left.hasCredentials === true ? 0 : 1;
|
|
174
|
+
const rightCred = right.hasCredentials === true ? 0 : 1;
|
|
175
|
+
if (leftCred !== rightCred)
|
|
176
|
+
return leftCred - rightCred;
|
|
177
|
+
return intakeModelRef(left.provider, left.id).localeCompare(intakeModelRef(right.provider, right.id));
|
|
178
|
+
});
|
|
179
|
+
let extraCount = 0;
|
|
180
|
+
for (const entry of extras) {
|
|
181
|
+
if (extraCount >= SEMANTIC_INTAKE_MAX_EXTRA_MODELS)
|
|
182
|
+
break;
|
|
183
|
+
const before = queue.length;
|
|
184
|
+
push(intakeModelRef(entry.provider, entry.id));
|
|
185
|
+
if (queue.length > before)
|
|
186
|
+
extraCount += 1;
|
|
187
|
+
}
|
|
188
|
+
return queue;
|
|
189
|
+
}
|
|
129
190
|
export function extractJsonObject(text) {
|
|
130
191
|
const fenced = /```(?:json)?\s*([\s\S]*?)```/iu.exec(text);
|
|
131
192
|
const candidate = fenced?.[1]?.trim() || text.trim();
|
|
@@ -493,6 +554,7 @@ export async function runSemanticIntake(input) {
|
|
|
493
554
|
};
|
|
494
555
|
}
|
|
495
556
|
let assistantText;
|
|
557
|
+
let attemptedModels = [];
|
|
496
558
|
if (input.fixtureAssistantText !== undefined) {
|
|
497
559
|
assistantText = input.fixtureAssistantText;
|
|
498
560
|
}
|
|
@@ -500,7 +562,33 @@ export async function runSemanticIntake(input) {
|
|
|
500
562
|
const execute = input.executePi ?? executePiStep;
|
|
501
563
|
const manifest = await loadHarnessManifest(input.repoRoot);
|
|
502
564
|
const models = resolveExecutorModelMatrices(manifest);
|
|
503
|
-
|
|
565
|
+
let available = [];
|
|
566
|
+
try {
|
|
567
|
+
available = input.listAvailableModels
|
|
568
|
+
? await input.listAvailableModels()
|
|
569
|
+
: await listPiSdkAvailableModels();
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
available = [];
|
|
573
|
+
}
|
|
574
|
+
const queue = buildSemanticIntakeModelQueue({
|
|
575
|
+
harness: models.pi,
|
|
576
|
+
available,
|
|
577
|
+
});
|
|
578
|
+
if (queue.length === 0) {
|
|
579
|
+
await writeAttemptMarker(input.repoRoot, input.taskId, {
|
|
580
|
+
ok: false,
|
|
581
|
+
code: "SEMANTIC_INTAKE_PI_FAILED",
|
|
582
|
+
failureCategory: "unavailable",
|
|
583
|
+
at: new Date().toISOString(),
|
|
584
|
+
attemptedModels: [],
|
|
585
|
+
});
|
|
586
|
+
return {
|
|
587
|
+
ok: false,
|
|
588
|
+
code: "SEMANTIC_INTAKE_PI_FAILED",
|
|
589
|
+
message: "semantic intake has no MED/HIGH/LOW or Pi-supported models to try",
|
|
590
|
+
};
|
|
591
|
+
}
|
|
504
592
|
const attached = usableDocs
|
|
505
593
|
.map((d) => d.absolutePath)
|
|
506
594
|
.filter((p) => Boolean(p));
|
|
@@ -510,39 +598,71 @@ export async function runSemanticIntake(input) {
|
|
|
510
598
|
documents: usableDocs,
|
|
511
599
|
repoRoot: input.repoRoot,
|
|
512
600
|
});
|
|
513
|
-
const
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
601
|
+
const attempted = [];
|
|
602
|
+
let lastFailure = "";
|
|
603
|
+
let chosenText;
|
|
604
|
+
for (const modelRef of queue) {
|
|
605
|
+
try {
|
|
606
|
+
const result = await execute({
|
|
607
|
+
attachedFiles: attached,
|
|
608
|
+
modelConfig: resolveDagPiModelConfig(modelRef),
|
|
609
|
+
prompt,
|
|
610
|
+
repoRoot: input.repoRoot,
|
|
611
|
+
step: "analyze",
|
|
612
|
+
toolNames: ["read", "grep", "find", "ls"],
|
|
613
|
+
timeoutMs: SEMANTIC_INTAKE_ATTEMPT_TIMEOUT_MS,
|
|
614
|
+
stallTimeoutMs: SEMANTIC_INTAKE_ATTEMPT_TIMEOUT_MS,
|
|
615
|
+
userMessage: "Structure the imported requirement documents into the required JSON only.",
|
|
616
|
+
validateOutput: (text) => {
|
|
617
|
+
try {
|
|
618
|
+
const parsed = parseSemanticRequirementDraft(text);
|
|
619
|
+
assertSemanticDraftAcceptable(parsed);
|
|
620
|
+
return [];
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
return [error instanceof Error ? error.message : String(error)];
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
});
|
|
627
|
+
attempted.push({
|
|
628
|
+
model: result.modelDisplay || modelRef,
|
|
629
|
+
ok: result.ok,
|
|
630
|
+
failureCategory: result.failureCategory,
|
|
631
|
+
timedOut: result.timedOut,
|
|
632
|
+
});
|
|
633
|
+
if (result.ok) {
|
|
634
|
+
chosenText = result.assistantText;
|
|
635
|
+
break;
|
|
529
636
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
637
|
+
lastFailure =
|
|
638
|
+
`${result.failureCategory}: ${result.stderr || result.assistantText}`.slice(0, 2000);
|
|
639
|
+
}
|
|
640
|
+
catch (error) {
|
|
641
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
642
|
+
attempted.push({
|
|
643
|
+
model: modelRef,
|
|
644
|
+
ok: false,
|
|
645
|
+
failureCategory: "unknown",
|
|
646
|
+
});
|
|
647
|
+
lastFailure = message.slice(0, 2000);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
attemptedModels = attempted;
|
|
651
|
+
if (chosenText === undefined) {
|
|
533
652
|
await writeAttemptMarker(input.repoRoot, input.taskId, {
|
|
534
653
|
ok: false,
|
|
535
654
|
code: "SEMANTIC_INTAKE_PI_FAILED",
|
|
536
|
-
failureCategory:
|
|
655
|
+
failureCategory: attempted.at(-1)?.failureCategory,
|
|
537
656
|
at: new Date().toISOString(),
|
|
657
|
+
attemptedModels: attempted,
|
|
538
658
|
});
|
|
539
659
|
return {
|
|
540
660
|
ok: false,
|
|
541
661
|
code: "SEMANTIC_INTAKE_PI_FAILED",
|
|
542
|
-
message: `semantic intake pi failed
|
|
662
|
+
message: `semantic intake pi failed after ${attempted.length} model(s): ${lastFailure}`.slice(0, 2000),
|
|
543
663
|
};
|
|
544
664
|
}
|
|
545
|
-
assistantText =
|
|
665
|
+
assistantText = chosenText;
|
|
546
666
|
}
|
|
547
667
|
try {
|
|
548
668
|
const semantic = parseSemanticRequirementDraft(assistantText);
|
|
@@ -576,6 +696,7 @@ export async function runSemanticIntake(input) {
|
|
|
576
696
|
code: "SEMANTIC_INTAKE_OK",
|
|
577
697
|
artifactPath: SEMANTIC_INTAKE_ARTIFACT_REL,
|
|
578
698
|
at: new Date().toISOString(),
|
|
699
|
+
...(attemptedModels.length > 0 ? { attemptedModels } : {}),
|
|
579
700
|
});
|
|
580
701
|
return { ok: true, draft, artifactPath, semantic };
|
|
581
702
|
}
|
|
@@ -39,6 +39,7 @@ const OPERATION_TITLES = {
|
|
|
39
39
|
dagDoctor: "执行诊断",
|
|
40
40
|
dagReport: "执行结果",
|
|
41
41
|
dagRerunPlan: "恢复评估",
|
|
42
|
+
dagInterruptEligibility: "中止资格",
|
|
42
43
|
standaloneTaskRerun: "恢复执行",
|
|
43
44
|
workerTaskRetry: "重试任务",
|
|
44
45
|
promoteRun: "任务推进",
|
|
@@ -90,6 +91,11 @@ const TOOL_SEMANTICS = {
|
|
|
90
91
|
success: "恢复条件评估已经完成",
|
|
91
92
|
error: "暂时无法确定安全的恢复方式",
|
|
92
93
|
},
|
|
94
|
+
dagInterruptEligibility: {
|
|
95
|
+
running: "正在检查能否协作中止此运行",
|
|
96
|
+
success: "中止资格已经确认",
|
|
97
|
+
error: "当前运行不能协作中止",
|
|
98
|
+
},
|
|
93
99
|
standaloneTaskRerun: {
|
|
94
100
|
running: "正在从失败位置恢复执行",
|
|
95
101
|
success: "已经从失败位置继续执行",
|
|
@@ -27,6 +27,7 @@ function readWorkspaceParams(query) {
|
|
|
27
27
|
...(read("dagRunId") ? { dagRunId: read("dagRunId") } : {}),
|
|
28
28
|
...(read("fromNodeId") ? { fromNodeId: read("fromNodeId") } : {}),
|
|
29
29
|
...(read("featureId") ? { featureId: read("featureId") } : {}),
|
|
30
|
+
...(read("intent") ? { intent: read("intent") } : {}),
|
|
30
31
|
};
|
|
31
32
|
}
|
|
32
33
|
/** Parse `#/tasks?taskId=…` hash routes (Observe-style virtual paths). */
|
|
@@ -241,6 +241,9 @@ async function finalizeFromWorkerResult(operationId, result, deps) {
|
|
|
241
241
|
});
|
|
242
242
|
stateEvent(deps.events, updated, state, updated.errorMessage);
|
|
243
243
|
appendSafeResultEvent(deps, operationId, state, { ok, exitCode: result.exitCode }, runFacts);
|
|
244
|
+
if (ok && operation?.action === "dagRequestInterrupt") {
|
|
245
|
+
await projectInterruptRequestOntoTarget(operation, result.json, deps);
|
|
246
|
+
}
|
|
244
247
|
return updated;
|
|
245
248
|
}
|
|
246
249
|
/** Result event carries the same safe summary persisted on the record. */
|
|
@@ -303,3 +306,48 @@ export function scheduleOperation(operationId, deps) {
|
|
|
303
306
|
});
|
|
304
307
|
});
|
|
305
308
|
}
|
|
309
|
+
async function projectInterruptRequestOntoTarget(operation, json, deps) {
|
|
310
|
+
const params = operation.actionParams ?? {};
|
|
311
|
+
const targetOperationId = typeof params.targetOperationId === "string"
|
|
312
|
+
? params.targetOperationId.trim()
|
|
313
|
+
: "";
|
|
314
|
+
const runId = typeof params.runId === "string" ? params.runId.trim() : "";
|
|
315
|
+
if (!targetOperationId)
|
|
316
|
+
return;
|
|
317
|
+
const rec = json && typeof json === "object" ? json : {};
|
|
318
|
+
const request = rec.request && typeof rec.request === "object"
|
|
319
|
+
? rec.request
|
|
320
|
+
: rec;
|
|
321
|
+
const status = (typeof rec.interruptStatus === "string" && rec.interruptStatus) ||
|
|
322
|
+
(typeof request.status === "string" && request.status) ||
|
|
323
|
+
"requested";
|
|
324
|
+
if (status !== "requested" &&
|
|
325
|
+
status !== "acknowledged" &&
|
|
326
|
+
status !== "settled" &&
|
|
327
|
+
status !== "needs-reconcile") {
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const target = await deps.store.get(targetOperationId);
|
|
331
|
+
if (!target)
|
|
332
|
+
return;
|
|
333
|
+
await deps.store.update(targetOperationId, {
|
|
334
|
+
...(runId && !target.dagRunId ? { dagRunId: runId } : {}),
|
|
335
|
+
interrupt: {
|
|
336
|
+
status,
|
|
337
|
+
...(typeof request.requestId === "string"
|
|
338
|
+
? { requestId: request.requestId }
|
|
339
|
+
: {}),
|
|
340
|
+
...(typeof request.reasonCode === "string"
|
|
341
|
+
? { reasonCode: request.reasonCode }
|
|
342
|
+
: {}),
|
|
343
|
+
...(typeof request.requestedAt === "string"
|
|
344
|
+
? { requestedAt: request.requestedAt }
|
|
345
|
+
: {}),
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
deps.events.append(targetOperationId, {
|
|
349
|
+
kind: "state",
|
|
350
|
+
message: `interrupt ${status}`,
|
|
351
|
+
data: { interruptStatus: status, ...(runId ? { runId } : {}) },
|
|
352
|
+
});
|
|
353
|
+
}
|
|
@@ -158,6 +158,7 @@ export async function waitForOperationChange(input) {
|
|
|
158
158
|
return new Promise((resolve, reject) => {
|
|
159
159
|
let settled = false;
|
|
160
160
|
let timer;
|
|
161
|
+
let pollTimer;
|
|
161
162
|
let unsubscribe;
|
|
162
163
|
/** Highest seq observed via listener/listFrom during this wait. */
|
|
163
164
|
let observedSeq = afterSeq;
|
|
@@ -166,6 +167,10 @@ export async function waitForOperationChange(input) {
|
|
|
166
167
|
clearTimeout(timer);
|
|
167
168
|
timer = undefined;
|
|
168
169
|
}
|
|
170
|
+
if (pollTimer !== undefined) {
|
|
171
|
+
clearInterval(pollTimer);
|
|
172
|
+
pollTimer = undefined;
|
|
173
|
+
}
|
|
169
174
|
if (unsubscribe) {
|
|
170
175
|
unsubscribe();
|
|
171
176
|
unsubscribe = undefined;
|
|
@@ -303,6 +308,42 @@ export async function waitForOperationChange(input) {
|
|
|
303
308
|
})();
|
|
304
309
|
}, maxWaitMs);
|
|
305
310
|
timer.unref?.();
|
|
311
|
+
if (input.poll) {
|
|
312
|
+
pollTimer = setInterval(() => {
|
|
313
|
+
void (async () => {
|
|
314
|
+
if (settled)
|
|
315
|
+
return;
|
|
316
|
+
try {
|
|
317
|
+
const changed = await input.poll?.();
|
|
318
|
+
if (!changed || settled)
|
|
319
|
+
return;
|
|
320
|
+
const relisted = events.listFrom(operationId, afterSeq);
|
|
321
|
+
if ("error" in relisted)
|
|
322
|
+
return;
|
|
323
|
+
const nextWake = wakeFilter(relisted.events);
|
|
324
|
+
observedSeq = Math.max(observedSeq, relisted.events.length > 0
|
|
325
|
+
? relisted.events[relisted.events.length - 1].seq
|
|
326
|
+
: afterSeq);
|
|
327
|
+
if (nextWake.length === 0)
|
|
328
|
+
return;
|
|
329
|
+
const latest = await input.operations.get(operationId);
|
|
330
|
+
finish(settledSummary({
|
|
331
|
+
operationId,
|
|
332
|
+
operation: latest ?? operation,
|
|
333
|
+
afterSeq,
|
|
334
|
+
changed: true,
|
|
335
|
+
timedOut: false,
|
|
336
|
+
newEvents: nextWake,
|
|
337
|
+
observedSeq,
|
|
338
|
+
}));
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
// Poll is best-effort; the wait still times out.
|
|
342
|
+
}
|
|
343
|
+
})();
|
|
344
|
+
}, 1_000);
|
|
345
|
+
pollTimer.unref?.();
|
|
346
|
+
}
|
|
306
347
|
}
|
|
307
348
|
catch (error) {
|
|
308
349
|
finish(error instanceof OperationWaitError
|