@yeaft/webchat-agent 1.0.217 → 1.0.219
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/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +103 -90
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/yeaft/work-center/controller.js +65 -3
- package/yeaft/work-center/plan-mutation.js +120 -0
- package/yeaft/work-center/projection.js +107 -20
- package/yeaft/work-center/runner.js +83 -9
- package/yeaft/work-center/service.js +3 -6
- package/yeaft/work-center/store.js +136 -14
|
Binary file
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from './workflow.js';
|
|
9
9
|
import { renderSessionContextSnapshot } from './session-context.js';
|
|
10
10
|
import { normalizeEvidence } from './evidence.js';
|
|
11
|
-
import { applyAdditivePlanProposal } from './plan-mutation.js';
|
|
11
|
+
import { applyAdditivePlanProposal, applyReplanMutation } from './plan-mutation.js';
|
|
12
12
|
import { normalizeContractPatch, validateCompletedResult } from './completion-contract.js';
|
|
13
13
|
|
|
14
14
|
function normalizeTerminalResult(result, action) {
|
|
@@ -46,6 +46,8 @@ function normalizeTerminalResult(result, action) {
|
|
|
46
46
|
&& !Array.isArray(result.planProposal) ? result.planProposal : null,
|
|
47
47
|
replanRequest: result.replanRequest && typeof result.replanRequest === 'object'
|
|
48
48
|
&& !Array.isArray(result.replanRequest) ? result.replanRequest : null,
|
|
49
|
+
replanMutation: result.replanMutation && typeof result.replanMutation === 'object'
|
|
50
|
+
&& !Array.isArray(result.replanMutation) ? result.replanMutation : null,
|
|
49
51
|
};
|
|
50
52
|
if (normalized.outcome === 'waiting' && !normalized.waitingReason) {
|
|
51
53
|
throw new Error('waiting outcome requires waitingReason');
|
|
@@ -58,12 +60,14 @@ function normalizeTerminalResult(result, action) {
|
|
|
58
60
|
if (normalized.outcome !== 'completed') {
|
|
59
61
|
normalized.planProposal = null;
|
|
60
62
|
normalized.replanRequest = null;
|
|
63
|
+
normalized.replanMutation = null;
|
|
61
64
|
}
|
|
62
|
-
if (normalized.planProposal
|
|
65
|
+
if ([normalized.planProposal, normalized.replanRequest, normalized.replanMutation].filter(Boolean).length > 1) {
|
|
63
66
|
normalized.outcome = 'failed';
|
|
64
|
-
normalized.error = 'An Action cannot
|
|
67
|
+
normalized.error = 'An Action cannot submit more than one WorkItem plan mutation';
|
|
65
68
|
normalized.planProposal = null;
|
|
66
69
|
normalized.replanRequest = null;
|
|
70
|
+
normalized.replanMutation = null;
|
|
67
71
|
}
|
|
68
72
|
if (action.type === 'review' && normalized.outcome === 'completed' && !normalized.reviewDecision) {
|
|
69
73
|
normalized.outcome = 'failed';
|
|
@@ -323,10 +327,17 @@ export class WorkflowController {
|
|
|
323
327
|
throw new Error('Run has unconsumed Action input and cannot finish yet');
|
|
324
328
|
}
|
|
325
329
|
const result = normalizeTerminalResult(rawResult, activeAction);
|
|
330
|
+
if (result.outcome === 'completed'
|
|
331
|
+
&& activeAction.stageId?.startsWith('replan-')
|
|
332
|
+
&& !result.replanMutation) {
|
|
333
|
+
result.outcome = 'failed';
|
|
334
|
+
result.error = 'Work Center replan triage must submit SubmitWorkItemReplan';
|
|
335
|
+
}
|
|
326
336
|
validateCompletedResult(result, activeAction, activeWorkItem);
|
|
327
337
|
let validatedGeneratedWorkflow = null;
|
|
328
338
|
if (result.outcome === 'completed'
|
|
329
339
|
&& activeAction.type === 'triage'
|
|
340
|
+
&& !activeAction.stageId?.startsWith('replan-')
|
|
330
341
|
&& activeRun
|
|
331
342
|
&& this.store.getWorkItem(activeRun.workItemId)?.workflowSnapshot?.planningMode === 'ai') {
|
|
332
343
|
const current = this.store.getWorkItem(activeRun.workItemId);
|
|
@@ -364,6 +375,27 @@ export class WorkflowController {
|
|
|
364
375
|
result.error = error?.message || String(error);
|
|
365
376
|
}
|
|
366
377
|
}
|
|
378
|
+
let validatedReplanMutation = null;
|
|
379
|
+
let staleReplanMutation = null;
|
|
380
|
+
if (result.outcome === 'completed' && result.replanMutation) {
|
|
381
|
+
const currentWorkItem = this.store.getWorkItem(activeWorkItem.id);
|
|
382
|
+
if (Number(result.replanMutation.basePlanRevision) !== currentWorkItem.planRevision) {
|
|
383
|
+
staleReplanMutation = result.replanMutation;
|
|
384
|
+
} else {
|
|
385
|
+
try {
|
|
386
|
+
validatedReplanMutation = applyReplanMutation({
|
|
387
|
+
workItem: currentWorkItem,
|
|
388
|
+
action: activeAction,
|
|
389
|
+
actions: this.store.getWorkItemDetail(activeWorkItem.id).actions,
|
|
390
|
+
proposal: result.replanMutation,
|
|
391
|
+
availableVpIds: this.listAvailableVpIds?.(),
|
|
392
|
+
});
|
|
393
|
+
} catch (error) {
|
|
394
|
+
result.outcome = 'failed';
|
|
395
|
+
result.error = error?.message || String(error);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
367
399
|
if (result.outcome === 'completed' && result.replanRequest) {
|
|
368
400
|
const basePlanRevision = Number(result.replanRequest.basePlanRevision);
|
|
369
401
|
const proposalId = typeof result.replanRequest.proposalId === 'string'
|
|
@@ -451,6 +483,36 @@ export class WorkflowController {
|
|
|
451
483
|
: effectiveWorkItem;
|
|
452
484
|
const context = [...(action.context || []), contextEntry(action, result, activeRun)];
|
|
453
485
|
if (plannedWorkItem.workflowSnapshot?.executionMode === 'graph') {
|
|
486
|
+
if (staleReplanMutation) {
|
|
487
|
+
return {
|
|
488
|
+
actionStatus: 'completed', workItemStatus: 'needs_attention', graphAdvance: false,
|
|
489
|
+
keepCurrentAction: true,
|
|
490
|
+
planConflict: {
|
|
491
|
+
kind: 'plan_revision',
|
|
492
|
+
proposalId: staleReplanMutation.proposalId,
|
|
493
|
+
expectedPlanRevision: staleReplanMutation.basePlanRevision,
|
|
494
|
+
actualPlanRevision: workItem.planRevision,
|
|
495
|
+
},
|
|
496
|
+
eventType: 'workflow.plan_conflict',
|
|
497
|
+
eventData: { proposalId: staleReplanMutation.proposalId },
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (validatedReplanMutation) {
|
|
501
|
+
return {
|
|
502
|
+
actionStatus: 'completed', workItemStatus: 'ready', graphAdvance: true,
|
|
503
|
+
workflowSnapshot: validatedReplanMutation.workflowSnapshot,
|
|
504
|
+
expectedPlanRevision: validatedReplanMutation.basePlanRevision,
|
|
505
|
+
proposalId: validatedReplanMutation.proposalId,
|
|
506
|
+
replanMutation: validatedReplanMutation,
|
|
507
|
+
eventType: 'workflow.replanned',
|
|
508
|
+
eventData: {
|
|
509
|
+
retainedActionCount: validatedReplanMutation.retain.length,
|
|
510
|
+
replacedActionCount: validatedReplanMutation.replace.length,
|
|
511
|
+
removedActionCount: validatedReplanMutation.remove.length,
|
|
512
|
+
addedActionCount: validatedReplanMutation.add.length,
|
|
513
|
+
},
|
|
514
|
+
};
|
|
515
|
+
}
|
|
454
516
|
if (result.replanRequest) {
|
|
455
517
|
const replanStage = {
|
|
456
518
|
...plannedWorkItem.workflowSnapshot.stages[0],
|
|
@@ -12,6 +12,11 @@ function cleanProposalId(value) {
|
|
|
12
12
|
return id;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
function replanBarrierFrom(action) {
|
|
16
|
+
return (Array.isArray(action?.context) ? action.context : [])
|
|
17
|
+
.find(entry => entry?.type === 'replan-barrier') || null;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
function planActionFromStage(stage) {
|
|
16
21
|
return {
|
|
17
22
|
id: stage.id,
|
|
@@ -183,3 +188,118 @@ export function applyAdditivePlanProposal({ workItem, actions, proposal, availab
|
|
|
183
188
|
dependencyPatches,
|
|
184
189
|
};
|
|
185
190
|
}
|
|
191
|
+
|
|
192
|
+
export function applyReplanMutation({ workItem, action, actions, proposal, availableVpIds = null }) {
|
|
193
|
+
if (workItem.workflowSnapshot?.executionMode !== 'graph'
|
|
194
|
+
|| action?.type !== 'triage'
|
|
195
|
+
|| !action?.stageId?.startsWith('replan-')) {
|
|
196
|
+
throw new Error('Work Center replan mutation requires a replan triage Action');
|
|
197
|
+
}
|
|
198
|
+
if (!proposal || typeof proposal !== 'object' || Array.isArray(proposal)) {
|
|
199
|
+
throw new Error('Work Center replan mutation must be an object');
|
|
200
|
+
}
|
|
201
|
+
const proposalId = cleanProposalId(proposal.proposalId);
|
|
202
|
+
const basePlanRevision = Number(proposal.basePlanRevision);
|
|
203
|
+
if (!Number.isInteger(basePlanRevision) || basePlanRevision !== workItem.planRevision) {
|
|
204
|
+
throw new Error('Work Center replan mutation has a stale basePlanRevision');
|
|
205
|
+
}
|
|
206
|
+
const barrier = replanBarrierFrom(action);
|
|
207
|
+
if (!barrier || !Array.isArray(barrier.candidateActionIds)) {
|
|
208
|
+
throw new Error('Work Center replan Action is missing its frozen candidate set');
|
|
209
|
+
}
|
|
210
|
+
const candidateIds = barrier.candidateActionIds;
|
|
211
|
+
const actionById = new Map(actions.map(candidate => [candidate.id, candidate]));
|
|
212
|
+
const candidates = new Map(candidateIds.map(id => [id, actionById.get(id)]));
|
|
213
|
+
for (const [id, candidate] of candidates) {
|
|
214
|
+
if (!candidate || candidate.status !== 'superseded') {
|
|
215
|
+
throw new Error(`Work Center replan candidate is missing or no longer superseded: ${id}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const classified = new Set();
|
|
220
|
+
const classify = (actionId, kind) => {
|
|
221
|
+
const id = typeof actionId === 'string' ? actionId.trim() : '';
|
|
222
|
+
if (!candidates.has(id)) throw new Error(`Work Center replan ${kind} references a non-candidate Action: ${id || '(missing)'}`);
|
|
223
|
+
if (classified.has(id)) throw new Error(`Work Center replan candidate is classified more than once: ${id}`);
|
|
224
|
+
classified.add(id);
|
|
225
|
+
return candidates.get(id);
|
|
226
|
+
};
|
|
227
|
+
const retained = (Array.isArray(proposal.retain) ? proposal.retain : []).map(entry => ({
|
|
228
|
+
action: classify(entry?.actionId, 'retain'), input: entry?.action,
|
|
229
|
+
}));
|
|
230
|
+
const replaced = (Array.isArray(proposal.replace) ? proposal.replace : []).map(entry => ({
|
|
231
|
+
action: classify(entry?.actionId, 'replace'), input: entry?.action,
|
|
232
|
+
}));
|
|
233
|
+
const removed = (Array.isArray(proposal.remove) ? proposal.remove : []).map(id => classify(id, 'remove'));
|
|
234
|
+
const missing = candidateIds.filter(id => !classified.has(id));
|
|
235
|
+
if (missing.length > 0) throw new Error(`Work Center replan must classify every frozen candidate: ${missing.join(', ')}`);
|
|
236
|
+
|
|
237
|
+
const completed = actions.filter(candidate => candidate.status === 'completed' && candidate.type !== 'triage');
|
|
238
|
+
const currentStages = new Map((workItem.workflowSnapshot.stages || []).map(stage => [stage.id, stage]));
|
|
239
|
+
const historicalStageIds = new Set(actions.map(candidate => candidate.stageId));
|
|
240
|
+
const futureIds = new Set();
|
|
241
|
+
const canonicalFuture = (raw, expectedId = null) => {
|
|
242
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
243
|
+
throw new Error('Work Center replan classification requires a full Action specification');
|
|
244
|
+
}
|
|
245
|
+
const id = canonicalActionId(raw.id);
|
|
246
|
+
if (!id || (expectedId && id !== expectedId)) {
|
|
247
|
+
throw new Error(`Work Center retained Action must keep stage identity: ${expectedId || '(missing)'}`);
|
|
248
|
+
}
|
|
249
|
+
if (futureIds.has(id)) throw new Error(`Work Center replan Action id is duplicated: ${id}`);
|
|
250
|
+
futureIds.add(id);
|
|
251
|
+
return {
|
|
252
|
+
...raw,
|
|
253
|
+
id,
|
|
254
|
+
dependsOnActionIds: canonicalExplicitActionIds(raw.dependsOnActionIds, `Action "${id}" dependencies`),
|
|
255
|
+
changesRequestedActionId: Object.hasOwn(raw, 'changesRequestedActionId')
|
|
256
|
+
? canonicalExplicitActionId(raw.changesRequestedActionId, `Action "${id}" review target`)
|
|
257
|
+
: undefined,
|
|
258
|
+
};
|
|
259
|
+
};
|
|
260
|
+
const retainedInputs = retained.map(entry => canonicalFuture(entry.input, entry.action.stageId));
|
|
261
|
+
const replacementInputs = replaced.map(entry => {
|
|
262
|
+
const input = canonicalFuture(entry.input);
|
|
263
|
+
if (historicalStageIds.has(input.id)) throw new Error(`Work Center replacement Action reuses historical stage identity: ${input.id}`);
|
|
264
|
+
return input;
|
|
265
|
+
});
|
|
266
|
+
const addedInputs = (Array.isArray(proposal.add) ? proposal.add : []).map(raw => {
|
|
267
|
+
const input = canonicalFuture(raw);
|
|
268
|
+
if (historicalStageIds.has(input.id)) throw new Error(`Work Center added Action reuses historical stage identity: ${input.id}`);
|
|
269
|
+
return input;
|
|
270
|
+
});
|
|
271
|
+
const completedInputs = completed.map(candidate => {
|
|
272
|
+
const stage = currentStages.get(candidate.stageId);
|
|
273
|
+
if (!stage) throw new Error(`Work Center completed Action is missing from the frozen workflow: ${candidate.stageId}`);
|
|
274
|
+
return planActionFromStage(stage);
|
|
275
|
+
});
|
|
276
|
+
const synthetic = {
|
|
277
|
+
...workItem,
|
|
278
|
+
workflowSnapshot: { ...workItem.workflowSnapshot, actionTemplates: [], stages: [workItem.workflowSnapshot.stages[0]] },
|
|
279
|
+
};
|
|
280
|
+
const workflowSnapshot = applyGeneratedPlan(synthetic, {
|
|
281
|
+
workItemType: workItem.workflowSnapshot.workItemType,
|
|
282
|
+
actions: stableTopologicalActions([...completedInputs, ...retainedInputs, ...replacementInputs, ...addedInputs]),
|
|
283
|
+
}, { availableVpIds });
|
|
284
|
+
const stageById = new Map(workflowSnapshot.stages.map(stage => [stage.id, stage]));
|
|
285
|
+
const context = (Array.isArray(action.context) ? action.context : [])
|
|
286
|
+
.filter(entry => entry?.type !== 'replan-barrier');
|
|
287
|
+
return {
|
|
288
|
+
proposalId,
|
|
289
|
+
basePlanRevision,
|
|
290
|
+
workflowSnapshot,
|
|
291
|
+
retain: retained.map(entry => ({
|
|
292
|
+
action: entry.action,
|
|
293
|
+
nextAction: actionForStage(stageById.get(entry.action.stageId), { ...workItem, workflowSnapshot }, context),
|
|
294
|
+
})),
|
|
295
|
+
replace: replaced.map((entry, index) => ({
|
|
296
|
+
action: entry.action,
|
|
297
|
+
nextAction: {
|
|
298
|
+
...actionForStage(stageById.get(replacementInputs[index].id), { ...workItem, workflowSnapshot }, context),
|
|
299
|
+
replacesActionId: entry.action.id,
|
|
300
|
+
},
|
|
301
|
+
})),
|
|
302
|
+
add: addedInputs.map(input => actionForStage(stageById.get(input.id), { ...workItem, workflowSnapshot }, context)),
|
|
303
|
+
remove: removed.map(candidate => candidate.id),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
@@ -148,6 +148,39 @@ function sumExecutionStats(values) {
|
|
|
148
148
|
}, emptyExecutionStats());
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
function actionGeneration(value) {
|
|
152
|
+
return Math.max(1, count(value) || 1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function runGeneration(run) {
|
|
156
|
+
return actionGeneration(run?.actionGeneration ?? run?.executionManifest?.actionGeneration);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function runSpecHash(run) {
|
|
160
|
+
return typeof run?.actionSpecHash === 'string' && run.actionSpecHash
|
|
161
|
+
? run.actionSpecHash
|
|
162
|
+
: typeof run?.executionManifest?.actionSpecHash === 'string'
|
|
163
|
+
? run.executionManifest.actionSpecHash
|
|
164
|
+
: '';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function threadRuns(action, runs) {
|
|
168
|
+
const source = (Array.isArray(runs) ? runs : []).filter(run => run?.actionId === action?.id);
|
|
169
|
+
const selectedSpecByGeneration = new Map((Array.isArray(action?.identityHistory) ? action.identityHistory : [])
|
|
170
|
+
.filter(identity => typeof identity?.specHash === 'string' && identity.specHash)
|
|
171
|
+
.map(identity => [actionGeneration(identity.generation), identity.specHash]));
|
|
172
|
+
const currentGeneration = actionGeneration(action?.generation);
|
|
173
|
+
if (typeof action?.specHash === 'string' && action.specHash) {
|
|
174
|
+
selectedSpecByGeneration.set(currentGeneration, action.specHash);
|
|
175
|
+
}
|
|
176
|
+
return source.filter(run => {
|
|
177
|
+
const generation = runGeneration(run);
|
|
178
|
+
const selectedSpec = selectedSpecByGeneration.get(generation) || '';
|
|
179
|
+
const spec = runSpecHash(run);
|
|
180
|
+
return selectedSpec ? spec === selectedSpec : generation === 1 && !spec;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
151
184
|
function normalizeProjectedMessage(message) {
|
|
152
185
|
if (!message || typeof message !== 'object') return null;
|
|
153
186
|
const text = typeof message.text === 'string'
|
|
@@ -167,13 +200,14 @@ function normalizeProjectedMessage(message) {
|
|
|
167
200
|
...(message.progressRevision == null ? {} : { progressRevision: count(message.progressRevision) }),
|
|
168
201
|
...(message.generation == null ? {} : { generation: Math.max(1, count(message.generation) || 1) }),
|
|
169
202
|
...(message.attempt == null ? {} : { attempt: Math.max(1, count(message.attempt) || 1) }),
|
|
203
|
+
...(message.runId == null ? {} : { runId: String(message.runId) }),
|
|
170
204
|
};
|
|
171
205
|
}
|
|
172
206
|
|
|
173
|
-
function actionInputMessages(action, events) {
|
|
207
|
+
function actionInputMessages(action, events, generation = actionGeneration(action?.generation), includeThreadIdentity = false) {
|
|
174
208
|
return (Array.isArray(events) ? events : [])
|
|
175
209
|
.filter(event => event?.actionId === action?.id
|
|
176
|
-
&&
|
|
210
|
+
&& actionGeneration(event.actionGeneration) === generation
|
|
177
211
|
&& ['action.guidance_added', 'action.input_added'].includes(event.type))
|
|
178
212
|
.map(event => normalizeProjectedMessage({
|
|
179
213
|
id: `event:${event.id}`,
|
|
@@ -183,11 +217,12 @@ function actionInputMessages(action, events) {
|
|
|
183
217
|
text: event.data?.text || event.data?.guidance || '',
|
|
184
218
|
attachments: event.data?.attachments,
|
|
185
219
|
createdAt: event.createdAt,
|
|
220
|
+
...(includeThreadIdentity ? { generation } : {}),
|
|
186
221
|
}))
|
|
187
222
|
.filter(Boolean);
|
|
188
223
|
}
|
|
189
224
|
|
|
190
|
-
function runResponseMessage(run) {
|
|
225
|
+
function runResponseMessage(run, includeThreadIdentity = false) {
|
|
191
226
|
return normalizeProjectedMessage({
|
|
192
227
|
id: `run:${run.id}`,
|
|
193
228
|
role: 'assistant',
|
|
@@ -197,15 +232,18 @@ function runResponseMessage(run) {
|
|
|
197
232
|
createdAt: count(run.startedAt),
|
|
198
233
|
updatedAt: count(run.endedAt || run.startedAt),
|
|
199
234
|
progressRevision: count(run.progressRevision),
|
|
200
|
-
|
|
201
|
-
|
|
235
|
+
...(includeThreadIdentity ? {
|
|
236
|
+
generation: runGeneration(run),
|
|
237
|
+
attempt: run.actionAttempt,
|
|
238
|
+
runId: run.id,
|
|
239
|
+
} : {}),
|
|
202
240
|
});
|
|
203
241
|
}
|
|
204
242
|
|
|
205
|
-
function loopOutputMessages(action, events, matchingRunIds) {
|
|
243
|
+
function loopOutputMessages(action, events, matchingRunIds, generation = actionGeneration(action?.generation), includeThreadIdentity = false) {
|
|
206
244
|
return (Array.isArray(events) ? events : [])
|
|
207
245
|
.filter(event => event?.actionId === action?.id
|
|
208
|
-
&&
|
|
246
|
+
&& actionGeneration(event.actionGeneration ?? event.data?.actionGeneration) === generation
|
|
209
247
|
&& matchingRunIds.has(event.runId)
|
|
210
248
|
&& event.type === 'run.loop_output')
|
|
211
249
|
.map(event => normalizeProjectedMessage({
|
|
@@ -215,33 +253,73 @@ function loopOutputMessages(action, events, matchingRunIds) {
|
|
|
215
253
|
status: 'completed',
|
|
216
254
|
text: event.data?.response || '',
|
|
217
255
|
createdAt: event.createdAt,
|
|
218
|
-
|
|
219
|
-
|
|
256
|
+
...(includeThreadIdentity ? {
|
|
257
|
+
generation: event.actionGeneration ?? event.data?.actionGeneration,
|
|
258
|
+
attempt: event.data?.actionAttempt,
|
|
259
|
+
runId: event.runId,
|
|
260
|
+
} : {}),
|
|
220
261
|
}))
|
|
221
262
|
.filter(Boolean);
|
|
222
263
|
}
|
|
223
264
|
|
|
224
|
-
function
|
|
225
|
-
const matchingRuns =
|
|
226
|
-
? runs.filter(run => run?.actionId === action?.id && runMatchesActionIdentity(run, action))
|
|
227
|
-
: [];
|
|
265
|
+
function messagesForGeneration(action, runs, events, generation, includeThreadIdentity = false) {
|
|
266
|
+
const matchingRuns = threadRuns(action, runs).filter(run => runGeneration(run) === generation);
|
|
228
267
|
const matchingRunIds = new Set(matchingRuns.map(run => run.id));
|
|
229
268
|
const runsWithLoopOutput = new Set((Array.isArray(events) ? events : [])
|
|
230
269
|
.filter(event => event?.actionId === action?.id
|
|
231
|
-
&&
|
|
270
|
+
&& actionGeneration(event.actionGeneration ?? event.data?.actionGeneration) === generation
|
|
232
271
|
&& matchingRunIds.has(event.runId)
|
|
233
272
|
&& event.type === 'run.loop_output')
|
|
234
273
|
.map(event => event.runId));
|
|
235
|
-
return [
|
|
274
|
+
return [
|
|
275
|
+
...actionInputMessages(action, events, generation, includeThreadIdentity),
|
|
276
|
+
...loopOutputMessages(action, events, matchingRunIds, generation, includeThreadIdentity),
|
|
277
|
+
...matchingRuns
|
|
236
278
|
.sort((left, right) => count(left.startedAt) - count(right.startedAt))
|
|
237
279
|
.filter(run => !runsWithLoopOutput.has(run.id))
|
|
238
|
-
.map(run => runResponseMessage(run))
|
|
280
|
+
.map(run => runResponseMessage(run, includeThreadIdentity))
|
|
239
281
|
.filter(Boolean)]
|
|
240
282
|
.sort((left, right) => left.createdAt - right.createdAt
|
|
241
283
|
|| (left.role === 'user' ? -1 : 1)
|
|
242
284
|
|| left.id.localeCompare(right.id));
|
|
243
285
|
}
|
|
244
286
|
|
|
287
|
+
function actionMessages(action, runs, events) {
|
|
288
|
+
return messagesForGeneration(action, runs, events, actionGeneration(action?.generation));
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function projectActionThread(action, runs, events) {
|
|
292
|
+
const allRuns = threadRuns(action, runs);
|
|
293
|
+
const generations = new Set([
|
|
294
|
+
actionGeneration(action?.generation),
|
|
295
|
+
...allRuns.map(runGeneration),
|
|
296
|
+
...(Array.isArray(events) ? events : [])
|
|
297
|
+
.filter(event => event?.actionId === action?.id
|
|
298
|
+
&& ['action.guidance_added', 'action.input_added', 'run.loop_output'].includes(event.type))
|
|
299
|
+
.map(event => actionGeneration(event.actionGeneration ?? event.data?.actionGeneration)),
|
|
300
|
+
]);
|
|
301
|
+
return [...generations].sort((left, right) => left - right).map(generation => {
|
|
302
|
+
const canonical = generation === actionGeneration(action?.generation);
|
|
303
|
+
return {
|
|
304
|
+
generation,
|
|
305
|
+
canonical,
|
|
306
|
+
messages: canonical ? [] : messagesForGeneration(action, allRuns, events, generation, true).slice(-MAX_ACTION_MESSAGES),
|
|
307
|
+
runs: allRuns.filter(run => runGeneration(run) === generation)
|
|
308
|
+
.sort((left, right) => count(left.startedAt) - count(right.startedAt) || String(left.id).localeCompare(String(right.id)))
|
|
309
|
+
.map(run => ({
|
|
310
|
+
id: run.id,
|
|
311
|
+
attempt: Math.max(1, count(run.actionAttempt) || 1),
|
|
312
|
+
status: run.status || 'running',
|
|
313
|
+
startedAt: count(run.startedAt),
|
|
314
|
+
endedAt: count(run.endedAt),
|
|
315
|
+
progressRevision: count(run.progressRevision),
|
|
316
|
+
loopCount: count(run.loopCount),
|
|
317
|
+
toolCount: count(run.toolCount),
|
|
318
|
+
})),
|
|
319
|
+
};
|
|
320
|
+
}).filter(entry => entry.messages.length > 0 || entry.runs.length > 0 || entry.canonical);
|
|
321
|
+
}
|
|
322
|
+
|
|
245
323
|
|
|
246
324
|
|
|
247
325
|
const MAX_FAILURE_REASON_LENGTH = 2_000;
|
|
@@ -457,6 +535,7 @@ function projectAction(action, runs, events, includeBody = true) {
|
|
|
457
535
|
response: execution.response,
|
|
458
536
|
failure: execution.failure,
|
|
459
537
|
messages: execution.messages,
|
|
538
|
+
thread: Array.isArray(runs) ? projectActionThread(action, runs, events) : (action.thread || []),
|
|
460
539
|
liveMessage: execution.liveMessage,
|
|
461
540
|
} : {}),
|
|
462
541
|
};
|
|
@@ -478,6 +557,7 @@ function stripActionBody(action, keepFailure = false) {
|
|
|
478
557
|
const projected = { ...action };
|
|
479
558
|
delete projected.response;
|
|
480
559
|
delete projected.messages;
|
|
560
|
+
delete projected.thread;
|
|
481
561
|
delete projected.liveMessage;
|
|
482
562
|
if (!keepFailure) delete projected.failure;
|
|
483
563
|
if (projected.brief) {
|
|
@@ -539,6 +619,7 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
|
|
|
539
619
|
if (keep) {
|
|
540
620
|
delete keep.response;
|
|
541
621
|
delete keep.messages;
|
|
622
|
+
delete keep.thread;
|
|
542
623
|
delete keep.liveMessage;
|
|
543
624
|
}
|
|
544
625
|
if (jsonByteLength(dto) <= MAX_WORK_ITEM_BROWSER_DTO_BYTES) return dto;
|
|
@@ -844,12 +925,18 @@ export function projectActionMessagePage(action, runs, events, options = {}) {
|
|
|
844
925
|
};
|
|
845
926
|
}
|
|
846
927
|
|
|
928
|
+
export function actionThreadIncludesRun(action, runs, runId) {
|
|
929
|
+
return threadRuns(action, runs).some(run => run.id === runId);
|
|
930
|
+
}
|
|
931
|
+
|
|
847
932
|
export function projectActionRequestIndex(action, entries) {
|
|
933
|
+
const source = Array.isArray(entries) ? entries : [];
|
|
934
|
+
const allowedRunIds = new Set(threadRuns(action, source.map(({ run }) => run)).map(run => run.id));
|
|
848
935
|
return {
|
|
849
936
|
actionId: action.id,
|
|
850
937
|
generation: Math.max(1, count(action.generation) || 1),
|
|
851
|
-
requests:
|
|
852
|
-
.filter(({ run }) =>
|
|
938
|
+
requests: source
|
|
939
|
+
.filter(({ run }) => allowedRunIds.has(run?.id))
|
|
853
940
|
.map(({ run, turn }) => ({
|
|
854
941
|
id: turn.turnId,
|
|
855
942
|
runId: run.id,
|
|
@@ -872,8 +959,8 @@ export function projectActionRequestIndex(action, entries) {
|
|
|
872
959
|
};
|
|
873
960
|
}
|
|
874
961
|
|
|
875
|
-
export function projectActionRequestDetail(action, run, history) {
|
|
876
|
-
if (!
|
|
962
|
+
export function projectActionRequestDetail(action, run, history, runs = [run]) {
|
|
963
|
+
if (!actionThreadIncludesRun(action, runs, run?.id)) return null;
|
|
877
964
|
const turn = Array.isArray(history?.turns) ? history.turns[0] : null;
|
|
878
965
|
if (!turn) return null;
|
|
879
966
|
const sourceLoops = Array.isArray(history?.loops) ? history.loops : [];
|
|
@@ -437,6 +437,52 @@ export function createRequestWorkItemReplanTool({ workItem, collector, isRunActi
|
|
|
437
437
|
});
|
|
438
438
|
}
|
|
439
439
|
|
|
440
|
+
export function createSubmitWorkItemReplanTool({ vps, workItem, action, actions, collector, isRunActive }) {
|
|
441
|
+
const vpCatalog = planningVpCatalog(vps);
|
|
442
|
+
const vpIds = vpCatalog.map(vp => vp.id);
|
|
443
|
+
const barrier = (Array.isArray(action.context) ? action.context : [])
|
|
444
|
+
.find(entry => entry?.type === 'replan-barrier');
|
|
445
|
+
const candidateIds = Array.isArray(barrier?.candidateActionIds) ? barrier.candidateActionIds : [];
|
|
446
|
+
const actionById = new Map(actions.map(candidate => [candidate.id, candidate]));
|
|
447
|
+
const candidateSummary = candidateIds.map(id => {
|
|
448
|
+
const candidate = actionById.get(id);
|
|
449
|
+
return `${id}/${candidate?.stageId || 'missing'} (${candidate?.type || 'unknown'})`;
|
|
450
|
+
}).join('; ');
|
|
451
|
+
const candidateIdSchema = candidateIds.length > 0
|
|
452
|
+
? { type: 'string', enum: candidateIds }
|
|
453
|
+
: { type: 'string' };
|
|
454
|
+
const candidateLimit = Math.min(8, candidateIds.length);
|
|
455
|
+
const classification = { type: 'object', additionalProperties: false,
|
|
456
|
+
required: ['actionId', 'action'], properties: {
|
|
457
|
+
actionId: candidateIdSchema,
|
|
458
|
+
action: plannedActionSchema(vpIds),
|
|
459
|
+
} };
|
|
460
|
+
return defineTool({
|
|
461
|
+
name: 'SubmitWorkItemReplan',
|
|
462
|
+
description: `Submit the complete replacement topology after a replan barrier. Classify every frozen candidate exactly once as retain, replace, or remove. Retain keeps its database Action identity and stage id but requires the complete updated specification. Replace creates a new Action linked to the old database Action. Add is only for new work. Frozen candidates: ${candidateSummary}. Available VPs: ${vpCatalog.map(vp => vp.id).join(', ')}.`,
|
|
463
|
+
parameters: { type: 'object', additionalProperties: false,
|
|
464
|
+
required: ['summary', 'evidence', 'acceptanceChecks', 'proposalId', 'basePlanRevision', 'retain', 'replace', 'remove', 'add'],
|
|
465
|
+
properties: {
|
|
466
|
+
...terminalPlanningFields(),
|
|
467
|
+
proposalId: { type: 'string', minLength: 1, maxLength: 128 },
|
|
468
|
+
basePlanRevision: { type: 'integer', const: workItem.planRevision },
|
|
469
|
+
retain: { type: 'array', maxItems: candidateLimit, items: classification },
|
|
470
|
+
replace: { type: 'array', maxItems: candidateLimit, items: classification },
|
|
471
|
+
remove: { type: 'array', maxItems: candidateLimit, uniqueItems: true, items: candidateIdSchema },
|
|
472
|
+
add: { type: 'array', maxItems: 8, items: plannedActionSchema(vpIds) },
|
|
473
|
+
} },
|
|
474
|
+
async execute(input, ctx = {}) {
|
|
475
|
+
if (!isRunActive()) throw new Error('Work Center Run is no longer active');
|
|
476
|
+
if (collector.value) throw new Error('A WorkItem plan was already submitted for this Run');
|
|
477
|
+
collector.value = structuredClone(input);
|
|
478
|
+
ctx.requestEndTurn?.({ kind: 'work_item_replan_submitted', proposalId: input.proposalId });
|
|
479
|
+
return JSON.stringify({ submitted: true, proposalId: input.proposalId });
|
|
480
|
+
},
|
|
481
|
+
isConcurrencySafe: () => false,
|
|
482
|
+
isReadOnly: () => false,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
440
486
|
export function createWorkItemToolRegistry({ workDir, attachmentFiles = [], isRunActive, mcpTools = [], runTools = [] }) {
|
|
441
487
|
const canonicalDir = canonicalWorkDir(path.resolve(workDir));
|
|
442
488
|
const canonicalAttachmentFiles = attachmentFiles.map(file => ({
|
|
@@ -504,11 +550,15 @@ function completionContract(action, workItem) {
|
|
|
504
550
|
const triageField = action.type === 'triage'
|
|
505
551
|
? ',\n "contractPatch": { "goal": "optional refined goal", "acceptanceCriteria": ["optional refined criterion"] }'
|
|
506
552
|
: '';
|
|
507
|
-
const planField = action.type === 'triage'
|
|
553
|
+
const planField = action.type === 'triage'
|
|
554
|
+
&& !action.stageId?.startsWith('replan-')
|
|
555
|
+
&& workItem?.workflowSnapshot?.planningMode === 'ai'
|
|
508
556
|
? ',\n "plan": { "workItemType": "specific-lowercase-slug", "actions": [{ "id": "stable-id", "name": "User-facing name", "type": "extensible-lowercase-slug (built-ins include research|design|diagnose|implement|migrate|test|review|document|operate|deliver|integrate|write|custom)", "capability": "specific executor capability", "objective": "task-specific concrete work this Action must do", "approach": "task-specific repository-aware method the executor must follow", "expectedOutcome": "task-specific verifiable result this Action must produce", "dependsOnActionIds": ["earlier Action id; [] means concurrent root"], "workspaceMode": "read|isolated-write|integrate|shared", "separateFromActionTypes": ["optional prior Action type"], "changesRequestedActionId": "for review: optional earlier editable Action id; omit to use nearest", "maxAttempts": 2 }] }'
|
|
509
557
|
: '';
|
|
510
|
-
const toolSubmission = action.type === 'triage' &&
|
|
511
|
-
? '\nSubmit the
|
|
558
|
+
const toolSubmission = action.type === 'triage' && action.stageId?.startsWith('replan-')
|
|
559
|
+
? '\nSubmit the replan only with SubmitWorkItemReplan. Classify every frozen candidate exactly once; do not emit terminal JSON after calling it.'
|
|
560
|
+
: action.type === 'triage' && workItem?.workflowSnapshot?.planningMode === 'ai'
|
|
561
|
+
? '\nSubmit the initial plan with SubmitWorkItemPlan. The legacy terminal JSON plan below exists only for compatibility; do not use it when the tool is available.'
|
|
512
562
|
: workItem?.workflowSnapshot?.executionMode === 'graph'
|
|
513
563
|
? '\nIf execution discovered strictly additive work, use ProposeWorkItemActions. If the contract or existing unfinished topology must change, use RequestWorkItemReplan. Both tools submit the completed Action and end the turn; do not emit terminal JSON after calling one.'
|
|
514
564
|
: '';
|
|
@@ -901,19 +951,29 @@ export class WorkItemRunner {
|
|
|
901
951
|
const mcpToolNames = workspaceRuntime.mcpTools.map(tool => tool.name);
|
|
902
952
|
const planCollector = { value: null };
|
|
903
953
|
const mutationCollector = { value: null };
|
|
954
|
+
const replanToolEnabled = executionAction.type === 'triage'
|
|
955
|
+
&& executionAction.stageId?.startsWith('replan-');
|
|
904
956
|
const planToolEnabled = executionAction.type === 'triage'
|
|
905
|
-
&& workItem?.workflowSnapshot?.planningMode === 'ai'
|
|
957
|
+
&& workItem?.workflowSnapshot?.planningMode === 'ai'
|
|
958
|
+
&& !replanToolEnabled;
|
|
906
959
|
const runTools = [];
|
|
907
960
|
if (planToolEnabled) runTools.push(createSubmitWorkItemPlanTool({
|
|
908
961
|
vps: this.registry.listVps(),
|
|
909
962
|
workItem,
|
|
910
963
|
collector: planCollector,
|
|
911
964
|
isRunActive,
|
|
912
|
-
reservedStageIds:
|
|
913
|
-
|
|
914
|
-
|
|
965
|
+
reservedStageIds: [],
|
|
966
|
+
}));
|
|
967
|
+
if (replanToolEnabled) runTools.push(createSubmitWorkItemReplanTool({
|
|
968
|
+
vps: this.registry.listVps(),
|
|
969
|
+
workItem,
|
|
970
|
+
action: executionAction,
|
|
971
|
+
actions: this.store.getWorkItemDetail(workItem.id).actions,
|
|
972
|
+
collector: planCollector,
|
|
973
|
+
isRunActive,
|
|
915
974
|
}));
|
|
916
|
-
if (!planToolEnabled &&
|
|
975
|
+
if (!planToolEnabled && !replanToolEnabled
|
|
976
|
+
&& workItem?.workflowSnapshot?.executionMode === 'graph') {
|
|
917
977
|
runTools.push(createProposeWorkItemActionsTool({
|
|
918
978
|
vps: this.registry.listVps(), workItem,
|
|
919
979
|
actions: this.store.getWorkItemDetail(workItem.id).actions,
|
|
@@ -1125,7 +1185,8 @@ export class WorkItemRunner {
|
|
|
1125
1185
|
}
|
|
1126
1186
|
const response = publicWorkItemResponse(text);
|
|
1127
1187
|
reportProgress(true);
|
|
1128
|
-
const submittedPlan = planCollector.value;
|
|
1188
|
+
const submittedPlan = !replanToolEnabled ? planCollector.value : null;
|
|
1189
|
+
const submittedReplanMutation = replanToolEnabled ? planCollector.value : null;
|
|
1129
1190
|
const submittedExpansion = mutationCollector.value?.kind === 'expand'
|
|
1130
1191
|
? mutationCollector.value.input : null;
|
|
1131
1192
|
const submittedReplan = mutationCollector.value?.kind === 'replan'
|
|
@@ -1137,6 +1198,19 @@ export class WorkItemRunner {
|
|
|
1137
1198
|
contractPatch: submittedPlan.contractPatch || null,
|
|
1138
1199
|
plan: { workItemType: submittedPlan.workItemType, actions: submittedPlan.actions },
|
|
1139
1200
|
acceptanceChecks: submittedPlan.acceptanceChecks,
|
|
1201
|
+
} : submittedReplanMutation ? {
|
|
1202
|
+
outcome: 'completed',
|
|
1203
|
+
summary: submittedReplanMutation.summary,
|
|
1204
|
+
evidence: submittedReplanMutation.evidence,
|
|
1205
|
+
acceptanceChecks: submittedReplanMutation.acceptanceChecks,
|
|
1206
|
+
replanMutation: {
|
|
1207
|
+
proposalId: submittedReplanMutation.proposalId,
|
|
1208
|
+
basePlanRevision: submittedReplanMutation.basePlanRevision,
|
|
1209
|
+
retain: submittedReplanMutation.retain,
|
|
1210
|
+
replace: submittedReplanMutation.replace,
|
|
1211
|
+
remove: submittedReplanMutation.remove,
|
|
1212
|
+
add: submittedReplanMutation.add,
|
|
1213
|
+
},
|
|
1140
1214
|
} : submittedExpansion ? {
|
|
1141
1215
|
outcome: 'completed', summary: submittedExpansion.summary,
|
|
1142
1216
|
evidence: submittedExpansion.evidence, acceptanceChecks: submittedExpansion.acceptanceChecks,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { runMatchesActionIdentity } from './action-identity.js';
|
|
5
4
|
import { WorkItemStore } from './store.js';
|
|
6
5
|
import { WorkflowController } from './controller.js';
|
|
7
6
|
import { WorkItemWatcher } from './watcher.js';
|
|
@@ -143,8 +142,7 @@ export class WorkCenterService {
|
|
|
143
142
|
const detail = this.#requiredItem(payload.id);
|
|
144
143
|
const action = this.#requiredAction(detail, payload.actionId);
|
|
145
144
|
const entries = [];
|
|
146
|
-
for (const run of detail.runs.filter(item => item.actionId === action.id
|
|
147
|
-
&& runMatchesActionIdentity(item, action))) {
|
|
145
|
+
for (const run of detail.runs.filter(item => item.actionId === action.id)) {
|
|
148
146
|
const history = await this.#debugHistory(run, { indexOnly: true });
|
|
149
147
|
for (const turn of Array.isArray(history?.turns) ? history.turns : []) {
|
|
150
148
|
entries.push({ run, turn });
|
|
@@ -156,11 +154,10 @@ export class WorkCenterService {
|
|
|
156
154
|
const detail = this.#requiredItem(payload.id);
|
|
157
155
|
const action = this.#requiredAction(detail, payload.actionId);
|
|
158
156
|
const requestId = requiredString(payload.requestId, 'requestId');
|
|
159
|
-
const run = detail.runs.find(item => item.actionId === action.id
|
|
160
|
-
&& item.id === payload.runId && runMatchesActionIdentity(item, action));
|
|
157
|
+
const run = detail.runs.find(item => item.actionId === action.id && item.id === payload.runId);
|
|
161
158
|
if (!run) throw new Error('Action request not found');
|
|
162
159
|
const history = await this.#debugHistory(run, { detailTurnId: requestId });
|
|
163
|
-
const projected = projectActionRequestDetail(action, run, history);
|
|
160
|
+
const projected = projectActionRequestDetail(action, run, history, detail.runs);
|
|
164
161
|
if (!projected) throw new Error('Action request detail is no longer available');
|
|
165
162
|
return projected;
|
|
166
163
|
}
|