@yeaft/webchat-agent 1.0.409 → 1.0.410
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/connection/message-router.js +24 -2
- package/llm-config-cli.js +36 -3
- package/local-runtime/server/handlers/agent-output.js +14 -0
- package/local-runtime/server/handlers/agent-sync.js +6 -1
- package/local-runtime/server/handlers/client-conversation.js +4 -0
- package/local-runtime/server/handlers/client-misc.js +27 -0
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +166 -64
- 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/yeaft/config-api.js +109 -48
- package/yeaft/config.js +56 -9
- package/yeaft/engine.js +98 -43
- package/yeaft/plugins.js +170 -0
- package/yeaft/session.js +7 -4
- package/yeaft/sessions/feature-flag.js +42 -9
- package/yeaft/tools/mcp-tools.js +1 -0
- package/yeaft/tools/registry.js +51 -7
- package/yeaft/tools/types.js +6 -0
- package/yeaft/web-bridge.js +317 -89
- package/yeaft/work-center/runner.js +5 -1
|
Binary file
|
package/package.json
CHANGED
package/yeaft/config-api.js
CHANGED
|
@@ -13,8 +13,33 @@ import { join } from 'path';
|
|
|
13
13
|
import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
14
14
|
import { normalizeProviderModels, parseModelRef, serializeModelForPersistence } from './models.js';
|
|
15
15
|
import { normaliseTelemetrySection, normaliseYeaftSection } from './config.js';
|
|
16
|
+
import { normalizePluginConfig } from './plugins.js';
|
|
16
17
|
import { isGitHubCopilotProvider, serializeKnownProviderForPersistence } from './llm/known-providers.js';
|
|
17
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Read config.json before any public mutation. A missing file is a valid
|
|
21
|
+
* first-run state, but an existing malformed file, non-object root, or invalid
|
|
22
|
+
* Plugins schema must never be replaced by an unrelated Settings/MCP write.
|
|
23
|
+
* Otherwise the runtime's fail-closed policy could silently become inheritance
|
|
24
|
+
* (all capabilities enabled) on the next config reload.
|
|
25
|
+
*
|
|
26
|
+
* @param {string} configPath
|
|
27
|
+
* @returns {Record<string, unknown>}
|
|
28
|
+
* @throws {Error} when an existing config cannot be safely preserved
|
|
29
|
+
*/
|
|
30
|
+
function readConfigForWrite(configPath) {
|
|
31
|
+
if (!existsSync(configPath)) return {};
|
|
32
|
+
const json = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
33
|
+
if (!json || typeof json !== 'object' || Array.isArray(json)
|
|
34
|
+
|| Object.getPrototypeOf(json) !== Object.prototype) {
|
|
35
|
+
throw new Error('config.json must contain an object');
|
|
36
|
+
}
|
|
37
|
+
if (Object.prototype.hasOwnProperty.call(json, 'plugins')) {
|
|
38
|
+
normalizePluginConfig(json.plugins);
|
|
39
|
+
}
|
|
40
|
+
return json;
|
|
41
|
+
}
|
|
42
|
+
|
|
18
43
|
/**
|
|
19
44
|
* Read the LLM-relevant portion of config.json.
|
|
20
45
|
*
|
|
@@ -119,15 +144,13 @@ export function updateLlmConfig(update, dir) {
|
|
|
119
144
|
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
120
145
|
const configPath = join(root, 'config.json');
|
|
121
146
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
existing = {};
|
|
130
|
-
}
|
|
147
|
+
// Preserve all existing fields only when the on-disk document and its
|
|
148
|
+
// Plugins policy are valid. Never turn a failed read into a fresh config.
|
|
149
|
+
let existing;
|
|
150
|
+
try {
|
|
151
|
+
existing = readConfigForWrite(configPath);
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { error: `Failed to read config.json: ${err?.message || err}` };
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
// Validate providers structure
|
|
@@ -265,14 +288,14 @@ export function updateYeaftSettings(update, dir) {
|
|
|
265
288
|
}
|
|
266
289
|
}
|
|
267
290
|
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
291
|
+
// Preserve all existing fields only when the on-disk document and its
|
|
292
|
+
// Plugins policy are valid. A Settings update must not repair bad JSON into
|
|
293
|
+
// a config whose missing Plugins fields inherit all capabilities.
|
|
294
|
+
let existing;
|
|
295
|
+
try {
|
|
296
|
+
existing = readConfigForWrite(configPath);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
return { error: `Failed to read config.json: ${err?.message || err}` };
|
|
276
299
|
}
|
|
277
300
|
|
|
278
301
|
const prev = normaliseYeaftSection(existing.yeaft);
|
|
@@ -336,7 +359,12 @@ export function updateTelemetrySettings(update, dir) {
|
|
|
336
359
|
}
|
|
337
360
|
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
338
361
|
const configPath = join(root, 'config.json');
|
|
339
|
-
|
|
362
|
+
let existing;
|
|
363
|
+
try {
|
|
364
|
+
existing = readConfigForWrite(configPath);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
return { error: `Failed to read config.json: ${err?.message || err}` };
|
|
367
|
+
}
|
|
340
368
|
const merged = normaliseTelemetrySection({
|
|
341
369
|
...(existing.telemetry && typeof existing.telemetry === 'object' ? existing.telemetry : {}),
|
|
342
370
|
...update,
|
|
@@ -427,13 +455,11 @@ export function updateSearchSettings(update, dir) {
|
|
|
427
455
|
return { error: 'tavilyApiKey must be a string' };
|
|
428
456
|
}
|
|
429
457
|
|
|
430
|
-
let existing
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
existing = {};
|
|
436
|
-
}
|
|
458
|
+
let existing;
|
|
459
|
+
try {
|
|
460
|
+
existing = readConfigForWrite(configPath);
|
|
461
|
+
} catch (err) {
|
|
462
|
+
return { error: `Failed to read config.json: ${err?.message || err}` };
|
|
437
463
|
}
|
|
438
464
|
const prev = (existing && typeof existing.search === 'object' && existing.search) || {};
|
|
439
465
|
const merged = { ...prev };
|
|
@@ -500,6 +526,49 @@ export async function fetchTavilyUsage(dir) {
|
|
|
500
526
|
}
|
|
501
527
|
}
|
|
502
528
|
|
|
529
|
+
// ─── Agent plugin selection ────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Read the Agent-local plugin allowlists. Missing category fields mean
|
|
533
|
+
* inheritance (all discovered capabilities remain available).
|
|
534
|
+
*/
|
|
535
|
+
export function getPluginConfig(dir) {
|
|
536
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
537
|
+
const configPath = join(root, 'config.json');
|
|
538
|
+
try {
|
|
539
|
+
const json = readConfigForWrite(configPath);
|
|
540
|
+
return { plugins: normalizePluginConfig(json.plugins) };
|
|
541
|
+
} catch (err) {
|
|
542
|
+
return { error: `Failed to read plugin config: ${err?.message || err}` };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Persist Agent-local plugin allowlists without touching providers, MCP server
|
|
548
|
+
* definitions, or any other config.json field.
|
|
549
|
+
*/
|
|
550
|
+
export function updatePluginConfig(plugins, dir) {
|
|
551
|
+
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
552
|
+
const configPath = join(root, 'config.json');
|
|
553
|
+
let normalized;
|
|
554
|
+
let existing;
|
|
555
|
+
try {
|
|
556
|
+
existing = readConfigForWrite(configPath);
|
|
557
|
+
normalized = normalizePluginConfig(plugins);
|
|
558
|
+
} catch (err) {
|
|
559
|
+
return { error: `Failed to read plugin config: ${err?.message || err}` };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (Object.keys(normalized).length === 0) delete existing.plugins;
|
|
563
|
+
else existing.plugins = normalized;
|
|
564
|
+
try {
|
|
565
|
+
writeFileSync(configPath, JSON.stringify(existing, null, 2) + '\n', 'utf8');
|
|
566
|
+
} catch (err) {
|
|
567
|
+
return { error: `Failed to write plugin config: ${err?.message || err}` };
|
|
568
|
+
}
|
|
569
|
+
return { plugins: normalized };
|
|
570
|
+
}
|
|
571
|
+
|
|
503
572
|
// ─── MCP server config (mcpServers array in config.json) ──
|
|
504
573
|
|
|
505
574
|
/**
|
|
@@ -579,24 +648,6 @@ function validateMcpServer(entry) {
|
|
|
579
648
|
return null;
|
|
580
649
|
}
|
|
581
650
|
|
|
582
|
-
/**
|
|
583
|
-
* Read existing config.json (silently start fresh on missing / corrupt).
|
|
584
|
-
* Internal helper used by the MCP CRUD trio to share one parse path.
|
|
585
|
-
*
|
|
586
|
-
* @param {string} configPath
|
|
587
|
-
* @returns {object}
|
|
588
|
-
*/
|
|
589
|
-
function readConfigJson(configPath) {
|
|
590
|
-
if (!existsSync(configPath)) return {};
|
|
591
|
-
try {
|
|
592
|
-
const raw = readFileSync(configPath, 'utf8');
|
|
593
|
-
const json = JSON.parse(raw);
|
|
594
|
-
return (json && typeof json === 'object') ? json : {};
|
|
595
|
-
} catch {
|
|
596
|
-
return {};
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
651
|
/**
|
|
601
652
|
* List MCP servers currently saved in config.json. Returns an array — empty
|
|
602
653
|
* when none configured. Each entry is the normalised on-disk shape, NOT
|
|
@@ -609,12 +660,12 @@ export function listMcpServers(dir) {
|
|
|
609
660
|
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
610
661
|
const configPath = join(root, 'config.json');
|
|
611
662
|
try {
|
|
612
|
-
const json =
|
|
663
|
+
const json = readConfigForWrite(configPath);
|
|
613
664
|
const raw = Array.isArray(json.mcpServers) ? json.mcpServers : [];
|
|
614
665
|
const servers = raw.map(normaliseMcpServer).filter(Boolean);
|
|
615
666
|
return { servers };
|
|
616
|
-
} catch (
|
|
617
|
-
return { error: `Failed to read config.json: ${
|
|
667
|
+
} catch (err) {
|
|
668
|
+
return { error: `Failed to read config.json: ${err?.message || err}` };
|
|
618
669
|
}
|
|
619
670
|
}
|
|
620
671
|
|
|
@@ -634,7 +685,12 @@ export function upsertMcpServer(server, dir) {
|
|
|
634
685
|
|
|
635
686
|
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
636
687
|
const configPath = join(root, 'config.json');
|
|
637
|
-
|
|
688
|
+
let existing;
|
|
689
|
+
try {
|
|
690
|
+
existing = readConfigForWrite(configPath);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
return { error: `Failed to read config.json: ${err?.message || err}` };
|
|
693
|
+
}
|
|
638
694
|
const list = Array.isArray(existing.mcpServers) ? existing.mcpServers.slice() : [];
|
|
639
695
|
|
|
640
696
|
const normalised = normaliseMcpServer(server);
|
|
@@ -677,7 +733,12 @@ export function removeMcpServer(name, dir) {
|
|
|
677
733
|
const target = name.trim();
|
|
678
734
|
const root = dir || process.env.YEAFT_DIR || DEFAULT_YEAFT_DIR;
|
|
679
735
|
const configPath = join(root, 'config.json');
|
|
680
|
-
|
|
736
|
+
let existing;
|
|
737
|
+
try {
|
|
738
|
+
existing = readConfigForWrite(configPath);
|
|
739
|
+
} catch (err) {
|
|
740
|
+
return { error: `Failed to read config.json: ${err?.message || err}` };
|
|
741
|
+
}
|
|
681
742
|
const list = Array.isArray(existing.mcpServers) ? existing.mcpServers.slice() : [];
|
|
682
743
|
const next = list.filter(s => !(s && typeof s === 'object' && s.name === target));
|
|
683
744
|
const removed = next.length !== list.length;
|
package/yeaft/config.js
CHANGED
|
@@ -26,6 +26,7 @@ import { DEFAULT_YEAFT_DIR } from './init.js';
|
|
|
26
26
|
import { getModelEffortOptions, getThinkingCapability, modelSupportsEffort, resolveModel, parseModelRef, normalizeProviderModels, resolveContextWindow, resolveMaxOutputTokens } from './models.js';
|
|
27
27
|
import { inferProtocolFromModelId } from './llm/router.js';
|
|
28
28
|
import { normalizeKnownProviderForRuntime } from './llm/known-providers.js';
|
|
29
|
+
import { createDenyAllPluginConfig, normalizePluginConfig } from './plugins.js';
|
|
29
30
|
import { readWorkspaceFile } from './workspace-file.js';
|
|
30
31
|
|
|
31
32
|
/** Default configuration values. */
|
|
@@ -367,6 +368,7 @@ function loadLegacyConfig(dir, overrides) {
|
|
|
367
368
|
// task-318: legacy path never had the `yeaft` section — defaults.
|
|
368
369
|
yeaft: normaliseYeaftSection(null),
|
|
369
370
|
telemetry: normaliseTelemetrySection(null),
|
|
371
|
+
plugins: {},
|
|
370
372
|
providers: null,
|
|
371
373
|
primaryModel: null,
|
|
372
374
|
fastModel: null,
|
|
@@ -413,12 +415,20 @@ export function loadConfig(overrides = {}) {
|
|
|
413
415
|
// Determine data directory
|
|
414
416
|
const dir = overrides.dir || process.env.YEAFT_DIR || DEFAULTS.dir;
|
|
415
417
|
|
|
416
|
-
// Try config.json first
|
|
418
|
+
// Try config.json first. A missing file preserves legacy behavior, but an
|
|
419
|
+
// existing file that cannot be parsed (or is not an object) must not reopen
|
|
420
|
+
// a persisted Plugin restriction through the legacy fallback.
|
|
421
|
+
const configPath = join(dir, 'config.json');
|
|
422
|
+
const hasConfigFile = existsSync(configPath);
|
|
417
423
|
const jsonConfig = readConfigJson(dir);
|
|
418
424
|
|
|
419
|
-
if (!jsonConfig) {
|
|
420
|
-
|
|
421
|
-
|
|
425
|
+
if (!jsonConfig || typeof jsonConfig !== 'object' || Array.isArray(jsonConfig)) {
|
|
426
|
+
const config = loadLegacyConfig(dir, overrides);
|
|
427
|
+
if (hasConfigFile) {
|
|
428
|
+
config.plugins = createDenyAllPluginConfig();
|
|
429
|
+
config.pluginConfigError = 'config.json is invalid or must contain an object';
|
|
430
|
+
}
|
|
431
|
+
return config;
|
|
422
432
|
}
|
|
423
433
|
|
|
424
434
|
// ─── Build config from config.json ────────────────────────
|
|
@@ -461,6 +471,18 @@ export function loadConfig(overrides = {}) {
|
|
|
461
471
|
const resolvedMaxOutput = overrides.maxOutputTokens ?? jsonConfig.maxOutputTokens
|
|
462
472
|
?? resolveMaxOutputTokens(modelIdForInfo, { modelInfo });
|
|
463
473
|
|
|
474
|
+
// Missing plugins keeps the historical all-enabled behavior. A present but
|
|
475
|
+
// invalid schema must not collapse to `{}` because that is also all-enabled;
|
|
476
|
+
// use explicit empty allowlists until the user repairs config.json instead.
|
|
477
|
+
let plugins;
|
|
478
|
+
let pluginConfigError = null;
|
|
479
|
+
try {
|
|
480
|
+
plugins = normalizePluginConfig(jsonConfig.plugins);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
plugins = createDenyAllPluginConfig();
|
|
483
|
+
pluginConfigError = err?.message || String(err);
|
|
484
|
+
}
|
|
485
|
+
|
|
464
486
|
const config = {
|
|
465
487
|
// Model
|
|
466
488
|
model: overrides.model || model,
|
|
@@ -503,6 +525,13 @@ export function loadConfig(overrides = {}) {
|
|
|
503
525
|
yeaft: normaliseYeaftSection(jsonConfig.yeaft),
|
|
504
526
|
telemetry: normaliseTelemetrySection(jsonConfig.telemetry),
|
|
505
527
|
|
|
528
|
+
// Agent-level tools / skills / MCP server allowlists. Missing fields mean
|
|
529
|
+
// all currently discovered capabilities remain enabled. A persisted schema
|
|
530
|
+
// error is represented by explicit empty allowlists so runtime policy fails
|
|
531
|
+
// closed instead of reopening every capability.
|
|
532
|
+
plugins,
|
|
533
|
+
pluginConfigError,
|
|
534
|
+
|
|
506
535
|
// Legacy fields (null when using config.json)
|
|
507
536
|
apiKey: overrides.apiKey || null,
|
|
508
537
|
openaiApiKey: null,
|
|
@@ -600,7 +629,7 @@ export function loadConfig(overrides = {}) {
|
|
|
600
629
|
* @returns {{ servers: object[], skipped: { name: string, reason: string, source: string }[] }}
|
|
601
630
|
*/
|
|
602
631
|
export function loadMCPConfig(yeaftDir, jsonConfig, workDir, options = {}) {
|
|
603
|
-
const yeaftGlobal =
|
|
632
|
+
const yeaftGlobal = loadAgentMCPConfig(yeaftDir, jsonConfig);
|
|
604
633
|
const externalUser = loadExternalUserMCPServers();
|
|
605
634
|
const project = workDir
|
|
606
635
|
? loadProjectMCPServers(workDir, options)
|
|
@@ -608,7 +637,7 @@ export function loadMCPConfig(yeaftDir, jsonConfig, workDir, options = {}) {
|
|
|
608
637
|
|
|
609
638
|
const servers = [];
|
|
610
639
|
const seen = new Set();
|
|
611
|
-
for (const tier of [yeaftGlobal, externalUser.servers, project.servers]) {
|
|
640
|
+
for (const tier of [yeaftGlobal.servers, externalUser.servers, project.servers]) {
|
|
612
641
|
for (const s of tier) {
|
|
613
642
|
if (!s?.name || seen.has(s.name)) continue;
|
|
614
643
|
seen.add(s.name);
|
|
@@ -619,6 +648,24 @@ export function loadMCPConfig(yeaftDir, jsonConfig, workDir, options = {}) {
|
|
|
619
648
|
return { servers, skipped: [...externalUser.skipped, ...project.skipped] };
|
|
620
649
|
}
|
|
621
650
|
|
|
651
|
+
/**
|
|
652
|
+
* Load only the Agent-owned MCP configuration tier. Plugin Center uses this
|
|
653
|
+
* catalog source so it reflects current Agent configuration rather than
|
|
654
|
+
* borrowed user/project MCP sources or a live runtime snapshot.
|
|
655
|
+
*
|
|
656
|
+
* An explicitly present `config.json.mcpServers` array is authoritative,
|
|
657
|
+
* including an empty array. The legacy `mcp.json` fallback applies only when
|
|
658
|
+
* that field is absent.
|
|
659
|
+
*
|
|
660
|
+
* @param {string} yeaftDir
|
|
661
|
+
* @param {object} [jsonConfig] — Already-parsed config.json (optional, avoids re-read)
|
|
662
|
+
* @returns {{ servers: object[], skipped: object[] }}
|
|
663
|
+
*/
|
|
664
|
+
export function loadAgentMCPConfig(yeaftDir, jsonConfig) {
|
|
665
|
+
const configured = jsonConfig === undefined ? readConfigJson(yeaftDir) : jsonConfig;
|
|
666
|
+
return { servers: loadGlobalMCPServers(yeaftDir, configured), skipped: [] };
|
|
667
|
+
}
|
|
668
|
+
|
|
622
669
|
/**
|
|
623
670
|
* Load the global (~/.yeaft) MCP server list.
|
|
624
671
|
*
|
|
@@ -631,10 +678,10 @@ export function loadMCPConfig(yeaftDir, jsonConfig, workDir, options = {}) {
|
|
|
631
678
|
* @returns {object[]}
|
|
632
679
|
*/
|
|
633
680
|
function loadGlobalMCPServers(yeaftDir, jsonConfig) {
|
|
634
|
-
//
|
|
681
|
+
// An explicitly present config.json array is authoritative, even when it
|
|
682
|
+
// is empty. Falling back in that case would resurrect removed MCP servers.
|
|
635
683
|
if (jsonConfig && Array.isArray(jsonConfig.mcpServers)) {
|
|
636
|
-
|
|
637
|
-
if (valid.length > 0) return valid;
|
|
684
|
+
return jsonConfig.mcpServers.filter(s => s.name && s.command);
|
|
638
685
|
}
|
|
639
686
|
|
|
640
687
|
// Fallback: standalone mcp.json
|
package/yeaft/engine.js
CHANGED
|
@@ -56,6 +56,7 @@ import { COLLAB_TOOL_POLICY, isToolErrorOutput, localizeVisibleText, normalizeTo
|
|
|
56
56
|
import { CONDITIONAL_BUILTIN_TOOL_NAMES, resolveActiveToolNames } from './tools/activation.js';
|
|
57
57
|
import { discoverToolCapabilities } from './tools/discover-tools.js';
|
|
58
58
|
import { agentBelongsToScope, getAgentRegistry } from './tools/agent.js';
|
|
59
|
+
import { createPluginSkillManager } from './plugins.js';
|
|
59
60
|
import { extractDisplayImages, stripDisplayImageData } from './image-assets.js';
|
|
60
61
|
import { acknowledgePendingNotifications, formatNotificationsForPrompt, peekPendingNotifications } from './sub-agent/notifications.js';
|
|
61
62
|
import {
|
|
@@ -203,6 +204,39 @@ function stripLeadingSkillCommandFromPromptParts(promptParts, skillManager) {
|
|
|
203
204
|
});
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
function resolveSkillPromptState({ skillManager, prompt, explicitSkillName }) {
|
|
208
|
+
let resolvedSkillContent = '';
|
|
209
|
+
let resolvedSkills = [];
|
|
210
|
+
let skillResolutionError = null;
|
|
211
|
+
if (!skillManager) {
|
|
212
|
+
return { resolvedSkillContent, resolvedSkills, skillResolutionError };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (explicitSkillName) {
|
|
216
|
+
resolvedSkillContent = skillManager.getPromptContent(explicitSkillName);
|
|
217
|
+
const skill = skillManager.list?.().find(item => item.name === explicitSkillName)
|
|
218
|
+
|| (resolvedSkillContent ? { name: explicitSkillName } : null);
|
|
219
|
+
if (resolvedSkillContent && skill) {
|
|
220
|
+
resolvedSkills = [{ ...skill, explicit: true }];
|
|
221
|
+
} else {
|
|
222
|
+
skillResolutionError = `Requested skill "${explicitSkillName}" was not found.`;
|
|
223
|
+
resolvedSkillContent = `## Skill command error\n\n${skillResolutionError} Continue without that skill and tell the user it is unavailable.`;
|
|
224
|
+
}
|
|
225
|
+
} else if (prompt && typeof skillManager.findRelevant === 'function') {
|
|
226
|
+
resolvedSkills = skillManager.findRelevant(prompt).map(skill => ({
|
|
227
|
+
name: skill.name,
|
|
228
|
+
description: skill.description || '',
|
|
229
|
+
trigger: skill.trigger || '',
|
|
230
|
+
category: skill.category,
|
|
231
|
+
tier: skill._tier,
|
|
232
|
+
explicit: false,
|
|
233
|
+
}));
|
|
234
|
+
resolvedSkillContent = resolvedSkills.map(skill => skillManager.getPromptContent(skill.name)).join('\n\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { resolvedSkillContent, resolvedSkills, skillResolutionError };
|
|
238
|
+
}
|
|
239
|
+
|
|
206
240
|
function resolveRetryPolicy(config) {
|
|
207
241
|
const raw = config?.llmRetry || {};
|
|
208
242
|
const num = (v, d) => (Number.isFinite(v) && v >= 0 ? v : d);
|
|
@@ -617,6 +651,9 @@ export class Engine {
|
|
|
617
651
|
/** @type {import('./skills.js').SkillManager|null} */
|
|
618
652
|
#skillManager;
|
|
619
653
|
|
|
654
|
+
/** @type {import('./skills.js').SkillManager|null} */
|
|
655
|
+
#baseSkillManager;
|
|
656
|
+
|
|
620
657
|
/** @type {import('./mcp.js').MCPManager|null} */
|
|
621
658
|
#mcpManager;
|
|
622
659
|
|
|
@@ -838,7 +875,10 @@ export class Engine {
|
|
|
838
875
|
this.#amsRegistry = amsRegistry || null;
|
|
839
876
|
this.#toolRegistry = toolRegistry || null;
|
|
840
877
|
this.#taskManager = taskManager || null;
|
|
841
|
-
this.#
|
|
878
|
+
this.#baseSkillManager = skillManager || null;
|
|
879
|
+
this.#skillManager = this.#baseSkillManager && Array.isArray(config?.plugins?.skills)
|
|
880
|
+
? createPluginSkillManager(this.#baseSkillManager, config.plugins)
|
|
881
|
+
: this.#baseSkillManager;
|
|
842
882
|
this.#mcpManager = mcpManager || null;
|
|
843
883
|
this.#yeaftDir = yeaftDir || null;
|
|
844
884
|
this.#managedCliReady = managedCliReady || null;
|
|
@@ -939,6 +979,9 @@ export class Engine {
|
|
|
939
979
|
refreshConfig(config) {
|
|
940
980
|
if (!config || typeof config !== 'object') return;
|
|
941
981
|
this.#config = config;
|
|
982
|
+
this.#skillManager = this.#baseSkillManager && Array.isArray(config.plugins?.skills)
|
|
983
|
+
? createPluginSkillManager(this.#baseSkillManager, config.plugins)
|
|
984
|
+
: this.#baseSkillManager;
|
|
942
985
|
const fastModelId = config.fastModelId || config.model;
|
|
943
986
|
this.#fastConfig = fastModelId !== config.model
|
|
944
987
|
? { ...config, model: fastModelId }
|
|
@@ -954,7 +997,10 @@ export class Engine {
|
|
|
954
997
|
*/
|
|
955
998
|
setRuntimeManagers(managers = {}) {
|
|
956
999
|
if (Object.prototype.hasOwnProperty.call(managers, 'skillManager')) {
|
|
957
|
-
this.#
|
|
1000
|
+
this.#baseSkillManager = managers.skillManager || null;
|
|
1001
|
+
this.#skillManager = this.#baseSkillManager && Array.isArray(this.#config?.plugins?.skills)
|
|
1002
|
+
? createPluginSkillManager(this.#baseSkillManager, this.#config.plugins)
|
|
1003
|
+
: this.#baseSkillManager;
|
|
958
1004
|
}
|
|
959
1005
|
if (Object.prototype.hasOwnProperty.call(managers, 'mcpManager')) {
|
|
960
1006
|
this.#mcpManager = managers.mcpManager || null;
|
|
@@ -984,6 +1030,7 @@ export class Engine {
|
|
|
984
1030
|
if (this.#toolRegistry) {
|
|
985
1031
|
return this.#toolRegistry.getToolDefs(this.#config?.language || 'en', {
|
|
986
1032
|
collabToolPolicy,
|
|
1033
|
+
plugins: this.#config?.plugins,
|
|
987
1034
|
activeToolNames,
|
|
988
1035
|
});
|
|
989
1036
|
}
|
|
@@ -1220,10 +1267,11 @@ export class Engine {
|
|
|
1220
1267
|
* @param {string} [args.explicitSkillName] — leading /skill:<name> command, if present
|
|
1221
1268
|
* @returns {string}
|
|
1222
1269
|
*/
|
|
1223
|
-
#buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectInstruction, projectLabel, workCenterInstructions, projectDoc, taskCtx, activeTasks, activeToolNames = null, promptNotices = [], explicitSkillName, resolvedSkillContent = null } = {}) {
|
|
1224
|
-
// Skill selection
|
|
1225
|
-
//
|
|
1226
|
-
// fallback for internal callers that do not need selection
|
|
1270
|
+
#buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectInstruction, projectLabel, workCenterInstructions, projectDoc, taskCtx, activeTasks, collabToolPolicy = null, activeToolNames = null, promptNotices = [], explicitSkillName, resolvedSkillContent = null } = {}) {
|
|
1271
|
+
// #runQuery resolves Skill selection at each provider-request boundary so
|
|
1272
|
+
// a live Plugin policy change cannot leave stale content in the next prompt.
|
|
1273
|
+
// Keep the local fallback for internal callers that do not need selection
|
|
1274
|
+
// events.
|
|
1227
1275
|
let skillContent = typeof resolvedSkillContent === 'string' ? resolvedSkillContent : '';
|
|
1228
1276
|
if (resolvedSkillContent === null && this.#skillManager) {
|
|
1229
1277
|
if (explicitSkillName) {
|
|
@@ -1234,11 +1282,13 @@ export class Engine {
|
|
|
1234
1282
|
}
|
|
1235
1283
|
}
|
|
1236
1284
|
|
|
1237
|
-
//
|
|
1238
|
-
//
|
|
1239
|
-
// catalogue because the API tool definitions are already authoritative.
|
|
1285
|
+
// Prompt guidance must describe the same canonical capability
|
|
1286
|
+
// intersection that reaches provider schemas and execution.
|
|
1240
1287
|
const registeredToolNames = this.#toolRegistry
|
|
1241
|
-
? this.#toolRegistry.getToolNames(
|
|
1288
|
+
? this.#toolRegistry.getToolNames({
|
|
1289
|
+
plugins: this.#config?.plugins,
|
|
1290
|
+
collabToolPolicy,
|
|
1291
|
+
})
|
|
1242
1292
|
: Array.from(this.#tools.keys());
|
|
1243
1293
|
const toolNames = activeToolNames instanceof Set
|
|
1244
1294
|
? registeredToolNames.filter(name => activeToolNames.has(name))
|
|
@@ -2422,8 +2472,12 @@ export class Engine {
|
|
|
2422
2472
|
})
|
|
2423
2473
|
: '';
|
|
2424
2474
|
const registeredToolNames = this.#toolRegistry
|
|
2425
|
-
? this.#toolRegistry.getToolNames(
|
|
2475
|
+
? this.#toolRegistry.getToolNames({
|
|
2476
|
+
collabToolPolicy: effectiveCollabToolPolicy,
|
|
2477
|
+
plugins: this.#config?.plugins,
|
|
2478
|
+
})
|
|
2426
2479
|
: Array.from(this.#tools.keys());
|
|
2480
|
+
const registeredToolNameSet = new Set(registeredToolNames);
|
|
2427
2481
|
const resolveCurrentActiveToolNames = () => this.#toolRegistry
|
|
2428
2482
|
? resolveActiveToolNames({
|
|
2429
2483
|
toolNames: registeredToolNames,
|
|
@@ -2444,6 +2498,7 @@ export class Engine {
|
|
|
2444
2498
|
const discoveryTraversals = new Map();
|
|
2445
2499
|
const currentDiscoverableTools = () => this.#toolRegistry
|
|
2446
2500
|
? this.#toolRegistry.getAllTools()
|
|
2501
|
+
.filter(tool => registeredToolNameSet.has(tool.name))
|
|
2447
2502
|
.filter(tool => CONDITIONAL_BUILTIN_TOOL_NAMES.has(tool.name) || tool.name.startsWith('mcp__'))
|
|
2448
2503
|
: [];
|
|
2449
2504
|
const discoveryDirectorySnapshot = (tools, language) => tools
|
|
@@ -2469,32 +2524,11 @@ export class Engine {
|
|
|
2469
2524
|
}
|
|
2470
2525
|
};
|
|
2471
2526
|
let activeToolNames = resolveCurrentActiveToolNames();
|
|
2472
|
-
let resolvedSkillContent =
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
resolvedSkillContent = this.#skillManager.getPromptContent(explicitSkillName);
|
|
2478
|
-
const skill = this.#skillManager.list?.().find(item => item.name === explicitSkillName)
|
|
2479
|
-
|| (resolvedSkillContent ? { name: explicitSkillName } : null);
|
|
2480
|
-
if (resolvedSkillContent && skill) {
|
|
2481
|
-
resolvedSkills = [{ ...skill, explicit: true }];
|
|
2482
|
-
} else {
|
|
2483
|
-
skillResolutionError = `Requested skill "${explicitSkillName}" was not found.`;
|
|
2484
|
-
resolvedSkillContent = `## Skill command error\n\n${skillResolutionError} Continue without that skill and tell the user it is unavailable.`;
|
|
2485
|
-
}
|
|
2486
|
-
} else if (prompt && typeof this.#skillManager.findRelevant === 'function') {
|
|
2487
|
-
resolvedSkills = this.#skillManager.findRelevant(prompt).map(skill => ({
|
|
2488
|
-
name: skill.name,
|
|
2489
|
-
description: skill.description || '',
|
|
2490
|
-
trigger: skill.trigger || '',
|
|
2491
|
-
category: skill.category,
|
|
2492
|
-
tier: skill._tier,
|
|
2493
|
-
explicit: false,
|
|
2494
|
-
}));
|
|
2495
|
-
resolvedSkillContent = resolvedSkills.map(skill => this.#skillManager.getPromptContent(skill.name)).join('\n\n');
|
|
2496
|
-
}
|
|
2497
|
-
}
|
|
2527
|
+
let { resolvedSkillContent, resolvedSkills, skillResolutionError } = resolveSkillPromptState({
|
|
2528
|
+
skillManager: this.#skillManager,
|
|
2529
|
+
prompt,
|
|
2530
|
+
explicitSkillName,
|
|
2531
|
+
});
|
|
2498
2532
|
|
|
2499
2533
|
let promptNotices = [];
|
|
2500
2534
|
const buildCurrentSystemPrompt = () => this.#buildSystemPrompt({
|
|
@@ -2508,6 +2542,7 @@ export class Engine {
|
|
|
2508
2542
|
workCenterInstructions,
|
|
2509
2543
|
projectDoc: projectDocContext.text,
|
|
2510
2544
|
activeTasks,
|
|
2545
|
+
collabToolPolicy: effectiveCollabToolPolicy,
|
|
2511
2546
|
activeToolNames,
|
|
2512
2547
|
promptNotices,
|
|
2513
2548
|
explicitSkillName,
|
|
@@ -2678,12 +2713,10 @@ export class Engine {
|
|
|
2678
2713
|
sessionId: sessionId || null,
|
|
2679
2714
|
at: queryStartedAt,
|
|
2680
2715
|
};
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
yield { type: 'skill_error', turnId: queryTurnId, skillName: explicitSkillName, message: skillResolutionError };
|
|
2686
|
-
}
|
|
2716
|
+
// Skill selection is emitted at the provider-request boundary below. A
|
|
2717
|
+
// persisted Plugin update may replace the filtered SkillManager while this
|
|
2718
|
+
// query is paused on a tool, so reporting it before that boundary could
|
|
2719
|
+
// claim a Skill was loaded even though its content never reached a request.
|
|
2687
2720
|
|
|
2688
2721
|
// Surface the exact memory that entered the prompt. This must be based on
|
|
2689
2722
|
// the AMS snapshot, not raw FTS candidates, otherwise debug can claim memory
|
|
@@ -2729,6 +2762,11 @@ export class Engine {
|
|
|
2729
2762
|
let cumulativeInputTokens = 0;
|
|
2730
2763
|
let cumulativeOutputTokens = 0;
|
|
2731
2764
|
let activeProviderRequest = null;
|
|
2765
|
+
// Skill events describe the selection injected into each provider request.
|
|
2766
|
+
// The first request must report its initial selection; later loops report
|
|
2767
|
+
// only newly added Skills or a newly introduced explicit-command error.
|
|
2768
|
+
let reportedSkillNames = new Set();
|
|
2769
|
+
let reportedSkillError = null;
|
|
2732
2770
|
// task-707: tool-callable end-turn signal. Tools (currently only
|
|
2733
2771
|
// `route_forward`) can set this via toolCtx.requestEndTurn(reason)
|
|
2734
2772
|
// to break out of the tool-loop after the current batch finishes
|
|
@@ -2936,6 +2974,22 @@ export class Engine {
|
|
|
2936
2974
|
else discoveredToolNames.delete(name);
|
|
2937
2975
|
}
|
|
2938
2976
|
toolDefs = this.#getToolDefs(effectiveCollabToolPolicy, activeToolNames);
|
|
2977
|
+
({ resolvedSkillContent, resolvedSkills, skillResolutionError } = resolveSkillPromptState({
|
|
2978
|
+
skillManager: this.#skillManager,
|
|
2979
|
+
prompt,
|
|
2980
|
+
explicitSkillName,
|
|
2981
|
+
}));
|
|
2982
|
+
const currentSkillNames = new Set(resolvedSkills.map(skill => skill.name));
|
|
2983
|
+
for (const skill of resolvedSkills) {
|
|
2984
|
+
if (!reportedSkillNames.has(skill.name)) {
|
|
2985
|
+
yield { type: 'skill_loaded', turnId: queryTurnId, skill };
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
if (skillResolutionError && skillResolutionError !== reportedSkillError) {
|
|
2989
|
+
yield { type: 'skill_error', turnId: queryTurnId, skillName: explicitSkillName, message: skillResolutionError };
|
|
2990
|
+
}
|
|
2991
|
+
reportedSkillNames = currentSkillNames;
|
|
2992
|
+
reportedSkillError = skillResolutionError;
|
|
2939
2993
|
systemPrompt = buildCurrentSystemPrompt();
|
|
2940
2994
|
|
|
2941
2995
|
try {
|
|
@@ -4187,6 +4241,7 @@ export class Engine {
|
|
|
4187
4241
|
const hasTool = this.#toolRegistry
|
|
4188
4242
|
? this.#toolRegistry.isAllowed(tc.name, {
|
|
4189
4243
|
collabToolPolicy: effectiveCollabToolPolicy,
|
|
4244
|
+
plugins: this.#config?.plugins,
|
|
4190
4245
|
activeToolNames,
|
|
4191
4246
|
})
|
|
4192
4247
|
: this.#tools.has(tc.name);
|