@yeaft/webchat-agent 1.0.360 → 1.0.362
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/package.json +1 -1
- package/yeaft/engine.js +6 -2
- package/yeaft/web-bridge.js +11 -10
- package/yeaft/work-center/controller.js +5 -1
- package/yeaft/work-center/coordinator.js +8 -4
- package/yeaft/work-center/plan-mutation.js +68 -13
- package/yeaft/work-center/runner.js +60 -10
- package/yeaft/work-center/workflow.js +44 -9
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":"1.0.
|
|
1
|
+
{"version":"1.0.362"}
|
package/package.json
CHANGED
package/yeaft/engine.js
CHANGED
|
@@ -2158,8 +2158,12 @@ export class Engine {
|
|
|
2158
2158
|
const executionOrigin = inboundEnvelope?.msg?.meta?.injectedBy === 'route_forward'
|
|
2159
2159
|
? 'route_forward'
|
|
2160
2160
|
: null;
|
|
2161
|
-
|
|
2162
|
-
|
|
2161
|
+
// The bridge-provided VP turn id is also persisted on assistant messages and
|
|
2162
|
+
// is therefore the identity the UI sends back when opening turn debug. Keep
|
|
2163
|
+
// the engine event/trace id identical; a second random id makes the trace
|
|
2164
|
+
// impossible to retrieve from a rendered assistant turn.
|
|
2165
|
+
const queryTurnId = vpTurnId || randomUUID();
|
|
2166
|
+
this.#currentQueryTurnId = queryTurnId;
|
|
2163
2167
|
// Bind the live query scope before tools can register async work. The
|
|
2164
2168
|
// constructor's Session id is only guaranteed for bridge-owned engines;
|
|
2165
2169
|
// standalone/CLI callers pass it per query.
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -1035,18 +1035,19 @@ async function waitForRoutePromises(msgId) {
|
|
|
1035
1035
|
}
|
|
1036
1036
|
|
|
1037
1037
|
/**
|
|
1038
|
-
* Drop the cached coordinator + router for a
|
|
1039
|
-
* in-flight
|
|
1040
|
-
*
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
1038
|
+
* Drop the cached coordinator + router for a Session. Metadata changes must
|
|
1039
|
+
* not abort in-flight work: a rename, announcement sync, or default-VP update
|
|
1040
|
+
* can race any provider request, and the running coordinator already owns a
|
|
1041
|
+
* consistent snapshot for that turn. Future turns rebuild from disk.
|
|
1042
|
+
*
|
|
1043
|
+
* Destructive lifecycle operations opt into runtime teardown.
|
|
1044
1044
|
*
|
|
1045
1045
|
* Idempotent — safe to call when no entry exists.
|
|
1046
1046
|
*/
|
|
1047
|
-
function invalidateGroupContext(sessionId) {
|
|
1047
|
+
function invalidateGroupContext(sessionId, { abortRuntime = false } = {}) {
|
|
1048
1048
|
if (!sessionId) return;
|
|
1049
1049
|
sessionContexts.delete(sessionId);
|
|
1050
|
+
if (!abortRuntime) return;
|
|
1050
1051
|
const prefix = `${sessionId}::`;
|
|
1051
1052
|
for (const [k, ctrl] of vpAborts) {
|
|
1052
1053
|
if (!k.startsWith(prefix)) continue;
|
|
@@ -1066,7 +1067,7 @@ function invalidateGroupContext(sessionId) {
|
|
|
1066
1067
|
if (k.startsWith(prefix)) vpThreads.delete(k);
|
|
1067
1068
|
}
|
|
1068
1069
|
// Reap per-(group,vp) TodoWrite snapshots for this group so a
|
|
1069
|
-
// deleted/
|
|
1070
|
+
// deleted/archived group doesn't pin a stale checklist forever.
|
|
1070
1071
|
for (const k of vpCurrentTodos.keys()) {
|
|
1071
1072
|
if (k.startsWith(prefix)) vpCurrentTodos.delete(k);
|
|
1072
1073
|
}
|
|
@@ -3438,7 +3439,7 @@ export function handleYeaftArchiveSession(msg) {
|
|
|
3438
3439
|
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
3439
3440
|
const result = archiveSession(yeaftDir, sessionId);
|
|
3440
3441
|
projectContextBySession.delete(sessionId);
|
|
3441
|
-
invalidateGroupContext(sessionId);
|
|
3442
|
+
invalidateGroupContext(sessionId, { abortRuntime: true });
|
|
3442
3443
|
sendSessionCrudResult({
|
|
3443
3444
|
op: 'archive',
|
|
3444
3445
|
requestId,
|
|
@@ -3478,7 +3479,7 @@ export function handleYeaftDeleteSession(msg) {
|
|
|
3478
3479
|
// turns for the deleted group. Engines for the deleted group are
|
|
3479
3480
|
// also dropped — unlike rename/announcement updates, the group is
|
|
3480
3481
|
// gone for good and there's nothing to preserve.
|
|
3481
|
-
invalidateGroupContext(sessionId);
|
|
3482
|
+
invalidateGroupContext(sessionId, { abortRuntime: true });
|
|
3482
3483
|
const prefix = `${sessionId}::`;
|
|
3483
3484
|
for (const k of Array.from(vpEngines.keys())) {
|
|
3484
3485
|
if (k.startsWith(prefix)) {
|
|
@@ -380,6 +380,10 @@ export class WorkflowController {
|
|
|
380
380
|
actions: this.store.getWorkItemDetail(activeWorkItem.id).actions,
|
|
381
381
|
proposal: result.planProposal,
|
|
382
382
|
availableVpIds: this.listAvailableVpIds?.(),
|
|
383
|
+
reviewAction: activeAction.type === 'review'
|
|
384
|
+
&& result.reviewDecision === 'changes_requested'
|
|
385
|
+
? activeAction
|
|
386
|
+
: null,
|
|
383
387
|
});
|
|
384
388
|
} catch (error) {
|
|
385
389
|
result.outcome = 'failed';
|
|
@@ -546,7 +550,7 @@ export class WorkflowController {
|
|
|
546
550
|
eventData: { reason: result.replanRequest.reason },
|
|
547
551
|
};
|
|
548
552
|
}
|
|
549
|
-
if (action.type === 'review' && result.reviewDecision === 'changes_requested') {
|
|
553
|
+
if (action.type === 'review' && result.reviewDecision === 'changes_requested' && !planProposal) {
|
|
550
554
|
const targetStage = plannedWorkItem.workflowSnapshot.stages
|
|
551
555
|
.find(stage => stage.id === action.changesRequestedStageId);
|
|
552
556
|
if (!targetStage) throw new Error('Work Center review return target is missing');
|
|
@@ -12,6 +12,7 @@ import { normalizeContractPatch } from './completion-contract.js';
|
|
|
12
12
|
import { applyCoordinatorReplan } from './plan-mutation.js';
|
|
13
13
|
import { buildWorkItemAttachmentContext } from './attachments.js';
|
|
14
14
|
import { sanitizeDiagnosticText } from './debug-projection.js';
|
|
15
|
+
import { generatedActionGraphRules } from './workflow.js';
|
|
15
16
|
|
|
16
17
|
const COORDINATOR_MAX_REPLY_CHARS = 8_000;
|
|
17
18
|
const COORDINATOR_MAX_INSTRUCTION_CHARS = 8_000;
|
|
@@ -181,8 +182,8 @@ Decision rules:
|
|
|
181
182
|
- answer: use for explanation or status questions. Do not include contractPatch, guidance, or actions.
|
|
182
183
|
- guide_actions: use only when the contract and graph stay valid. guidance must contain one or more {"stageId":"existing unfinished stage id","instruction":"specific next instruction"}. Do not include contractPatch or actions.
|
|
183
184
|
- replan: use when the WorkItem contract or unfinished topology changes. contractPatch may be null or contain title, goal, and/or acceptanceCriteria. actions must be the COMPLETE desired unfinished Action graph after this decision; omit completed Actions. Each Action requires id, name, type, objective, approach, expectedOutcome, capability, candidateVpIds, assignmentReason, dependsOnActionIds, workspaceMode, and may include separateFromActionTypes, changesRequestedActionId, maxAttempts. Dependencies may reference immutable completed stage ids or earlier Actions in this actions array.
|
|
185
|
+
- Replan graph contract: ${generatedActionGraphRules()}
|
|
184
186
|
- request_human: use only during automatic failure recovery, and only when no safe retry, guidance, or replan can be decided without human information. Set question to the exact information or decision required. Do not include contractPatch, guidance, or actions.
|
|
185
|
-
- Every replan must keep exactly one final acceptance gate: normally one deliver Action, or one terminal review when no delivery operation is required. It must be the unique graph sink and transitively depend on all other Actions.
|
|
186
187
|
- Action references are stage ids, never internal database Action ids.
|
|
187
188
|
- Stage ids in the snapshot may be bounded aliases. Echo them exactly; the runtime resolves them to durable identities.
|
|
188
189
|
- Never return destructive cancellation. Tell the user to use the explicit cancel control instead.`;
|
|
@@ -313,8 +314,8 @@ function permanentRecoveryDecision(error, language) {
|
|
|
313
314
|
function coordinatorDecisionError(error, language) {
|
|
314
315
|
const diagnostic = sanitizeDiagnosticText(error?.message || String(error || ''), 2_000);
|
|
315
316
|
const wrapped = new Error(coordinatorLanguage(language) === 'zh'
|
|
316
|
-
? 'Work Center Coordinator
|
|
317
|
-
: 'Work Center Coordinator
|
|
317
|
+
? 'Work Center Coordinator 生成的操作方案未通过校验。你的消息已经保留;重试会重新生成方案。'
|
|
318
|
+
: 'The Work Center Coordinator proposal did not pass validation. Your message was preserved; retry to generate a new proposal.');
|
|
318
319
|
wrapped.coordinatorClassified = true;
|
|
319
320
|
wrapped.coordinatorRetryable = false;
|
|
320
321
|
wrapped.coordinatorPhase = 'decision';
|
|
@@ -411,9 +412,12 @@ export function normalizeCoordinatorResponse(value, detail, options = {}) {
|
|
|
411
412
|
};
|
|
412
413
|
}
|
|
413
414
|
const contractPatch = normalizeContractPatch(source.contractPatch);
|
|
414
|
-
if (!Array.isArray(source.actions)
|
|
415
|
+
if (!Array.isArray(source.actions)) {
|
|
415
416
|
throw new Error('Work Center Coordinator replan requires the complete unfinished Action graph');
|
|
416
417
|
}
|
|
418
|
+
if (source.actions.length < 1 || source.actions.length > 8) {
|
|
419
|
+
throw new Error('Work Center Coordinator replan requires between 1 and 8 unfinished Actions');
|
|
420
|
+
}
|
|
417
421
|
return {
|
|
418
422
|
reply,
|
|
419
423
|
decision: {
|
|
@@ -42,6 +42,46 @@ function planActionFromStage(stage) {
|
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
+
function dependencyAncestors(stageId, stagesById) {
|
|
46
|
+
const ancestors = new Set();
|
|
47
|
+
const visit = id => {
|
|
48
|
+
if (ancestors.has(id)) return;
|
|
49
|
+
ancestors.add(id);
|
|
50
|
+
for (const dependencyId of stagesById.get(id)?.dependsOnStageIds || []) visit(dependencyId);
|
|
51
|
+
};
|
|
52
|
+
for (const dependencyId of stagesById.get(stageId)?.dependsOnStageIds || []) visit(dependencyId);
|
|
53
|
+
return ancestors;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validateReviewRemediationGate(workflowSnapshot, reviewAction, addedIds) {
|
|
57
|
+
if (reviewAction?.type !== 'review') return;
|
|
58
|
+
const stages = (workflowSnapshot.stages || []).filter(stage => stage.type !== 'triage');
|
|
59
|
+
const byId = new Map(stages.map(stage => [stage.id, stage]));
|
|
60
|
+
const freshReviews = stages.filter(stage => (
|
|
61
|
+
stage.type === 'review'
|
|
62
|
+
&& addedIds.has(stage.id)
|
|
63
|
+
&& addedIds.has(stage.changesRequestedStageId)
|
|
64
|
+
&& dependencyAncestors(stage.id, byId).has(reviewAction.stageId)
|
|
65
|
+
));
|
|
66
|
+
const gate = stages.find(stage => stage.type === 'deliver')
|
|
67
|
+
|| stages.find(stage => stage.type === 'review'
|
|
68
|
+
&& !stages.some(candidate => (candidate.dependsOnStageIds || []).includes(stage.id)));
|
|
69
|
+
const gateAncestors = gate ? dependencyAncestors(gate.id, byId) : new Set();
|
|
70
|
+
const gatedReviews = freshReviews.filter(review => (
|
|
71
|
+
gate?.id === review.id || gateAncestors.has(review.id)
|
|
72
|
+
));
|
|
73
|
+
const addedWork = stages.filter(stage => (
|
|
74
|
+
addedIds.has(stage.id) && stage.type !== 'review' && stage.type !== 'deliver'
|
|
75
|
+
));
|
|
76
|
+
const uncovered = addedWork.filter(stage => (
|
|
77
|
+
!gatedReviews.some(review => dependencyAncestors(review.id, byId).has(stage.id))
|
|
78
|
+
));
|
|
79
|
+
if (addedWork.length === 0 || gatedReviews.length === 0 || uncovered.length > 0) {
|
|
80
|
+
const suffix = uncovered.length > 0 ? `; uncovered Actions: ${uncovered.map(stage => stage.id).join(', ')}` : '';
|
|
81
|
+
throw new Error(`Work Center changes_requested additive plan requires remediation and a fresh review covering every added Action before delivery${suffix}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
45
85
|
function stableTopologicalActions(actions) {
|
|
46
86
|
const byId = new Map(actions.map((action, index) => [action.id, { action, index }]));
|
|
47
87
|
const incoming = new Map(actions.map(action => [action.id, 0]));
|
|
@@ -112,7 +152,9 @@ function validateDependencyPatches(actions, patches, addedIds) {
|
|
|
112
152
|
return normalized;
|
|
113
153
|
}
|
|
114
154
|
|
|
115
|
-
export function applyAdditivePlanProposal({
|
|
155
|
+
export function applyAdditivePlanProposal({
|
|
156
|
+
workItem, actions, proposal, availableVpIds = null, reviewAction = null,
|
|
157
|
+
}) {
|
|
116
158
|
if (workItem.workflowSnapshot?.executionMode !== 'graph') {
|
|
117
159
|
throw new Error('Work Center additive planning requires graph execution');
|
|
118
160
|
}
|
|
@@ -144,12 +186,14 @@ export function applyAdditivePlanProposal({ workItem, actions, proposal, availab
|
|
|
144
186
|
raw.dependsOnActionIds,
|
|
145
187
|
`Action "${id}" dependencies`,
|
|
146
188
|
),
|
|
147
|
-
|
|
148
|
-
?
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
189
|
+
...(hasReturnTarget
|
|
190
|
+
? {
|
|
191
|
+
changesRequestedActionId: canonicalExplicitActionId(
|
|
192
|
+
raw.changesRequestedActionId,
|
|
193
|
+
`Action "${id}" review target`,
|
|
194
|
+
),
|
|
195
|
+
}
|
|
196
|
+
: {}),
|
|
153
197
|
};
|
|
154
198
|
});
|
|
155
199
|
const dependencyPatches = validateDependencyPatches(actions, proposal.dependencyPatches, addedIds);
|
|
@@ -192,6 +236,7 @@ export function applyAdditivePlanProposal({ workItem, actions, proposal, availab
|
|
|
192
236
|
availableVpIds,
|
|
193
237
|
maxActions: MAX_WORK_ITEM_ACTIONS,
|
|
194
238
|
});
|
|
239
|
+
validateReviewRemediationGate(workflowSnapshot, reviewAction, addedIds);
|
|
195
240
|
const addedStages = workflowSnapshot.stages.filter(stage => addedIds.has(stage.id));
|
|
196
241
|
return {
|
|
197
242
|
proposalId,
|
|
@@ -253,9 +298,14 @@ export function applyCoordinatorReplan({ workItem, actions, proposal, availableV
|
|
|
253
298
|
...raw,
|
|
254
299
|
id,
|
|
255
300
|
dependsOnActionIds,
|
|
256
|
-
|
|
257
|
-
?
|
|
258
|
-
|
|
301
|
+
...(Object.hasOwn(raw, 'changesRequestedActionId')
|
|
302
|
+
? {
|
|
303
|
+
changesRequestedActionId: canonicalExplicitActionId(
|
|
304
|
+
raw.changesRequestedActionId,
|
|
305
|
+
`Coordinator Action "${id}" review target`,
|
|
306
|
+
),
|
|
307
|
+
}
|
|
308
|
+
: {}),
|
|
259
309
|
};
|
|
260
310
|
});
|
|
261
311
|
|
|
@@ -363,9 +413,14 @@ export function applyReplanMutation({ workItem, action, actions, proposal, avail
|
|
|
363
413
|
...raw,
|
|
364
414
|
id,
|
|
365
415
|
dependsOnActionIds: canonicalExplicitActionIds(raw.dependsOnActionIds, `Action "${id}" dependencies`),
|
|
366
|
-
|
|
367
|
-
?
|
|
368
|
-
|
|
416
|
+
...(Object.hasOwn(raw, 'changesRequestedActionId')
|
|
417
|
+
? {
|
|
418
|
+
changesRequestedActionId: canonicalExplicitActionId(
|
|
419
|
+
raw.changesRequestedActionId,
|
|
420
|
+
`Action "${id}" review target`,
|
|
421
|
+
),
|
|
422
|
+
}
|
|
423
|
+
: {}),
|
|
369
424
|
};
|
|
370
425
|
};
|
|
371
426
|
const retainedInputs = retained.map(entry => canonicalFuture(entry.input, entry.action.stageId));
|
|
@@ -413,14 +413,32 @@ export function createSubmitWorkItemPlanTool({
|
|
|
413
413
|
});
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
-
function terminalPlanningFields() {
|
|
416
|
+
function terminalPlanningFields(options = {}) {
|
|
417
417
|
return {
|
|
418
418
|
summary: { type: 'string', minLength: 1, maxLength: 2_000 },
|
|
419
419
|
evidence: { type: 'array', minItems: 1, maxItems: 20, items: { type: 'string', minLength: 1, maxLength: 1_000 } },
|
|
420
420
|
acceptanceChecks: { type: 'array', items: { type: 'object', additionalProperties: false, required: ['criterion', 'status', 'evidence'], properties: { criterion: { type: 'string' }, status: { type: 'string', enum: ['passed', 'deferred', 'not_applicable'] }, evidence: { type: 'string', minLength: 1, maxLength: 1_000 } } } },
|
|
421
|
+
...(options.review === true ? {
|
|
422
|
+
reviewDecision: { type: 'string', const: 'changes_requested' },
|
|
423
|
+
} : {}),
|
|
421
424
|
};
|
|
422
425
|
}
|
|
423
426
|
|
|
427
|
+
function reviewPlanningRequirements(action) {
|
|
428
|
+
return action?.type === 'review' ? ['reviewDecision'] : [];
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function assertReviewPlanningDecision(input, action) {
|
|
432
|
+
if (action?.type !== 'review') return;
|
|
433
|
+
if (input?.reviewDecision !== 'changes_requested') {
|
|
434
|
+
throw new Error('Review planning controls require reviewDecision "changes_requested"');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function reviewPlanningResult(input, action) {
|
|
439
|
+
return action?.type === 'review' ? { reviewDecision: input.reviewDecision } : {};
|
|
440
|
+
}
|
|
441
|
+
|
|
424
442
|
function plannedActionSchema(vpIds, { requireCandidates = true } = {}) {
|
|
425
443
|
const required = ['id', 'name', 'type', 'objective', 'approach', 'expectedOutcome', 'dependsOnActionIds', 'workspaceMode'];
|
|
426
444
|
if (requireCandidates) required.push('candidateVpIds', 'assignmentReason');
|
|
@@ -445,11 +463,15 @@ export function createProposeWorkItemActionsTool({
|
|
|
445
463
|
: '';
|
|
446
464
|
return defineTool({
|
|
447
465
|
name: 'ProposeWorkItemActions',
|
|
448
|
-
description: `Propose an additive change to the current WorkItem DAG. It is applied only if this Action completes and its Run lease plus basePlanRevision remain valid. Use stable stageId values in dependsOnActionIds, changesRequestedActionId, and dependencyPatches[].addDependsOnActionIds. The only internal id field is dependencyPatches[].actionId, which must use the displayed internalActionId of an eligible ready attempt=0 target.${currentIdentity} Existing Actions: ${existing.map(action => `stageId=${action.stageId} (internalActionId=${action.id}, ${action.status}, attempt ${action.attempt})`).join('; ')}. Available VPs: ${vpCatalog.map(vp => `${vp.id} (${vp.role || vp.area || 'VP'})`).join('; ')}. Only add new Actions and optionally add dependencies to attempt=0 ready Actions. This tool validates the complete additive DAG immediately; if validation fails, correct the proposal in the same turn.`,
|
|
466
|
+
description: `Propose an additive change to the current WorkItem DAG. It is applied only if this Action completes and its Run lease plus basePlanRevision remain valid. Use stable stageId values in dependsOnActionIds, changesRequestedActionId, and dependencyPatches[].addDependsOnActionIds. The only internal id field is dependencyPatches[].actionId, which must use the displayed internalActionId of an eligible ready attempt=0 target.${currentIdentity} Existing Actions: ${existing.map(action => `stageId=${action.stageId} (internalActionId=${action.id}, ${action.status}, attempt ${action.attempt})`).join('; ')}. Available VPs: ${vpCatalog.map(vp => `${vp.id} (${vp.role || vp.area || 'VP'})`).join('; ')}. Only add new Actions and optionally add dependencies to attempt=0 ready Actions. A Review submitting changes_requested must add remediation followed by a fresh Review and make delivery depend on that fresh approval gate; otherwise request a replan. This tool validates the complete additive DAG immediately; if validation fails, correct the proposal in the same turn.`,
|
|
449
467
|
parameters: { type: 'object', additionalProperties: false,
|
|
450
|
-
required: [
|
|
468
|
+
required: [
|
|
469
|
+
'summary', 'evidence', 'acceptanceChecks', 'proposalId', 'basePlanRevision', 'actions',
|
|
470
|
+
...reviewPlanningRequirements(currentAction),
|
|
471
|
+
],
|
|
451
472
|
properties: {
|
|
452
|
-
...terminalPlanningFields(
|
|
473
|
+
...terminalPlanningFields({ review: currentAction?.type === 'review' }),
|
|
474
|
+
proposalId: { type: 'string', minLength: 1, maxLength: 128 },
|
|
453
475
|
basePlanRevision: { type: 'integer', const: workItem.planRevision },
|
|
454
476
|
actions: { type: 'array', minItems: 1, maxItems: 8, items: plannedActionSchema(vpIds) },
|
|
455
477
|
dependencyPatches: { type: 'array', maxItems: 8, items: { type: 'object', additionalProperties: false, required: ['actionId', 'addDependsOnActionIds'], properties: { actionId: { type: 'string', enum: existing.filter(action => action.status === 'ready' && action.attempt === 0).map(action => action.id) }, addDependsOnActionIds: { type: 'array', minItems: 1, uniqueItems: true, items: { type: 'string' } } } } },
|
|
@@ -457,14 +479,22 @@ export function createProposeWorkItemActionsTool({
|
|
|
457
479
|
async execute(input, ctx = {}) {
|
|
458
480
|
if (!isRunActive()) throw new Error('Work Center Run is no longer active');
|
|
459
481
|
if (collector.value) throw new Error('A WorkItem plan mutation was already submitted for this Run');
|
|
482
|
+
assertReviewPlanningDecision(input, currentAction);
|
|
460
483
|
applyAdditivePlanProposal({
|
|
461
484
|
workItem,
|
|
462
485
|
actions,
|
|
463
486
|
proposal: input,
|
|
464
487
|
availableVpIds: vpIds,
|
|
488
|
+
reviewAction: currentAction,
|
|
465
489
|
});
|
|
466
490
|
if (!isRunActive()) throw new Error('Work Center Run is no longer active');
|
|
467
|
-
collector.value = {
|
|
491
|
+
collector.value = {
|
|
492
|
+
kind: 'expand',
|
|
493
|
+
input: {
|
|
494
|
+
...structuredClone(input),
|
|
495
|
+
...reviewPlanningResult(input, currentAction),
|
|
496
|
+
},
|
|
497
|
+
};
|
|
468
498
|
ctx.requestEndTurn?.({ kind: 'work_item_actions_proposed', proposalId: input.proposalId });
|
|
469
499
|
return JSON.stringify({ submitted: true, proposalId: input.proposalId, actionCount: input.actions.length });
|
|
470
500
|
},
|
|
@@ -472,21 +502,34 @@ export function createProposeWorkItemActionsTool({
|
|
|
472
502
|
});
|
|
473
503
|
}
|
|
474
504
|
|
|
475
|
-
export function createRequestWorkItemReplanTool({
|
|
505
|
+
export function createRequestWorkItemReplanTool({
|
|
506
|
+
workItem, collector, isRunActive, currentAction = null,
|
|
507
|
+
}) {
|
|
476
508
|
return defineTool({
|
|
477
509
|
name: 'RequestWorkItemReplan',
|
|
478
510
|
description: 'Request an explicit replan barrier when additive Actions are insufficient because the contract or existing future topology must change. The current Action must still complete. Work Center will preserve completed history, fence sibling Runs, supersede only unfinished Actions, and insert a new triage/replan Action. Active integration finalization prevents the barrier.',
|
|
479
511
|
parameters: { type: 'object', additionalProperties: false,
|
|
480
|
-
required: [
|
|
512
|
+
required: [
|
|
513
|
+
'summary', 'evidence', 'acceptanceChecks', 'proposalId', 'basePlanRevision', 'reason',
|
|
514
|
+
...reviewPlanningRequirements(currentAction),
|
|
515
|
+
],
|
|
481
516
|
properties: {
|
|
482
|
-
...terminalPlanningFields(
|
|
517
|
+
...terminalPlanningFields({ review: currentAction?.type === 'review' }),
|
|
518
|
+
proposalId: { type: 'string', minLength: 1, maxLength: 128 },
|
|
483
519
|
basePlanRevision: { type: 'integer', const: workItem.planRevision },
|
|
484
520
|
reason: { type: 'string', minLength: 1, maxLength: 4_000 },
|
|
485
521
|
} },
|
|
486
522
|
async execute(input, ctx = {}) {
|
|
487
523
|
if (!isRunActive()) throw new Error('Work Center Run is no longer active');
|
|
488
524
|
if (collector.value) throw new Error('A WorkItem plan mutation was already submitted for this Run');
|
|
489
|
-
|
|
525
|
+
assertReviewPlanningDecision(input, currentAction);
|
|
526
|
+
collector.value = {
|
|
527
|
+
kind: 'replan',
|
|
528
|
+
input: {
|
|
529
|
+
...structuredClone(input),
|
|
530
|
+
...reviewPlanningResult(input, currentAction),
|
|
531
|
+
},
|
|
532
|
+
};
|
|
490
533
|
ctx.requestEndTurn?.({ kind: 'work_item_replan_requested', proposalId: input.proposalId });
|
|
491
534
|
return JSON.stringify({ submitted: true, proposalId: input.proposalId });
|
|
492
535
|
},
|
|
@@ -1082,7 +1125,12 @@ export class WorkItemRunner {
|
|
|
1082
1125
|
actions: this.store.getWorkItemDetail(workItem.id).actions,
|
|
1083
1126
|
collector: mutationCollector, isRunActive, currentAction: executionAction,
|
|
1084
1127
|
}));
|
|
1085
|
-
runTools.push(createRequestWorkItemReplanTool({
|
|
1128
|
+
runTools.push(createRequestWorkItemReplanTool({
|
|
1129
|
+
workItem,
|
|
1130
|
+
collector: mutationCollector,
|
|
1131
|
+
isRunActive,
|
|
1132
|
+
currentAction: executionAction,
|
|
1133
|
+
}));
|
|
1086
1134
|
}
|
|
1087
1135
|
const runToolNames = runTools.map(tool => tool.name);
|
|
1088
1136
|
const toolPolicySnapshot = workItemToolPolicySnapshot(
|
|
@@ -1389,6 +1437,7 @@ export class WorkItemRunner {
|
|
|
1389
1437
|
} : submittedExpansion ? {
|
|
1390
1438
|
outcome: 'completed', summary: submittedExpansion.summary,
|
|
1391
1439
|
evidence: submittedExpansion.evidence, acceptanceChecks: submittedExpansion.acceptanceChecks,
|
|
1440
|
+
...reviewPlanningResult(submittedExpansion, executionAction),
|
|
1392
1441
|
planProposal: {
|
|
1393
1442
|
proposalId: submittedExpansion.proposalId,
|
|
1394
1443
|
basePlanRevision: submittedExpansion.basePlanRevision,
|
|
@@ -1398,6 +1447,7 @@ export class WorkItemRunner {
|
|
|
1398
1447
|
} : submittedReplan ? {
|
|
1399
1448
|
outcome: 'completed', summary: submittedReplan.summary,
|
|
1400
1449
|
evidence: submittedReplan.evidence, acceptanceChecks: submittedReplan.acceptanceChecks,
|
|
1450
|
+
...reviewPlanningResult(submittedReplan, executionAction),
|
|
1401
1451
|
replanRequest: {
|
|
1402
1452
|
proposalId: submittedReplan.proposalId,
|
|
1403
1453
|
basePlanRevision: submittedReplan.basePlanRevision,
|
|
@@ -362,6 +362,23 @@ export function listWorkItemTypeTemplates(settings) {
|
|
|
362
362
|
}));
|
|
363
363
|
}
|
|
364
364
|
|
|
365
|
+
export const MAX_INITIAL_PLAN_ACTIONS = 8;
|
|
366
|
+
export const MAX_WORK_ITEM_ACTIONS = 64;
|
|
367
|
+
|
|
368
|
+
export function generatedActionGraphRules(options = {}) {
|
|
369
|
+
const maxActions = Math.min(
|
|
370
|
+
Math.max(Number(options.maxActions) || MAX_INITIAL_PLAN_ACTIONS, 1),
|
|
371
|
+
MAX_WORK_ITEM_ACTIONS,
|
|
372
|
+
);
|
|
373
|
+
const concurrency = Number.isInteger(Number(options.maxConcurrentActions))
|
|
374
|
+
? Math.min(Math.max(Number(options.maxConcurrentActions), 1), 12)
|
|
375
|
+
: null;
|
|
376
|
+
const concurrencyRule = concurrency
|
|
377
|
+
? ` The scheduler can run up to ${concurrency} Actions concurrently.`
|
|
378
|
+
: '';
|
|
379
|
+
return `Always submit the smallest reliable graph of 1 to ${maxActions} task-specific Actions; never omit Actions or copy template brief text. Every graph must end in exactly one final acceptance gate: normally one deliver Action, or one terminal review when no delivery operation is required. The final gate must be the unique graph sink and every other Action must be its transitive dependency, so final acceptance cannot run before required evidence.${concurrencyRule} Before submitting, compare each pair of Actions and add a dependency only when one consumes a concrete result or side effect of the other; ordering by narrative, phase name, or list position is not a dependency. Split independent analysis, verification, and repository changes into sibling Actions so the scheduler can use concurrency. Use workspaceMode read only for Actions guaranteed not to mutate files, Git state, services, or external systems; use isolated-write for independent Git changes, integrate for an integrate Action that combines isolated-write dependencies, and shared for serial side effects. An Action with type integrate must use workspaceMode integrate, and type integrate is only valid when combining isolated-write Actions. If any Action uses isolated-write, add exactly one Action with type integrate and workspaceMode integrate; it must depend directly on every isolated-write Action, and all later Actions must consume those writes through the integration Action. Non-Git or dirty workspaces are serialized automatically; do not fake parallelism by marking a mutating Action as read. Every review Action must depend directly or transitively on the editable non-review, non-deliver Action it reviews, so the scheduler cannot claim the Review before that result exists. Set changesRequestedActionId to a non-empty dependency ancestor Action id, or omit the property to use the nearest eligible dependency ancestor; never send null or an empty string. Every generated Action must state objective, approach, expectedOutcome, capability, dependencies, and workspaceMode. The objective, approach, and expectedOutcome must be specific to this WorkItem and that Action: describe the concrete work, the repository-aware execution method, and the verifiable result that will guide the executor. Generic Action-type boilerplate is invalid. Add only Actions required by this task. Do not copy a generic workflow.`;
|
|
380
|
+
}
|
|
381
|
+
|
|
365
382
|
export function resolvePlanningWorkflowSnapshot(settings, requestedWorkItemType = null) {
|
|
366
383
|
const normalized = normalizeWorkCenterSettings(settings);
|
|
367
384
|
const requestedType = typeof requestedWorkItemType === 'string'
|
|
@@ -379,7 +396,9 @@ export function resolvePlanningWorkflowSnapshot(settings, requestedWorkItemType
|
|
|
379
396
|
const typeInstruction = requestedType
|
|
380
397
|
? `The user explicitly selected workItemType "${requestedType}". Keep that exact type.`
|
|
381
398
|
: 'Infer one specific workItemType from the contract.';
|
|
382
|
-
const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReference workflow catalog:\n${catalog || '(none)'}\nUse the catalog only to understand established task categories and sequencing patterns.
|
|
399
|
+
const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReference workflow catalog:\n${catalog || '(none)'}\nUse the catalog only to understand established task categories and sequencing patterns. ${generatedActionGraphRules({
|
|
400
|
+
maxConcurrentActions: normalized.maxConcurrentActions,
|
|
401
|
+
})}`;
|
|
383
402
|
return normalizeWorkflowDefinition({
|
|
384
403
|
id: 'ai-planned',
|
|
385
404
|
name: 'AI planned',
|
|
@@ -466,9 +485,6 @@ export function validateGeneratedCompletionGate(stages) {
|
|
|
466
485
|
}
|
|
467
486
|
}
|
|
468
487
|
|
|
469
|
-
export const MAX_INITIAL_PLAN_ACTIONS = 8;
|
|
470
|
-
export const MAX_WORK_ITEM_ACTIONS = 64;
|
|
471
|
-
|
|
472
488
|
export function applyGeneratedPlan(workItem, rawPlan, options = {}) {
|
|
473
489
|
const source = workflowFrom(workItem);
|
|
474
490
|
const forceGraph = options.forceGraph !== false;
|
|
@@ -582,23 +598,42 @@ export function applyGeneratedPlan(workItem, rawPlan, options = {}) {
|
|
|
582
598
|
}
|
|
583
599
|
return stage;
|
|
584
600
|
});
|
|
601
|
+
const generatedById = new Map(generated.map(stage => [stage.id, stage]));
|
|
602
|
+
const dependencyAncestors = stage => {
|
|
603
|
+
const ancestors = new Set();
|
|
604
|
+
const visit = stageId => {
|
|
605
|
+
if (ancestors.has(stageId)) return;
|
|
606
|
+
ancestors.add(stageId);
|
|
607
|
+
for (const dependencyId of generatedById.get(stageId)?.dependsOnStageIds || []) visit(dependencyId);
|
|
608
|
+
};
|
|
609
|
+
for (const dependencyId of stage.dependsOnStageIds) visit(dependencyId);
|
|
610
|
+
return ancestors;
|
|
611
|
+
};
|
|
585
612
|
for (const [index, stage] of generated.entries()) {
|
|
586
613
|
if (stage.type !== 'review') continue;
|
|
614
|
+
const ancestors = dependencyAncestors(stage);
|
|
587
615
|
const candidates = generated.slice(0, index)
|
|
588
|
-
.filter(candidate =>
|
|
616
|
+
.filter(candidate => (
|
|
617
|
+
candidate.type !== 'review'
|
|
618
|
+
&& candidate.type !== 'deliver'
|
|
619
|
+
&& ancestors.has(candidate.id)
|
|
620
|
+
));
|
|
589
621
|
if (Object.prototype.hasOwnProperty.call(stage, 'changesRequestedStageId')) {
|
|
590
|
-
const requested =
|
|
591
|
-
if (!requested) {
|
|
622
|
+
const requested = generatedById.get(stage.changesRequestedStageId);
|
|
623
|
+
if (!requested || requested.type === 'review' || requested.type === 'deliver') {
|
|
592
624
|
throw new Error(`AI-planned review Action "${stage.id}" points to an invalid return Action`);
|
|
593
625
|
}
|
|
626
|
+
if (!ancestors.has(requested.id)) {
|
|
627
|
+
throw new Error(`AI-planned review Action "${stage.id}" review target "${requested.id}" must be a dependency ancestor`);
|
|
628
|
+
}
|
|
594
629
|
stage.changesRequestedStageId = requested.id;
|
|
595
630
|
} else {
|
|
596
631
|
stage.changesRequestedStageId = candidates.at(-1)?.id || '';
|
|
597
632
|
}
|
|
598
633
|
if (!stage.changesRequestedStageId) {
|
|
599
|
-
throw new Error(`AI-planned review Action "${stage.id}" requires an
|
|
634
|
+
throw new Error(`AI-planned review Action "${stage.id}" requires an editable dependency ancestor`);
|
|
600
635
|
}
|
|
601
|
-
const returnTarget =
|
|
636
|
+
const returnTarget = generatedById.get(stage.changesRequestedStageId);
|
|
602
637
|
stage.assignmentPolicy = {
|
|
603
638
|
...stage.assignmentPolicy,
|
|
604
639
|
separateFromStageTypes: uniqueStrings([
|