@yeaft/webchat-agent 1.0.156 → 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/llm/openai-responses.js +13 -11
- package/yeaft/models.js +20 -5
- 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
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
} from './adapter.js';
|
|
42
42
|
import {
|
|
43
43
|
normalizeEffort,
|
|
44
|
+
getModelEffortOptions,
|
|
44
45
|
getThinkingCapability,
|
|
45
46
|
mapEffortToOpenAIReasoning,
|
|
46
47
|
} from '../models.js';
|
|
@@ -57,10 +58,9 @@ function thinkingV1Enabled() {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
/**
|
|
60
|
-
* Translate a normalised effort
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* adaptive efforts must be dropped, not downgraded.
|
|
61
|
+
* Translate a normalised effort into the OpenAI Responses `reasoning.effort`
|
|
62
|
+
* field. Model capability filtering happens at the call site because `max` is
|
|
63
|
+
* supported by GPT-5.6 but not by older OpenAI reasoning models.
|
|
64
64
|
*/
|
|
65
65
|
function effortForResponses(effort) {
|
|
66
66
|
return mapEffortToOpenAIReasoning(effort);
|
|
@@ -248,7 +248,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
248
248
|
// ─── Streaming ──────────────────────────────────────────
|
|
249
249
|
|
|
250
250
|
/**
|
|
251
|
-
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'minimal'|'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void }} params
|
|
251
|
+
* @param {{ model: string, system: string, messages: import('./adapter.js').UnifiedMessage[], tools?: import('./adapter.js').UnifiedToolDef[], maxTokens?: number, effort?: 'minimal'|'low'|'medium'|'high'|'xhigh'|'max', effortSource?: 'user'|'auto', effortContext?: object, extraBody?: object, signal?: AbortSignal, onRawExchange?: ({rawRequest, rawResponse}) => void, onRequestStart?: () => void }} params
|
|
252
252
|
*
|
|
253
253
|
* NOTE on `extraBody`: any keys you spread here are merged verbatim into
|
|
254
254
|
* the wire body and — because the verbatim debug feature is intentionally
|
|
@@ -257,7 +257,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
257
257
|
* `api-key` headers are auto-redacted (see `redactRawRequest` in
|
|
258
258
|
* `adapter.js`); request-body fields are caller-controlled.
|
|
259
259
|
*/
|
|
260
|
-
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, extraBody, signal, onRawExchange, onRequestStart }) {
|
|
260
|
+
async *stream({ model, system, messages, tools, maxTokens = 16384, effort, effortSource, effortContext = {}, extraBody, signal, onRawExchange, onRequestStart }) {
|
|
261
261
|
if (signal?.aborted) throw new LLMAbortError();
|
|
262
262
|
|
|
263
263
|
const body = {
|
|
@@ -276,8 +276,9 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
276
276
|
// Unknown / unsupported models silently drop the field.
|
|
277
277
|
const normEffort = normalizeEffort(effort);
|
|
278
278
|
if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
|
|
279
|
-
const cap = getThinkingCapability(model);
|
|
280
|
-
|
|
279
|
+
const cap = getThinkingCapability(model, effortContext);
|
|
280
|
+
const supportedEfforts = getModelEffortOptions(model, effortContext);
|
|
281
|
+
if (cap.supportsThinking && cap.thinkingProtocol === 'openai-reasoning' && supportedEfforts.includes(normEffort)) {
|
|
281
282
|
const wireEffort = effortForResponses(normEffort);
|
|
282
283
|
if (wireEffort) body.reasoning = { effort: wireEffort };
|
|
283
284
|
}
|
|
@@ -517,7 +518,7 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
517
518
|
* expose them, mirror the stream() instrumentation. Parity with
|
|
518
519
|
* anthropic.js's `call()`.
|
|
519
520
|
*/
|
|
520
|
-
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, extraBody, signal, onRequestStart }) {
|
|
521
|
+
async call({ model, system, messages, maxTokens = 4096, effort, effortSource, effortContext = {}, extraBody, signal, onRequestStart }) {
|
|
521
522
|
if (signal?.aborted) throw new LLMAbortError();
|
|
522
523
|
|
|
523
524
|
const body = {
|
|
@@ -530,8 +531,9 @@ export class OpenAIResponsesAdapter extends LLMAdapter {
|
|
|
530
531
|
// Mirror stream()'s thinking injection for non-streaming side queries.
|
|
531
532
|
const normEffort = normalizeEffort(effort);
|
|
532
533
|
if ((thinkingV1Enabled() || effortSource === 'user') && normEffort) {
|
|
533
|
-
const cap = getThinkingCapability(model);
|
|
534
|
-
|
|
534
|
+
const cap = getThinkingCapability(model, effortContext);
|
|
535
|
+
const supportedEfforts = getModelEffortOptions(model, effortContext);
|
|
536
|
+
if (cap.supportsThinking && cap.thinkingProtocol === 'openai-reasoning' && supportedEfforts.includes(normEffort)) {
|
|
535
537
|
const wireEffort = effortForResponses(normEffort);
|
|
536
538
|
if (wireEffort) body.reasoning = { effort: wireEffort };
|
|
537
539
|
}
|
package/yeaft/models.js
CHANGED
|
@@ -34,7 +34,7 @@ import { lookupModelLimitSync } from './llm/models-dev.js';
|
|
|
34
34
|
* @property {'anthropic' | 'anthropic-adaptive' | 'openai-reasoning' | 'none'} [thinkingProtocol] — task-327a:
|
|
35
35
|
* 'anthropic' → thinking:{type:'enabled', budget_tokens:N}
|
|
36
36
|
* 'anthropic-adaptive' → thinking:{type:'adaptive'} + output_config:{effort}
|
|
37
|
-
* 'openai-reasoning' → reasoning:{effort:'minimal'|'low'|'medium'|'high'|'xhigh'}
|
|
37
|
+
* 'openai-reasoning' → reasoning:{effort:'minimal'|'low'|'medium'|'high'|'xhigh'|'max'}
|
|
38
38
|
* 'none' (default) → parameter silently dropped by router
|
|
39
39
|
* @property {'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null} [defaultEffort] — adapter-level default
|
|
40
40
|
* when caller doesn't specify effort (null = no default / decision-tree decides).
|
|
@@ -405,12 +405,12 @@ export const ANTHROPIC_THINKING_BUDGETS = {
|
|
|
405
405
|
};
|
|
406
406
|
|
|
407
407
|
/**
|
|
408
|
-
* Map a Yeaft effort level to the OpenAI reasoning.effort enum.
|
|
409
|
-
*
|
|
410
|
-
*
|
|
408
|
+
* Map a Yeaft effort level to the OpenAI reasoning.effort enum. Supported
|
|
409
|
+
* values are model-dependent; capability filtering happens before this wire
|
|
410
|
+
* translation. Unsupported values are dropped; the adapter MUST NOT error.
|
|
411
411
|
*
|
|
412
412
|
* @param {Effort} effort
|
|
413
|
-
* @returns {'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | null}
|
|
413
|
+
* @returns {'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' | null}
|
|
414
414
|
*/
|
|
415
415
|
export function mapEffortToOpenAIReasoning(effort) {
|
|
416
416
|
if (!effort) return null;
|
|
@@ -420,11 +420,13 @@ export function mapEffortToOpenAIReasoning(effort) {
|
|
|
420
420
|
case 'medium': return 'medium';
|
|
421
421
|
case 'high': return 'high';
|
|
422
422
|
case 'xhigh': return 'xhigh';
|
|
423
|
+
case 'max': return 'max';
|
|
423
424
|
default: return null;
|
|
424
425
|
}
|
|
425
426
|
}
|
|
426
427
|
|
|
427
428
|
export const OPENAI_REASONING_EFFORT_OPTIONS = ['minimal', 'low', 'medium', 'high', 'xhigh'];
|
|
429
|
+
export const OPENAI_MAX_REASONING_EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
428
430
|
export const ANTHROPIC_MANUAL_EFFORT_OPTIONS = ['low', 'medium', 'high'];
|
|
429
431
|
export const ANTHROPIC_ADAPTIVE_EFFORT_OPTIONS = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
430
432
|
export const ANTHROPIC_ADAPTIVE_MAX_EFFORT_OPTIONS = ['low', 'medium', 'high', 'max'];
|
|
@@ -454,6 +456,19 @@ function inferThinkingCapability(model) {
|
|
|
454
456
|
const id = parseModelRef(model).modelId.toLowerCase();
|
|
455
457
|
if (!id) return null;
|
|
456
458
|
|
|
459
|
+
// GPT-5.6 adds the model-specific `max` tier. Keep this ahead of the generic
|
|
460
|
+
// GPT inference so older OpenAI models do not advertise a value they reject.
|
|
461
|
+
// Provider suffixes such as gpt-5.6-sol are variants of the same model family.
|
|
462
|
+
if (/^gpt-5\.6($|[-.])/.test(id)) {
|
|
463
|
+
return {
|
|
464
|
+
supportsThinking: true,
|
|
465
|
+
thinkingProtocol: 'openai-reasoning',
|
|
466
|
+
defaultEffort: null,
|
|
467
|
+
maxBudgetTokens: null,
|
|
468
|
+
effortOptions: OPENAI_MAX_REASONING_EFFORT_OPTIONS,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
457
472
|
if (/^(gpt-5|o1|o3|o4|chatgpt-|codex-)/.test(id)) {
|
|
458
473
|
return { supportsThinking: true, thinkingProtocol: 'openai-reasoning', defaultEffort: null, maxBudgetTokens: null };
|
|
459
474
|
}
|
|
@@ -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,
|