@chllming/wave-orchestration 0.8.2 → 0.8.4
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 +40 -2
- package/README.md +47 -11
- package/docs/README.md +6 -2
- package/docs/concepts/what-is-a-wave.md +1 -1
- package/docs/plans/architecture-hardening-migration.md +8 -1
- package/docs/plans/current-state.md +17 -7
- package/docs/plans/end-state-architecture.md +82 -69
- package/docs/plans/examples/wave-example-live-proof.md +1 -1
- package/docs/plans/migration.md +235 -61
- package/docs/plans/wave-orchestrator.md +37 -11
- package/docs/reference/cli-reference.md +39 -15
- package/docs/reference/coordination-and-closure.md +30 -6
- package/docs/reference/npmjs-trusted-publishing.md +5 -4
- package/docs/reference/sample-waves.md +4 -4
- package/package.json +1 -1
- package/releases/manifest.json +39 -0
- package/scripts/wave-orchestrator/agent-state.mjs +0 -491
- package/scripts/wave-orchestrator/autonomous.mjs +10 -6
- package/scripts/wave-orchestrator/{launcher-closure.mjs → closure-engine.mjs} +190 -74
- package/scripts/wave-orchestrator/control-cli.mjs +8 -0
- package/scripts/wave-orchestrator/coord-cli.mjs +8 -0
- package/scripts/wave-orchestrator/{launcher-derived-state.mjs → derived-state-engine.mjs} +34 -146
- package/scripts/wave-orchestrator/feedback.mjs +11 -1
- package/scripts/wave-orchestrator/{launcher-gates.mjs → gate-engine.mjs} +395 -139
- package/scripts/wave-orchestrator/human-input-resolution.mjs +348 -0
- package/scripts/wave-orchestrator/human-input-workflow.mjs +104 -0
- package/scripts/wave-orchestrator/implementation-engine.mjs +120 -0
- package/scripts/wave-orchestrator/launcher-runtime.mjs +5 -6
- package/scripts/wave-orchestrator/launcher.mjs +271 -724
- package/scripts/wave-orchestrator/projection-writer.mjs +256 -0
- package/scripts/wave-orchestrator/reconcile-format.mjs +32 -0
- package/scripts/wave-orchestrator/reducer-snapshot.mjs +297 -0
- package/scripts/wave-orchestrator/replay.mjs +3 -1
- package/scripts/wave-orchestrator/result-envelope.mjs +589 -0
- package/scripts/wave-orchestrator/retry-control.mjs +5 -0
- package/scripts/wave-orchestrator/{launcher-retry.mjs → retry-engine.mjs} +267 -18
- package/scripts/wave-orchestrator/role-helpers.mjs +51 -0
- package/scripts/wave-orchestrator/{launcher-supervisor.mjs → session-supervisor.mjs} +178 -103
- package/scripts/wave-orchestrator/shared.mjs +1 -0
- package/scripts/wave-orchestrator/traces.mjs +10 -1
- package/scripts/wave-orchestrator/wave-files.mjs +11 -9
- package/scripts/wave-orchestrator/wave-state-reducer.mjs +52 -5
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
appendCoordinationRecord,
|
|
5
|
+
clarificationClosureCondition,
|
|
6
|
+
clarificationLinkedRequests,
|
|
7
|
+
isOpenCoordinationStatus,
|
|
8
|
+
readMaterializedCoordinationState,
|
|
9
|
+
} from "./coordination-store.mjs";
|
|
10
|
+
import { readWaveHumanFeedbackRequests } from "./coordination.mjs";
|
|
11
|
+
import {
|
|
12
|
+
readControlPlaneEvents,
|
|
13
|
+
readWaveControlPlaneState,
|
|
14
|
+
syncWaveControlPlaneProjections,
|
|
15
|
+
waveControlPlaneLogPath,
|
|
16
|
+
} from "./control-plane.mjs";
|
|
17
|
+
import { readWaveLedger } from "./ledger.mjs";
|
|
18
|
+
import { readRunResultEnvelope } from "./gate-engine.mjs";
|
|
19
|
+
import { buildResumePlan, clearWaveRelaunchPlan } from "./retry-engine.mjs";
|
|
20
|
+
import { readWaveProofRegistry } from "./proof-registry.mjs";
|
|
21
|
+
import { buildDependencySnapshot, buildRequestAssignments, syncAssignmentRecords } from "./routing-state.mjs";
|
|
22
|
+
import { buildLanePaths } from "./shared.mjs";
|
|
23
|
+
import { reduceWaveState } from "./wave-state-reducer.mjs";
|
|
24
|
+
import { parseWaveFiles } from "./wave-files.mjs";
|
|
25
|
+
import { writeWaveRetryOverride } from "./retry-control.mjs";
|
|
26
|
+
import { resolveWaveRoleBindings } from "./role-helpers.mjs";
|
|
27
|
+
|
|
28
|
+
function coordinationLogPath(lanePaths, waveNumber) {
|
|
29
|
+
return path.join(lanePaths.coordinationDir, `wave-${waveNumber}.jsonl`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function ledgerPath(lanePaths, waveNumber) {
|
|
33
|
+
return path.join(lanePaths.ledgerDir, `wave-${waveNumber}.json`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function coordinationTriagePath(lanePaths, waveNumber) {
|
|
37
|
+
return path.join(lanePaths.feedbackTriageDir, `wave-${waveNumber}.jsonl`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function appendCoordinationStatusUpdate(logPath, record, status, options = {}) {
|
|
41
|
+
return appendCoordinationRecord(logPath, {
|
|
42
|
+
...record,
|
|
43
|
+
status,
|
|
44
|
+
summary: options.summary || record.summary,
|
|
45
|
+
detail: options.detail || record.detail,
|
|
46
|
+
source: options.source || "operator",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function appendTriageEscalationUpdateIfPresent(lanePaths, waveNumber, record) {
|
|
51
|
+
const triagePath = coordinationTriagePath(lanePaths, waveNumber);
|
|
52
|
+
if (!fs.existsSync(triagePath) || record?.kind !== "human-escalation") {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
appendCoordinationRecord(triagePath, record);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function loadWave(lanePaths, waveNumber) {
|
|
59
|
+
const waves = parseWaveFiles(lanePaths.wavesDir, { laneProfile: lanePaths.laneProfile });
|
|
60
|
+
const wave = waves.find((entry) => entry.wave === waveNumber);
|
|
61
|
+
if (!wave) {
|
|
62
|
+
throw new Error(`Wave ${waveNumber} not found in ${lanePaths.wavesDir}`);
|
|
63
|
+
}
|
|
64
|
+
return wave;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function taskRunInfoForAgent(lanePaths, wave, agent, proofRegistry) {
|
|
68
|
+
const safeName = `wave-${wave.wave}-${agent.slug}`;
|
|
69
|
+
return {
|
|
70
|
+
agent,
|
|
71
|
+
lane: lanePaths.lane,
|
|
72
|
+
wave: wave.wave,
|
|
73
|
+
resultsDir: lanePaths.resultsDir,
|
|
74
|
+
logPath: path.join(lanePaths.logsDir, `${safeName}.log`),
|
|
75
|
+
statusPath: path.join(lanePaths.statusDir, `${safeName}.status`),
|
|
76
|
+
proofRegistry,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function feedbackLinkMatchesRecord(record, requestId) {
|
|
81
|
+
const normalizedRequestId = String(requestId || "").trim();
|
|
82
|
+
if (!normalizedRequestId) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
if (String(record?.id || "").trim() === normalizedRequestId) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
Array.isArray(record?.artifactRefs) &&
|
|
90
|
+
record.artifactRefs.some((ref) => String(ref || "").trim() === normalizedRequestId)
|
|
91
|
+
) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
Array.isArray(record?.dependsOn) &&
|
|
96
|
+
record.dependsOn.some((value) => String(value || "").trim() === normalizedRequestId)
|
|
97
|
+
) {
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function linkedClarificationIdsForRecords(records) {
|
|
104
|
+
const ids = new Set();
|
|
105
|
+
for (const record of records || []) {
|
|
106
|
+
if (record?.kind === "clarification-request" && record?.id) {
|
|
107
|
+
ids.add(record.id);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const closureCondition = String(record?.closureCondition || "").trim();
|
|
111
|
+
if (closureCondition.startsWith("clarification:")) {
|
|
112
|
+
ids.add(closureCondition.slice("clarification:".length));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return [...ids].filter(Boolean);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function resolveFeedbackLinkedCoordination({
|
|
119
|
+
lanePaths,
|
|
120
|
+
wave,
|
|
121
|
+
requestId,
|
|
122
|
+
operator = "human-operator",
|
|
123
|
+
detail = "",
|
|
124
|
+
}) {
|
|
125
|
+
const logPath = coordinationLogPath(lanePaths, wave.wave);
|
|
126
|
+
const state = readMaterializedCoordinationState(logPath);
|
|
127
|
+
const resolvedRecords = [];
|
|
128
|
+
|
|
129
|
+
const directlyLinked = state.latestRecords.filter((record) =>
|
|
130
|
+
isOpenCoordinationStatus(record.status) && feedbackLinkMatchesRecord(record, requestId),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
for (const record of directlyLinked) {
|
|
134
|
+
const updated = appendCoordinationStatusUpdate(logPath, record, "resolved", {
|
|
135
|
+
detail: detail || `Resolved after answered human input ${requestId}.`,
|
|
136
|
+
summary: record.summary,
|
|
137
|
+
source: operator,
|
|
138
|
+
});
|
|
139
|
+
resolvedRecords.push(updated);
|
|
140
|
+
appendTriageEscalationUpdateIfPresent(lanePaths, wave.wave, updated);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const nextState = readMaterializedCoordinationState(logPath);
|
|
144
|
+
const clarificationIds = linkedClarificationIdsForRecords([
|
|
145
|
+
...directlyLinked,
|
|
146
|
+
...resolvedRecords,
|
|
147
|
+
]);
|
|
148
|
+
for (const clarificationId of clarificationIds) {
|
|
149
|
+
const clarification = nextState.byId.get(clarificationId);
|
|
150
|
+
if (!clarification || !isOpenCoordinationStatus(clarification.status)) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const updatedClarification = appendCoordinationStatusUpdate(logPath, clarification, "resolved", {
|
|
154
|
+
detail: detail || `Resolved after answered human input ${requestId}.`,
|
|
155
|
+
summary: clarification.summary,
|
|
156
|
+
source: operator,
|
|
157
|
+
});
|
|
158
|
+
resolvedRecords.push(updatedClarification);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const resolvedState = readMaterializedCoordinationState(logPath);
|
|
162
|
+
for (const clarificationId of clarificationIds) {
|
|
163
|
+
const linkedRequests = clarificationLinkedRequests(resolvedState, clarificationId).filter((entry) =>
|
|
164
|
+
isOpenCoordinationStatus(entry.status),
|
|
165
|
+
);
|
|
166
|
+
for (const linked of linkedRequests) {
|
|
167
|
+
const updatedLinked = appendCoordinationStatusUpdate(logPath, linked, "resolved", {
|
|
168
|
+
detail: `Resolved via clarification ${clarificationId}.`,
|
|
169
|
+
summary: linked.summary,
|
|
170
|
+
source: operator,
|
|
171
|
+
});
|
|
172
|
+
resolvedRecords.push(updatedLinked);
|
|
173
|
+
}
|
|
174
|
+
for (const escalation of (resolvedState.humanEscalations || []).filter(
|
|
175
|
+
(entry) =>
|
|
176
|
+
isOpenCoordinationStatus(entry.status) &&
|
|
177
|
+
entry.closureCondition === clarificationClosureCondition(clarificationId),
|
|
178
|
+
)) {
|
|
179
|
+
const updatedEscalation = appendCoordinationStatusUpdate(logPath, escalation, "resolved", {
|
|
180
|
+
detail: detail || `Resolved via clarification ${clarificationId}.`,
|
|
181
|
+
summary: escalation.summary,
|
|
182
|
+
source: operator,
|
|
183
|
+
});
|
|
184
|
+
resolvedRecords.push(updatedEscalation);
|
|
185
|
+
appendTriageEscalationUpdateIfPresent(lanePaths, wave.wave, updatedEscalation);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const assignmentState = readMaterializedCoordinationState(logPath);
|
|
190
|
+
const ledger = readWaveLedger(ledgerPath(lanePaths, wave.wave)) || { phase: "planned" };
|
|
191
|
+
const assignments = buildRequestAssignments({
|
|
192
|
+
coordinationState: assignmentState,
|
|
193
|
+
agents: wave.agents,
|
|
194
|
+
ledger,
|
|
195
|
+
capabilityRouting: lanePaths.capabilityRouting,
|
|
196
|
+
});
|
|
197
|
+
syncAssignmentRecords(logPath, {
|
|
198
|
+
lane: lanePaths.lane,
|
|
199
|
+
wave: wave.wave,
|
|
200
|
+
assignments,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
resolvedRecords,
|
|
205
|
+
clarificationIds,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function buildResumePlanFromDisk({ lanePaths, wave }) {
|
|
210
|
+
const proofRegistry = readWaveProofRegistry(lanePaths, wave.wave);
|
|
211
|
+
const agentRuns = wave.agents.map((agent) =>
|
|
212
|
+
taskRunInfoForAgent(lanePaths, wave, agent, proofRegistry),
|
|
213
|
+
);
|
|
214
|
+
const agentEnvelopes = Object.fromEntries(
|
|
215
|
+
agentRuns
|
|
216
|
+
.map((runInfo) => {
|
|
217
|
+
const envelopeResult = readRunResultEnvelope(runInfo, wave, { mode: "compat" });
|
|
218
|
+
return [runInfo.agent.agentId, envelopeResult?.valid ? envelopeResult.envelope : null];
|
|
219
|
+
})
|
|
220
|
+
.filter(([, envelope]) => Boolean(envelope)),
|
|
221
|
+
);
|
|
222
|
+
const coordinationState = readMaterializedCoordinationState(coordinationLogPath(lanePaths, wave.wave));
|
|
223
|
+
const feedbackRequests = readWaveHumanFeedbackRequests({
|
|
224
|
+
feedbackRequestsDir: lanePaths.feedbackRequestsDir,
|
|
225
|
+
lane: lanePaths.lane,
|
|
226
|
+
waveNumber: wave.wave,
|
|
227
|
+
agentIds: wave.agents.map((agent) => agent.agentId),
|
|
228
|
+
orchestratorId: "",
|
|
229
|
+
});
|
|
230
|
+
const ledger = readWaveLedger(ledgerPath(lanePaths, wave.wave)) || { phase: "planned" };
|
|
231
|
+
const dependencySnapshot = buildDependencySnapshot({
|
|
232
|
+
dirPath: lanePaths.crossLaneDependenciesDir,
|
|
233
|
+
lane: lanePaths.lane,
|
|
234
|
+
waveNumber: wave.wave,
|
|
235
|
+
agents: wave.agents,
|
|
236
|
+
ledger,
|
|
237
|
+
capabilityRouting: lanePaths.capabilityRouting,
|
|
238
|
+
});
|
|
239
|
+
const reducerState = reduceWaveState({
|
|
240
|
+
controlPlaneEvents: readControlPlaneEvents(waveControlPlaneLogPath(lanePaths, wave.wave)),
|
|
241
|
+
coordinationRecords: coordinationState.latestRecords || [],
|
|
242
|
+
agentEnvelopes,
|
|
243
|
+
waveDefinition: wave,
|
|
244
|
+
dependencyTickets: dependencySnapshot,
|
|
245
|
+
feedbackRequests,
|
|
246
|
+
laneConfig: {
|
|
247
|
+
lane: lanePaths.lane,
|
|
248
|
+
...resolveWaveRoleBindings(wave, lanePaths, wave.agents),
|
|
249
|
+
validationMode: "live",
|
|
250
|
+
evalTargets: wave.evalTargets,
|
|
251
|
+
benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
|
|
252
|
+
laneProfile: lanePaths.laneProfile,
|
|
253
|
+
requireIntegrationStewardFromWave: lanePaths.requireIntegrationStewardFromWave,
|
|
254
|
+
capabilityRouting: lanePaths.capabilityRouting,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
return buildResumePlan(reducerState, {
|
|
258
|
+
waveDefinition: wave,
|
|
259
|
+
lanePaths,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function maybeWriteAutoResumeRequest({
|
|
264
|
+
lanePaths,
|
|
265
|
+
wave,
|
|
266
|
+
requestedBy = "human-operator",
|
|
267
|
+
reason = "",
|
|
268
|
+
}) {
|
|
269
|
+
const controlState = readWaveControlPlaneState(lanePaths, wave.wave);
|
|
270
|
+
if (controlState.activeAttempt || controlState.activeRerunRequest) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const resumePlan = buildResumePlanFromDisk({ lanePaths, wave });
|
|
274
|
+
if (!resumePlan.canResume || resumePlan.reason === "human-request") {
|
|
275
|
+
return { resumePlan, request: null };
|
|
276
|
+
}
|
|
277
|
+
if (resumePlan.resumeFromPhase === "completed") {
|
|
278
|
+
return { resumePlan, request: null };
|
|
279
|
+
}
|
|
280
|
+
clearWaveRelaunchPlan(lanePaths, wave.wave);
|
|
281
|
+
const payload = {
|
|
282
|
+
requestedBy,
|
|
283
|
+
reason:
|
|
284
|
+
reason ||
|
|
285
|
+
`Auto continuation after answered human input; resume from ${resumePlan.resumeFromPhase}.`,
|
|
286
|
+
preserveReusableAgentIds: resumePlan.reusableAgentIds,
|
|
287
|
+
reuseProofBundleIds: resumePlan.reusableProofBundleIds,
|
|
288
|
+
applyOnce: true,
|
|
289
|
+
};
|
|
290
|
+
if (resumePlan.resumeFromPhase === "implementation") {
|
|
291
|
+
payload.selectedAgentIds = resumePlan.invalidatedAgentIds;
|
|
292
|
+
} else {
|
|
293
|
+
payload.resumePhase = resumePlan.resumeFromPhase;
|
|
294
|
+
}
|
|
295
|
+
const request = writeWaveRetryOverride(lanePaths, wave.wave, payload);
|
|
296
|
+
return { resumePlan, request };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function answerHumanInputAndReconcile({
|
|
300
|
+
lanePaths,
|
|
301
|
+
wave,
|
|
302
|
+
requestId,
|
|
303
|
+
answeredPayload,
|
|
304
|
+
operator = "human-operator",
|
|
305
|
+
}) {
|
|
306
|
+
const detail =
|
|
307
|
+
`Resolved after human input ${requestId} was answered by ${operator}` +
|
|
308
|
+
(answeredPayload?.response?.text ? `: ${answeredPayload.response.text}` : ".");
|
|
309
|
+
const resolution = resolveFeedbackLinkedCoordination({
|
|
310
|
+
lanePaths,
|
|
311
|
+
wave,
|
|
312
|
+
requestId,
|
|
313
|
+
operator,
|
|
314
|
+
detail,
|
|
315
|
+
});
|
|
316
|
+
syncWaveControlPlaneProjections(
|
|
317
|
+
lanePaths,
|
|
318
|
+
wave.wave,
|
|
319
|
+
readWaveControlPlaneState(lanePaths, wave.wave),
|
|
320
|
+
);
|
|
321
|
+
const autoResume = maybeWriteAutoResumeRequest({
|
|
322
|
+
lanePaths,
|
|
323
|
+
wave,
|
|
324
|
+
requestedBy: operator,
|
|
325
|
+
reason: `Auto continuation after answered human input ${requestId}.`,
|
|
326
|
+
});
|
|
327
|
+
return {
|
|
328
|
+
resolution,
|
|
329
|
+
autoResume,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function answerHumanInputByRequest({
|
|
334
|
+
lane,
|
|
335
|
+
waveNumber,
|
|
336
|
+
requestId,
|
|
337
|
+
operator = "human-operator",
|
|
338
|
+
runId = null,
|
|
339
|
+
}) {
|
|
340
|
+
const lanePaths = buildLanePaths(lane, { adhocRunId: runId || null });
|
|
341
|
+
const wave = loadWave(lanePaths, waveNumber);
|
|
342
|
+
return answerHumanInputAndReconcile({
|
|
343
|
+
lanePaths,
|
|
344
|
+
wave,
|
|
345
|
+
requestId,
|
|
346
|
+
operator,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
@@ -287,3 +287,107 @@ export function computeHumanInputMetrics(requests) {
|
|
|
287
287
|
: null,
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
|
+
|
|
291
|
+
export function buildHumanFeedbackWorkflowUpdate({
|
|
292
|
+
request,
|
|
293
|
+
lane,
|
|
294
|
+
waveNumber,
|
|
295
|
+
existingEscalation = null,
|
|
296
|
+
}) {
|
|
297
|
+
const question = request?.question || "n/a";
|
|
298
|
+
const context = request?.context ? `; context=${request.context}` : "";
|
|
299
|
+
const agentId = request?.agentId || "human";
|
|
300
|
+
const responseOperator = request?.responseOperator || "human-operator";
|
|
301
|
+
const responseText = request?.responseText || "(empty response)";
|
|
302
|
+
if (request?.status === "pending") {
|
|
303
|
+
return {
|
|
304
|
+
combinedEvent: {
|
|
305
|
+
level: "warn",
|
|
306
|
+
agentId: request.agentId,
|
|
307
|
+
message: `Human feedback requested (${request.id}): ${question}`,
|
|
308
|
+
},
|
|
309
|
+
coordinationNotice: {
|
|
310
|
+
event: "human_feedback_requested",
|
|
311
|
+
waves: [waveNumber],
|
|
312
|
+
status: "waiting-human",
|
|
313
|
+
details: `request_id=${request.id}; agent=${request.agentId}; question=${question}${context}`,
|
|
314
|
+
actionRequested:
|
|
315
|
+
`Launcher operator should ask or answer in the parent session, then run: pnpm exec wave control task act answer --lane ${lane} --wave ${waveNumber} --id ${request.id} --response "<answer>" --operator "<name>"`,
|
|
316
|
+
},
|
|
317
|
+
consoleLines: [
|
|
318
|
+
`[human-feedback] wave=${waveNumber} agent=${request.agentId} request=${request.id} pending: ${question}`,
|
|
319
|
+
`[human-feedback] respond with: pnpm exec wave control task act answer --lane ${lane} --wave ${waveNumber} --id ${request.id} --response "<answer>" --operator "<name>"`,
|
|
320
|
+
],
|
|
321
|
+
coordinationUpdates: [
|
|
322
|
+
{
|
|
323
|
+
id: request.id,
|
|
324
|
+
lane,
|
|
325
|
+
wave: waveNumber,
|
|
326
|
+
agentId,
|
|
327
|
+
kind: "human-feedback",
|
|
328
|
+
targets: request.agentId ? [`agent:${request.agentId}`] : [],
|
|
329
|
+
priority: "high",
|
|
330
|
+
summary: question,
|
|
331
|
+
detail: request.context || "",
|
|
332
|
+
status: "open",
|
|
333
|
+
source: "feedback",
|
|
334
|
+
},
|
|
335
|
+
],
|
|
336
|
+
triageUpdates: [],
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
if (request?.status === "answered") {
|
|
340
|
+
const escalationId = `escalation-${request.id}`;
|
|
341
|
+
const escalationRecord = {
|
|
342
|
+
id: escalationId,
|
|
343
|
+
lane,
|
|
344
|
+
wave: waveNumber,
|
|
345
|
+
agentId,
|
|
346
|
+
kind: "human-escalation",
|
|
347
|
+
targets:
|
|
348
|
+
existingEscalation?.targets ||
|
|
349
|
+
(request.agentId ? [`agent:${request.agentId}`] : []),
|
|
350
|
+
dependsOn: existingEscalation?.dependsOn || [],
|
|
351
|
+
closureCondition: existingEscalation?.closureCondition || "",
|
|
352
|
+
priority: "high",
|
|
353
|
+
summary: question,
|
|
354
|
+
detail: responseText,
|
|
355
|
+
artifactRefs: [request.id],
|
|
356
|
+
status: "resolved",
|
|
357
|
+
source: "feedback",
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
combinedEvent: {
|
|
361
|
+
level: "info",
|
|
362
|
+
agentId: request.agentId,
|
|
363
|
+
message: `Human feedback answered (${request.id}) by ${responseOperator}: ${responseText}`,
|
|
364
|
+
},
|
|
365
|
+
coordinationNotice: {
|
|
366
|
+
event: "human_feedback_answered",
|
|
367
|
+
waves: [waveNumber],
|
|
368
|
+
status: "resolved",
|
|
369
|
+
details: `request_id=${request.id}; agent=${request.agentId}; operator=${responseOperator}; response=${responseText}`,
|
|
370
|
+
actionRequested: "None",
|
|
371
|
+
},
|
|
372
|
+
consoleLines: [],
|
|
373
|
+
coordinationUpdates: [
|
|
374
|
+
escalationRecord,
|
|
375
|
+
{
|
|
376
|
+
id: request.id,
|
|
377
|
+
lane,
|
|
378
|
+
wave: waveNumber,
|
|
379
|
+
agentId,
|
|
380
|
+
kind: "human-feedback",
|
|
381
|
+
targets: request.agentId ? [`agent:${request.agentId}`] : [],
|
|
382
|
+
priority: "high",
|
|
383
|
+
summary: question,
|
|
384
|
+
detail: responseText,
|
|
385
|
+
status: "resolved",
|
|
386
|
+
source: "feedback",
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
triageUpdates: [escalationRecord],
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { readStatusCodeIfPresent } from "./dashboard-state.mjs";
|
|
2
|
+
import { resolveRetryOverrideRuns } from "./retry-control.mjs";
|
|
3
|
+
import {
|
|
4
|
+
applyPersistedRelaunchPlan,
|
|
5
|
+
persistedRelaunchPlanMatchesCurrentState,
|
|
6
|
+
resolveRelaunchRuns,
|
|
7
|
+
selectInitialWaveRuns,
|
|
8
|
+
selectReusablePreCompletedAgentIds,
|
|
9
|
+
} from "./retry-engine.mjs";
|
|
10
|
+
|
|
11
|
+
export function planInitialWaveAttempt({
|
|
12
|
+
agentRuns,
|
|
13
|
+
lanePaths,
|
|
14
|
+
wave,
|
|
15
|
+
derivedState,
|
|
16
|
+
proofRegistry,
|
|
17
|
+
retryOverride,
|
|
18
|
+
persistedRelaunchPlan,
|
|
19
|
+
}) {
|
|
20
|
+
const preCompletedAgentIds = selectReusablePreCompletedAgentIds(agentRuns, lanePaths, {
|
|
21
|
+
retryOverride,
|
|
22
|
+
wave,
|
|
23
|
+
derivedState,
|
|
24
|
+
proofRegistry,
|
|
25
|
+
});
|
|
26
|
+
const staleCompletedAgentIds = agentRuns
|
|
27
|
+
.filter(
|
|
28
|
+
(run) =>
|
|
29
|
+
!preCompletedAgentIds.has(run.agent.agentId) &&
|
|
30
|
+
readStatusCodeIfPresent(run.statusPath) === 0,
|
|
31
|
+
)
|
|
32
|
+
.map((run) => run.agent.agentId);
|
|
33
|
+
|
|
34
|
+
const persistedPlanIsCurrent =
|
|
35
|
+
!persistedRelaunchPlan ||
|
|
36
|
+
persistedRelaunchPlanMatchesCurrentState(
|
|
37
|
+
agentRuns,
|
|
38
|
+
persistedRelaunchPlan,
|
|
39
|
+
lanePaths,
|
|
40
|
+
wave,
|
|
41
|
+
);
|
|
42
|
+
const effectivePersistedPlan = persistedPlanIsCurrent ? persistedRelaunchPlan : null;
|
|
43
|
+
const availableRuns = agentRuns.filter((run) => !preCompletedAgentIds.has(run.agent.agentId));
|
|
44
|
+
const persistedRuns = applyPersistedRelaunchPlan(
|
|
45
|
+
availableRuns,
|
|
46
|
+
effectivePersistedPlan,
|
|
47
|
+
lanePaths,
|
|
48
|
+
wave,
|
|
49
|
+
);
|
|
50
|
+
const overrideResolution = resolveRetryOverrideRuns(availableRuns, retryOverride, lanePaths, wave);
|
|
51
|
+
|
|
52
|
+
let selectedRuns = [];
|
|
53
|
+
let source = "initial";
|
|
54
|
+
if (overrideResolution.unknownAgentIds.length === 0 && overrideResolution.runs.length > 0) {
|
|
55
|
+
selectedRuns = overrideResolution.runs;
|
|
56
|
+
source = "override";
|
|
57
|
+
} else if (persistedRuns.length > 0) {
|
|
58
|
+
selectedRuns = persistedRuns;
|
|
59
|
+
source = "persisted-relaunch";
|
|
60
|
+
} else {
|
|
61
|
+
selectedRuns = selectInitialWaveRuns(availableRuns, lanePaths, wave);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
preCompletedAgentIds,
|
|
66
|
+
staleCompletedAgentIds,
|
|
67
|
+
availableRuns,
|
|
68
|
+
selectedRuns,
|
|
69
|
+
source,
|
|
70
|
+
overrideResolution,
|
|
71
|
+
persistedPlanIsCurrent,
|
|
72
|
+
shouldClearPersistedRelaunchPlan: Boolean(
|
|
73
|
+
persistedRelaunchPlan && !persistedPlanIsCurrent,
|
|
74
|
+
),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function planRetryWaveAttempt({
|
|
79
|
+
agentRuns,
|
|
80
|
+
failures,
|
|
81
|
+
derivedState,
|
|
82
|
+
lanePaths,
|
|
83
|
+
wave,
|
|
84
|
+
retryOverride,
|
|
85
|
+
waveState = null,
|
|
86
|
+
}) {
|
|
87
|
+
const relaunchResolution = resolveRelaunchRuns(
|
|
88
|
+
agentRuns,
|
|
89
|
+
failures,
|
|
90
|
+
derivedState,
|
|
91
|
+
lanePaths,
|
|
92
|
+
wave,
|
|
93
|
+
waveState ? { waveState } : {},
|
|
94
|
+
);
|
|
95
|
+
const overrideResolution = resolveRetryOverrideRuns(
|
|
96
|
+
agentRuns,
|
|
97
|
+
retryOverride,
|
|
98
|
+
lanePaths,
|
|
99
|
+
wave,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
let selectedRuns = [];
|
|
103
|
+
let source = "retry";
|
|
104
|
+
let barrier = relaunchResolution.barrier || null;
|
|
105
|
+
if (overrideResolution.unknownAgentIds.length === 0 && overrideResolution.runs.length > 0) {
|
|
106
|
+
selectedRuns = overrideResolution.runs;
|
|
107
|
+
barrier = null;
|
|
108
|
+
source = "override";
|
|
109
|
+
} else if (!barrier) {
|
|
110
|
+
selectedRuns = relaunchResolution.runs;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
selectedRuns,
|
|
115
|
+
source,
|
|
116
|
+
barrier,
|
|
117
|
+
relaunchResolution,
|
|
118
|
+
overrideResolution,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -14,6 +14,7 @@ import { readStatusCodeIfPresent } from "./dashboard-state.mjs";
|
|
|
14
14
|
import { buildExecutorLaunchSpec } from "./executors.mjs";
|
|
15
15
|
import { hashAgentPromptFingerprint, prefetchContext7ForSelection } from "./context7.mjs";
|
|
16
16
|
import { killTmuxSessionIfExists } from "./terminals.mjs";
|
|
17
|
+
import { resolveWaveRoleBindings } from "./role-helpers.mjs";
|
|
17
18
|
import {
|
|
18
19
|
resolveAgentSkills,
|
|
19
20
|
summarizeResolvedSkills,
|
|
@@ -71,6 +72,7 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
|
|
|
71
72
|
inboxText,
|
|
72
73
|
promptOverride = "",
|
|
73
74
|
orchestratorId,
|
|
75
|
+
attempt = 1,
|
|
74
76
|
agentRateLimitRetries,
|
|
75
77
|
agentRateLimitBaseDelaySeconds,
|
|
76
78
|
agentRateLimitMaxDelaySeconds,
|
|
@@ -105,6 +107,7 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
|
|
|
105
107
|
const prompt =
|
|
106
108
|
String(promptOverride || "").trim() ||
|
|
107
109
|
buildExecutionPrompt({
|
|
110
|
+
...resolveWaveRoleBindings(resolvedWaveDefinition, lanePaths),
|
|
108
111
|
lane: lanePaths.lane,
|
|
109
112
|
wave,
|
|
110
113
|
agent,
|
|
@@ -120,10 +123,6 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
|
|
|
120
123
|
evalTargets: resolvedWaveDefinition.evalTargets,
|
|
121
124
|
benchmarkCatalogPath: lanePaths.laneProfile?.paths?.benchmarkCatalogPath,
|
|
122
125
|
sharedPlanDocs: lanePaths.sharedPlanDocs,
|
|
123
|
-
contQaAgentId: lanePaths.contQaAgentId,
|
|
124
|
-
contEvalAgentId: lanePaths.contEvalAgentId,
|
|
125
|
-
integrationAgentId: lanePaths.integrationAgentId,
|
|
126
|
-
documentationAgentId: lanePaths.documentationAgentId,
|
|
127
126
|
});
|
|
128
127
|
const promptHash = hashAgentPromptFingerprint(agent);
|
|
129
128
|
fs.writeFileSync(promptPath, `${prompt}\n`, "utf8");
|
|
@@ -216,8 +215,8 @@ export async function launchAgentSession(lanePaths, params, { runTmuxFn }) {
|
|
|
216
215
|
`export WAVE_EXECUTOR_MODE=${shellQuote(resolvedExecutorMode)}`,
|
|
217
216
|
...executionLines,
|
|
218
217
|
`node -e ${shellQuote(
|
|
219
|
-
"const fs=require('node:fs'); const statusPath=process.argv[1]; const payload={code:Number(process.argv[2]),promptHash:process.argv[3]||null,orchestratorId:process.argv[4]||null,completedAt:new Date().toISOString()}; fs.writeFileSync(statusPath, JSON.stringify(payload, null, 2)+'\\n', 'utf8');",
|
|
220
|
-
)} ${shellQuote(statusPath)} "$status" ${shellQuote(promptHash)} ${shellQuote(orchestratorId || "")}`,
|
|
218
|
+
"const fs=require('node:fs'); const statusPath=process.argv[1]; const payload={code:Number(process.argv[2]),promptHash:process.argv[3]||null,orchestratorId:process.argv[4]||null,attempt:Number(process.argv[5])||1,completedAt:new Date().toISOString()}; fs.writeFileSync(statusPath, JSON.stringify(payload, null, 2)+'\\n', 'utf8');",
|
|
219
|
+
)} ${shellQuote(statusPath)} "$status" ${shellQuote(promptHash)} ${shellQuote(orchestratorId || "")} ${shellQuote(String(attempt || 1))}`,
|
|
221
220
|
`echo "[${lanePaths.lane}-wave-launcher] ${sessionName} finished with code $status"`,
|
|
222
221
|
"exec bash -l",
|
|
223
222
|
].join("\n");
|