@yeaft/webchat-agent 1.0.157 → 1.0.158

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.157",
3
+ "version": "1.0.158",
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",
@@ -32,6 +32,10 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
32
32
  items: { type: 'string' },
33
33
  description: { en: 'Verifiable completion criteria', zh: '可验证的完成条件' },
34
34
  },
35
+ workItemType: {
36
+ type: 'string',
37
+ description: { en: 'Optional explicit Work Item type; omit or use auto for LLM inference', zh: '可选的工作项类型;省略或填写 auto 时由 LLM 推断' },
38
+ },
35
39
  workDir: {
36
40
  type: 'string',
37
41
  description: { en: 'Existing project directory for execution', zh: '执行时使用的已存在项目目录' },
@@ -59,6 +63,7 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
59
63
  title,
60
64
  goal,
61
65
  acceptanceCriteria: cleanCriteria(input.acceptanceCriteria),
66
+ workItemType: typeof input.workItemType === 'string' ? input.workItemType.trim() : 'auto',
62
67
  workDir: typeof input.workDir === 'string' ? input.workDir.trim() : (ctx.cwd || ''),
63
68
  // The Agent-local Work Center settings choose the default workflow and
64
69
  // freeze its policy snapshot. Tool callers create the contract; they do
@@ -22,7 +22,7 @@ const BROWSER_DETAIL_OPS = new Set(['get', 'create', 'update', 'start', 'cancel'
22
22
  // client-supplied value and only emits files resolved from owned upload ids.
23
23
  const BROWSER_FILE_FIELDS = Object.freeze({
24
24
  create: [
25
- 'title', 'goal', 'acceptanceCriteria', 'workDir', 'reuseMemory', 'origin',
25
+ 'title', 'goal', 'acceptanceCriteria', 'workItemType', 'workDir', 'reuseMemory', 'origin',
26
26
  'linkedSessionIds', 'files', 'start',
27
27
  ],
28
28
  guide: ['id', 'guidance', 'actionId', 'revision', 'files'],
@@ -195,6 +195,7 @@ export class WorkflowController {
195
195
  assignmentPolicy: previous.assignmentPolicy,
196
196
  modelPolicy: previous.modelPolicy,
197
197
  requiredRole: previous.requiredRole,
198
+ brief: previous.brief,
198
199
  };
199
200
  return {
200
201
  ...step,
@@ -220,6 +221,7 @@ export class WorkflowController {
220
221
  assignmentPolicy: previous.assignmentPolicy,
221
222
  modelPolicy: previous.modelPolicy,
222
223
  requiredRole: previous.requiredRole,
224
+ brief: previous.brief,
223
225
  }
224
226
  : initialActionFor(workItem);
225
227
  const context = Array.isArray(previous?.context) ? [...previous.context] : [];
@@ -1,3 +1,5 @@
1
+ import { normalizeActionBrief } from './workflow.js';
2
+
1
3
  function currentAction(detail) {
2
4
  if (!detail?.currentActionId || !Array.isArray(detail.actions)) return null;
3
5
  return detail.actions.find(action => action.id === detail.currentActionId) || null;
@@ -43,6 +45,7 @@ function actionExecution(action, runs) {
43
45
  ...executionStats(action),
44
46
  response: '',
45
47
  progressRevision: 0,
48
+ messages: [],
46
49
  };
47
50
  }
48
51
  const stats = sumExecutionStats(matchingRuns);
@@ -50,10 +53,21 @@ function actionExecution(action, runs) {
50
53
  count(right.progressRevision) - count(left.progressRevision)
51
54
  || count(right.startedAt) - count(left.startedAt)
52
55
  ))[0];
56
+ const messages = [...matchingRuns]
57
+ .sort((left, right) => count(left.startedAt) - count(right.startedAt))
58
+ .map((run, index) => ({
59
+ id: `${action.id}:${index + 1}`,
60
+ status: run.status || 'running',
61
+ text: typeof run.response === 'string' ? run.response.trim().slice(0, 16_000) : '',
62
+ createdAt: count(run.startedAt),
63
+ updatedAt: count(run.endedAt || run.startedAt),
64
+ }))
65
+ .filter(message => message.text);
53
66
  return {
54
67
  ...stats,
55
68
  response: typeof latest?.response === 'string' ? latest.response : '',
56
69
  progressRevision: count(latest?.progressRevision),
70
+ messages,
57
71
  };
58
72
  }
59
73
 
@@ -87,12 +101,14 @@ function projectAction(action, runs) {
87
101
  stageId: action.stageId || action.type,
88
102
  assignmentPolicy: projectAssignmentPolicy(action.assignmentPolicy),
89
103
  requiredRole: action.requiredRole || '',
104
+ brief: normalizeActionBrief(action.brief, action.type),
90
105
  status: action.status,
91
106
  executionStats: executionStats(execution),
92
107
  loopCount: execution.loopCount,
93
108
  toolCount: execution.toolCount,
94
109
  response: execution.response,
95
110
  progressRevision: execution.progressRevision,
111
+ messages: execution.messages,
96
112
  };
97
113
  }
98
114
 
@@ -108,6 +124,7 @@ function projectActionStats(detail) {
108
124
  toolCount: projected.toolCount,
109
125
  response: projected.response,
110
126
  progressRevision: projected.progressRevision,
127
+ messages: projected.messages,
111
128
  };
112
129
  });
113
130
  }
@@ -145,6 +162,10 @@ export function projectWorkItemDetail(detail) {
145
162
  attachments: projectAttachments(detail.attachments),
146
163
  createdAt: detail.createdAt,
147
164
  updatedAt: detail.updatedAt,
165
+ actionCount: Array.isArray(detail.actions) ? detail.actions.length : 0,
166
+ actionSummary: Array.isArray(detail.actions)
167
+ ? detail.actions.map(action => action.type).filter(Boolean).join(' → ')
168
+ : '',
148
169
  actions: Array.isArray(detail.actions)
149
170
  ? detail.actions.map(action => projectAction(action, detail.runs))
150
171
  : [],
@@ -315,7 +315,7 @@ function completionContract(action, workItem) {
315
315
  ? ',\n "contractPatch": { "goal": "optional refined goal", "acceptanceCriteria": ["optional refined criterion"] }'
316
316
  : '';
317
317
  const planField = action.type === 'triage' && workItem?.workflowSnapshot?.planningMode === 'ai'
318
- ? ',\n "plan": { "workItemType": "specific-lowercase-slug", "actions": [{ "id": "stable-id", "name": "User-facing name", "type": "extensible-lowercase-slug (built-ins include research|design|diagnose|implement|migrate|test|review|document|operate|deliver|write|custom)", "capability": "specific executor capability", "objective": "independently executable and verifiable Action objective", "separateFromActionTypes": ["optional prior Action type"], "changesRequestedActionId": "for review: optional earlier editable Action id; omit to use nearest", "maxAttempts": 2 }] }'
318
+ ? ',\n "plan": { "workItemType": "specific-lowercase-slug", "actions": [{ "id": "stable-id", "name": "User-facing name", "type": "extensible-lowercase-slug (built-ins include research|design|diagnose|implement|migrate|test|review|document|operate|deliver|write|custom)", "capability": "specific executor capability", "objective": "what this Action must do", "approach": "how this Action should do it", "expectedOutcome": "verifiable result this Action must produce", "separateFromActionTypes": ["optional prior Action type"], "changesRequestedActionId": "for review: optional earlier editable Action id; omit to use nearest", "maxAttempts": 2 }] }'
319
319
  : '';
320
320
  const acceptanceChecks = (workItem?.acceptanceCriteria || []).map(criterion => ({
321
321
  criterion,
@@ -15,6 +15,7 @@ import { projectWorkItemDetail, projectWorkItemSummary } from './projection.js';
15
15
  import { readWorkCenterSettings, writeWorkCenterSettings } from './settings.js';
16
16
  import {
17
17
  defaultWorkCenterStageInstructions,
18
+ listWorkItemTypeTemplates,
18
19
  resolvePlanningWorkflowSnapshot,
19
20
  resolveWorkflowSnapshot,
20
21
  } from './workflow.js';
@@ -45,10 +46,14 @@ export class WorkCenterService {
45
46
  this.runtimeInfoProvider = typeof options.runtimeInfoProvider === 'function'
46
47
  ? options.runtimeInfoProvider
47
48
  : async () => ({ vps: [], models: [], primaryModel: null, fastModel: null });
48
- this.runtimeInfo = async () => ({
49
- ...(await this.runtimeInfoProvider()),
50
- defaultStageInstructions: defaultWorkCenterStageInstructions(),
51
- });
49
+ this.runtimeInfo = async () => {
50
+ const settings = this.settingsReader(this.yeaftDir);
51
+ return {
52
+ ...(await this.runtimeInfoProvider()),
53
+ defaultStageInstructions: defaultWorkCenterStageInstructions(),
54
+ workItemTypes: listWorkItemTypeTemplates(settings),
55
+ };
56
+ };
52
57
  this.ownerBootId = options.ownerBootId || randomUUID();
53
58
  this.attachmentRoot = options.attachmentRoot || join(yeaftDir, 'work-center', 'attachments');
54
59
  this.store = options.store || new WorkItemStore(join(yeaftDir, 'work-center', 'work-center.db'));
@@ -92,7 +97,7 @@ export class WorkCenterService {
92
97
  const workflowTemplate = explicitWorkflow || 'ai-planned';
93
98
  const workflowSnapshot = explicitWorkflow
94
99
  ? resolveWorkflowSnapshot(settings, explicitWorkflow, payload.stageOverrides)
95
- : resolvePlanningWorkflowSnapshot(settings);
100
+ : resolvePlanningWorkflowSnapshot(settings, payload.workItemType);
96
101
  const runtime = !Object.hasOwn(payload, 'workDir') && !settings.defaultWorkDir
97
102
  ? await this.runtimeInfo()
98
103
  : null;
@@ -5,7 +5,7 @@ import { randomUUID } from 'node:crypto';
5
5
  import { normalizeEvidence } from './evidence.js';
6
6
  import { normalizeActionCheckpoint } from './action-checkpoint.js';
7
7
 
8
- const SCHEMA_VERSION = 7;
8
+ const SCHEMA_VERSION = 8;
9
9
  const OPEN_ACTION_STATUSES = "'ready','running','waiting'";
10
10
  const MAX_REUSABLE_CONTEXT_ITEMS = 12;
11
11
  const MAX_RUN_RESPONSE_CHARS = 65_536;
@@ -75,6 +75,7 @@ function mapAction(row) {
75
75
  modelPolicy: parseJson(row.model_policy, null),
76
76
  requiredRole: row.required_role || '',
77
77
  instruction: row.instruction,
78
+ brief: parseJson(row.brief, null),
78
79
  context: parseJson(row.context, []),
79
80
  contractRevision: row.contract_revision,
80
81
  status: row.status,
@@ -202,6 +203,7 @@ export class WorkItemStore {
202
203
  assignment_policy TEXT,
203
204
  model_policy TEXT,
204
205
  instruction TEXT NOT NULL,
206
+ brief TEXT,
205
207
  context TEXT NOT NULL DEFAULT '[]',
206
208
  contract_revision INTEGER NOT NULL DEFAULT 1,
207
209
  status TEXT NOT NULL,
@@ -275,6 +277,9 @@ export class WorkItemStore {
275
277
  if (!hasColumn(this.db, 'work_items', 'reuse_memory')) {
276
278
  this.db.exec('ALTER TABLE work_items ADD COLUMN reuse_memory INTEGER NOT NULL DEFAULT 1');
277
279
  }
280
+ if (!hasColumn(this.db, 'actions', 'brief')) {
281
+ this.db.exec('ALTER TABLE actions ADD COLUMN brief TEXT');
282
+ }
278
283
  if (!hasColumn(this.db, 'actions', 'context')) {
279
284
  this.db.exec("ALTER TABLE actions ADD COLUMN context TEXT NOT NULL DEFAULT '[]'");
280
285
  }
@@ -401,6 +406,7 @@ export class WorkItemStore {
401
406
  modelPolicy: input.modelPolicy || null,
402
407
  requiredRole: input.requiredRole || '',
403
408
  instruction: input.instruction || '',
409
+ brief: input.brief && typeof input.brief === 'object' ? input.brief : null,
404
410
  context: Array.isArray(input.context) ? input.context : [],
405
411
  contractRevision: Number.isInteger(input.contractRevision) ? input.contractRevision : 1,
406
412
  status: input.status || 'ready',
@@ -413,9 +419,9 @@ export class WorkItemStore {
413
419
  };
414
420
  this.db.prepare(`INSERT INTO actions
415
421
  (id, work_item_id, sequence, type, required_role, stage_id, assignment_policy, model_policy,
416
- instruction, context, contract_revision, status, attempt, max_attempts, current_run_id,
422
+ instruction, brief, context, contract_revision, status, attempt, max_attempts, current_run_id,
417
423
  lease_epoch, created_at, updated_at)
418
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?)`).run(
424
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, 0, ?, ?)`).run(
419
425
  action.id,
420
426
  workItemId,
421
427
  action.sequence,
@@ -425,6 +431,7 @@ export class WorkItemStore {
425
431
  stringify(action.assignmentPolicy),
426
432
  stringify(action.modelPolicy),
427
433
  action.instruction,
434
+ stringify(action.brief),
428
435
  stringify(action.context),
429
436
  action.contractRevision,
430
437
  action.status,
@@ -34,6 +34,38 @@ const DEFAULT_STAGE_INSTRUCTIONS = Object.freeze({
34
34
  custom: 'Complete the Action objective using repository facts and the WorkItem contract. State the approach, handle relevant risks and boundary conditions, produce the requested artifact or change, verify the result, and return concrete evidence plus any residual uncertainty.',
35
35
  });
36
36
 
37
+ const DEFAULT_ACTION_BRIEFS = Object.freeze({
38
+ triage: ['Turn the request into a precise, executable Work Item contract and plan.', 'Inspect the relevant facts, resolve scope and risks, then select the smallest reliable Action sequence.', 'A frozen Work Item type, validated contract, and executable Action plan.'],
39
+ research: ['Resolve the question or uncertainty named by this Action with verifiable evidence.', 'Inspect repository facts and authoritative sources, separating observations from inference.', 'A concise evidence-backed conclusion that the next Action can use.'],
40
+ design: ['Define the smallest maintainable solution for this Action.', 'Inspect existing architecture, data flow, compatibility constraints, and failure boundaries.', 'Concrete implementation guidance with explicit trade-offs and risks.'],
41
+ diagnose: ['Establish the root cause of the reported behavior.', 'Reproduce the behavior, trace the responsible path, and distinguish evidence from hypotheses.', 'A proven root cause, minimal correction, and regression-test plan.'],
42
+ implement: ['Implement the required change with the smallest correct diff.', 'Follow repository conventions, handle relevant boundaries, and add focused tests while making the change.', 'A maintainable implementation with focused verification evidence.'],
43
+ migrate: ['Move existing data or behavior to the required shape without losing semantics.', 'Fence compatibility and rollback boundaries, then validate legacy and already-current states.', 'A repeatable or safely fenced migration with representative evidence.'],
44
+ test: ['Verify the current result against the Work Item acceptance criteria.', 'Run focused checks first, then broader risk-appropriate tests including failure and compatibility cases.', 'Reproducible pass/fail evidence for every applicable acceptance criterion.'],
45
+ review: ['Review the current result independently against the Work Item contract.', 'Prioritize correctness, security, data loss, compatibility, and missing tests over style preferences.', 'An approval or concrete change request supported by evidence.'],
46
+ document: ['Update the documentation required by this Action.', 'Align terminology and examples with actual behavior and verify links, commands, and language requirements.', 'Accurate, usable documentation that matches the delivered behavior.'],
47
+ operate: ['Perform the requested operational change safely.', 'Check preconditions, apply safety fences, preserve rollback options, and verify authoritative state.', 'A verified operational postcondition with handoff and rollback evidence.'],
48
+ deliver: ['Deliver the approved Work Item result using repository release policy.', 'Recheck immutable reviewed state, run final gates, and publish only the requested artifacts.', 'A traceable delivery with commit, artifact, deployment, and residual-risk evidence.'],
49
+ write: ['Produce the written deliverable requested by this Action.', 'Use available evidence, the intended audience, and the required terminology and structure.', 'A complete written artifact verified against the acceptance criteria.'],
50
+ custom: ['Complete the domain-specific objective defined for this Action.', 'Use repository facts, handle relevant risks and boundaries, and verify the produced result.', 'The requested artifact or change with concrete evidence and residual uncertainty.'],
51
+ });
52
+
53
+ function defaultActionBrief(type) {
54
+ const [objective, approach, expectedOutcome] = DEFAULT_ACTION_BRIEFS[STAGE_TYPES.has(type) ? type : 'custom'];
55
+ return { objective, approach, expectedOutcome };
56
+ }
57
+
58
+ export function normalizeActionBrief(value, type) {
59
+ const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
60
+ const defaults = defaultActionBrief(type);
61
+ return Object.fromEntries(Object.keys(defaults).map(key => [
62
+ key,
63
+ typeof source[key] === 'string' && source[key].trim()
64
+ ? source[key].trim().slice(0, 2_000)
65
+ : defaults[key],
66
+ ]));
67
+ }
68
+
37
69
  const DEFAULT_SOFTWARE_CHANGE_STAGES = Object.freeze([
38
70
  {
39
71
  id: 'triage', name: 'Triage', type: 'triage',
@@ -145,6 +177,7 @@ export function normalizeWorkflowDefinition(value, index = 0) {
145
177
  id: stageId,
146
178
  name: String(source.name || stageId).trim() || stageId,
147
179
  type,
180
+ ...normalizeActionBrief(source, type),
148
181
  instruction: typeof source.instruction === 'string' && source.instruction.trim()
149
182
  ? source.instruction.trim()
150
183
  : defaultWorkCenterStageInstruction(type),
@@ -179,6 +212,9 @@ export function normalizeWorkflowDefinition(value, index = 0) {
179
212
  globalInstructions: normalizeGlobalInstructions(value.globalInstructions),
180
213
  modelPolicy: normalizeModelPolicy(value.modelPolicy),
181
214
  actionInstructions: normalizeActionInstructions(value.actionInstructions),
215
+ actionTemplates: Array.isArray(value.actionTemplates)
216
+ ? value.actionTemplates.map(template => normalizeWorkflowDefinition(template))
217
+ : [],
182
218
  stages,
183
219
  };
184
220
  }
@@ -243,20 +279,52 @@ export function normalizeWorkCenterSettings(value) {
243
279
  };
244
280
  }
245
281
 
246
- export function resolvePlanningWorkflowSnapshot(settings) {
282
+ export function listWorkItemTypeTemplates(settings) {
247
283
  const normalized = normalizeWorkCenterSettings(settings);
248
- const triageInstruction = `${normalized.actionInstructions.triage}\n\nChoose a specific workItemType and the smallest reliable sequence of 1 to 8 Actions for this WorkItem. Action types are extensible lowercase slugs: use a built-in type when its reusable policy fits, or define a precise domain type when it does not. For every Action, provide an objective that is independently executable and verifiable, plus the capability needed to select the right VP. Add research, design, diagnosis, migration, documentation, operations, testing, independent review, or delivery only when the task requires them. Do not copy a generic workflow.`;
284
+ return normalized.workflows.map(workflow => ({
285
+ id: workflow.workItemType || workflow.id,
286
+ name: workflow.name,
287
+ actionCount: workflow.stages.length,
288
+ }));
289
+ }
290
+
291
+ export function resolvePlanningWorkflowSnapshot(settings, requestedWorkItemType = null) {
292
+ const normalized = normalizeWorkCenterSettings(settings);
293
+ const requestedType = typeof requestedWorkItemType === 'string'
294
+ && requestedWorkItemType.trim()
295
+ && requestedWorkItemType.trim().toLowerCase() !== 'auto'
296
+ ? cleanId(requestedWorkItemType, '').slice(0, 64)
297
+ : '';
298
+ const actionTemplates = normalized.workflows.map(workflow => normalizeWorkflowDefinition({
299
+ ...workflow,
300
+ workItemType: workflow.workItemType || workflow.id,
301
+ }));
302
+ const matchingTemplate = requestedType
303
+ ? actionTemplates.find(template => template.workItemType === requestedType)
304
+ : null;
305
+ if (matchingTemplate) return matchingTemplate;
306
+
307
+ const catalog = actionTemplates
308
+ .map(template => `${template.workItemType}: ${template.name} (${template.stages.map(stage => stage.type).join(' -> ')})`)
309
+ .join('\n');
310
+ const typeInstruction = requestedType
311
+ ? `The user explicitly selected workItemType "${requestedType}". Keep that exact type.`
312
+ : 'Infer one specific workItemType from the contract.';
313
+ const triageInstruction = `${normalized.actionInstructions.triage}\n\n${typeInstruction}\nReusable Action templates:\n${catalog || '(none)'}\nIf the final type matches a reusable template, return its workItemType and omit actions so Work Center can apply that frozen template. Otherwise generate the smallest reliable sequence of 1 to 8 Actions. Action types are extensible lowercase slugs: use a built-in type when its reusable policy fits, or define a precise domain type when it does not. Every generated Action must state objective (what to do), approach (how to do it), expectedOutcome (the verifiable result), and capability. Add only Actions required by this task. Do not copy a generic workflow.`;
249
314
  return normalizeWorkflowDefinition({
250
315
  id: 'ai-planned',
251
316
  name: 'AI planned',
252
317
  planningMode: 'ai',
318
+ workItemType: requestedType || null,
253
319
  globalInstructions: normalized.globalInstructions,
254
320
  modelPolicy: normalized.modelPolicy,
255
321
  actionInstructions: normalized.actionInstructions,
322
+ actionTemplates,
256
323
  stages: [{
257
324
  id: 'triage',
258
325
  name: 'Triage',
259
326
  type: 'triage',
327
+ ...defaultActionBrief('triage'),
260
328
  instruction: triageInstruction,
261
329
  assignmentPolicy: {
262
330
  mode: 'auto', capability: 'triage', candidateVpIds: [], fixedVpId: null,
@@ -305,8 +373,24 @@ export function applyGeneratedPlan(workItem, rawPlan) {
305
373
  }
306
374
  const workItemType = cleanId(rawPlan.workItemType, '').slice(0, 64);
307
375
  if (!workItemType) throw new Error('AI-planned triage requires a valid workItemType');
376
+ if (source.workItemType && workItemType !== source.workItemType) {
377
+ throw new Error(`AI-planned triage must keep the selected workItemType "${source.workItemType}"`);
378
+ }
379
+ const reusableTemplate = source.actionTemplates.find(template => template.workItemType === workItemType);
380
+ if (reusableTemplate) {
381
+ const templateActions = reusableTemplate.stages.filter(stage => stage.type !== 'triage');
382
+ if (templateActions.length === 0) {
383
+ throw new Error(`Reusable Action template "${workItemType}" has no executable Actions`);
384
+ }
385
+ return normalizeWorkflowDefinition({
386
+ ...source,
387
+ workItemType,
388
+ actionTemplates: [],
389
+ stages: [source.stages[0], ...templateActions],
390
+ });
391
+ }
308
392
  if (!Array.isArray(rawPlan.actions) || rawPlan.actions.length < 1 || rawPlan.actions.length > 8) {
309
- throw new Error('AI-planned triage requires between 1 and 8 Actions');
393
+ throw new Error('AI-planned triage requires between 1 and 8 Actions when no reusable template exists');
310
394
  }
311
395
  const seen = new Set(['triage']);
312
396
  const generated = rawPlan.actions.map((rawAction, index) => {
@@ -318,6 +402,13 @@ export function applyGeneratedPlan(workItem, rawPlan) {
318
402
  seen.add(id);
319
403
  const objective = typeof input.objective === 'string' ? input.objective.trim().slice(0, 2_000) : '';
320
404
  if (!objective) throw new Error(`AI-planned Action "${id}" requires an objective`);
405
+ const defaults = defaultActionBrief(type);
406
+ const approach = typeof input.approach === 'string' && input.approach.trim()
407
+ ? input.approach.trim().slice(0, 2_000)
408
+ : defaults.approach;
409
+ const expectedOutcome = typeof input.expectedOutcome === 'string' && input.expectedOutcome.trim()
410
+ ? input.expectedOutcome.trim().slice(0, 2_000)
411
+ : defaults.expectedOutcome;
321
412
  const actionInstruction = Object.hasOwn(source.actionInstructions, type)
322
413
  ? source.actionInstructions[type]
323
414
  : source.actionInstructions.custom;
@@ -325,7 +416,10 @@ export function applyGeneratedPlan(workItem, rawPlan) {
325
416
  id,
326
417
  name: String(input.name || id).trim().slice(0, 120) || id,
327
418
  type,
328
- instruction: `${actionInstruction}\n\nAction type: ${type}\nAction objective for this WorkItem:\n${objective}`,
419
+ objective,
420
+ approach,
421
+ expectedOutcome,
422
+ instruction: actionInstruction,
329
423
  assignmentPolicy: {
330
424
  mode: 'auto',
331
425
  capability: cleanId(input.capability, type),
@@ -448,7 +542,10 @@ function renderContext(context = []) {
448
542
  export function actionInstruction(stage, workItem, context = []) {
449
543
  const criteria = (workItem.acceptanceCriteria || []).map(item => `- ${item}`).join('\n') || '- No explicit criteria';
450
544
  const common = `WorkItem: ${workItem.title}\nGoal: ${workItem.goal}\nAcceptance criteria:\n${criteria}${renderContext(context)}`;
451
- return `${common}\n\n${stage.instruction || defaultWorkCenterStageInstruction(stage.type)}`;
545
+ const policy = stage.instruction || defaultWorkCenterStageInstruction(stage.type);
546
+ const brief = normalizeActionBrief(stage.brief || stage, stage.type);
547
+ const contract = `Action type: ${stage.type}\nWhat to do:\n${brief.objective}\n\nHow to do it:\n${brief.approach}\n\nExpected result:\n${brief.expectedOutcome}`;
548
+ return `${common}\n\n${policy}\n\n${contract}`;
452
549
  }
453
550
 
454
551
  export function actionForStage(stage, workItem, context = []) {
@@ -459,6 +556,7 @@ export function actionForStage(stage, workItem, context = []) {
459
556
  modelPolicy: stage.modelPolicy,
460
557
  // Storage compatibility for databases created before assignment policies.
461
558
  requiredRole: stage.assignmentPolicy.mode === 'fixed' ? stage.assignmentPolicy.fixedVpId : '',
559
+ brief: normalizeActionBrief(stage, stage.type),
462
560
  context,
463
561
  instruction: actionInstruction(stage, workItem, context),
464
562
  maxAttempts: stage.maxAttempts,