@yeaft/webchat-agent 1.0.247 → 1.0.248
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 +59 -22
- 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/action-identity.js +23 -0
- package/yeaft/work-center/bridge.js +17 -2
- package/yeaft/work-center/completion-contract.js +25 -3
- package/yeaft/work-center/controller.js +52 -75
- package/yeaft/work-center/coordinator.js +323 -0
- package/yeaft/work-center/mainline-projection.js +86 -36
- package/yeaft/work-center/plan-mutation.js +99 -2
- package/yeaft/work-center/projection.js +23 -0
- package/yeaft/work-center/runner.js +1 -1
- package/yeaft/work-center/service.js +19 -5
- package/yeaft/work-center/store.js +1160 -147
- package/yeaft/work-center/workflow.js +75 -5
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
currentActionInputEventIds,
|
|
4
|
+
eventMatchesActionGeneration,
|
|
5
|
+
runMatchesActionIdentity,
|
|
6
|
+
} from './action-identity.js';
|
|
3
7
|
import { normalizeSessionContextSnapshot } from './session-context.js';
|
|
4
8
|
|
|
5
9
|
export const MAINLINE_CONTEXT_HARD_LIMIT_BYTES = 64 * 1024;
|
|
@@ -64,32 +68,93 @@ function clamp(value, minimum, maximum) {
|
|
|
64
68
|
return Math.min(maximum, Math.max(minimum, value));
|
|
65
69
|
}
|
|
66
70
|
|
|
67
|
-
function
|
|
68
|
-
return
|
|
69
|
-
|
|
71
|
+
function inputEventView(event) {
|
|
72
|
+
return {
|
|
73
|
+
eventId: event.id,
|
|
74
|
+
inputId: event.data?.inputId || null,
|
|
75
|
+
actionId: event.actionId || null,
|
|
76
|
+
text: event.data?.text || '',
|
|
77
|
+
attachments: Array.isArray(event.data?.attachments) ? event.data.attachments : [],
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function canonicalActionUserContext(events, action) {
|
|
82
|
+
const actionEvents = (Array.isArray(events) ? events : [])
|
|
83
|
+
.filter(event => event?.actionId === action?.id
|
|
84
|
+
&& ['action.guidance_added', 'action.input_added'].includes(event.type))
|
|
70
85
|
.slice()
|
|
71
|
-
.sort((left, right) => count(left.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
.sort((left, right) => count(left.id) - count(right.id));
|
|
87
|
+
const inputEvents = actionEvents.filter(event => event.type === 'action.input_added');
|
|
88
|
+
const validInputEventIds = currentActionInputEventIds(events, action);
|
|
89
|
+
const eventByInputId = new Map(inputEvents
|
|
90
|
+
.filter(event => event.data?.inputId)
|
|
91
|
+
.map(event => [event.data.inputId, event]));
|
|
92
|
+
const currentInputEvents = inputEvents.filter(event => validInputEventIds.has(String(event.id)));
|
|
93
|
+
const usedEventIds = new Set();
|
|
94
|
+
const contextEntries = (Array.isArray(action?.context) ? action.context : [])
|
|
95
|
+
.filter(entry => ['input', 'guidance', 'coordinator-guidance'].includes(entry?.type)
|
|
96
|
+
&& typeof entry.summary === 'string');
|
|
97
|
+
const values = contextEntries.flatMap((entry, index) => {
|
|
98
|
+
if (entry.type !== 'input') {
|
|
99
|
+
const event = actionEvents.find(candidate => !usedEventIds.has(candidate.id)
|
|
100
|
+
&& candidate.type === 'action.guidance_added'
|
|
101
|
+
&& (candidate.data?.guidance || '') === entry.summary) || null;
|
|
102
|
+
if (event) usedEventIds.add(event.id);
|
|
103
|
+
return [{
|
|
104
|
+
eventId: event?.id ?? null,
|
|
105
|
+
inputId: null,
|
|
106
|
+
actionId: action.id,
|
|
107
|
+
text: entry.summary,
|
|
108
|
+
attachments: Array.isArray(entry.attachments)
|
|
109
|
+
? entry.attachments
|
|
110
|
+
: Array.isArray(event?.data?.attachments) ? event.data.attachments : [],
|
|
111
|
+
}];
|
|
112
|
+
}
|
|
113
|
+
let event = entry.inputId ? eventByInputId.get(entry.inputId) : null;
|
|
114
|
+
if (!event && typeof entry.inputId === 'string' && entry.inputId.startsWith('legacy-event:')) {
|
|
115
|
+
const legacyEventId = Number(entry.inputId.slice('legacy-event:'.length));
|
|
116
|
+
event = inputEvents.find(candidate => candidate.id === legacyEventId) || null;
|
|
117
|
+
}
|
|
118
|
+
if (!event && !entry.inputId) {
|
|
119
|
+
event = currentInputEvents.find(candidate => !usedEventIds.has(candidate.id)
|
|
120
|
+
&& (candidate.data?.text || '') === entry.summary) || null;
|
|
121
|
+
}
|
|
122
|
+
if (!entry.inputId && !event) return [];
|
|
123
|
+
if (event) usedEventIds.add(event.id);
|
|
124
|
+
return [{
|
|
125
|
+
eventId: event?.id ?? null,
|
|
126
|
+
inputId: entry.inputId || event?.data?.inputId || `legacy-context:${index}`,
|
|
127
|
+
actionId: action.id,
|
|
128
|
+
text: entry.summary,
|
|
129
|
+
attachments: Array.isArray(entry.attachments)
|
|
130
|
+
? entry.attachments
|
|
131
|
+
: Array.isArray(event?.data?.attachments) ? event.data.attachments : [],
|
|
132
|
+
}];
|
|
133
|
+
});
|
|
134
|
+
return { values, usedEventIds, validInputEventIds };
|
|
78
135
|
}
|
|
79
136
|
|
|
80
137
|
function guidanceView(events, action) {
|
|
81
|
-
|
|
138
|
+
const allEvents = Array.isArray(events) ? events : [];
|
|
139
|
+
const canonicalEntries = canonicalActionUserContext(allEvents, action);
|
|
140
|
+
const currentEvents = allEvents
|
|
82
141
|
.filter(event => event?.actionId === action?.id
|
|
83
|
-
&&
|
|
84
|
-
|
|
142
|
+
&& ((event.type === 'action.input_added'
|
|
143
|
+
&& canonicalEntries.validInputEventIds.has(String(event.id)))
|
|
144
|
+
|| (event.type === 'action.guidance_added' && eventMatchesActionGeneration(event, action)))
|
|
145
|
+
&& !canonicalEntries.usedEventIds.has(event.id))
|
|
85
146
|
.slice()
|
|
86
147
|
.sort((left, right) => count(left.id) - count(right.id))
|
|
87
|
-
.map(event =>
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
148
|
+
.map(event => event.type === 'action.input_added'
|
|
149
|
+
? inputEventView(event)
|
|
150
|
+
: {
|
|
151
|
+
eventId: event.id,
|
|
152
|
+
inputId: null,
|
|
153
|
+
actionId: event.actionId || null,
|
|
154
|
+
text: event.data?.guidance || '',
|
|
155
|
+
attachments: Array.isArray(event.data?.attachments) ? event.data.attachments : [],
|
|
156
|
+
});
|
|
157
|
+
return [...canonicalEntries.values, ...currentEvents];
|
|
93
158
|
}
|
|
94
159
|
|
|
95
160
|
/**
|
|
@@ -259,21 +324,7 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
|
|
|
259
324
|
return false;
|
|
260
325
|
};
|
|
261
326
|
const sessionContext = normalizeSessionContextSnapshot(detail.sessionContext);
|
|
262
|
-
const workItemMessages = workItemMessageView(detail.messages);
|
|
263
327
|
const guidance = guidanceView(detail.events, action);
|
|
264
|
-
const newestFirstMessages = workItemMessages.slice().reverse();
|
|
265
|
-
for (const [index, message] of newestFirstMessages.entries()) {
|
|
266
|
-
const next = {
|
|
267
|
-
...snapshot.userContext,
|
|
268
|
-
workItemMessages: [message, ...snapshot.userContext.workItemMessages],
|
|
269
|
-
includedCount: snapshot.userContext.includedCount + 1,
|
|
270
|
-
omittedCount: 0,
|
|
271
|
-
};
|
|
272
|
-
const included = trySet('userContext', next);
|
|
273
|
-
if (index === 0 && !included) {
|
|
274
|
-
throw mainlineContextBlocked('Latest WorkItem message exceeds the Mainline prompt budget');
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
328
|
const otherUserEntries = [
|
|
278
329
|
...guidance.map(value => ({ kind: 'guidance', value })),
|
|
279
330
|
...sessionContext.map(value => ({ kind: 'sessionContext', value })),
|
|
@@ -287,8 +338,7 @@ export function buildMainlineContextSnapshot(detail, action, budgetInput = {}) {
|
|
|
287
338
|
};
|
|
288
339
|
trySet('userContext', next);
|
|
289
340
|
}
|
|
290
|
-
snapshot.userContext.omittedCount =
|
|
291
|
-
- snapshot.userContext.includedCount;
|
|
341
|
+
snapshot.userContext.omittedCount = otherUserEntries.length - snapshot.userContext.includedCount;
|
|
292
342
|
|
|
293
343
|
const siblingEntries = Object.entries(projection.canonicalActionResults)
|
|
294
344
|
.filter(([actionId]) => actionId !== action.id && !dependencies.some(item => item.actionId === actionId))
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
canonicalActionId,
|
|
5
5
|
canonicalExplicitActionId,
|
|
6
6
|
canonicalExplicitActionIds,
|
|
7
|
+
validateGeneratedCompletionGate,
|
|
7
8
|
} from './workflow.js';
|
|
8
9
|
|
|
9
10
|
function cleanProposalId(value) {
|
|
@@ -174,9 +175,15 @@ export function applyAdditivePlanProposal({ workItem, actions, proposal, availab
|
|
|
174
175
|
}),
|
|
175
176
|
...canonicalActions,
|
|
176
177
|
];
|
|
178
|
+
const orderedActions = stableTopologicalActions(mergedActions);
|
|
179
|
+
validateGeneratedCompletionGate(orderedActions.map(action => ({
|
|
180
|
+
id: action.id,
|
|
181
|
+
type: action.type,
|
|
182
|
+
dependsOnStageIds: action.dependsOnActionIds || [],
|
|
183
|
+
})));
|
|
177
184
|
const rawPlan = {
|
|
178
185
|
workItemType: workItem.workflowSnapshot.workItemType,
|
|
179
|
-
actions:
|
|
186
|
+
actions: orderedActions,
|
|
180
187
|
};
|
|
181
188
|
const workflowSnapshot = applyGeneratedPlan(synthetic, rawPlan, { availableVpIds });
|
|
182
189
|
const addedStages = workflowSnapshot.stages.filter(stage => addedIds.has(stage.id));
|
|
@@ -189,6 +196,96 @@ export function applyAdditivePlanProposal({ workItem, actions, proposal, availab
|
|
|
189
196
|
};
|
|
190
197
|
}
|
|
191
198
|
|
|
199
|
+
export function applyCoordinatorReplan({ workItem, actions, proposal, availableVpIds = null }) {
|
|
200
|
+
if (workItem.workflowSnapshot?.executionMode !== 'graph'
|
|
201
|
+
|| workItem.workflowSnapshot?.planningMode !== 'ai') {
|
|
202
|
+
throw new Error('Work Center Coordinator replan requires an AI-planned Action graph');
|
|
203
|
+
}
|
|
204
|
+
if (!proposal || typeof proposal !== 'object' || Array.isArray(proposal)) {
|
|
205
|
+
throw new Error('Work Center Coordinator replan must be an object');
|
|
206
|
+
}
|
|
207
|
+
const proposalId = cleanProposalId(proposal.proposalId);
|
|
208
|
+
const basePlanRevision = Number(proposal.basePlanRevision);
|
|
209
|
+
if (!Number.isInteger(basePlanRevision) || basePlanRevision !== workItem.planRevision) {
|
|
210
|
+
throw new Error('Work Center Coordinator replan has a stale basePlanRevision');
|
|
211
|
+
}
|
|
212
|
+
if (!Array.isArray(proposal.actions) || proposal.actions.length < 1 || proposal.actions.length > 8) {
|
|
213
|
+
throw new Error('Work Center Coordinator replan requires between 1 and 8 unfinished Actions');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const active = actions.filter(candidate => !['superseded', 'cancelled'].includes(candidate.status));
|
|
217
|
+
const completed = active.filter(candidate => candidate.status === 'completed');
|
|
218
|
+
const completedStageIds = new Set(completed.map(candidate => candidate.stageId));
|
|
219
|
+
const unfinished = active.filter(candidate => candidate.status !== 'completed');
|
|
220
|
+
const unfinishedByStage = new Map(unfinished.map(candidate => [candidate.stageId, candidate]));
|
|
221
|
+
const historicalStageIds = new Set(actions.map(candidate => candidate.stageId));
|
|
222
|
+
const currentStages = new Map((workItem.workflowSnapshot.stages || []).map(stage => [stage.id, stage]));
|
|
223
|
+
const futureStageIds = new Set();
|
|
224
|
+
|
|
225
|
+
const normalizedFuture = proposal.actions.map(raw => {
|
|
226
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
227
|
+
throw new Error('Work Center Coordinator replan requires full Action specifications');
|
|
228
|
+
}
|
|
229
|
+
const id = canonicalActionId(raw.id);
|
|
230
|
+
if (!id || futureStageIds.has(id) || completedStageIds.has(id)) {
|
|
231
|
+
throw new Error(`Work Center Coordinator Action id is missing, duplicated, or completed: ${id || '(missing)'}`);
|
|
232
|
+
}
|
|
233
|
+
if (historicalStageIds.has(id) && !unfinishedByStage.has(id)) {
|
|
234
|
+
throw new Error(`Work Center Coordinator Action reuses historical stage identity: ${id}`);
|
|
235
|
+
}
|
|
236
|
+
futureStageIds.add(id);
|
|
237
|
+
const dependsOnActionIds = canonicalExplicitActionIds(
|
|
238
|
+
raw.dependsOnActionIds,
|
|
239
|
+
`Coordinator Action "${id}" dependencies`,
|
|
240
|
+
);
|
|
241
|
+
for (const dependencyId of dependsOnActionIds) {
|
|
242
|
+
if (!completedStageIds.has(dependencyId) && !futureStageIds.has(dependencyId)) {
|
|
243
|
+
throw new Error(`Work Center Coordinator Action "${id}" references a missing or future dependency "${dependencyId}"`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
...raw,
|
|
248
|
+
id,
|
|
249
|
+
dependsOnActionIds,
|
|
250
|
+
changesRequestedActionId: Object.hasOwn(raw, 'changesRequestedActionId')
|
|
251
|
+
? canonicalExplicitActionId(raw.changesRequestedActionId, `Coordinator Action "${id}" review target`)
|
|
252
|
+
: undefined,
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
const completedInputs = completed.filter(candidate => candidate.type !== 'triage').map(candidate => {
|
|
257
|
+
const stage = currentStages.get(candidate.stageId);
|
|
258
|
+
if (!stage) throw new Error(`Work Center completed Action is missing from the frozen workflow: ${candidate.stageId}`);
|
|
259
|
+
return planActionFromStage(stage);
|
|
260
|
+
});
|
|
261
|
+
const synthetic = {
|
|
262
|
+
...workItem,
|
|
263
|
+
workflowSnapshot: {
|
|
264
|
+
...workItem.workflowSnapshot,
|
|
265
|
+
actionTemplates: [],
|
|
266
|
+
stages: [workItem.workflowSnapshot.stages[0]],
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
const workflowSnapshot = applyGeneratedPlan(synthetic, {
|
|
270
|
+
workItemType: workItem.workflowSnapshot.workItemType,
|
|
271
|
+
actions: [...completedInputs, ...normalizedFuture],
|
|
272
|
+
}, { availableVpIds });
|
|
273
|
+
const stageById = new Map(workflowSnapshot.stages.map(stage => [stage.id, stage]));
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
proposalId,
|
|
277
|
+
reason: typeof proposal.reason === 'string' ? proposal.reason.trim().slice(0, 4_000) : '',
|
|
278
|
+
basePlanRevision,
|
|
279
|
+
workflowSnapshot,
|
|
280
|
+
unfinished,
|
|
281
|
+
nextActions: normalizedFuture.map(input => {
|
|
282
|
+
const prior = unfinishedByStage.get(input.id) || null;
|
|
283
|
+
const nextAction = actionForStage(stageById.get(input.id), { ...workItem, workflowSnapshot }, []);
|
|
284
|
+
return { prior, nextAction };
|
|
285
|
+
}),
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
192
289
|
export function applyReplanMutation({ workItem, action, actions, proposal, availableVpIds = null }) {
|
|
193
290
|
if (workItem.workflowSnapshot?.executionMode !== 'graph'
|
|
194
291
|
|| action?.type !== 'triage'
|
|
@@ -283,7 +380,7 @@ export function applyReplanMutation({ workItem, action, actions, proposal, avail
|
|
|
283
380
|
}, { availableVpIds });
|
|
284
381
|
const stageById = new Map(workflowSnapshot.stages.map(stage => [stage.id, stage]));
|
|
285
382
|
const context = (Array.isArray(action.context) ? action.context : [])
|
|
286
|
-
.filter(entry => entry?.type !== 'replan-barrier');
|
|
383
|
+
.filter(entry => entry?.type !== 'replan-barrier' && entry?.type !== 'input');
|
|
287
384
|
return {
|
|
288
385
|
proposalId,
|
|
289
386
|
basePlanRevision,
|
|
@@ -732,6 +732,7 @@ function enforceWorkItemBrowserDtoBudget(value, options = {}) {
|
|
|
732
732
|
omittedActionCount: originalCount,
|
|
733
733
|
createdAt: count(workItem.createdAt),
|
|
734
734
|
updatedAt: count(workItem.updatedAt),
|
|
735
|
+
coordinatorRevision: count(workItem.coordinatorRevision),
|
|
735
736
|
};
|
|
736
737
|
if (options.event === true) dto.workItem = minimalWorkItem;
|
|
737
738
|
else return minimalWorkItem;
|
|
@@ -845,6 +846,9 @@ export function projectWorkItemDetail(detail) {
|
|
|
845
846
|
const projected = {
|
|
846
847
|
id: detail.id,
|
|
847
848
|
revision: detail.revision,
|
|
849
|
+
planRevision: count(detail.planRevision),
|
|
850
|
+
ledgerRevision: count(detail.ledgerRevision),
|
|
851
|
+
coordinatorRevision: count(detail.coordinatorRevision),
|
|
848
852
|
title: detail.title,
|
|
849
853
|
goal: detail.goal,
|
|
850
854
|
acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
|
|
@@ -870,8 +874,21 @@ export function projectWorkItemDetail(detail) {
|
|
|
870
874
|
linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
|
|
871
875
|
messages: (Array.isArray(detail.messages) ? detail.messages : []).slice(-100).map(message => ({
|
|
872
876
|
id: String(message.id || ''),
|
|
877
|
+
turnId: String(message.turnId || message.id || ''),
|
|
878
|
+
role: message.role === 'assistant' ? 'assistant' : message.role === 'legacy_instruction' ? 'legacy_instruction' : 'user',
|
|
873
879
|
text: truncateUtf8(message.text || '', MAX_ACTION_MESSAGE_CHARS),
|
|
880
|
+
status: ['thinking', 'completed', 'failed'].includes(message.status) ? message.status : 'completed',
|
|
881
|
+
error: truncateUtf8(message.error || '', MAX_ACTION_DIAGNOSTIC_CHARS) || null,
|
|
882
|
+
decision: message.decision && typeof message.decision === 'object' ? {
|
|
883
|
+
kind: ['answer', 'guide_actions', 'replan'].includes(message.decision.kind)
|
|
884
|
+
? message.decision.kind : null,
|
|
885
|
+
reason: truncateUtf8(message.decision.reason || '', MAX_ACTION_DIAGNOSTIC_CHARS),
|
|
886
|
+
changedContract: message.decision.changedContract === true,
|
|
887
|
+
affectedActionIds: Array.isArray(message.decision.affectedActionIds)
|
|
888
|
+
? message.decision.affectedActionIds.map(id => String(id)).slice(0, 8) : [],
|
|
889
|
+
} : null,
|
|
874
890
|
createdAt: count(message.createdAt),
|
|
891
|
+
updatedAt: count(message.updatedAt || message.createdAt),
|
|
875
892
|
})),
|
|
876
893
|
attachments: projectAttachments(detail.attachments),
|
|
877
894
|
createdAt: detail.createdAt,
|
|
@@ -900,6 +917,9 @@ export function projectWorkItemSummary(detail) {
|
|
|
900
917
|
return {
|
|
901
918
|
id: detail.id,
|
|
902
919
|
revision: detail.revision,
|
|
920
|
+
planRevision: count(detail.planRevision),
|
|
921
|
+
ledgerRevision: count(detail.ledgerRevision),
|
|
922
|
+
coordinatorRevision: count(detail.coordinatorRevision),
|
|
903
923
|
title: detail.title,
|
|
904
924
|
goal: detail.goal,
|
|
905
925
|
workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
|
|
@@ -931,6 +951,9 @@ export function projectWorkItemSummary(detail) {
|
|
|
931
951
|
return {
|
|
932
952
|
id: detail.id,
|
|
933
953
|
revision: detail.revision,
|
|
954
|
+
planRevision: count(detail.planRevision),
|
|
955
|
+
ledgerRevision: count(detail.ledgerRevision),
|
|
956
|
+
coordinatorRevision: count(detail.coordinatorRevision),
|
|
934
957
|
title: detail.title,
|
|
935
958
|
goal: detail.goal,
|
|
936
959
|
workItemType: detail.workflowSnapshot?.workItemType || detail.workItemType || null,
|
|
@@ -578,7 +578,7 @@ function completionContract(action, workItem) {
|
|
|
578
578
|
"acceptanceChecks": ${JSON.stringify(acceptanceChecks)},
|
|
579
579
|
"waitingReason": null,
|
|
580
580
|
"error": null${reviewField}${triageField}${planField}
|
|
581
|
-
}\nFor completed, provide at least one concrete evidence item and exactly one acceptanceChecks entry for every current acceptance criterion, in the same order, with status passed, deferred, or not_applicable and a non-empty evidence reference. Triage must use its proposed criteria when submitting a contractPatch.
|
|
581
|
+
}\nFor completed, provide at least one concrete evidence item and exactly one acceptanceChecks entry for every current acceptance criterion, in the same order, with status passed, deferred, or not_applicable and a non-empty evidence reference. Triage must use its proposed criteria when submitting a contractPatch. An intermediate Action may defer criteria outside its task-specific expected result; the final deliver Action, and an approved review with no downstream work, require every criterion to pass. If a criterion is no longer applicable, ask the WorkItem Coordinator to revise the contract instead of pretending it passed. This is a deterministic submission gate, not independent proof: later verification and delivery Actions must verify the claims. A model turn ending is not completion. Use waiting when user or external input is required. Use retryable only for a transient failure. Do not start background jobs or delegate this Action.`;
|
|
582
582
|
}
|
|
583
583
|
|
|
584
584
|
function safeCheckpointUrl(value) {
|
|
@@ -105,6 +105,7 @@ export class WorkCenterService {
|
|
|
105
105
|
this.controller = options.controller || new WorkflowController(this.store, {
|
|
106
106
|
listAvailableVpIds: options.listAvailableVpIds,
|
|
107
107
|
});
|
|
108
|
+
this.coordinator = options.coordinator || null;
|
|
108
109
|
this.onEvent = typeof options.onEvent === 'function' ? options.onEvent : () => {};
|
|
109
110
|
this.watcher = new WorkItemWatcher({
|
|
110
111
|
store: this.store,
|
|
@@ -271,14 +272,22 @@ export class WorkCenterService {
|
|
|
271
272
|
return { id, deleted: true, cleanupWarning };
|
|
272
273
|
}
|
|
273
274
|
case 'work_item_message': {
|
|
275
|
+
if (!this.coordinator) throw new Error('Work Center Coordinator is unavailable');
|
|
274
276
|
const id = requiredString(payload.id, 'id');
|
|
275
|
-
const
|
|
277
|
+
const turn = this.coordinator.message(id, {
|
|
276
278
|
text: typeof payload.text === 'string' ? payload.text : '',
|
|
277
279
|
revision: payload.revision,
|
|
280
|
+
planRevision: payload.planRevision,
|
|
281
|
+
ledgerRevision: payload.ledgerRevision,
|
|
282
|
+
coordinatorRevision: payload.coordinatorRevision,
|
|
283
|
+
}, {
|
|
284
|
+
onUpdate: (type, workItem) => {
|
|
285
|
+
this.watcher.abortInvalidWorkItemRuns(id);
|
|
286
|
+
this.#emit({ type, workItem });
|
|
287
|
+
},
|
|
278
288
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return detail;
|
|
289
|
+
turn.task.catch(() => {});
|
|
290
|
+
return { accepted: true, turnId: turn.detail.messages?.at(-1)?.turnId || null };
|
|
282
291
|
}
|
|
283
292
|
case 'retry_action': {
|
|
284
293
|
const id = requiredString(payload.id, 'id');
|
|
@@ -296,6 +305,10 @@ export class WorkCenterService {
|
|
|
296
305
|
}
|
|
297
306
|
case 'action_input': {
|
|
298
307
|
const id = requiredString(payload.id, 'id');
|
|
308
|
+
const generation = Number(payload.generation);
|
|
309
|
+
if (!Number.isInteger(generation) || generation < 1) {
|
|
310
|
+
throw new Error('generation must be a positive integer');
|
|
311
|
+
}
|
|
299
312
|
const workItem = this.#requiredItem(id);
|
|
300
313
|
let addedAttachments = [];
|
|
301
314
|
let detail;
|
|
@@ -308,7 +321,7 @@ export class WorkCenterService {
|
|
|
308
321
|
text: typeof payload.text === 'string' ? payload.text : '',
|
|
309
322
|
actionId: typeof payload.actionId === 'string' ? payload.actionId : '',
|
|
310
323
|
revision: payload.revision,
|
|
311
|
-
generation
|
|
324
|
+
generation,
|
|
312
325
|
addedAttachmentCount: addedAttachments.length,
|
|
313
326
|
addedAttachments,
|
|
314
327
|
attachments: [...(workItem.attachments || []), ...addedAttachments],
|
|
@@ -429,6 +442,7 @@ export class WorkCenterService {
|
|
|
429
442
|
}
|
|
430
443
|
|
|
431
444
|
async shutdown() {
|
|
445
|
+
await this.coordinator?.shutdown?.();
|
|
432
446
|
await this.watcher.stop();
|
|
433
447
|
try { await this.watcher.runner?.shutdown?.(); } catch {}
|
|
434
448
|
try { await this.watcher.runner?.trace?.close?.(); } catch {}
|