@yeaft/webchat-agent 1.0.550 → 1.0.552
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/index.js +1 -1
- package/local-runtime/server/client-protocol.js +4 -0
- package/local-runtime/server/handlers/agent-work-center.js +2 -0
- package/local-runtime/server/handlers/client-work-center.js +7 -0
- package/local-runtime/server/handlers/client-workbench.js +36 -1
- package/local-runtime/server/work-center-workspace-cache.js +63 -0
- package/local-runtime/server/workbench-preview.js +1 -1
- package/local-runtime/server/workbench-route.js +62 -18
- package/local-runtime/server/ws-client.js +4 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +268 -207
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +1 -1
- package/workbench/file-ops.js +25 -13
- package/workbench/file-search.js +4 -1
- package/workbench/git-ops.js +32 -26
- package/workbench/work-item-path.js +41 -0
- package/yeaft/work-center/assignment.js +8 -3
- package/yeaft/work-center/coordinator.js +10 -2
- package/yeaft/work-center/planner.js +8 -2
- package/yeaft/work-center/projection.js +5 -0
- package/yeaft/work-center/runner.js +18 -3
- package/yeaft/work-center/workflow.js +41 -12
|
Binary file
|
package/package.json
CHANGED
package/workbench/file-ops.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, basename, dirname, extname, isAbsolute, relative, resolve } from
|
|
|
4
4
|
import { platform } from 'os';
|
|
5
5
|
import ctx from '../context.js';
|
|
6
6
|
import { resolveAndValidatePath, BINARY_EXTENSIONS, VIDEO_EXTENSIONS } from './utils.js';
|
|
7
|
+
import { resolveWorkItemPath } from './work-item-path.js';
|
|
7
8
|
import { sendWorkbenchResult } from './request-routing.js';
|
|
8
9
|
|
|
9
10
|
export const MAX_WORKBENCH_PREVIEW_BYTES = 20 * 1024 * 1024;
|
|
@@ -271,7 +272,7 @@ export async function handleWriteFile(msg) {
|
|
|
271
272
|
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
272
273
|
|
|
273
274
|
try {
|
|
274
|
-
const resolved =
|
|
275
|
+
const resolved = await resolveWorkItemPath(msg, filePath, workDir);
|
|
275
276
|
await writeFile(resolved, content, 'utf-8');
|
|
276
277
|
|
|
277
278
|
sendWorkbenchResult(ctx, msg, {
|
|
@@ -300,7 +301,8 @@ export async function handleWriteFile(msg) {
|
|
|
300
301
|
}
|
|
301
302
|
|
|
302
303
|
export async function handleListDirectory(msg) {
|
|
303
|
-
const { conversationId, requestId,
|
|
304
|
+
const { conversationId, requestId, _requestUserId, _requestClientId } = msg;
|
|
305
|
+
const dirPath = msg.dirPath || (msg.workbenchRoute?.runtimeProvider === 'work-center' ? msg.workDir : msg.dirPath);
|
|
304
306
|
const directoryPickerScope = msg.directoryPickerScope === 'agent' ? 'agent' : undefined;
|
|
305
307
|
const conv = ctx.conversations.get(conversationId);
|
|
306
308
|
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
@@ -362,7 +364,7 @@ export async function handleListDirectory(msg) {
|
|
|
362
364
|
}
|
|
363
365
|
|
|
364
366
|
try {
|
|
365
|
-
const resolved =
|
|
367
|
+
const resolved = await resolveWorkItemPath(msg, dirPath, workDir);
|
|
366
368
|
const entries = await readdir(resolved, { withFileTypes: true });
|
|
367
369
|
const result = [];
|
|
368
370
|
|
|
@@ -426,7 +428,7 @@ export async function handleCreateFile(msg) {
|
|
|
426
428
|
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
427
429
|
|
|
428
430
|
try {
|
|
429
|
-
const resolved =
|
|
431
|
+
const resolved = await resolveWorkItemPath(msg, filePath, workDir);
|
|
430
432
|
if (isDirectory) {
|
|
431
433
|
await mkdir(resolved, { recursive: true });
|
|
432
434
|
} else {
|
|
@@ -464,7 +466,7 @@ export async function handleDeleteFiles(msg) {
|
|
|
464
466
|
|
|
465
467
|
for (const p of paths) {
|
|
466
468
|
try {
|
|
467
|
-
const resolved =
|
|
469
|
+
const resolved = await resolveWorkItemPath(msg, p, workDir);
|
|
468
470
|
const s = await stat(resolved);
|
|
469
471
|
if (s.isDirectory()) {
|
|
470
472
|
await rm(resolved, { recursive: true, force: true });
|
|
@@ -503,7 +505,7 @@ export async function handleMoveFiles(msg) {
|
|
|
503
505
|
if (!paths || paths.length === 0) throw new Error('No paths specified');
|
|
504
506
|
if (!destination) throw new Error('No destination specified');
|
|
505
507
|
|
|
506
|
-
const destResolved =
|
|
508
|
+
const destResolved = await resolveWorkItemPath(msg, destination, workDir);
|
|
507
509
|
// Ensure destination directory exists
|
|
508
510
|
await mkdir(destResolved, { recursive: true });
|
|
509
511
|
|
|
@@ -512,9 +514,9 @@ export async function handleMoveFiles(msg) {
|
|
|
512
514
|
|
|
513
515
|
for (const p of paths) {
|
|
514
516
|
try {
|
|
515
|
-
const srcResolved =
|
|
517
|
+
const srcResolved = await resolveWorkItemPath(msg, p, workDir);
|
|
516
518
|
const name = (newName && paths.length === 1) ? newName : basename(srcResolved);
|
|
517
|
-
const destPath = join(destResolved, name);
|
|
519
|
+
const destPath = await resolveWorkItemPath(msg, join(destResolved, name), workDir);
|
|
518
520
|
if (existsSync(destPath)) {
|
|
519
521
|
throw new Error('Target already exists: ' + name);
|
|
520
522
|
}
|
|
@@ -551,7 +553,7 @@ export async function handleCopyFiles(msg) {
|
|
|
551
553
|
if (!paths || paths.length === 0) throw new Error('No paths specified');
|
|
552
554
|
if (!destination) throw new Error('No destination specified');
|
|
553
555
|
|
|
554
|
-
const destResolved =
|
|
556
|
+
const destResolved = await resolveWorkItemPath(msg, destination, workDir);
|
|
555
557
|
await mkdir(destResolved, { recursive: true });
|
|
556
558
|
|
|
557
559
|
const copied = [];
|
|
@@ -559,7 +561,7 @@ export async function handleCopyFiles(msg) {
|
|
|
559
561
|
|
|
560
562
|
for (const p of paths) {
|
|
561
563
|
try {
|
|
562
|
-
const srcResolved =
|
|
564
|
+
const srcResolved = await resolveWorkItemPath(msg, p, workDir);
|
|
563
565
|
const name = basename(srcResolved);
|
|
564
566
|
let destPath = join(destResolved, name);
|
|
565
567
|
|
|
@@ -574,9 +576,19 @@ export async function handleCopyFiles(msg) {
|
|
|
574
576
|
} while (existsSync(destPath));
|
|
575
577
|
}
|
|
576
578
|
|
|
579
|
+
await resolveWorkItemPath(msg, destPath, workDir);
|
|
577
580
|
const srcStat = await stat(srcResolved);
|
|
578
581
|
if (srcStat.isDirectory()) {
|
|
579
|
-
await cp(srcResolved, destPath, {
|
|
582
|
+
await cp(srcResolved, destPath, {
|
|
583
|
+
recursive: true,
|
|
584
|
+
...(msg.workbenchRoute?.runtimeProvider === 'work-center' ? {
|
|
585
|
+
filter: async (source, target) => {
|
|
586
|
+
await resolveWorkItemPath(msg, source, workDir);
|
|
587
|
+
await resolveWorkItemPath(msg, target, workDir);
|
|
588
|
+
return true;
|
|
589
|
+
},
|
|
590
|
+
} : {}),
|
|
591
|
+
});
|
|
580
592
|
} else {
|
|
581
593
|
await copyFile(srcResolved, destPath);
|
|
582
594
|
}
|
|
@@ -611,7 +623,7 @@ export async function handleUploadToDir(msg) {
|
|
|
611
623
|
try {
|
|
612
624
|
if (!files || files.length === 0) throw new Error('No files specified');
|
|
613
625
|
|
|
614
|
-
const targetDir =
|
|
626
|
+
const targetDir = await resolveWorkItemPath(msg, dirPath || workDir, workDir);
|
|
615
627
|
await mkdir(targetDir, { recursive: true });
|
|
616
628
|
|
|
617
629
|
const saved = [];
|
|
@@ -619,7 +631,7 @@ export async function handleUploadToDir(msg) {
|
|
|
619
631
|
|
|
620
632
|
for (const file of files) {
|
|
621
633
|
try {
|
|
622
|
-
const dest = join(targetDir, file.name);
|
|
634
|
+
const dest = await resolveWorkItemPath(msg, join(targetDir, file.name), workDir);
|
|
623
635
|
const buffer = Buffer.from(file.data, 'base64');
|
|
624
636
|
await writeFile(dest, buffer);
|
|
625
637
|
saved.push(file.name);
|
package/workbench/file-search.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir, stat } from 'fs/promises';
|
|
2
2
|
import { join, relative, resolve } from 'path';
|
|
3
3
|
import ctx from '../context.js';
|
|
4
|
+
import { resolveWorkItemPath } from './work-item-path.js';
|
|
4
5
|
import { sendWorkbenchResult } from './request-routing.js';
|
|
5
6
|
|
|
6
7
|
export async function handleFileSearch(msg) {
|
|
@@ -15,7 +16,9 @@ export async function handleFileSearch(msg) {
|
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
const resolved =
|
|
19
|
+
const resolved = msg.workbenchRoute?.runtimeProvider === 'work-center'
|
|
20
|
+
? await resolveWorkItemPath(msg, searchRoot, workDir)
|
|
21
|
+
: resolve(searchRoot);
|
|
19
22
|
const results = [];
|
|
20
23
|
const MAX_RESULTS = 100;
|
|
21
24
|
const lowerQuery = query.toLowerCase();
|
package/workbench/git-ops.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
|
-
import { readFile, writeFile } from 'fs/promises';
|
|
2
|
-
import { join, resolve } from 'path';
|
|
1
|
+
import { readFile, realpath, writeFile } from 'fs/promises';
|
|
2
|
+
import { join, relative, resolve } from 'path';
|
|
3
3
|
import ctx from '../context.js';
|
|
4
4
|
import { execAsync, resolveAndValidatePath, getGitRoot, validateGitPath } from './utils.js';
|
|
5
|
+
import { resolveWorkItemPath } from './work-item-path.js';
|
|
5
6
|
import { sendWorkbenchResult } from './request-routing.js';
|
|
6
7
|
|
|
8
|
+
// Git paths and bulk operations are repository-scoped. A WorkItem must own
|
|
9
|
+
// that root, rather than silently widening a subdirectory workspace to it.
|
|
10
|
+
async function getWorkbenchGitRoot(msg, workDir) {
|
|
11
|
+
if (msg.workbenchRoute?.runtimeProvider !== 'work-center') return getGitRoot(workDir);
|
|
12
|
+
// Do not fall back to cwd on lookup failure: Git itself may ascend from it.
|
|
13
|
+
const { stdout } = await execAsync('git rev-parse --show-toplevel', {
|
|
14
|
+
cwd: workDir, timeout: 5000, windowsHide: true,
|
|
15
|
+
});
|
|
16
|
+
const gitRoot = stdout.trim();
|
|
17
|
+
const [workspace, repository] = await Promise.all([realpath(workDir), realpath(gitRoot)]);
|
|
18
|
+
if (relative(workspace, repository) !== '') {
|
|
19
|
+
const error = new Error('Git requires the WorkItem workspace to be the repository root. Files and Terminal remain available.');
|
|
20
|
+
error.code = 'WORK_ITEM_GIT_ROOT_REQUIRED';
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
return gitRoot;
|
|
24
|
+
}
|
|
25
|
+
|
|
7
26
|
export async function handleGitStatus(msg) {
|
|
8
27
|
const { conversationId, _requestUserId } = msg;
|
|
9
28
|
const conv = ctx.conversations.get(conversationId);
|
|
@@ -11,15 +30,7 @@ export async function handleGitStatus(msg) {
|
|
|
11
30
|
|
|
12
31
|
try {
|
|
13
32
|
// Get git repo root to ensure paths are consistent
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const { stdout: rootOut } = await execAsync('git rev-parse --show-toplevel', {
|
|
17
|
-
cwd: workDir,
|
|
18
|
-
timeout: 5000,
|
|
19
|
-
windowsHide: true
|
|
20
|
-
});
|
|
21
|
-
gitRoot = rootOut.trim();
|
|
22
|
-
} catch {}
|
|
33
|
+
const gitRoot = await getWorkbenchGitRoot(msg, workDir);
|
|
23
34
|
|
|
24
35
|
const { stdout: statusOut } = await execAsync('git status --porcelain', {
|
|
25
36
|
cwd: gitRoot,
|
|
@@ -76,6 +87,7 @@ export async function handleGitStatus(msg) {
|
|
|
76
87
|
conversationId,
|
|
77
88
|
_requestUserId,
|
|
78
89
|
error: e.message,
|
|
90
|
+
...(e.code ? { errorCode: e.code } : {}),
|
|
79
91
|
isGitRepo: !e.message.includes('not a git repository') && !e.message.includes('ENOENT')
|
|
80
92
|
});
|
|
81
93
|
}
|
|
@@ -100,20 +112,14 @@ export async function handleGitDiff(msg) {
|
|
|
100
112
|
}
|
|
101
113
|
|
|
102
114
|
// Get git repo root — git status paths are relative to this, not workDir
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
const { stdout: rootOut } = await execAsync('git rev-parse --show-toplevel', {
|
|
106
|
-
cwd: workDir,
|
|
107
|
-
timeout: 5000,
|
|
108
|
-
windowsHide: true
|
|
109
|
-
});
|
|
110
|
-
gitRoot = rootOut.trim();
|
|
111
|
-
} catch {}
|
|
115
|
+
const gitRoot = await getWorkbenchGitRoot(msg, workDir);
|
|
112
116
|
|
|
113
117
|
if (untracked) {
|
|
114
118
|
// Untracked files: resolve path relative to git root
|
|
115
119
|
const fullPath = resolve(gitRoot, filePath);
|
|
116
|
-
|
|
120
|
+
// The root check above proved canonical workspace ownership. Validate
|
|
121
|
+
// against that same Git root, including when cwd is a symlink alias.
|
|
122
|
+
const resolved = await resolveWorkItemPath(msg, resolveAndValidatePath(fullPath, gitRoot), gitRoot);
|
|
117
123
|
const content = await readFile(resolved, 'utf-8');
|
|
118
124
|
sendWorkbenchResult(ctx, msg, {
|
|
119
125
|
type: 'git_diff_result',
|
|
@@ -205,7 +211,7 @@ export async function handleGitAdd(msg) {
|
|
|
205
211
|
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
206
212
|
|
|
207
213
|
try {
|
|
208
|
-
const gitRoot = await
|
|
214
|
+
const gitRoot = await getWorkbenchGitRoot(msg, workDir);
|
|
209
215
|
|
|
210
216
|
if (addAll) {
|
|
211
217
|
await execAsync('git add -A', { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
@@ -229,7 +235,7 @@ export async function handleGitReset(msg) {
|
|
|
229
235
|
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
230
236
|
|
|
231
237
|
try {
|
|
232
|
-
const gitRoot = await
|
|
238
|
+
const gitRoot = await getWorkbenchGitRoot(msg, workDir);
|
|
233
239
|
|
|
234
240
|
if (resetAll) {
|
|
235
241
|
await execAsync('git reset HEAD', { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
@@ -258,7 +264,7 @@ export async function handleGitRestore(msg) {
|
|
|
258
264
|
return;
|
|
259
265
|
}
|
|
260
266
|
|
|
261
|
-
const gitRoot = await
|
|
267
|
+
const gitRoot = await getWorkbenchGitRoot(msg, workDir);
|
|
262
268
|
await execAsync(`git restore -- "${filePath}"`, { cwd: gitRoot, timeout: 10000, windowsHide: true });
|
|
263
269
|
sendWorkbenchResult(ctx, msg, { type: 'git_op_result', conversationId, _requestUserId, operation: 'restore', success: true, message: `Restored: ${filePath}` });
|
|
264
270
|
} catch (e) {
|
|
@@ -277,7 +283,7 @@ export async function handleGitCommit(msg) {
|
|
|
277
283
|
return;
|
|
278
284
|
}
|
|
279
285
|
|
|
280
|
-
const gitRoot = await
|
|
286
|
+
const gitRoot = await getWorkbenchGitRoot(msg, workDir);
|
|
281
287
|
|
|
282
288
|
// Write commit message to temp file to avoid shell injection
|
|
283
289
|
const tmpFile = join(gitRoot, '.git', 'WEBCHAT_COMMIT_MSG');
|
|
@@ -303,7 +309,7 @@ export async function handleGitPush(msg) {
|
|
|
303
309
|
const workDir = msg.workDir || conv?.workDir || ctx.CONFIG.workDir;
|
|
304
310
|
|
|
305
311
|
try {
|
|
306
|
-
const gitRoot = await
|
|
312
|
+
const gitRoot = await getWorkbenchGitRoot(msg, workDir);
|
|
307
313
|
const { stdout, stderr } = await execAsync('git push', {
|
|
308
314
|
cwd: gitRoot, timeout: 60000, windowsHide: true
|
|
309
315
|
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { lstat, realpath } from 'node:fs/promises';
|
|
2
|
+
import { dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
function assertWithin(root, candidate) {
|
|
5
|
+
const path = relative(root, candidate);
|
|
6
|
+
if (path === '..' || path.startsWith(`..${sep}`) || isAbsolute(path)) {
|
|
7
|
+
const error = new Error('File is outside the WorkItem workspace.');
|
|
8
|
+
error.code = 'FILE_OUTSIDE_WORKSPACE';
|
|
9
|
+
throw error;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a WorkItem file operation without following a symlink outside its
|
|
15
|
+
* Agent-resolved workspace. New destinations validate the nearest existing
|
|
16
|
+
* ancestor; dangling symlinks fail closed. Other Workbench routes keep their
|
|
17
|
+
* existing path-picker behavior. This is not a concurrent filesystem sandbox.
|
|
18
|
+
*/
|
|
19
|
+
export async function resolveWorkItemPath(msg, filePath, workDir) {
|
|
20
|
+
const candidate = resolve(workDir, filePath);
|
|
21
|
+
if (msg.workbenchRoute?.runtimeProvider !== 'work-center') return candidate;
|
|
22
|
+
if (!isAbsolute(workDir)) throw new Error('WorkItem workspace must be absolute.');
|
|
23
|
+
const root = await realpath(workDir);
|
|
24
|
+
// Check both lexical and canonical ownership. Return the original path so a
|
|
25
|
+
// delete/rename still operates on a symlink itself, not on its target.
|
|
26
|
+
assertWithin(resolve(workDir), candidate);
|
|
27
|
+
let ancestor = candidate;
|
|
28
|
+
for (;;) {
|
|
29
|
+
try {
|
|
30
|
+
await lstat(ancestor);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
if (error.code !== 'ENOENT') throw error;
|
|
33
|
+
const parent = dirname(ancestor);
|
|
34
|
+
if (parent === ancestor) throw error;
|
|
35
|
+
ancestor = parent;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
assertWithin(root, await realpath(ancestor));
|
|
39
|
+
return candidate;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -162,17 +162,21 @@ function availableModel(config, ref) {
|
|
|
162
162
|
if (models.length === 0) return null;
|
|
163
163
|
const parsed = parseModelRef(ref);
|
|
164
164
|
return models.find(model => model.ref === ref)
|
|
165
|
-
|| models.find(model =>
|
|
165
|
+
|| models.find(model => model.id === parsed.modelId
|
|
166
|
+
&& (!parsed.providerName || model.provider === parsed.providerName))
|
|
166
167
|
|| null;
|
|
167
168
|
}
|
|
168
169
|
|
|
169
|
-
export function resolveWorkItemModel(config, vp, rawPolicy) {
|
|
170
|
+
export function resolveWorkItemModel(config, vp, rawPolicy, modelTags = {}) {
|
|
170
171
|
const policy = normalizeModelPolicy(rawPolicy);
|
|
171
172
|
let model;
|
|
172
173
|
let source;
|
|
173
174
|
if (policy.mode === 'specific') {
|
|
174
175
|
model = policy.model;
|
|
175
176
|
source = 'stage-specific';
|
|
177
|
+
} else if (policy.mode === 'tag') {
|
|
178
|
+
model = modelTags[policy.tag] || null;
|
|
179
|
+
source = `tag:${policy.tag}`;
|
|
176
180
|
} else if (policy.mode === 'primary') {
|
|
177
181
|
model = config.primaryModel || config.model || null;
|
|
178
182
|
source = 'agent-primary';
|
|
@@ -191,8 +195,9 @@ export function resolveWorkItemModel(config, vp, rawPolicy) {
|
|
|
191
195
|
if (Array.isArray(config.availableModels) && config.availableModels.length > 0 && !available) {
|
|
192
196
|
throw policyError(`Configured Work Center model is unavailable: ${model}`);
|
|
193
197
|
}
|
|
198
|
+
if (policy.mode === 'tag' && available?.ref) model = available.ref;
|
|
194
199
|
const effortOptions = Array.isArray(available?.effortOptions) ? available.effortOptions : [];
|
|
195
|
-
const effortOrder = ['
|
|
200
|
+
const effortOrder = ['medium', 'high', 'xhigh'];
|
|
196
201
|
const requestedIndex = effortOrder.indexOf(policy.effort);
|
|
197
202
|
const effort = !policy.effort || effortOptions.length === 0
|
|
198
203
|
? null
|
|
@@ -20,7 +20,10 @@ import { isDynamicWorkItem } from './execution-mode.js';
|
|
|
20
20
|
import { applyCoordinatorReplan } from './plan-mutation.js';
|
|
21
21
|
import { buildWorkItemAttachmentContext } from './attachments.js';
|
|
22
22
|
import { sanitizeDiagnosticText } from './debug-projection.js';
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
DEFAULT_WORK_CENTER_MODEL_TAGS,
|
|
25
|
+
generatedActionGraphRules,
|
|
26
|
+
} from './workflow.js';
|
|
24
27
|
import { workItemCapabilityContext } from './capabilities.js';
|
|
25
28
|
|
|
26
29
|
const COORDINATOR_MAX_REPLY_CHARS = 8_000;
|
|
@@ -900,7 +903,12 @@ export class WorkItemCoordinator {
|
|
|
900
903
|
...(settings?.modelPolicy || {}),
|
|
901
904
|
effort: settings?.actionModelPolicies?.triage?.effort || settings?.modelPolicy?.effort || 'high',
|
|
902
905
|
};
|
|
903
|
-
resolved = resolveWorkItemModel(
|
|
906
|
+
resolved = resolveWorkItemModel(
|
|
907
|
+
runtime.config,
|
|
908
|
+
assignment.vp,
|
|
909
|
+
coordinatorPolicy,
|
|
910
|
+
settings?.modelTags || DEFAULT_WORK_CENTER_MODEL_TAGS,
|
|
911
|
+
);
|
|
904
912
|
} catch (error) {
|
|
905
913
|
throw coordinatorExecutionError(error, 'selection', language);
|
|
906
914
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolveWorkItemModel, selectWorkItemVp } from './assignment.js';
|
|
2
|
-
import { resolveWorkflowSnapshot } from './workflow.js';
|
|
2
|
+
import { normalizeWorkCenterSettings, resolveWorkflowSnapshot } from './workflow.js';
|
|
3
3
|
|
|
4
4
|
function publicVp(vp) {
|
|
5
5
|
return vp ? {
|
|
@@ -17,6 +17,7 @@ function publicVp(vp) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function previewWorkCenterPlan({ settings, workflowId, stageOverrides, registry, config }) {
|
|
20
|
+
const normalizedSettings = normalizeWorkCenterSettings(settings);
|
|
20
21
|
const workflow = resolveWorkflowSnapshot(settings, workflowId, stageOverrides);
|
|
21
22
|
const vps = registry.listVps();
|
|
22
23
|
const syntheticRuns = [];
|
|
@@ -28,7 +29,12 @@ export function previewWorkCenterPlan({ settings, workflowId, stageOverrides, re
|
|
|
28
29
|
vps,
|
|
29
30
|
priorRuns: syntheticRuns,
|
|
30
31
|
});
|
|
31
|
-
const model = resolveWorkItemModel(
|
|
32
|
+
const model = resolveWorkItemModel(
|
|
33
|
+
config,
|
|
34
|
+
assignment.vp,
|
|
35
|
+
stage.modelPolicy,
|
|
36
|
+
normalizedSettings.modelTags,
|
|
37
|
+
);
|
|
32
38
|
syntheticRuns.push({
|
|
33
39
|
actionType: stage.type,
|
|
34
40
|
roleSnapshot: { actionType: stage.type },
|
|
@@ -970,6 +970,11 @@ export function projectWorkItemDetail(detail, options = {}) {
|
|
|
970
970
|
}
|
|
971
971
|
const projected = {
|
|
972
972
|
id: detail.id,
|
|
973
|
+
// The browser needs the Agent-resolved workspace only to bind an explicitly
|
|
974
|
+
// selected WorkItem Workbench. It is not included in board/list projections.
|
|
975
|
+
workbench: typeof detail.workDir === 'string' && detail.workDir.trim()
|
|
976
|
+
? { workDir: detail.workDir.trim() }
|
|
977
|
+
: null,
|
|
973
978
|
revision: detail.revision,
|
|
974
979
|
planRevision: count(detail.planRevision),
|
|
975
980
|
ledgerRevision: count(detail.ledgerRevision),
|
|
@@ -37,7 +37,11 @@ import { loadMCPConfig } from '../config.js';
|
|
|
37
37
|
import { MCPManager } from '../mcp.js';
|
|
38
38
|
import { buildMcpFlattenedTools } from '../tools/mcp-tools.js';
|
|
39
39
|
import { recallWorkspaceSessionContext } from './workspace-context.js';
|
|
40
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
applyGeneratedPlan,
|
|
42
|
+
BUILT_IN_ACTION_TYPES,
|
|
43
|
+
DEFAULT_WORK_CENTER_MODEL_TAGS,
|
|
44
|
+
} from './workflow.js';
|
|
41
45
|
import { isDynamicWorkItem, usesMainlineContext } from './execution-mode.js';
|
|
42
46
|
import {
|
|
43
47
|
applyAdditivePlanProposal,
|
|
@@ -1081,8 +1085,10 @@ export class WorkItemRunner {
|
|
|
1081
1085
|
async run({ workItem, action, run, signal, ownerBootId, onProgress, registerProgressReader, registerInputWake, onEngineEvent = null }) {
|
|
1082
1086
|
assertCreateVpActionAuthority(workItem, action, this.registry);
|
|
1083
1087
|
const runtime = await this.runtimeProvider();
|
|
1088
|
+
const settings = this.policyProvider ? await this.policyProvider() : null;
|
|
1084
1089
|
const currentSettings = ['ai', 'coordinator'].includes(workItem?.workflowSnapshot?.planningMode)
|
|
1085
|
-
|
|
1090
|
+
? settings
|
|
1091
|
+
: null;
|
|
1086
1092
|
const currentModelPolicy = currentSettings?.actionModelPolicies?.[action.type]
|
|
1087
1093
|
|| currentSettings?.actionModelPolicies?.custom
|
|
1088
1094
|
|| currentSettings?.modelPolicy
|
|
@@ -1125,7 +1131,16 @@ export class WorkItemRunner {
|
|
|
1125
1131
|
error.retryable = false;
|
|
1126
1132
|
throw error;
|
|
1127
1133
|
}
|
|
1128
|
-
const
|
|
1134
|
+
const fallbackModel = runtime.config.primaryModel || runtime.config.model || null;
|
|
1135
|
+
const modelTags = settings?.modelTags || Object.fromEntries(
|
|
1136
|
+
Object.keys(DEFAULT_WORK_CENTER_MODEL_TAGS).map(tag => [tag, fallbackModel]),
|
|
1137
|
+
);
|
|
1138
|
+
const resolvedModel = resolveWorkItemModel(
|
|
1139
|
+
runtime.config,
|
|
1140
|
+
vp,
|
|
1141
|
+
executionAction.modelPolicy,
|
|
1142
|
+
modelTags,
|
|
1143
|
+
);
|
|
1129
1144
|
const memoryBlock = recallWorkItemMemory(
|
|
1130
1145
|
{ ...runtime, yeaftDir: runtime.yeaftDir || this.yeaftDir },
|
|
1131
1146
|
workItem,
|
|
@@ -21,12 +21,35 @@ export const BUILT_IN_ACTION_TYPES = Object.freeze([
|
|
|
21
21
|
]);
|
|
22
22
|
const STAGE_TYPES = new Set(BUILT_IN_ACTION_TYPES);
|
|
23
23
|
const ASSIGNMENT_MODES = new Set(['auto', 'pool', 'fixed', 'planned']);
|
|
24
|
-
const MODEL_MODES = new Set(['inherit', 'primary', 'fast', 'specific']);
|
|
25
|
-
const MODEL_EFFORTS = new Set(['
|
|
24
|
+
const MODEL_MODES = new Set(['inherit', 'primary', 'fast', 'specific', 'tag']);
|
|
25
|
+
const MODEL_EFFORTS = new Set(['medium', 'high', 'xhigh']);
|
|
26
26
|
const WORKSPACE_MODES = new Set(['shared', 'read', 'isolated-write', 'integrate']);
|
|
27
|
-
const HIGH_EFFORT_ACTION_TYPES = new Set(['triage', 'research', 'design', 'diagnose', 'review']);
|
|
28
27
|
const ACTION_CONTEXT_QUOTE_MAX_BYTES = 8 * 1024;
|
|
29
28
|
|
|
29
|
+
export const DEFAULT_WORK_CENTER_MODEL_TAGS = Object.freeze({
|
|
30
|
+
fast: 'gpt-5.6-luna',
|
|
31
|
+
balanced: 'gpt-5.6-sol',
|
|
32
|
+
ultimate: 'gpt-6-astra',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const ULTIMATE_ACTION_TYPES = new Set(['triage', 'research', 'design', 'diagnose', 'review']);
|
|
36
|
+
const FAST_ACTION_TYPES = new Set(['document', 'deliver', 'write']);
|
|
37
|
+
|
|
38
|
+
function normalizeModelTags(value) {
|
|
39
|
+
const source = value && typeof value === 'object' && !Array.isArray(value)
|
|
40
|
+
? value
|
|
41
|
+
: DEFAULT_WORK_CENTER_MODEL_TAGS;
|
|
42
|
+
return Object.fromEntries(Object.entries(source)
|
|
43
|
+
.map(([tag, model]) => [canonicalActionId(tag), typeof model === 'string' ? model.trim() : ''])
|
|
44
|
+
.filter(([tag, model]) => tag && model));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function defaultActionModelTag(type) {
|
|
48
|
+
if (ULTIMATE_ACTION_TYPES.has(type)) return { tag: 'ultimate', effort: 'xhigh' };
|
|
49
|
+
if (FAST_ACTION_TYPES.has(type)) return { tag: 'fast', effort: 'medium' };
|
|
50
|
+
return { tag: 'balanced', effort: 'high' };
|
|
51
|
+
}
|
|
52
|
+
|
|
30
53
|
const DEFAULT_STAGE_INSTRUCTIONS = Object.freeze({
|
|
31
54
|
triage: 'Turn the request into an executable contract. Inspect relevant repository facts before deciding the flow. Classify the WorkItem, identify constraints, risks, dependencies, and missing acceptance criteria, then plan only the Actions needed for this task. Do not implement. If the goal or acceptance criteria must change, submit a contractPatch and explain why.',
|
|
32
55
|
research: 'Answer the Action objective with verifiable evidence. Search the repository and, when needed, authoritative external sources. Separate observed facts from inference, record unresolved uncertainty, and produce a concise conclusion that a later Action can use.',
|
|
@@ -97,26 +120,26 @@ const DEFAULT_SOFTWARE_CHANGE_STAGES = Object.freeze([
|
|
|
97
120
|
{
|
|
98
121
|
id: 'triage', name: 'Triage', type: 'triage',
|
|
99
122
|
assignmentPolicy: { mode: 'auto', capability: 'triage', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
|
|
100
|
-
modelPolicy: { mode: '
|
|
123
|
+
modelPolicy: { mode: 'tag', tag: 'ultimate', effort: 'xhigh' },
|
|
101
124
|
maxAttempts: 2,
|
|
102
125
|
},
|
|
103
126
|
{
|
|
104
127
|
id: 'implement', name: 'Implement', type: 'implement',
|
|
105
128
|
assignmentPolicy: { mode: 'auto', capability: 'implement', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
|
|
106
|
-
modelPolicy: { mode: '
|
|
129
|
+
modelPolicy: { mode: 'tag', tag: 'balanced', effort: 'high' },
|
|
107
130
|
maxAttempts: 2,
|
|
108
131
|
},
|
|
109
132
|
{
|
|
110
133
|
id: 'review', name: 'Review', type: 'review',
|
|
111
134
|
assignmentPolicy: { mode: 'auto', capability: 'review', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: ['implement'] },
|
|
112
|
-
modelPolicy: { mode: '
|
|
135
|
+
modelPolicy: { mode: 'tag', tag: 'ultimate', effort: 'xhigh' },
|
|
113
136
|
maxAttempts: 2,
|
|
114
137
|
changesRequestedStageId: 'implement',
|
|
115
138
|
},
|
|
116
139
|
{
|
|
117
140
|
id: 'deliver', name: 'Deliver', type: 'deliver',
|
|
118
141
|
assignmentPolicy: { mode: 'auto', capability: 'deliver', candidateVpIds: [], fixedVpId: null, separateFromStageTypes: [] },
|
|
119
|
-
modelPolicy: { mode: '
|
|
142
|
+
modelPolicy: { mode: 'tag', tag: 'fast', effort: 'medium' },
|
|
120
143
|
maxAttempts: 2,
|
|
121
144
|
},
|
|
122
145
|
]);
|
|
@@ -183,14 +206,18 @@ export function normalizeModelPolicy(value) {
|
|
|
183
206
|
const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
184
207
|
const mode = MODEL_MODES.has(source.mode) ? source.mode : 'inherit';
|
|
185
208
|
const model = typeof source.model === 'string' && source.model.trim() ? source.model.trim() : null;
|
|
209
|
+
const tag = typeof source.tag === 'string' && source.tag.trim()
|
|
210
|
+
? canonicalActionId(source.tag)
|
|
211
|
+
: null;
|
|
186
212
|
const effort = MODEL_EFFORTS.has(source.effort) ? source.effort : null;
|
|
187
213
|
if (mode === 'specific' && !model) throw new Error('Specific Work Center model policy requires a model');
|
|
188
|
-
|
|
214
|
+
if (mode === 'tag' && !tag) throw new Error('Tagged Work Center model policy requires a tag');
|
|
215
|
+
return { mode, model, tag, effort };
|
|
189
216
|
}
|
|
190
217
|
|
|
191
218
|
export function defaultActionModelPolicy(type, fallback = null) {
|
|
192
|
-
|
|
193
|
-
return {
|
|
219
|
+
if (fallback) return normalizeModelPolicy(fallback);
|
|
220
|
+
return normalizeModelPolicy({ mode: 'tag', ...defaultActionModelTag(type) });
|
|
194
221
|
}
|
|
195
222
|
|
|
196
223
|
export function normalizeActionModelPolicies(value, fallback = null) {
|
|
@@ -308,8 +335,9 @@ export function defaultWorkCenterSettings() {
|
|
|
308
335
|
maxConcurrentActions: 3,
|
|
309
336
|
defaultWorkDir: '',
|
|
310
337
|
globalInstructions: '',
|
|
311
|
-
|
|
312
|
-
|
|
338
|
+
modelTags: { ...DEFAULT_WORK_CENTER_MODEL_TAGS },
|
|
339
|
+
modelPolicy: normalizeModelPolicy({ mode: 'tag', tag: 'balanced', effort: 'high' }),
|
|
340
|
+
coordinatorModelPolicy: normalizeModelPolicy({ mode: 'tag', tag: 'ultimate', effort: 'xhigh' }),
|
|
313
341
|
actionModelPolicies: normalizeActionModelPolicies(),
|
|
314
342
|
actionInstructions: normalizeActionInstructions(),
|
|
315
343
|
workflows: [normalizeWorkflowDefinition({
|
|
@@ -347,6 +375,7 @@ export function normalizeWorkCenterSettings(value) {
|
|
|
347
375
|
maxConcurrentActions: Math.min(Math.max(Number(source.maxConcurrentActions) || 3, 1), 12),
|
|
348
376
|
defaultWorkDir: typeof source.defaultWorkDir === 'string' ? source.defaultWorkDir.trim() : '',
|
|
349
377
|
globalInstructions: normalizeGlobalInstructions(source.globalInstructions),
|
|
378
|
+
modelTags: normalizeModelTags(source.modelTags),
|
|
350
379
|
modelPolicy: normalizeModelPolicy(source.modelPolicy || migratedModelPolicy),
|
|
351
380
|
coordinatorModelPolicy: normalizeModelPolicy(
|
|
352
381
|
source.coordinatorModelPolicy || { ...(source.modelPolicy || migratedModelPolicy), effort: 'high' },
|