@yeaft/webchat-agent 1.0.38 → 1.0.39

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.38",
3
+ "version": "1.0.39",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/yeaft/engine.js CHANGED
@@ -95,6 +95,36 @@ const RETRY_DEFAULTS = Object.freeze({
95
95
  jitterRatio: 0.25,
96
96
  });
97
97
 
98
+ const EXPLICIT_SKILL_COMMAND_RE = /^\/skill:([^\s]+)(\s+|$)/;
99
+
100
+ function parseExplicitSkillCommand(prompt) {
101
+ if (typeof prompt !== 'string') {
102
+ return { skillName: null, cleanedPrompt: prompt };
103
+ }
104
+ const match = prompt.match(EXPLICIT_SKILL_COMMAND_RE);
105
+ if (!match) {
106
+ return { skillName: null, cleanedPrompt: prompt };
107
+ }
108
+ return {
109
+ skillName: match[1],
110
+ cleanedPrompt: prompt.slice(match[0].length),
111
+ };
112
+ }
113
+
114
+ function stripLeadingSkillCommandFromPromptParts(promptParts) {
115
+ if (!Array.isArray(promptParts) || promptParts.length === 0) return promptParts;
116
+ let stripped = false;
117
+ return promptParts.map(part => {
118
+ if (stripped || !part || part.type !== 'text' || typeof part.text !== 'string') {
119
+ return part;
120
+ }
121
+ const parsed = parseExplicitSkillCommand(part.text);
122
+ if (!parsed.skillName) return part;
123
+ stripped = true;
124
+ return { ...part, text: parsed.cleanedPrompt };
125
+ });
126
+ }
127
+
98
128
  function resolveRetryPolicy(config) {
99
129
  const raw = config?.llmRetry || {};
100
130
  const num = (v, d) => (Number.isFinite(v) && v >= 0 ? v : d);
@@ -932,13 +962,21 @@ export class Engine {
932
962
  * @param {string} [args.sessionAnnouncement]
933
963
  * @param {string} [args.projectDoc] — resolved CLAUDE.md / AGENTS.md text (already truncated)
934
964
  * @param {object} [args.taskCtx] — legacy task-context sub-block (optional)
965
+ * @param {string} [args.explicitSkillName] — leading /skill:<name> command, if present
935
966
  * @returns {string}
936
967
  */
937
- #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectDoc, taskCtx, activeTasks } = {}) {
938
- // Get relevant skill content if SkillManager is wired
968
+ #buildSystemPrompt({ prompt, memoryInjection, vpPersona, activeScope, sessionAnnouncement, projectDoc, taskCtx, activeTasks, explicitSkillName } = {}) {
969
+ // Get relevant skill content if SkillManager is wired. A leading
970
+ // /skill:<name> is explicit, not relevance matching: load that skill by
971
+ // name or inject a visible prompt warning when the command is unknown.
939
972
  let skillContent = '';
940
- if (this.#skillManager && prompt) {
941
- skillContent = this.#skillManager.getRelevantPromptContent(prompt);
973
+ if (this.#skillManager) {
974
+ if (explicitSkillName) {
975
+ skillContent = this.#skillManager.getPromptContent(explicitSkillName)
976
+ || `## Skill command error\n\nRequested skill "${explicitSkillName}" was not found. Continue without that skill and tell the user it is unavailable.`;
977
+ } else if (prompt) {
978
+ skillContent = this.#skillManager.getRelevantPromptContent(prompt);
979
+ }
942
980
  }
943
981
 
944
982
  // Get tool names from the appropriate source
@@ -1583,7 +1621,11 @@ export class Engine {
1583
1621
  // the merge, so an invalid caller value (e.g. 'ULTRA') does not shadow a
1584
1622
  // valid prompt prefix.
1585
1623
  const parsed = parseEffortPrefix(prompt);
1586
- const effectivePrompt = parsed.cleanedPrompt;
1624
+ const parsedSkill = parseExplicitSkillCommand(parsed.cleanedPrompt);
1625
+ const effectivePrompt = parsedSkill.cleanedPrompt;
1626
+ const effectivePromptParts = parsedSkill.skillName
1627
+ ? stripLeadingSkillCommandFromPromptParts(promptParts)
1628
+ : promptParts;
1587
1629
  const configuredEffort = normalizeEffort(this.#config?.modelEffort);
1588
1630
  const effectiveUserEffort = normalizeEffort(userEffort) || parsed.effort || configuredEffort || null;
1589
1631
  const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
@@ -1623,7 +1665,7 @@ export class Engine {
1623
1665
 
1624
1666
  try {
1625
1667
  this.#currentThreadId = threadId || MAIN_THREAD_ID;
1626
- yield* this.#runQuery({ prompt: effectivePrompt, promptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy });
1668
+ yield* this.#runQuery({ prompt: effectivePrompt, promptParts: effectivePromptParts, messages, signal: runSignal, userEffort: effectiveUserEffort, scenario, vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted, getCurrentTodos, setCurrentTodos, threadId: this.#currentThreadId, drainPendingUserMessages, collabToolPolicy: effectiveCollabToolPolicy, explicitSkillName: parsedSkill.skillName });
1627
1669
  } finally {
1628
1670
  if (signal) {
1629
1671
  try { signal.removeEventListener('abort', onExternalAbort); } catch { /* ignore */ }
@@ -1667,7 +1709,7 @@ export class Engine {
1667
1709
  * in a try/finally without indenting the whole loop.
1668
1710
  * @private
1669
1711
  */
1670
- async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null }) {
1712
+ async *#runQuery({ prompt, promptParts = null, messages, signal, userEffort = null, scenario = 'chat', vpPersona, router, senderVpId, inboundEnvelope, taskId, taskMembers, sessionId, sessionMembers, sessionTopics = null, vpPlan, sessionAnnouncement, workDir, userAlreadyPersisted = false, getCurrentTodos = null, setCurrentTodos = null, threadId = MAIN_THREAD_ID, drainPendingUserMessages = null, collabToolPolicy = null, explicitSkillName = null }) {
1671
1713
 
1672
1714
  const effectiveCollabToolPolicy = collabToolPolicy === COLLAB_TOOL_POLICY.SINGLE_VP || collabToolPolicy === COLLAB_TOOL_POLICY.MULTI_VP
1673
1715
  ? collabToolPolicy
@@ -1783,6 +1825,7 @@ export class Engine {
1783
1825
  sessionAnnouncement,
1784
1826
  projectDoc,
1785
1827
  activeTasks,
1828
+ explicitSkillName,
1786
1829
  });
1787
1830
 
1788
1831
  // ─── HARD INVARIANT: Compact ≠ Dream (read DESIGN-COMPACT-VS-DREAM.md) ─
@@ -73,6 +73,8 @@ import { buildMcpFlattenedTools } from './tools/mcp-tools.js';
73
73
  import { getAgentRegistry, agentBelongsToScope } from './tools/agent.js';
74
74
  import { isPromptableAgentStatus } from './sub-agent/status.js';
75
75
 
76
+ const SKILL_COMMAND_PREFIX = 'skill:';
77
+
76
78
  /** @type {import('./session.js').Session | null} */
77
79
  let session = null;
78
80
 
@@ -1690,6 +1692,37 @@ function sendSessionOutputFrame(data, { sessionId, chatId, vpId, turnId, threadI
1690
1692
  });
1691
1693
  }
1692
1694
 
1695
+ export function buildSkillSlashCommands(skillManager) {
1696
+ if (!skillManager || typeof skillManager.list !== 'function') return { commands: [], descriptions: {} };
1697
+ const commands = [];
1698
+ const descriptions = {};
1699
+ for (const skill of skillManager.list()) {
1700
+ if (!skill?.name || typeof skill.name !== 'string') continue;
1701
+ const commandName = `${SKILL_COMMAND_PREFIX}${skill.name}`;
1702
+ commands.push(commandName);
1703
+ descriptions[commandName] = skill.description || skill.trigger || 'Load Yeaft skill';
1704
+ }
1705
+ commands.sort((a, b) => a.localeCompare(b));
1706
+ return { commands, descriptions };
1707
+ }
1708
+
1709
+ function broadcastSkillSlashCommands(sessionLike) {
1710
+ const { commands, descriptions } = buildSkillSlashCommands(sessionLike?.skillManager);
1711
+ const slashCommands = [...new Set([...(ctx.slashCommands || []), ...commands])];
1712
+ const slashCommandDescriptions = {
1713
+ ...(ctx.slashCommandDescriptions || {}),
1714
+ ...descriptions,
1715
+ };
1716
+ ctx.slashCommands = slashCommands;
1717
+ ctx.slashCommandDescriptions = slashCommandDescriptions;
1718
+ sendToServer({
1719
+ type: 'slash_commands_update',
1720
+ conversationId: '__preload__',
1721
+ slashCommands,
1722
+ slashCommandDescriptions,
1723
+ });
1724
+ }
1725
+
1693
1726
  /** Send a Yeaft Session metadata event over the legacy-compatible envelope. */
1694
1727
  function sendSessionEvent(event, { sessionId, chatId, vpId, turnId, threadId } = {}) {
1695
1728
  sendToServer({
@@ -3375,6 +3408,7 @@ async function ensureSessionLoaded() {
3375
3408
 
3376
3409
  yeaftConversationId = `yeaft-${Date.now()}`;
3377
3410
  hydrateYeaftStatusFromSession(session, { reason: 'session_ready', emitEvent: true });
3411
+ broadcastSkillSlashCommands(session);
3378
3412
 
3379
3413
  // Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
3380
3414
  // — there's no global "all conversations" tape any more.