@yeaft/webchat-agent 1.0.247 → 1.0.249
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 +144 -114
- 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 +21 -5
- 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 +165 -115
- package/yeaft/work-center/runner.js +1 -1
- package/yeaft/work-center/service.js +33 -5
- package/yeaft/work-center/store.js +1161 -147
- package/yeaft/work-center/watcher.js +26 -4
- package/yeaft/work-center/workflow.js +75 -5
|
Binary file
|
package/package.json
CHANGED
|
@@ -7,6 +7,29 @@ export function eventMatchesActionGeneration(event, action) {
|
|
|
7
7
|
return generation(event.actionGeneration) === generation(action.generation);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
export function currentActionInputEventIds(events, action) {
|
|
11
|
+
const inputEvents = new Map((Array.isArray(events) ? events : [])
|
|
12
|
+
.filter(event => event?.type === 'action.input_added' && event.actionId === action?.id)
|
|
13
|
+
.map(event => [String(event.id), event]));
|
|
14
|
+
const valid = new Set();
|
|
15
|
+
for (const event of inputEvents.values()) {
|
|
16
|
+
if (eventMatchesActionGeneration(event, action)) valid.add(String(event.id));
|
|
17
|
+
}
|
|
18
|
+
for (const event of Array.isArray(events) ? events : []) {
|
|
19
|
+
if (event?.type !== 'action.input_rebound' || event.actionId !== action?.id
|
|
20
|
+
|| !eventMatchesActionGeneration(event, action)) continue;
|
|
21
|
+
const sourceEventIds = [
|
|
22
|
+
event.data?.sourceEventId,
|
|
23
|
+
...(Array.isArray(event.data?.sourceEventIds) ? event.data.sourceEventIds : []),
|
|
24
|
+
].filter(value => value != null);
|
|
25
|
+
for (const eventId of sourceEventIds) {
|
|
26
|
+
const key = String(eventId);
|
|
27
|
+
if (inputEvents.has(key)) valid.add(key);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return valid;
|
|
31
|
+
}
|
|
32
|
+
|
|
10
33
|
export function runMatchesActionIdentity(run, action) {
|
|
11
34
|
if (!run || !action) return false;
|
|
12
35
|
const actionGeneration = generation(action.generation);
|
|
@@ -5,7 +5,8 @@ import { defaultRegistry } from '../vp/registry.js';
|
|
|
5
5
|
import { scanVpLibrary } from '../vp/vp-store.js';
|
|
6
6
|
import { WorkCenterService } from './service.js';
|
|
7
7
|
import { WorkItemRunner } from './runner.js';
|
|
8
|
-
import {
|
|
8
|
+
import { WorkItemCoordinator } from './coordinator.js';
|
|
9
|
+
import { projectWorkCenterEvent } from './projection.js';
|
|
9
10
|
import { previewWorkCenterPlan } from './planner.js';
|
|
10
11
|
import { readWorkCenterSettings, writeWorkCenterSettings } from './settings.js';
|
|
11
12
|
import { defaultWorkCenterStageInstructions } from './workflow.js';
|
|
@@ -19,7 +20,7 @@ let shutdownPromise = null;
|
|
|
19
20
|
let serviceFactory = null;
|
|
20
21
|
|
|
21
22
|
const BROWSER_DETAIL_OPS = new Set([
|
|
22
|
-
'get', 'create', 'update', 'start', 'cancel', '
|
|
23
|
+
'get', 'create', 'update', 'start', 'cancel', 'action_input', 'retry_action', 'guide', 'retry',
|
|
23
24
|
]);
|
|
24
25
|
const BROWSER_ACTION_DEBUG_OPS = new Set(['get_action_messages', 'get_action_requests', 'get_action_request']);
|
|
25
26
|
// `files` is an internal server-to-Agent field. The browser relay rejects any
|
|
@@ -28,7 +29,7 @@ const BROWSER_FILE_FIELDS = Object.freeze({
|
|
|
28
29
|
create: [
|
|
29
30
|
'title', 'goal', 'acceptanceCriteria', 'workItemType', 'workDir', 'reuseMemory', 'files', 'start',
|
|
30
31
|
],
|
|
31
|
-
work_item_message: ['id', 'text', 'revision'],
|
|
32
|
+
work_item_message: ['id', 'text', 'revision', 'planRevision', 'ledgerRevision', 'coordinatorRevision'],
|
|
32
33
|
action_input: ['id', 'text', 'actionId', 'revision', 'generation', 'files'],
|
|
33
34
|
retry_action: ['id', 'actionId', 'revision', 'generation'],
|
|
34
35
|
delete: ['id', 'revision'],
|
|
@@ -117,9 +118,22 @@ async function createDefaultService() {
|
|
|
117
118
|
registry: defaultRegistry,
|
|
118
119
|
store: null,
|
|
119
120
|
});
|
|
121
|
+
const coordinator = new WorkItemCoordinator({
|
|
122
|
+
store: null,
|
|
123
|
+
runtimeProvider: async () => {
|
|
124
|
+
const runtime = await runner.runtimeProvider();
|
|
125
|
+
if (defaultRegistry.vpCount() === 0) {
|
|
126
|
+
for (const vp of scanVpLibrary({ dir: join(yeaftDir, 'virtual-persons') })) defaultRegistry.setVp(vp);
|
|
127
|
+
}
|
|
128
|
+
return runtime;
|
|
129
|
+
},
|
|
130
|
+
policyProvider: async () => readWorkCenterSettings(yeaftDir),
|
|
131
|
+
registry: defaultRegistry,
|
|
132
|
+
});
|
|
120
133
|
const created = new WorkCenterService({
|
|
121
134
|
yeaftDir,
|
|
122
135
|
runner,
|
|
136
|
+
coordinator,
|
|
123
137
|
runtimeInfoProvider: getSettingsRuntime,
|
|
124
138
|
listAvailableVpIds: () => defaultRegistry.listVps().map(vp => vp.id),
|
|
125
139
|
watcherOptions: {
|
|
@@ -130,6 +144,7 @@ async function createDefaultService() {
|
|
|
130
144
|
},
|
|
131
145
|
});
|
|
132
146
|
runner.store = created.store;
|
|
147
|
+
coordinator.store = created.store;
|
|
133
148
|
return created;
|
|
134
149
|
}
|
|
135
150
|
|
|
@@ -173,6 +188,7 @@ export async function handleWorkCenterRequest(msg) {
|
|
|
173
188
|
const op = typeof msg.op === 'string' ? msg.op : '';
|
|
174
189
|
try {
|
|
175
190
|
let data;
|
|
191
|
+
let workCenter = null;
|
|
176
192
|
if (op === 'get_settings') {
|
|
177
193
|
data = await readSettingsResponse();
|
|
178
194
|
} else if (op === 'update_settings') {
|
|
@@ -195,13 +211,13 @@ export async function handleWorkCenterRequest(msg) {
|
|
|
195
211
|
await resetYeaftSession();
|
|
196
212
|
data = await readSettingsResponse();
|
|
197
213
|
} else {
|
|
198
|
-
|
|
214
|
+
workCenter = await ensureWorkCenter();
|
|
199
215
|
const payload = Object.hasOwn(BROWSER_FILE_FIELDS, op)
|
|
200
216
|
? browserFilePayload(op, msg.payload)
|
|
201
217
|
: (BROWSER_ACTION_DEBUG_OPS.has(op) ? browserFilePayload(op, msg.payload) : (msg.payload || {}));
|
|
202
218
|
data = await workCenter.handle(op, payload);
|
|
203
219
|
}
|
|
204
|
-
if (BROWSER_DETAIL_OPS.has(op)) data =
|
|
220
|
+
if (BROWSER_DETAIL_OPS.has(op)) data = workCenter.projectBrowserDetail(data);
|
|
205
221
|
send({
|
|
206
222
|
type: 'work_center_response',
|
|
207
223
|
requestId,
|
|
@@ -44,9 +44,31 @@ export function validateCompletedResult(result, action, workItem) {
|
|
|
44
44
|
result.error = 'Completed Action requires one ordered acceptance check with evidence for every acceptance criterion';
|
|
45
45
|
return;
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
// Intermediate test Actions validate their own task-specific expected result.
|
|
48
|
+
// Requiring them to prove the entire WorkItem contract makes any DAG with
|
|
49
|
+
// later tests or reviews impossible to complete. Global proof belongs at the
|
|
50
|
+
// delivery boundary or an approved review with no unfinished downstream work.
|
|
51
|
+
const remainingStages = Array.isArray(workItem?.workflowSnapshot?.stages)
|
|
52
|
+
? workItem.workflowSnapshot.stages.filter(stage => stage?.id !== action.stageId)
|
|
53
|
+
: [];
|
|
54
|
+
const downstream = new Set([action.stageId]);
|
|
55
|
+
let expanded = true;
|
|
56
|
+
while (expanded) {
|
|
57
|
+
expanded = false;
|
|
58
|
+
for (const stage of remainingStages) {
|
|
59
|
+
if (downstream.has(stage?.id)) continue;
|
|
60
|
+
if (Array.isArray(stage?.dependsOnStageIds)
|
|
61
|
+
&& stage.dependsOnStageIds.some(stageId => downstream.has(stageId))) {
|
|
62
|
+
downstream.add(stage.id);
|
|
63
|
+
expanded = true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const hasDownstreamStage = remainingStages.some(stage => downstream.has(stage?.id));
|
|
68
|
+
const hasDeliverStage = remainingStages.some(stage => stage?.type === 'deliver');
|
|
69
|
+
const mustVerify = action.type === 'deliver'
|
|
70
|
+
|| (action.type === 'review' && result.reviewDecision === 'approved'
|
|
71
|
+
&& !hasDownstreamStage && !hasDeliverStage);
|
|
50
72
|
if (mustVerify && checks.some(check => check.status !== 'passed')) {
|
|
51
73
|
result.outcome = 'failed';
|
|
52
74
|
result.error = `${action.type} Action requires every acceptance check to pass`;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import {
|
|
2
3
|
actionForStage,
|
|
3
4
|
actionInstruction,
|
|
5
|
+
canonicalActionInstruction,
|
|
6
|
+
withoutActionInputContext,
|
|
4
7
|
applyGeneratedPlan,
|
|
5
8
|
getNextStep,
|
|
6
9
|
initialActionFor,
|
|
@@ -76,6 +79,28 @@ function normalizeTerminalResult(result, action) {
|
|
|
76
79
|
return normalized;
|
|
77
80
|
}
|
|
78
81
|
|
|
82
|
+
function canonicalReplacementAction(workItem, action, context) {
|
|
83
|
+
const replacement = {
|
|
84
|
+
type: action.type,
|
|
85
|
+
stageId: action.stageId || action.type,
|
|
86
|
+
assignmentPolicy: action.assignmentPolicy,
|
|
87
|
+
modelPolicy: action.modelPolicy,
|
|
88
|
+
requiredRole: action.requiredRole,
|
|
89
|
+
dependsOnStageIds: action.dependsOnStageIds,
|
|
90
|
+
workspaceMode: action.workspaceMode,
|
|
91
|
+
changesRequestedStageId: action.changesRequestedStageId,
|
|
92
|
+
brief: action.brief,
|
|
93
|
+
context,
|
|
94
|
+
maxAttempts: action.maxAttempts || 2,
|
|
95
|
+
};
|
|
96
|
+
replacement.instruction = canonicalActionInstruction(
|
|
97
|
+
workItem,
|
|
98
|
+
{ ...action, ...replacement },
|
|
99
|
+
context,
|
|
100
|
+
);
|
|
101
|
+
return replacement;
|
|
102
|
+
}
|
|
103
|
+
|
|
79
104
|
function contextEntry(action, result, run) {
|
|
80
105
|
return {
|
|
81
106
|
type: action.type,
|
|
@@ -161,50 +186,22 @@ export class WorkflowController {
|
|
|
161
186
|
const current = this.store.getWorkItem(id);
|
|
162
187
|
const graphMode = current?.workflowSnapshot?.executionMode === 'graph';
|
|
163
188
|
if (!expected.actionId || !Number.isInteger(expected.revision)
|
|
164
|
-
|| (graphMode && !Number.isInteger(expected.generation))) {
|
|
189
|
+
|| (graphMode && (!Number.isInteger(expected.generation) || expected.generation < 1))) {
|
|
165
190
|
throw new Error(`actionId, revision${graphMode ? ', and generation' : ''} are required for guidance`);
|
|
166
191
|
}
|
|
167
192
|
const detail = this.store.addActionGuidance(id, guidanceSummary, expected, (workItem, previous) => {
|
|
168
|
-
const context = [...(previous.context
|
|
193
|
+
const context = [...withoutActionInputContext(previous.context), {
|
|
169
194
|
type: 'guidance',
|
|
170
195
|
role: 'user',
|
|
171
196
|
summary: guidanceSummary,
|
|
172
197
|
evidence: [],
|
|
173
198
|
}];
|
|
174
|
-
|
|
175
|
-
type: previous.type,
|
|
176
|
-
stageId: previous.stageId || previous.type,
|
|
177
|
-
assignmentPolicy: previous.assignmentPolicy,
|
|
178
|
-
modelPolicy: previous.modelPolicy,
|
|
179
|
-
requiredRole: previous.requiredRole,
|
|
180
|
-
dependsOnStageIds: previous.dependsOnStageIds,
|
|
181
|
-
workspaceMode: previous.workspaceMode,
|
|
182
|
-
changesRequestedStageId: previous.changesRequestedStageId,
|
|
183
|
-
brief: previous.brief,
|
|
184
|
-
};
|
|
185
|
-
return {
|
|
186
|
-
...step,
|
|
187
|
-
context,
|
|
188
|
-
instruction: actionInstruction(step, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
|
|
189
|
-
maxAttempts: previous.maxAttempts || 2,
|
|
190
|
-
};
|
|
199
|
+
return canonicalReplacementAction(workItem, previous, context);
|
|
191
200
|
}, input.attachments, input.addedAttachments);
|
|
192
201
|
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
193
202
|
return detail;
|
|
194
203
|
}
|
|
195
204
|
|
|
196
|
-
message(id, input = {}) {
|
|
197
|
-
const text = typeof input.text === 'string' ? input.text.trim().slice(0, 8_000) : '';
|
|
198
|
-
if (!text) throw new Error('WorkItem message is required');
|
|
199
|
-
const revision = Number(input.revision);
|
|
200
|
-
if (!Number.isInteger(revision)) throw new Error('revision is required for WorkItem messages');
|
|
201
|
-
const detail = this.store.addWorkItemMessage(id, text, revision, (workItem, action) => (
|
|
202
|
-
actionInstruction(action, workItem, action.context || [], renderSessionContextSnapshot(workItem.sessionContext))
|
|
203
|
-
));
|
|
204
|
-
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
205
|
-
return detail;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
205
|
input(id, input = {}) {
|
|
209
206
|
const text = typeof input.text === 'string' ? input.text.trim().slice(0, 8_000) : '';
|
|
210
207
|
const addedAttachmentCount = Math.max(0, Number(input.addedAttachmentCount) || 0);
|
|
@@ -212,10 +209,14 @@ export class WorkflowController {
|
|
|
212
209
|
const workItem = this.store.getWorkItem(id);
|
|
213
210
|
if (!workItem) throw new Error(`WorkItem not found: ${id}`);
|
|
214
211
|
const targetAction = this.store.getAction(input.actionId);
|
|
212
|
+
const expectedGeneration = Number(input.generation);
|
|
213
|
+
if (!Number.isInteger(expectedGeneration) || expectedGeneration < 1) {
|
|
214
|
+
throw new Error('actionId, revision, and generation are required for Action input');
|
|
215
|
+
}
|
|
215
216
|
const graphMode = workItem.workflowSnapshot?.executionMode === 'graph';
|
|
216
|
-
const targetMatches =
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
const targetMatches = targetAction?.workItemId === id
|
|
218
|
+
&& targetAction.generation === expectedGeneration
|
|
219
|
+
&& (graphMode || workItem.currentActionId === input.actionId);
|
|
219
220
|
if (!targetMatches || workItem.revision !== input.revision) {
|
|
220
221
|
throw new Error('Action changed before input was applied; refresh and try again');
|
|
221
222
|
}
|
|
@@ -226,27 +227,8 @@ export class WorkflowController {
|
|
|
226
227
|
const inputSummary = text || `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`;
|
|
227
228
|
return this.store.addActionInput(id, inputSummary, {
|
|
228
229
|
actionId: input.actionId,
|
|
229
|
-
generation:
|
|
230
|
+
generation: expectedGeneration,
|
|
230
231
|
revision: input.revision,
|
|
231
|
-
}, (current, currentAction) => {
|
|
232
|
-
const context = [...(currentAction.context || []), {
|
|
233
|
-
type: 'input', role: 'user', summary: inputSummary, evidence: [],
|
|
234
|
-
}];
|
|
235
|
-
const step = {
|
|
236
|
-
type: currentAction.type,
|
|
237
|
-
stageId: currentAction.stageId || currentAction.type,
|
|
238
|
-
assignmentPolicy: currentAction.assignmentPolicy,
|
|
239
|
-
modelPolicy: currentAction.modelPolicy,
|
|
240
|
-
requiredRole: currentAction.requiredRole,
|
|
241
|
-
dependsOnStageIds: currentAction.dependsOnStageIds,
|
|
242
|
-
workspaceMode: currentAction.workspaceMode,
|
|
243
|
-
changesRequestedStageId: currentAction.changesRequestedStageId,
|
|
244
|
-
brief: currentAction.brief,
|
|
245
|
-
};
|
|
246
|
-
return {
|
|
247
|
-
context,
|
|
248
|
-
instruction: actionInstruction(step, current, context, renderSessionContextSnapshot(current.sessionContext)),
|
|
249
|
-
};
|
|
250
232
|
}, input.attachments, input.addedAttachments);
|
|
251
233
|
}
|
|
252
234
|
if (!['waiting', 'failed'].includes(targetAction.status)) {
|
|
@@ -258,6 +240,7 @@ export class WorkflowController {
|
|
|
258
240
|
expected: { actionId: input.actionId, generation: input.generation, revision: input.revision },
|
|
259
241
|
attachments: input.attachments,
|
|
260
242
|
inputEvent: {
|
|
243
|
+
inputId: randomUUID(),
|
|
261
244
|
text: text || `The user added ${addedAttachmentCount} attachment(s) as additional context for this Action.`,
|
|
262
245
|
attachments: input.addedAttachments,
|
|
263
246
|
},
|
|
@@ -271,20 +254,7 @@ export class WorkflowController {
|
|
|
271
254
|
if (previous?.status === 'waiting' && !answer && addedAttachmentCount === 0) {
|
|
272
255
|
throw new Error('answer or attachments are required to resume a waiting Action');
|
|
273
256
|
}
|
|
274
|
-
const
|
|
275
|
-
? {
|
|
276
|
-
type: previous.type,
|
|
277
|
-
stageId: previous.stageId || previous.type,
|
|
278
|
-
assignmentPolicy: previous.assignmentPolicy,
|
|
279
|
-
modelPolicy: previous.modelPolicy,
|
|
280
|
-
requiredRole: previous.requiredRole,
|
|
281
|
-
dependsOnStageIds: previous.dependsOnStageIds,
|
|
282
|
-
workspaceMode: previous.workspaceMode,
|
|
283
|
-
changesRequestedStageId: previous.changesRequestedStageId,
|
|
284
|
-
brief: previous.brief,
|
|
285
|
-
}
|
|
286
|
-
: initialActionFor(workItem);
|
|
287
|
-
const context = Array.isArray(previous?.context) ? [...previous.context] : [];
|
|
257
|
+
const context = withoutActionInputContext(previous?.context);
|
|
288
258
|
if (previousRun) {
|
|
289
259
|
context.push({
|
|
290
260
|
type: previous.type,
|
|
@@ -299,12 +269,19 @@ export class WorkflowController {
|
|
|
299
269
|
: null),
|
|
300
270
|
});
|
|
301
271
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
272
|
+
if (Number(workItem.executionSchemaVersion) === 2 && input.inputEvent?.inputId) {
|
|
273
|
+
context.push({
|
|
274
|
+
type: 'input',
|
|
275
|
+
role: 'user',
|
|
276
|
+
inputId: input.inputEvent.inputId,
|
|
277
|
+
summary: input.inputEvent.text || '',
|
|
278
|
+
attachments: Array.isArray(input.inputEvent.attachments) ? input.inputEvent.attachments : [],
|
|
279
|
+
evidence: [],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return previous
|
|
283
|
+
? canonicalReplacementAction(workItem, previous, context)
|
|
284
|
+
: { ...initialActionFor(workItem), context };
|
|
308
285
|
}, {
|
|
309
286
|
expected: input.expected || null,
|
|
310
287
|
attachments: input.attachments,
|
|
@@ -483,7 +460,7 @@ export class WorkflowController {
|
|
|
483
460
|
: planProposal
|
|
484
461
|
? { ...effectiveWorkItem, workflowSnapshot: planProposal.workflowSnapshot }
|
|
485
462
|
: effectiveWorkItem;
|
|
486
|
-
const context = [...(action.context
|
|
463
|
+
const context = [...withoutActionInputContext(action.context), contextEntry(action, result, activeRun)];
|
|
487
464
|
if (plannedWorkItem.workflowSnapshot?.executionMode === 'graph') {
|
|
488
465
|
if (staleReplanMutation) {
|
|
489
466
|
return {
|