@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.
- package/package.json +1 -1
- package/yeaft/config.js +6 -3
- package/yeaft/tools/create-work-item.js +3 -1
- package/yeaft/work-center/assignment.js +135 -0
- package/yeaft/work-center/bridge.js +50 -4
- package/yeaft/work-center/controller.js +72 -15
- package/yeaft/work-center/planner.js +66 -0
- package/yeaft/work-center/projection.js +97 -2
- package/yeaft/work-center/runner.js +53 -24
- package/yeaft/work-center/service.js +41 -5
- package/yeaft/work-center/settings.js +59 -0
- package/yeaft/work-center/store.js +136 -14
- package/yeaft/work-center/workflow.js +254 -33
package/package.json
CHANGED
package/yeaft/config.js
CHANGED
|
@@ -454,11 +454,14 @@ export function loadConfig(overrides = {}) {
|
|
|
454
454
|
const p = normalizeKnownProviderForRuntime(rawProvider);
|
|
455
455
|
const normalized = normalizeProviderModels(p);
|
|
456
456
|
for (const m of normalized) {
|
|
457
|
-
|
|
458
|
-
|
|
457
|
+
const ref = p.name ? `${p.name}/${m.id}` : m.id;
|
|
458
|
+
// A model id is only unique inside its provider. Keep provider-qualified
|
|
459
|
+
// duplicates so an explicit `provider/model` never disappears from the
|
|
460
|
+
// runtime catalog just because another provider exposes the same id.
|
|
461
|
+
if (!config.availableModels.some(am => am.ref === ref)) {
|
|
459
462
|
const entry = {
|
|
460
463
|
id: m.id,
|
|
461
|
-
ref
|
|
464
|
+
ref,
|
|
462
465
|
provider: p.name,
|
|
463
466
|
label: m.id,
|
|
464
467
|
};
|
|
@@ -60,7 +60,9 @@ Use this when work must continue beyond the current turn, needs role handoffs, r
|
|
|
60
60
|
goal,
|
|
61
61
|
acceptanceCriteria: cleanCriteria(input.acceptanceCriteria),
|
|
62
62
|
workDir: typeof input.workDir === 'string' ? input.workDir.trim() : (ctx.cwd || ''),
|
|
63
|
-
|
|
63
|
+
// The Agent-local Work Center settings choose the default workflow and
|
|
64
|
+
// freeze its policy snapshot. Tool callers create the contract; they do
|
|
65
|
+
// not get to smuggle a different dispatch policy into it.
|
|
64
66
|
origin: {
|
|
65
67
|
sessionId,
|
|
66
68
|
messageId: ctx.inboundEnvelope?.msgId || null,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { parseModelRef } from '../models.js';
|
|
2
|
+
import { normalizeAssignmentPolicy, normalizeModelPolicy } from './workflow.js';
|
|
3
|
+
|
|
4
|
+
function policyError(message) {
|
|
5
|
+
const error = new Error(message);
|
|
6
|
+
error.retryable = false;
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const CAPABILITY_TERMS = Object.freeze({
|
|
11
|
+
triage: ['triage', 'requirement', 'flow', 'product', 'strategy', 'cross-domain', 'analysis'],
|
|
12
|
+
implement: ['implement', 'engineer', 'developer', 'engineering', 'systems', 'code', 'execution'],
|
|
13
|
+
test: ['test', 'testing', 'quality', 'qa', 'verification', 'reliability'],
|
|
14
|
+
review: ['review', 'reviewer', 'refactor', 'architecture', 'code-smells', 'readability', 'maintainability'],
|
|
15
|
+
deliver: ['deliver', 'release', 'ship', 'git', 'engineering', 'systems', 'execution'],
|
|
16
|
+
research: ['research', 'science', 'analysis', 'evidence', 'investigation'],
|
|
17
|
+
write: ['write', 'writer', 'writing', 'documentation', 'editor', 'communication'],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
function vpSearchText(vp) {
|
|
21
|
+
return [
|
|
22
|
+
vp.id,
|
|
23
|
+
vp.name,
|
|
24
|
+
vp.nameZh,
|
|
25
|
+
vp.role,
|
|
26
|
+
vp.roleZh,
|
|
27
|
+
vp.area,
|
|
28
|
+
...(Array.isArray(vp.aliases) ? vp.aliases : []),
|
|
29
|
+
...(Array.isArray(vp.traits) ? vp.traits : []),
|
|
30
|
+
].filter(Boolean).join(' ').toLowerCase();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function capabilityScore(vp, capability) {
|
|
34
|
+
const needle = String(capability || '').trim().toLowerCase();
|
|
35
|
+
if (!needle) return 0;
|
|
36
|
+
const text = vpSearchText(vp);
|
|
37
|
+
const terms = CAPABILITY_TERMS[needle] || [needle];
|
|
38
|
+
let score = 0;
|
|
39
|
+
for (const term of terms) {
|
|
40
|
+
if (text.includes(term)) score += term === needle ? 6 : 2;
|
|
41
|
+
}
|
|
42
|
+
if (String(vp.area || '').toLowerCase() === needle) score += 4;
|
|
43
|
+
return score;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function priorVpIdsForSeparation(policy, priorRuns) {
|
|
47
|
+
const separatedTypes = new Set(policy.separateFromStageTypes || []);
|
|
48
|
+
if (separatedTypes.size === 0) return new Set();
|
|
49
|
+
return new Set((priorRuns || [])
|
|
50
|
+
.filter(run => separatedTypes.has(run.actionType) || separatedTypes.has(run.roleSnapshot?.actionType))
|
|
51
|
+
.map(run => run.vpSnapshot?.id)
|
|
52
|
+
.filter(Boolean));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function selectWorkItemVp({ policy: rawPolicy, stageType, vps, priorRuns = [] }) {
|
|
56
|
+
const policy = normalizeAssignmentPolicy(rawPolicy, stageType);
|
|
57
|
+
const all = Array.isArray(vps) ? vps.filter(vp => vp?.id) : [];
|
|
58
|
+
if (all.length === 0) throw policyError('Work Center has no available VPs');
|
|
59
|
+
const byId = new Map(all.map(vp => [vp.id, vp]));
|
|
60
|
+
const excluded = priorVpIdsForSeparation(policy, priorRuns);
|
|
61
|
+
|
|
62
|
+
if (policy.mode === 'fixed') {
|
|
63
|
+
const fixed = byId.get(policy.fixedVpId);
|
|
64
|
+
if (!fixed) throw policyError(`Fixed Work Center VP is unavailable: ${policy.fixedVpId}`);
|
|
65
|
+
if (excluded.has(fixed.id)) throw policyError(`Work Center separation policy excludes VP: ${fixed.id}`);
|
|
66
|
+
return { vp: fixed, reason: `fixed:${fixed.id}`, policy };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pool = policy.mode === 'pool'
|
|
70
|
+
? policy.candidateVpIds.map(id => byId.get(id)).filter(Boolean)
|
|
71
|
+
: all;
|
|
72
|
+
if (pool.length === 0) throw policyError('No configured Work Center VP candidates are available');
|
|
73
|
+
const eligible = pool.filter(vp => !excluded.has(vp.id));
|
|
74
|
+
if (eligible.length === 0) throw policyError('No Work Center VP satisfies the stage separation policy');
|
|
75
|
+
|
|
76
|
+
const ranked = eligible
|
|
77
|
+
.map(vp => ({ vp, score: capabilityScore(vp, policy.capability || stageType) }))
|
|
78
|
+
.sort((left, right) => right.score - left.score || left.vp.id.localeCompare(right.vp.id));
|
|
79
|
+
if (ranked[0].score === 0 && policy.mode === 'auto') {
|
|
80
|
+
throw policyError(`No Work Center VP matches capability: ${policy.capability || stageType}`);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
vp: ranked[0].vp,
|
|
84
|
+
reason: `${policy.mode}:${policy.capability || stageType}:score=${ranked[0].score}`,
|
|
85
|
+
policy,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function availableModel(config, ref) {
|
|
90
|
+
const models = Array.isArray(config.availableModels) ? config.availableModels : [];
|
|
91
|
+
if (models.length === 0) return null;
|
|
92
|
+
const parsed = parseModelRef(ref);
|
|
93
|
+
return models.find(model => model.ref === ref)
|
|
94
|
+
|| models.find(model => !parsed.providerName && model.id === parsed.modelId)
|
|
95
|
+
|| null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function resolveWorkItemModel(config, vp, rawPolicy) {
|
|
99
|
+
const policy = normalizeModelPolicy(rawPolicy);
|
|
100
|
+
let model;
|
|
101
|
+
let source;
|
|
102
|
+
if (policy.mode === 'specific') {
|
|
103
|
+
model = policy.model;
|
|
104
|
+
source = 'stage-specific';
|
|
105
|
+
} else if (policy.mode === 'primary') {
|
|
106
|
+
model = config.primaryModel || config.model || null;
|
|
107
|
+
source = 'agent-primary';
|
|
108
|
+
} else if (policy.mode === 'fast') {
|
|
109
|
+
model = config.fastModel || null;
|
|
110
|
+
source = 'agent-fast';
|
|
111
|
+
} else if (vp.modelHint === 'fast') {
|
|
112
|
+
model = config.fastModel || config.primaryModel || config.model || null;
|
|
113
|
+
source = config.fastModel ? 'vp-fast' : 'vp-fast-primary-fallback';
|
|
114
|
+
} else {
|
|
115
|
+
model = config.primaryModel || config.model || null;
|
|
116
|
+
source = vp.modelHint === 'primary' ? 'vp-primary' : 'agent-default';
|
|
117
|
+
}
|
|
118
|
+
if (!model) throw policyError(`Work Center model policy "${policy.mode}" has no configured model`);
|
|
119
|
+
const available = availableModel(config, model);
|
|
120
|
+
if (Array.isArray(config.availableModels) && config.availableModels.length > 0 && !available) {
|
|
121
|
+
throw policyError(`Configured Work Center model is unavailable: ${model}`);
|
|
122
|
+
}
|
|
123
|
+
const effortOptions = Array.isArray(available?.effortOptions) ? available.effortOptions : [];
|
|
124
|
+
if (policy.effort && !effortOptions.includes(policy.effort)) {
|
|
125
|
+
throw policyError(`Configured Work Center effort is unsupported by ${model}: ${policy.effort}`);
|
|
126
|
+
}
|
|
127
|
+
const parsed = parseModelRef(model);
|
|
128
|
+
return {
|
|
129
|
+
model,
|
|
130
|
+
effort: policy.effort || null,
|
|
131
|
+
provider: available?.provider || parsed.providerName || null,
|
|
132
|
+
source,
|
|
133
|
+
policy,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import ctx from '../../context.js';
|
|
2
2
|
import { sendToServer } from '../../connection/buffer.js';
|
|
3
|
-
import { ensureSessionLoaded } from '../web-bridge.js';
|
|
3
|
+
import { ensureSessionLoaded, resetYeaftSession } from '../web-bridge.js';
|
|
4
4
|
import { defaultRegistry } from '../vp/registry.js';
|
|
5
5
|
import { scanVpLibrary } from '../vp/vp-store.js';
|
|
6
6
|
import { WorkCenterService } from './service.js';
|
|
7
7
|
import { WorkItemRunner } from './runner.js';
|
|
8
|
-
import { projectWorkCenterEvent } from './projection.js';
|
|
8
|
+
import { projectWorkCenterEvent, projectWorkItemDetail } from './projection.js';
|
|
9
|
+
import { previewWorkCenterPlan } from './planner.js';
|
|
10
|
+
import { readWorkCenterSettings } from './settings.js';
|
|
9
11
|
import { join } from 'node:path';
|
|
10
12
|
|
|
11
13
|
let service = null;
|
|
@@ -14,6 +16,8 @@ let shuttingDown = false;
|
|
|
14
16
|
let shutdownPromise = null;
|
|
15
17
|
let serviceFactory = null;
|
|
16
18
|
|
|
19
|
+
const BROWSER_DETAIL_OPS = new Set(['get', 'create', 'update', 'start', 'cancel', 'guide', 'retry']);
|
|
20
|
+
|
|
17
21
|
function send(msg) {
|
|
18
22
|
sendToServer(msg);
|
|
19
23
|
}
|
|
@@ -29,7 +33,7 @@ async function createDefaultService() {
|
|
|
29
33
|
runtimeProvider: async () => {
|
|
30
34
|
const runtime = await getRuntime();
|
|
31
35
|
if (defaultRegistry.vpCount() === 0) {
|
|
32
|
-
for (const vp of scanVpLibrary({ dir: join(
|
|
36
|
+
for (const vp of scanVpLibrary({ dir: join(yeaftDir, 'virtual-persons') })) defaultRegistry.setVp(vp);
|
|
33
37
|
}
|
|
34
38
|
return {
|
|
35
39
|
...runtime,
|
|
@@ -42,6 +46,27 @@ async function createDefaultService() {
|
|
|
42
46
|
const created = new WorkCenterService({
|
|
43
47
|
yeaftDir,
|
|
44
48
|
runner,
|
|
49
|
+
runtimeInfoProvider: async () => {
|
|
50
|
+
const runtime = await getRuntime();
|
|
51
|
+
if (defaultRegistry.vpCount() === 0) {
|
|
52
|
+
for (const vp of scanVpLibrary({ dir: join(yeaftDir, 'virtual-persons') })) defaultRegistry.setVp(vp);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
vps: defaultRegistry.listVps().map(vp => ({
|
|
56
|
+
id: vp.id,
|
|
57
|
+
name: vp.name || vp.id,
|
|
58
|
+
nameZh: vp.nameZh || '',
|
|
59
|
+
role: vp.role || '',
|
|
60
|
+
roleZh: vp.roleZh || '',
|
|
61
|
+
area: vp.area || '',
|
|
62
|
+
traits: Array.isArray(vp.traits) ? vp.traits : [],
|
|
63
|
+
modelHint: vp.modelHint || null,
|
|
64
|
+
})),
|
|
65
|
+
models: Array.isArray(runtime.config.availableModels) ? runtime.config.availableModels : [],
|
|
66
|
+
primaryModel: runtime.config.primaryModel || runtime.config.model || null,
|
|
67
|
+
fastModel: runtime.config.fastModel || null,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
45
70
|
onEvent(event) {
|
|
46
71
|
send({ type: 'work_center_event', event: projectWorkCenterEvent(event) });
|
|
47
72
|
},
|
|
@@ -85,7 +110,28 @@ export async function handleWorkCenterRequest(msg) {
|
|
|
85
110
|
const op = typeof msg.op === 'string' ? msg.op : '';
|
|
86
111
|
try {
|
|
87
112
|
const workCenter = await ensureWorkCenter();
|
|
88
|
-
|
|
113
|
+
let data;
|
|
114
|
+
if (op === 'preview') {
|
|
115
|
+
const runtime = await getRuntime();
|
|
116
|
+
const yeaftDir = ctx.CONFIG?.yeaftDir;
|
|
117
|
+
if (!yeaftDir) throw new Error('Work Center requires a configured Yeaft directory');
|
|
118
|
+
if (defaultRegistry.vpCount() === 0) {
|
|
119
|
+
for (const vp of scanVpLibrary({ dir: join(yeaftDir, 'virtual-persons') })) defaultRegistry.setVp(vp);
|
|
120
|
+
}
|
|
121
|
+
data = previewWorkCenterPlan({
|
|
122
|
+
settings: readWorkCenterSettings(yeaftDir),
|
|
123
|
+
workflowId: msg.payload?.workflowTemplate,
|
|
124
|
+
stageOverrides: msg.payload?.stageOverrides,
|
|
125
|
+
registry: defaultRegistry,
|
|
126
|
+
config: runtime.config,
|
|
127
|
+
});
|
|
128
|
+
} else if (op === 'refresh_runtime') {
|
|
129
|
+
await resetYeaftSession();
|
|
130
|
+
data = await workCenter.handle('get_settings', {});
|
|
131
|
+
} else {
|
|
132
|
+
data = await workCenter.handle(op, msg.payload || {});
|
|
133
|
+
}
|
|
134
|
+
if (BROWSER_DETAIL_OPS.has(op)) data = projectWorkItemDetail(data);
|
|
89
135
|
send({
|
|
90
136
|
type: 'work_center_response',
|
|
91
137
|
requestId,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { actionInstruction, getNextStep, initialActionFor, RUN_OUTCOMES } from './workflow.js';
|
|
1
|
+
import { actionForStage, actionInstruction, getNextStep, initialActionFor, RUN_OUTCOMES } from './workflow.js';
|
|
2
2
|
import { normalizeEvidence } from './evidence.js';
|
|
3
3
|
|
|
4
4
|
function normalizeCriteria(value) {
|
|
@@ -48,10 +48,12 @@ function normalizeTerminalResult(result, action) {
|
|
|
48
48
|
return normalized;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
function contextEntry(action, result) {
|
|
51
|
+
function contextEntry(action, result, run) {
|
|
52
52
|
return {
|
|
53
53
|
type: action.type,
|
|
54
|
-
|
|
54
|
+
stageId: action.stageId || action.type,
|
|
55
|
+
vpId: run?.vpSnapshot?.id || action.requiredRole || null,
|
|
56
|
+
role: run?.roleSnapshot?.id || action.requiredRole || null,
|
|
55
57
|
summary: result.summary || '',
|
|
56
58
|
evidence: result.evidence || [],
|
|
57
59
|
reviewDecision: result.reviewDecision || null,
|
|
@@ -69,12 +71,29 @@ export class WorkflowController {
|
|
|
69
71
|
workflowTemplate: input.workflowTemplate || 'software-change',
|
|
70
72
|
acceptanceCriteria: Array.isArray(input.acceptanceCriteria) ? input.acceptanceCriteria : [],
|
|
71
73
|
};
|
|
72
|
-
|
|
74
|
+
let firstAction = input.start !== false ? initialActionFor(draft) : null;
|
|
75
|
+
if (firstAction && draft.reuseMemory !== false) {
|
|
76
|
+
const context = this.store.getReusableContext(draft.workDir, draft.id);
|
|
77
|
+
firstAction = {
|
|
78
|
+
...firstAction,
|
|
79
|
+
context,
|
|
80
|
+
instruction: actionInstruction(firstAction, draft, context),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
73
83
|
return this.store.createWorkItem(draft, firstAction);
|
|
74
84
|
}
|
|
75
85
|
|
|
76
86
|
start(id) {
|
|
77
|
-
const detail = this.store.startWorkItemAtomic(id,
|
|
87
|
+
const detail = this.store.startWorkItemAtomic(id, workItem => {
|
|
88
|
+
const action = initialActionFor(workItem);
|
|
89
|
+
if (workItem.reuseMemory === false) return action;
|
|
90
|
+
const context = this.store.getReusableContext(workItem.workDir, workItem.id);
|
|
91
|
+
return {
|
|
92
|
+
...action,
|
|
93
|
+
context,
|
|
94
|
+
instruction: actionInstruction(action, workItem, context),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
78
97
|
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
79
98
|
return detail;
|
|
80
99
|
}
|
|
@@ -91,6 +110,41 @@ export class WorkflowController {
|
|
|
91
110
|
return this.store.getWorkItemDetail(id);
|
|
92
111
|
}
|
|
93
112
|
|
|
113
|
+
guide(id, input = {}) {
|
|
114
|
+
const guidance = typeof input.guidance === 'string' ? input.guidance.trim().slice(0, 8_000) : '';
|
|
115
|
+
if (!guidance) throw new Error('guidance is required');
|
|
116
|
+
const expected = {
|
|
117
|
+
actionId: typeof input.actionId === 'string' ? input.actionId : '',
|
|
118
|
+
revision: Number(input.revision),
|
|
119
|
+
};
|
|
120
|
+
if (!expected.actionId || !Number.isInteger(expected.revision)) {
|
|
121
|
+
throw new Error('actionId and revision are required for guidance');
|
|
122
|
+
}
|
|
123
|
+
const detail = this.store.addActionGuidance(id, guidance, expected, (workItem, previous) => {
|
|
124
|
+
const context = [...(previous.context || []), {
|
|
125
|
+
type: 'guidance',
|
|
126
|
+
role: 'user',
|
|
127
|
+
summary: guidance,
|
|
128
|
+
evidence: [],
|
|
129
|
+
}];
|
|
130
|
+
const step = {
|
|
131
|
+
type: previous.type,
|
|
132
|
+
stageId: previous.stageId || previous.type,
|
|
133
|
+
assignmentPolicy: previous.assignmentPolicy,
|
|
134
|
+
modelPolicy: previous.modelPolicy,
|
|
135
|
+
requiredRole: previous.requiredRole,
|
|
136
|
+
};
|
|
137
|
+
return {
|
|
138
|
+
...step,
|
|
139
|
+
context,
|
|
140
|
+
instruction: actionInstruction(step, workItem, context),
|
|
141
|
+
maxAttempts: previous.maxAttempts || 2,
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
if (!detail) throw new Error(`WorkItem not found: ${id}`);
|
|
145
|
+
return detail;
|
|
146
|
+
}
|
|
147
|
+
|
|
94
148
|
retry(id, input = {}) {
|
|
95
149
|
const answer = typeof input.answer === 'string' ? input.answer.trim().slice(0, 8_000) : '';
|
|
96
150
|
const detail = this.store.retryWorkItemAtomic(id, (workItem, previous, previousRun) => {
|
|
@@ -98,13 +152,21 @@ export class WorkflowController {
|
|
|
98
152
|
throw new Error('answer is required to resume a waiting WorkItem');
|
|
99
153
|
}
|
|
100
154
|
const step = previous
|
|
101
|
-
? {
|
|
155
|
+
? {
|
|
156
|
+
type: previous.type,
|
|
157
|
+
stageId: previous.stageId || previous.type,
|
|
158
|
+
assignmentPolicy: previous.assignmentPolicy,
|
|
159
|
+
modelPolicy: previous.modelPolicy,
|
|
160
|
+
requiredRole: previous.requiredRole,
|
|
161
|
+
}
|
|
102
162
|
: initialActionFor(workItem);
|
|
103
163
|
const context = Array.isArray(previous?.context) ? [...previous.context] : [];
|
|
104
164
|
if (previousRun) {
|
|
105
165
|
context.push({
|
|
106
166
|
type: previous.type,
|
|
107
|
-
|
|
167
|
+
stageId: previous.stageId || previous.type,
|
|
168
|
+
vpId: previousRun.vpSnapshot?.id || previous.requiredRole || null,
|
|
169
|
+
role: previousRun.roleSnapshot?.id || previous.requiredRole || null,
|
|
108
170
|
summary: previousRun.summary || '',
|
|
109
171
|
evidence: normalizeEvidence(previousRun.evidence),
|
|
110
172
|
waitingReason: previousRun.waitingReason || null,
|
|
@@ -177,7 +239,7 @@ export class WorkflowController {
|
|
|
177
239
|
revision: workItem.revision + 1,
|
|
178
240
|
}
|
|
179
241
|
: workItem;
|
|
180
|
-
const nextStep = getNextStep(workItem
|
|
242
|
+
const nextStep = getNextStep(workItem, action.stageId || action.type, result);
|
|
181
243
|
if (!nextStep) {
|
|
182
244
|
return {
|
|
183
245
|
actionStatus: 'completed',
|
|
@@ -188,17 +250,12 @@ export class WorkflowController {
|
|
|
188
250
|
};
|
|
189
251
|
}
|
|
190
252
|
|
|
191
|
-
const context = [...(action.context || []), contextEntry(action, result)];
|
|
253
|
+
const context = [...(action.context || []), contextEntry(action, result, activeRun)];
|
|
192
254
|
return {
|
|
193
255
|
actionStatus: 'completed',
|
|
194
256
|
workItemStatus: 'ready',
|
|
195
257
|
contractPatch,
|
|
196
|
-
nextAction:
|
|
197
|
-
...nextStep,
|
|
198
|
-
context,
|
|
199
|
-
instruction: actionInstruction(nextStep, effectiveWorkItem, context),
|
|
200
|
-
maxAttempts: 2,
|
|
201
|
-
},
|
|
258
|
+
nextAction: actionForStage(nextStep, effectiveWorkItem, context),
|
|
202
259
|
eventType: 'action.completed',
|
|
203
260
|
eventData: {
|
|
204
261
|
nextActionType: nextStep.type,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { resolveWorkItemModel, selectWorkItemVp } from './assignment.js';
|
|
2
|
+
import { resolveWorkflowSnapshot } from './workflow.js';
|
|
3
|
+
|
|
4
|
+
function publicVp(vp) {
|
|
5
|
+
return vp ? {
|
|
6
|
+
id: vp.id,
|
|
7
|
+
name: vp.name || vp.id,
|
|
8
|
+
nameZh: vp.nameZh || '',
|
|
9
|
+
role: vp.role || '',
|
|
10
|
+
roleZh: vp.roleZh || '',
|
|
11
|
+
area: vp.area || '',
|
|
12
|
+
traits: Array.isArray(vp.traits) ? vp.traits : [],
|
|
13
|
+
modelHint: vp.modelHint || null,
|
|
14
|
+
} : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function previewWorkCenterPlan({ settings, workflowId, stageOverrides, registry, config }) {
|
|
18
|
+
const workflow = resolveWorkflowSnapshot(settings, workflowId, stageOverrides);
|
|
19
|
+
const vps = registry.listVps();
|
|
20
|
+
const syntheticRuns = [];
|
|
21
|
+
const stages = workflow.stages.map(stage => {
|
|
22
|
+
try {
|
|
23
|
+
const assignment = selectWorkItemVp({
|
|
24
|
+
policy: stage.assignmentPolicy,
|
|
25
|
+
stageType: stage.type,
|
|
26
|
+
vps,
|
|
27
|
+
priorRuns: syntheticRuns,
|
|
28
|
+
});
|
|
29
|
+
const model = resolveWorkItemModel(config, assignment.vp, stage.modelPolicy);
|
|
30
|
+
syntheticRuns.push({
|
|
31
|
+
actionType: stage.type,
|
|
32
|
+
roleSnapshot: { actionType: stage.type },
|
|
33
|
+
vpSnapshot: { id: assignment.vp.id },
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
id: stage.id,
|
|
37
|
+
name: stage.name,
|
|
38
|
+
type: stage.type,
|
|
39
|
+
assignmentPolicy: stage.assignmentPolicy,
|
|
40
|
+
modelPolicy: stage.modelPolicy,
|
|
41
|
+
selectedVp: publicVp(assignment.vp),
|
|
42
|
+
selectionReason: assignment.reason,
|
|
43
|
+
model: {
|
|
44
|
+
id: model.model,
|
|
45
|
+
provider: model.provider,
|
|
46
|
+
effort: model.effort,
|
|
47
|
+
source: model.source,
|
|
48
|
+
},
|
|
49
|
+
error: null,
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
return {
|
|
53
|
+
id: stage.id,
|
|
54
|
+
name: stage.name,
|
|
55
|
+
type: stage.type,
|
|
56
|
+
assignmentPolicy: stage.assignmentPolicy,
|
|
57
|
+
modelPolicy: stage.modelPolicy,
|
|
58
|
+
selectedVp: null,
|
|
59
|
+
selectionReason: null,
|
|
60
|
+
model: null,
|
|
61
|
+
error: error?.message || String(error),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return { workflow, stages, valid: stages.every(stage => !stage.error) };
|
|
66
|
+
}
|
|
@@ -3,10 +3,104 @@ function currentAction(detail) {
|
|
|
3
3
|
return detail.actions.find(action => action.id === detail.currentActionId) || null;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
function projectAction(action) {
|
|
7
|
+
if (!action) return null;
|
|
8
|
+
return {
|
|
9
|
+
id: action.id,
|
|
10
|
+
workItemId: action.workItemId,
|
|
11
|
+
sequence: action.sequence,
|
|
12
|
+
type: action.type,
|
|
13
|
+
stageId: action.stageId || action.type,
|
|
14
|
+
assignmentPolicy: action.assignmentPolicy || null,
|
|
15
|
+
modelPolicy: action.modelPolicy || null,
|
|
16
|
+
requiredRole: action.requiredRole || '',
|
|
17
|
+
status: action.status,
|
|
18
|
+
attempt: action.attempt,
|
|
19
|
+
maxAttempts: action.maxAttempts,
|
|
20
|
+
currentRunId: action.currentRunId || null,
|
|
21
|
+
createdAt: action.createdAt,
|
|
22
|
+
updatedAt: action.updatedAt,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function projectRun(run) {
|
|
27
|
+
if (!run) return null;
|
|
28
|
+
return {
|
|
29
|
+
id: run.id,
|
|
30
|
+
actionId: run.actionId,
|
|
31
|
+
workItemId: run.workItemId,
|
|
32
|
+
status: run.status,
|
|
33
|
+
startedAt: run.startedAt,
|
|
34
|
+
expiresAt: run.expiresAt,
|
|
35
|
+
endedAt: run.endedAt || null,
|
|
36
|
+
summary: run.summary || '',
|
|
37
|
+
evidence: Array.isArray(run.evidence) ? run.evidence : [],
|
|
38
|
+
waitingReason: run.waitingReason || '',
|
|
39
|
+
error: run.error || '',
|
|
40
|
+
reviewDecision: run.reviewDecision || null,
|
|
41
|
+
roleSnapshot: run.roleSnapshot ? {
|
|
42
|
+
id: run.roleSnapshot.id,
|
|
43
|
+
actionType: run.roleSnapshot.actionType,
|
|
44
|
+
selectionReason: run.roleSnapshot.selectionReason,
|
|
45
|
+
} : null,
|
|
46
|
+
vpSnapshot: run.vpSnapshot ? {
|
|
47
|
+
id: run.vpSnapshot.id,
|
|
48
|
+
name: run.vpSnapshot.name || run.vpSnapshot.id,
|
|
49
|
+
nameZh: run.vpSnapshot.nameZh || '',
|
|
50
|
+
role: run.vpSnapshot.role || '',
|
|
51
|
+
roleZh: run.vpSnapshot.roleZh || '',
|
|
52
|
+
} : null,
|
|
53
|
+
modelSnapshot: run.modelSnapshot ? {
|
|
54
|
+
id: run.modelSnapshot.id,
|
|
55
|
+
provider: run.modelSnapshot.provider || null,
|
|
56
|
+
effort: run.modelSnapshot.effort || null,
|
|
57
|
+
source: run.modelSnapshot.source || null,
|
|
58
|
+
} : null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function projectEvent(event) {
|
|
63
|
+
if (!event) return null;
|
|
64
|
+
return {
|
|
65
|
+
id: event.id,
|
|
66
|
+
workItemId: event.workItemId,
|
|
67
|
+
actionId: event.actionId || null,
|
|
68
|
+
runId: event.runId || null,
|
|
69
|
+
type: event.type,
|
|
70
|
+
createdAt: event.createdAt,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Authenticated browser detail DTO. Execution-only snapshots, VP persona,
|
|
76
|
+
* tool policy, prompts, event data, and local filesystem paths never cross the wire.
|
|
77
|
+
*/
|
|
78
|
+
export function projectWorkItemDetail(detail) {
|
|
79
|
+
if (!detail) return null;
|
|
80
|
+
return {
|
|
81
|
+
id: detail.id,
|
|
82
|
+
revision: detail.revision,
|
|
83
|
+
title: detail.title,
|
|
84
|
+
goal: detail.goal,
|
|
85
|
+
acceptanceCriteria: Array.isArray(detail.acceptanceCriteria) ? detail.acceptanceCriteria : [],
|
|
86
|
+
workflowTemplate: detail.workflowTemplate,
|
|
87
|
+
status: detail.status,
|
|
88
|
+
currentActionId: detail.currentActionId || null,
|
|
89
|
+
currentRunId: detail.currentRunId || null,
|
|
90
|
+
reuseMemory: detail.reuseMemory !== false,
|
|
91
|
+
origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
|
|
92
|
+
linkedSessionIds: Array.isArray(detail.linkedSessionIds) ? detail.linkedSessionIds : [],
|
|
93
|
+
createdAt: detail.createdAt,
|
|
94
|
+
updatedAt: detail.updatedAt,
|
|
95
|
+
actions: Array.isArray(detail.actions) ? detail.actions.map(projectAction) : [],
|
|
96
|
+
runs: Array.isArray(detail.runs) ? detail.runs.map(projectRun) : [],
|
|
97
|
+
events: Array.isArray(detail.events) ? detail.events.map(projectEvent) : [],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
6
101
|
/**
|
|
7
102
|
* Browser event projection. This deliberately excludes local filesystem paths,
|
|
8
103
|
* Run evidence/tool output, prompts, model snapshots, and execution errors.
|
|
9
|
-
* Clients fetch the selected detail explicitly through the authenticated get op.
|
|
10
104
|
*/
|
|
11
105
|
export function projectWorkItemSummary(detail) {
|
|
12
106
|
if (!detail) return null;
|
|
@@ -36,7 +130,8 @@ export function projectWorkItemSummary(detail) {
|
|
|
36
130
|
currentAction: action ? {
|
|
37
131
|
id: action.id,
|
|
38
132
|
type: action.type,
|
|
39
|
-
|
|
133
|
+
stageId: action.stageId || action.type,
|
|
134
|
+
assignmentMode: action.assignmentPolicy?.mode || (action.requiredRole ? 'fixed' : null),
|
|
40
135
|
status: action.status,
|
|
41
136
|
} : null,
|
|
42
137
|
origin: detail.origin?.sessionId ? { sessionId: detail.origin.sessionId } : null,
|