@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 +1 -1
- package/yeaft/tools/create-work-item.js +5 -0
- package/yeaft/work-center/bridge.js +1 -1
- package/yeaft/work-center/controller.js +2 -0
- package/yeaft/work-center/projection.js +21 -0
- package/yeaft/work-center/runner.js +1 -1
- package/yeaft/work-center/service.js +10 -5
- package/yeaft/work-center/store.js +10 -3
- package/yeaft/work-center/workflow.js +103 -5
package/package.json
CHANGED
|
@@ -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": "
|
|
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
|
-
|
|
50
|
-
|
|
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 =
|
|
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
|
|
282
|
+
export function listWorkItemTypeTemplates(settings) {
|
|
247
283
|
const normalized = normalizeWorkCenterSettings(settings);
|
|
248
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|