@yeaft/webchat-agent 1.0.129 → 1.0.131

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.
@@ -1,13 +1,35 @@
1
- const SOFTWARE_CHANGE_STEPS = Object.freeze([
2
- { type: 'triage', requiredRole: 'omni' },
3
- { type: 'implement', requiredRole: 'linus' },
4
- { type: 'review', requiredRole: 'martin' },
5
- { type: 'deliver', requiredRole: 'linus' },
6
- ]);
1
+ const STAGE_TYPES = new Set(['triage', 'implement', 'test', 'review', 'deliver', 'research', 'write', 'custom']);
2
+ const ASSIGNMENT_MODES = new Set(['auto', 'pool', 'fixed']);
3
+ const MODEL_MODES = new Set(['inherit', 'primary', 'fast', 'specific']);
4
+ const MODEL_EFFORTS = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
7
5
 
8
- export const WORKFLOW_TEMPLATES = Object.freeze({
9
- 'software-change': SOFTWARE_CHANGE_STEPS,
10
- });
6
+ const DEFAULT_SOFTWARE_CHANGE_STAGES = Object.freeze([
7
+ {
8
+ id: 'triage', name: 'Triage', type: 'triage',
9
+ assignmentPolicy: { mode: 'auto', capability: 'triage', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
10
+ modelPolicy: { mode: 'inherit', model: null, effort: null },
11
+ maxAttempts: 2,
12
+ },
13
+ {
14
+ id: 'implement', name: 'Implement', type: 'implement',
15
+ assignmentPolicy: { mode: 'auto', capability: 'implement', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
16
+ modelPolicy: { mode: 'inherit', model: null, effort: null },
17
+ maxAttempts: 2,
18
+ },
19
+ {
20
+ id: 'review', name: 'Review', type: 'review',
21
+ assignmentPolicy: { mode: 'auto', capability: 'review', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: ['implement'] },
22
+ modelPolicy: { mode: 'inherit', model: null, effort: null },
23
+ maxAttempts: 2,
24
+ changesRequestedStageId: 'implement',
25
+ },
26
+ {
27
+ id: 'deliver', name: 'Deliver', type: 'deliver',
28
+ assignmentPolicy: { mode: 'auto', capability: 'deliver', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
29
+ modelPolicy: { mode: 'inherit', model: null, effort: null },
30
+ maxAttempts: 2,
31
+ },
32
+ ]);
11
33
 
12
34
  export const RUN_OUTCOMES = Object.freeze([
13
35
  'completed',
@@ -16,23 +38,204 @@ export const RUN_OUTCOMES = Object.freeze([
16
38
  'failed',
17
39
  ]);
18
40
 
19
- export function getWorkflowSteps(template = 'software-change') {
20
- const steps = WORKFLOW_TEMPLATES[template];
21
- if (!steps) throw new Error(`Unsupported Work Center workflow: ${template}`);
22
- return steps;
41
+ function cleanId(value, fallback) {
42
+ const normalized = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '');
43
+ return normalized || fallback;
44
+ }
45
+
46
+ function uniqueStrings(value) {
47
+ if (!Array.isArray(value)) return [];
48
+ return [...new Set(value.map(item => String(item || '').trim()).filter(Boolean))];
49
+ }
50
+
51
+ export function normalizeAssignmentPolicy(value, stageType = 'custom') {
52
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
53
+ const mode = ASSIGNMENT_MODES.has(source.mode) ? source.mode : 'auto';
54
+ const fixedVpId = typeof source.fixedVpId === 'string' && source.fixedVpId.trim()
55
+ ? source.fixedVpId.trim()
56
+ : null;
57
+ const candidateVpIds = uniqueStrings(source.candidateVpIds);
58
+ if (mode === 'fixed' && !fixedVpId) throw new Error('Fixed Work Center assignment requires fixedVpId');
59
+ if (mode === 'pool' && candidateVpIds.length === 0) throw new Error('Work Center VP pool cannot be empty');
60
+ return {
61
+ mode,
62
+ capability: cleanId(source.capability, stageType),
63
+ candidateVpIds,
64
+ fixedVpId,
65
+ separateFromStageTypes: uniqueStrings(source.separateFromStageTypes),
66
+ };
67
+ }
68
+
69
+ export function normalizeModelPolicy(value) {
70
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
71
+ const mode = MODEL_MODES.has(source.mode) ? source.mode : 'inherit';
72
+ const model = typeof source.model === 'string' && source.model.trim() ? source.model.trim() : null;
73
+ const effort = MODEL_EFFORTS.has(source.effort) ? source.effort : null;
74
+ if (mode === 'specific' && !model) throw new Error('Specific Work Center model policy requires a model');
75
+ return { mode, model, effort };
76
+ }
77
+
78
+ export function normalizeWorkflowDefinition(value, index = 0) {
79
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
80
+ throw new Error('Work Center workflow must be an object');
81
+ }
82
+ const id = cleanId(value.id, `workflow-${index + 1}`);
83
+ const name = String(value.name || id).trim() || id;
84
+ if (!Array.isArray(value.stages) || value.stages.length === 0) {
85
+ throw new Error(`Work Center workflow "${id}" requires at least one stage`);
86
+ }
87
+ const seen = new Set();
88
+ const stages = value.stages.map((rawStage, stageIndex) => {
89
+ const source = rawStage && typeof rawStage === 'object' ? rawStage : {};
90
+ const type = STAGE_TYPES.has(source.type) ? source.type : 'custom';
91
+ const stageId = cleanId(source.id, `${type}-${stageIndex + 1}`);
92
+ if (seen.has(stageId)) throw new Error(`Duplicate Work Center stage id: ${stageId}`);
93
+ seen.add(stageId);
94
+ const stage = {
95
+ id: stageId,
96
+ name: String(source.name || stageId).trim() || stageId,
97
+ type,
98
+ instruction: typeof source.instruction === 'string' ? source.instruction.trim() : '',
99
+ assignmentPolicy: normalizeAssignmentPolicy(source.assignmentPolicy, type),
100
+ modelPolicy: normalizeModelPolicy(source.modelPolicy),
101
+ maxAttempts: Math.min(Math.max(Number(source.maxAttempts) || 2, 1), 5),
102
+ };
103
+ if (type === 'review') {
104
+ stage.changesRequestedStageId = cleanId(source.changesRequestedStageId, 'implement');
105
+ }
106
+ return stage;
107
+ });
108
+ for (const [stageIndex, stage] of stages.entries()) {
109
+ if (stage.type !== 'review') continue;
110
+ const targetIndex = stages.findIndex(candidate => candidate.id === stage.changesRequestedStageId);
111
+ if (targetIndex === -1) {
112
+ throw new Error(`Review stage "${stage.id}" points to missing stage "${stage.changesRequestedStageId}"`);
113
+ }
114
+ const target = stages[targetIndex];
115
+ if (targetIndex >= stageIndex || target.type === 'review' || target.type === 'deliver') {
116
+ throw new Error(`Review stage "${stage.id}" must return to an earlier editable stage`);
117
+ }
118
+ }
119
+ return { version: 1, id, name, stages };
120
+ }
121
+
122
+ export function defaultWorkCenterSettings() {
123
+ return {
124
+ version: 1,
125
+ revision: 1,
126
+ defaultWorkflowId: 'software-change',
127
+ startImmediately: true,
128
+ defaultWorkDir: '',
129
+ workflows: [normalizeWorkflowDefinition({
130
+ id: 'software-change',
131
+ name: 'Software change',
132
+ stages: DEFAULT_SOFTWARE_CHANGE_STAGES,
133
+ })],
134
+ };
23
135
  }
24
136
 
25
- export function getStep(template, type) {
26
- return getWorkflowSteps(template).find(step => step.type === type) || null;
137
+ export function normalizeWorkCenterSettings(value) {
138
+ const defaults = defaultWorkCenterSettings();
139
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
140
+ const workflows = Array.isArray(source.workflows) && source.workflows.length > 0
141
+ ? source.workflows.map(normalizeWorkflowDefinition)
142
+ : defaults.workflows;
143
+ const workflowIds = new Set();
144
+ for (const workflow of workflows) {
145
+ if (workflowIds.has(workflow.id)) throw new Error(`Duplicate Work Center workflow id: ${workflow.id}`);
146
+ workflowIds.add(workflow.id);
147
+ }
148
+ const defaultWorkflowId = workflowIds.has(source.defaultWorkflowId)
149
+ ? source.defaultWorkflowId
150
+ : workflows[0].id;
151
+ const revision = Number.isInteger(source.revision) && source.revision > 0 ? source.revision : 1;
152
+ return {
153
+ version: 1,
154
+ revision,
155
+ defaultWorkflowId,
156
+ startImmediately: source.startImmediately !== false,
157
+ defaultWorkDir: typeof source.defaultWorkDir === 'string' ? source.defaultWorkDir.trim() : '',
158
+ workflows,
159
+ };
27
160
  }
28
161
 
29
- export function getNextStep(template, type, result = {}) {
30
- const steps = getWorkflowSteps(template);
31
- const index = steps.findIndex(step => step.type === type);
32
- if (index === -1) throw new Error(`Action type ${type} is not in workflow ${template}`);
33
- if (type === 'review') {
162
+ export function resolveWorkflowSnapshot(settings, workflowId, stageOverrides = {}) {
163
+ const normalized = normalizeWorkCenterSettings(settings);
164
+ const id = workflowId || normalized.defaultWorkflowId;
165
+ const workflow = normalized.workflows.find(item => item.id === id);
166
+ if (!workflow) throw new Error(`Unsupported Work Center workflow: ${id}`);
167
+ const overrides = stageOverrides && typeof stageOverrides === 'object' && !Array.isArray(stageOverrides)
168
+ ? stageOverrides
169
+ : {};
170
+ return normalizeWorkflowDefinition({
171
+ ...workflow,
172
+ stages: workflow.stages.map(stage => {
173
+ const override = overrides[stage.id];
174
+ if (!override || typeof override !== 'object' || Array.isArray(override)) return stage;
175
+ return {
176
+ ...stage,
177
+ assignmentPolicy: override.assignmentPolicy
178
+ ? { ...stage.assignmentPolicy, ...override.assignmentPolicy }
179
+ : stage.assignmentPolicy,
180
+ modelPolicy: override.modelPolicy
181
+ ? { ...stage.modelPolicy, ...override.modelPolicy }
182
+ : stage.modelPolicy,
183
+ };
184
+ }),
185
+ });
186
+ }
187
+
188
+ const LEGACY_SOFTWARE_CHANGE_VPS = Object.freeze({
189
+ triage: 'omni',
190
+ implement: 'linus',
191
+ review: 'martin',
192
+ deliver: 'linus',
193
+ });
194
+
195
+ function workflowFrom(source = 'software-change') {
196
+ if (source && typeof source === 'object' && source.workflowSnapshot?.stages) {
197
+ return normalizeWorkflowDefinition(source.workflowSnapshot);
198
+ }
199
+ if (source && typeof source === 'object' && source.stages) return normalizeWorkflowDefinition(source);
200
+ const settings = defaultWorkCenterSettings();
201
+ const workflow = resolveWorkflowSnapshot(settings, typeof source === 'string' ? source : 'software-change');
202
+ // Existing WorkItems did not persist a workflow snapshot. Preserve their
203
+ // historical deterministic assignments; only newly created WorkItems use
204
+ // pool selection. This avoids silently changing in-flight durable work.
205
+ return {
206
+ ...workflow,
207
+ stages: workflow.stages.map(stage => ({
208
+ ...stage,
209
+ assignmentPolicy: {
210
+ ...stage.assignmentPolicy,
211
+ mode: 'fixed',
212
+ fixedVpId: LEGACY_SOFTWARE_CHANGE_VPS[stage.type],
213
+ },
214
+ })),
215
+ };
216
+ }
217
+
218
+ export function getWorkflowSteps(source = 'software-change') {
219
+ return workflowFrom(source).stages;
220
+ }
221
+
222
+ export function getStep(source, stageIdOrType) {
223
+ return getWorkflowSteps(source).find(stage => stage.id === stageIdOrType)
224
+ || getWorkflowSteps(source).find(stage => stage.type === stageIdOrType)
225
+ || null;
226
+ }
227
+
228
+ export function getNextStep(source, stageIdOrType, result = {}) {
229
+ const steps = getWorkflowSteps(source);
230
+ const index = steps.findIndex(stage => stage.id === stageIdOrType)
231
+ !== -1
232
+ ? steps.findIndex(stage => stage.id === stageIdOrType)
233
+ : steps.findIndex(stage => stage.type === stageIdOrType);
234
+ if (index === -1) throw new Error(`Work Center stage ${stageIdOrType} is not in the workflow snapshot`);
235
+ const current = steps[index];
236
+ if (current.type === 'review') {
34
237
  if (result.reviewDecision === 'changes_requested') {
35
- return steps.find(step => step.type === 'implement') || null;
238
+ return steps.find(stage => stage.id === current.changesRequestedStageId) || null;
36
239
  }
37
240
  if (result.reviewDecision !== 'approved') {
38
241
  throw new Error('Completed review requires approved or changes_requested');
@@ -54,44 +257,62 @@ function renderContext(context = []) {
54
257
  const decision = entry.reviewDecision ? `\nReview decision: ${entry.reviewDecision}` : '';
55
258
  const waitingReason = entry.waitingReason ? `\nWaiting reason: ${entry.waitingReason}` : '';
56
259
  const answer = entry.answer ? `\nUser answer: ${entry.answer}` : '';
57
- return `### ${entry.type} (${entry.role || 'unknown role'})\n${entry.summary || '(no summary)'}${decision}${waitingReason}${answer}${evidence}`;
260
+ const source = entry.sourceTitle ? ` from ${entry.sourceTitle}` : '';
261
+ return `### ${entry.type}${source} (${entry.vpId || entry.role || 'unknown VP'})\n${entry.summary || '(no summary)'}${decision}${waitingReason}${answer}${evidence}`;
58
262
  });
59
- return `\n\nPrior Action results:\n${blocks.join('\n\n')}`;
263
+ return `\n\nReusable Work Center context and prior Action results:\n${blocks.join('\n\n')}`;
60
264
  }
61
265
 
62
- export function actionInstruction(step, workItem, context = []) {
266
+ export function actionInstruction(stage, workItem, context = []) {
63
267
  const criteria = (workItem.acceptanceCriteria || []).map(item => `- ${item}`).join('\n') || '- No explicit criteria';
64
268
  const common = `WorkItem: ${workItem.title}\nGoal: ${workItem.goal}\nAcceptance criteria:\n${criteria}${renderContext(context)}`;
65
- switch (step.type) {
269
+ if (stage.instruction) return `${common}\n\n${stage.instruction}`;
270
+ switch (stage.type) {
66
271
  case 'triage':
67
272
  return `${common}\n\nAnalyze the request, verify scope and risks, and make the contract executable. Do not implement yet. If the goal or acceptance criteria need refinement, submit a contractPatch.`;
68
273
  case 'implement':
69
274
  return `${common}\n\nImplement the smallest correct change in the supplied work directory. Add and run relevant tests. Use the prior triage/review findings. Do not approve your own work.`;
275
+ case 'test':
276
+ return `${common}\n\nVerify the implementation against the acceptance criteria. Run focused tests and report reproducible evidence. Do not modify unrelated code.`;
70
277
  case 'review':
71
278
  return `${common}\n\nReview the implementation and evidence independently. Return approved or changes_requested with concrete findings.`;
72
279
  case 'deliver':
73
280
  return `${common}\n\nDeliver the approved change using the repository release policy. Verify the final remote state and provide evidence.`;
281
+ case 'research':
282
+ return `${common}\n\nResearch the question using verifiable sources. Separate evidence from inference and return a concise synthesis.`;
283
+ case 'write':
284
+ return `${common}\n\nProduce the requested written deliverable, then verify it against every acceptance criterion.`;
74
285
  default:
75
- return common;
286
+ return `${common}\n\nComplete this stage and return verifiable evidence.`;
76
287
  }
77
288
  }
78
289
 
79
- export function initialActionFor(workItem) {
80
- const step = getWorkflowSteps(workItem.workflowTemplate)[0];
290
+ export function actionForStage(stage, workItem, context = []) {
81
291
  return {
82
- ...step,
83
- context: [],
84
- instruction: actionInstruction(step, workItem, []),
85
- maxAttempts: 2,
292
+ type: stage.type,
293
+ stageId: stage.id,
294
+ assignmentPolicy: stage.assignmentPolicy,
295
+ modelPolicy: stage.modelPolicy,
296
+ // Storage compatibility for databases created before assignment policies.
297
+ requiredRole: stage.assignmentPolicy.mode === 'fixed' ? stage.assignmentPolicy.fixedVpId : '',
298
+ context,
299
+ instruction: actionInstruction(stage, workItem, context),
300
+ maxAttempts: stage.maxAttempts,
86
301
  };
87
302
  }
88
303
 
304
+ export function initialActionFor(workItem) {
305
+ const stage = getWorkflowSteps(workItem)[0];
306
+ return actionForStage(stage, workItem, []);
307
+ }
308
+
89
309
  export function actionContextFromRuns(runs = []) {
90
310
  return runs
91
311
  .filter(run => run && run.summary)
92
312
  .map(run => ({
93
313
  type: run.actionType || 'action',
94
- role: run.roleSnapshot?.id || run.vpSnapshot?.id || null,
314
+ vpId: run.vpSnapshot?.id || null,
315
+ role: run.roleSnapshot?.id || null,
95
316
  summary: run.summary,
96
317
  evidence: Array.isArray(run.evidence) ? run.evidence : [],
97
318
  reviewDecision: run.reviewDecision || null,