@yeaft/webchat-agent 1.0.408 → 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-file-terminal.js +26 -5
- 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/server/handlers/client-workbench.js +12 -2
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +188 -86
- 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 +14 -2
- package/yeaft/cli-session-runner.js +219 -10
- package/yeaft/config-api.js +109 -48
- package/yeaft/config.js +56 -9
- package/yeaft/engine.js +99 -44
- package/yeaft/plugins.js +170 -0
- package/yeaft/routing/router.js +59 -0
- package/yeaft/session.js +7 -4
- package/yeaft/sessions/coordinator.js +3 -2
- 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
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))
|
|
@@ -2219,7 +2269,7 @@ export class Engine {
|
|
|
2219
2269
|
const runtimeThreadId = (typeof threadId === 'string' && threadId.trim())
|
|
2220
2270
|
? threadId.trim()
|
|
2221
2271
|
: MAIN_THREAD_ID;
|
|
2222
|
-
const executionOrigin = inboundEnvelope?.msg?.meta?.injectedBy
|
|
2272
|
+
const executionOrigin = ['route_forward', 'route_forward_result'].includes(inboundEnvelope?.msg?.meta?.injectedBy)
|
|
2223
2273
|
? 'route_forward'
|
|
2224
2274
|
: null;
|
|
2225
2275
|
// The bridge-provided VP turn id is also persisted on assistant messages and
|
|
@@ -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);
|
package/yeaft/plugins.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* plugins.js — Agent-level selectable Yeaft capabilities.
|
|
3
|
+
*
|
|
4
|
+
* An Agent owns installed tools, skills, and MCP configuration. Missing plugin
|
|
5
|
+
* fields retain historical behavior (everything enabled); explicit arrays are
|
|
6
|
+
* allowlists and may intentionally be empty.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function normalizeNameList(value, field) {
|
|
10
|
+
if (value === undefined) return undefined;
|
|
11
|
+
if (!Array.isArray(value)) throw new Error(`plugins.${field} must be an array`);
|
|
12
|
+
|
|
13
|
+
const names = [];
|
|
14
|
+
const seen = new Set();
|
|
15
|
+
for (const raw of value) {
|
|
16
|
+
if (typeof raw !== 'string' || !raw.trim()) {
|
|
17
|
+
throw new Error(`plugins.${field} entries must be non-empty strings`);
|
|
18
|
+
}
|
|
19
|
+
const name = raw.trim();
|
|
20
|
+
if (!seen.has(name)) {
|
|
21
|
+
seen.add(name);
|
|
22
|
+
names.push(name);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return names;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Normalise persisted Agent plugin config while preserving inheritance. */
|
|
29
|
+
export function normalizePluginConfig(value) {
|
|
30
|
+
// Only an omitted field inherits the legacy all-enabled behavior. An
|
|
31
|
+
// explicit `null` is persisted schema, not absence, and must be rejected
|
|
32
|
+
// so every reader can fail closed consistently.
|
|
33
|
+
if (value === undefined) return {};
|
|
34
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
35
|
+
throw new Error('plugins must be an object');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const out = {};
|
|
39
|
+
for (const field of ['tools', 'skills', 'mcpServers']) {
|
|
40
|
+
const names = normalizeNameList(value[field], field);
|
|
41
|
+
if (names !== undefined) out[field] = names;
|
|
42
|
+
}
|
|
43
|
+
for (const key of Object.keys(value)) {
|
|
44
|
+
if (!['tools', 'skills', 'mcpServers'].includes(key)) {
|
|
45
|
+
throw new Error(`unknown plugins key: ${key}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Return an explicit deny-all policy for a persisted plugins schema error.
|
|
53
|
+
* This is deliberately distinct from `{}`, whose missing fields inherit the
|
|
54
|
+
* historical all-enabled behavior.
|
|
55
|
+
*/
|
|
56
|
+
export function createDenyAllPluginConfig() {
|
|
57
|
+
return { tools: [], skills: [], mcpServers: [] };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isPluginNameEnabled(plugins, field, name) {
|
|
61
|
+
if (!plugins || !Array.isArray(plugins[field])) return true;
|
|
62
|
+
return plugins[field].includes(name);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Keep the configured MCP catalog separate from the runtime connection set.
|
|
67
|
+
* The catalog must retain disabled servers so users can enable them later;
|
|
68
|
+
* runtimes must receive only the effective allowlisted subset.
|
|
69
|
+
*
|
|
70
|
+
* The caller owns the raw configured catalog; this helper never connects it.
|
|
71
|
+
*/
|
|
72
|
+
export function resolveMcpPluginPolicy(mcpConfig, plugins) {
|
|
73
|
+
const configured = {
|
|
74
|
+
...(mcpConfig || {}),
|
|
75
|
+
servers: Array.isArray(mcpConfig?.servers) ? mcpConfig.servers : [],
|
|
76
|
+
};
|
|
77
|
+
if (!Array.isArray(plugins?.mcpServers)) {
|
|
78
|
+
return { configured, effective: configured };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const allowed = new Set(plugins.mcpServers);
|
|
82
|
+
return {
|
|
83
|
+
configured,
|
|
84
|
+
effective: {
|
|
85
|
+
...configured,
|
|
86
|
+
servers: configured.servers.filter(server => allowed.has(server?.name)),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Live delegating view over a SkillManager. It does not mutate the shared
|
|
93
|
+
* manager because Sessions and project runtimes reuse that manager.
|
|
94
|
+
*/
|
|
95
|
+
export function createPluginSkillManager(skillManager, plugins) {
|
|
96
|
+
if (!skillManager) return null;
|
|
97
|
+
const hasExplicitSkills = Array.isArray(plugins?.skills);
|
|
98
|
+
const allowed = hasExplicitSkills ? new Set(plugins.skills) : null;
|
|
99
|
+
const isAllowed = name => !allowed || allowed.has(name);
|
|
100
|
+
const has = name => isAllowed(name) && !!skillManager.has?.(name);
|
|
101
|
+
const list = (...args) => (skillManager.list?.(...args) || [])
|
|
102
|
+
.filter(skill => isAllowed(skill?.name));
|
|
103
|
+
const get = name => has(name) ? skillManager.get?.(name) || null : null;
|
|
104
|
+
const resolve = name => has(name) ? skillManager.resolve?.(name) || null : null;
|
|
105
|
+
const view = (name, filePath) => has(name) ? skillManager.view?.(name, filePath) || null : null;
|
|
106
|
+
const findRelevant = (...args) => (skillManager.findRelevant?.(...args) || [])
|
|
107
|
+
.filter(skill => isAllowed(skill?.name));
|
|
108
|
+
const getPromptContent = name => has(name) ? skillManager.getPromptContent?.(name) || '' : '';
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
has,
|
|
112
|
+
get,
|
|
113
|
+
resolve,
|
|
114
|
+
list,
|
|
115
|
+
view,
|
|
116
|
+
findRelevant,
|
|
117
|
+
getPromptContent,
|
|
118
|
+
getRelevantPromptContent: (...args) => findRelevant(...args)
|
|
119
|
+
.map(skill => getPromptContent(skill.name))
|
|
120
|
+
.filter(Boolean)
|
|
121
|
+
.join('\n\n'),
|
|
122
|
+
listCategories: () => [...new Set(list().map(skill => skill.category).filter(Boolean))].sort(),
|
|
123
|
+
get size() { return list().length; },
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Build a browser-safe catalog from already discovered Agent assets. */
|
|
128
|
+
export function buildPluginCatalog({ toolRegistry, skillManager, mcpConfig, mcpManager } = {}) {
|
|
129
|
+
const tools = typeof toolRegistry?.getAllTools === 'function'
|
|
130
|
+
? toolRegistry.getAllTools()
|
|
131
|
+
.filter(tool => !tool?.mcpServer)
|
|
132
|
+
.map(tool => ({ id: tool.name, label: tool.name }))
|
|
133
|
+
.sort((a, b) => a.label.localeCompare(b.label))
|
|
134
|
+
: [];
|
|
135
|
+
|
|
136
|
+
const skills = typeof skillManager?.list === 'function'
|
|
137
|
+
? skillManager.list()
|
|
138
|
+
.map(skill => ({
|
|
139
|
+
id: skill.name,
|
|
140
|
+
label: skill.name,
|
|
141
|
+
description: skill.description || '',
|
|
142
|
+
category: skill.category || null,
|
|
143
|
+
}))
|
|
144
|
+
.sort((a, b) => a.label.localeCompare(b.label))
|
|
145
|
+
: [];
|
|
146
|
+
|
|
147
|
+
const statusByName = new Map((mcpManager?.status?.() || [])
|
|
148
|
+
.map(status => [status.name, status]));
|
|
149
|
+
// A configured catalog is authoritative, including an explicit empty array.
|
|
150
|
+
// Only callers that supplied no catalog at all use live status as a legacy
|
|
151
|
+
// fallback.
|
|
152
|
+
const configuredMcpServers = Array.isArray(mcpConfig?.servers)
|
|
153
|
+
? mcpConfig.servers
|
|
154
|
+
: (mcpManager?.status?.() || []).map(status => ({ name: status.name, command: '' }));
|
|
155
|
+
const mcpServers = configuredMcpServers
|
|
156
|
+
.filter(server => typeof server?.name === 'string' && server.name)
|
|
157
|
+
.map(server => {
|
|
158
|
+
const status = statusByName.get(server.name);
|
|
159
|
+
return {
|
|
160
|
+
id: server.name,
|
|
161
|
+
label: server.name,
|
|
162
|
+
description: server.command || '',
|
|
163
|
+
ready: status ? !!status.ready : null,
|
|
164
|
+
toolCount: status?.toolCount || 0,
|
|
165
|
+
};
|
|
166
|
+
})
|
|
167
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
168
|
+
|
|
169
|
+
return { tools, skills, mcpServers };
|
|
170
|
+
}
|
package/yeaft/routing/router.js
CHANGED
|
@@ -28,6 +28,39 @@
|
|
|
28
28
|
import { resolveMemberId } from '../sessions/roster.js';
|
|
29
29
|
import { createLoopGuard, extendCausedBy } from './loop-guard.js';
|
|
30
30
|
|
|
31
|
+
function routeForwardParentFromEnvelope(envelope) {
|
|
32
|
+
const msg = envelope?.msg;
|
|
33
|
+
const meta = msg?.meta;
|
|
34
|
+
if (!meta || typeof meta !== 'object') return null;
|
|
35
|
+
if (meta.injectedBy === 'route_forward_result') {
|
|
36
|
+
return meta.routeForwardParent && typeof meta.routeForwardParent === 'object'
|
|
37
|
+
? { ...meta.routeForwardParent }
|
|
38
|
+
: null;
|
|
39
|
+
}
|
|
40
|
+
if (meta.injectedBy !== 'route_forward') return null;
|
|
41
|
+
const forwardId = typeof msg.id === 'string' ? msg.id.trim() : '';
|
|
42
|
+
const sourceVpId = typeof meta.senderVpId === 'string' ? meta.senderVpId.trim() : '';
|
|
43
|
+
if (!forwardId || !sourceVpId) return null;
|
|
44
|
+
return {
|
|
45
|
+
forwardId,
|
|
46
|
+
sourceVpId,
|
|
47
|
+
sourceThreadId: typeof meta.sourceThreadId === 'string' && meta.sourceThreadId.trim()
|
|
48
|
+
? meta.sourceThreadId.trim()
|
|
49
|
+
: 'main',
|
|
50
|
+
expectedVpIds: Array.isArray(meta.routeForwardExpectedTargets)
|
|
51
|
+
? meta.routeForwardExpectedTargets.slice()
|
|
52
|
+
: [],
|
|
53
|
+
causedBy: Array.isArray(meta.causedBy) ? meta.causedBy.slice() : [],
|
|
54
|
+
dispatchErrors: Array.isArray(meta.routeForwardDispatchErrors)
|
|
55
|
+
? meta.routeForwardDispatchErrors.slice()
|
|
56
|
+
: [],
|
|
57
|
+
truncatedAtFanOutCap: Boolean(meta.routeForwardTruncatedAtFanOutCap),
|
|
58
|
+
parentRouteForward: meta.routeForwardParent && typeof meta.routeForwardParent === 'object'
|
|
59
|
+
? { ...meta.routeForwardParent }
|
|
60
|
+
: null,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
31
64
|
/**
|
|
32
65
|
* Build a router bound to a single GroupCoordinator + loop guard.
|
|
33
66
|
*
|
|
@@ -112,6 +145,7 @@ export function createRouter(deps = {}) {
|
|
|
112
145
|
// The guard runs against the *pre-dispatch* chain; that matches the
|
|
113
146
|
// spec's intent ("depth of forwards already taken").
|
|
114
147
|
const chain = extendCausedBy(args.inboundEnvelope || null, null);
|
|
148
|
+
const routeForwardParent = routeForwardParentFromEnvelope(args.inboundEnvelope);
|
|
115
149
|
|
|
116
150
|
// Loop guard: for broadcast, use 'all' as the target key so one VP
|
|
117
151
|
// spamming @all still gets throttled even if each cycle hits different
|
|
@@ -155,6 +189,7 @@ export function createRouter(deps = {}) {
|
|
|
155
189
|
senderVpId: from,
|
|
156
190
|
reason: args.reason || null,
|
|
157
191
|
causedBy: chain,
|
|
192
|
+
...(routeForwardParent ? { routeForwardParent } : {}),
|
|
158
193
|
sourceThreadId: typeof args.sourceThreadId === 'string' && args.sourceThreadId.trim()
|
|
159
194
|
? args.sourceThreadId.trim()
|
|
160
195
|
: null,
|
|
@@ -163,11 +198,35 @@ export function createRouter(deps = {}) {
|
|
|
163
198
|
opts,
|
|
164
199
|
);
|
|
165
200
|
|
|
201
|
+
// `deliver()` queues its work, so the target envelopes still share this
|
|
202
|
+
// stored message object when forward() returns. Record the accepted target
|
|
203
|
+
// set for the active runtime only: it lets a stream Session return one
|
|
204
|
+
// combined result to the caller after an @all fan-out finishes. The
|
|
205
|
+
// transient value is deliberately not required for durable replay.
|
|
206
|
+
if (report?.message?.meta && Array.isArray(report.dispatched)) {
|
|
207
|
+
report.message.meta.routeForwardExpectedTargets = report.dispatched.slice();
|
|
208
|
+
report.message.meta.routeForwardDispatchErrors = Array.isArray(report.errors)
|
|
209
|
+
? report.errors.slice()
|
|
210
|
+
: [];
|
|
211
|
+
report.message.meta.routeForwardTruncatedAtFanOutCap = Boolean(report.truncatedAtFanOutCap);
|
|
212
|
+
}
|
|
213
|
+
|
|
166
214
|
// Record AFTER Coordinator accepts. If Coordinator produced zero
|
|
167
215
|
// dispatches (e.g. task.members gate) we still count it as a hit —
|
|
168
216
|
// the forwarder still tried, and the guard's job is to throttle the
|
|
169
217
|
// sender's ability to keep trying.
|
|
170
218
|
guard.record({ sessionId: meta.id, targetVpId: guardKey });
|
|
219
|
+
if (!Array.isArray(report.dispatched) || report.dispatched.length === 0) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
error: 'no_targets_dispatched',
|
|
223
|
+
detail: {
|
|
224
|
+
errors: Array.isArray(report.errors) ? report.errors : [],
|
|
225
|
+
truncatedAtFanOutCap: Boolean(report.truncatedAtFanOutCap),
|
|
226
|
+
},
|
|
227
|
+
report,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
171
230
|
|
|
172
231
|
return {
|
|
173
232
|
ok: true,
|