@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.247",
3
+ "version": "1.0.249",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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 { projectWorkCenterEvent, projectWorkItemDetail } from './projection.js';
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', 'work_item_message', 'action_input', 'retry_action', 'guide', 'retry',
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
- const workCenter = await ensureWorkCenter();
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 = projectWorkItemDetail(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
- const mustVerify = action.type === 'test'
48
- || action.type === 'deliver'
49
- || (action.type === 'review' && result.reviewDecision === 'approved');
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
- const step = {
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 = graphMode
217
- ? targetAction?.workItemId === id && targetAction.generation === input.generation
218
- : workItem.currentActionId === input.actionId;
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: input.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 step = previous
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
- return {
303
- ...step,
304
- context,
305
- instruction: actionInstruction(step, workItem, context, renderSessionContextSnapshot(workItem.sessionContext)),
306
- maxAttempts: previous?.maxAttempts || 2,
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 || []), contextEntry(action, result, activeRun)];
463
+ const context = [...withoutActionInputContext(action.context), contextEntry(action, result, activeRun)];
487
464
  if (plannedWorkItem.workflowSnapshot?.executionMode === 'graph') {
488
465
  if (staleReplanMutation) {
489
466
  return {