@yeaft/webchat-agent 1.0.37 → 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 +1 -1
- package/yeaft/engine.js +50 -7
- package/yeaft/router/continuity.js +7 -6
- package/yeaft/web-bridge.js +94 -11
package/package.json
CHANGED
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
|
|
941
|
-
|
|
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
|
|
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) ─
|
|
@@ -85,10 +85,10 @@ export function extractPriorPlan(messages, vpId) {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
/**
|
|
88
|
-
* Return a copy of the messages array with
|
|
89
|
-
* message. The serialisers (anthropic/openai-responses) read this;
|
|
90
|
-
* NEVER part of the wire payload. Cheap because we only
|
|
91
|
-
* messages that actually have
|
|
88
|
+
* Return a copy of the messages array with engine-private metadata stripped
|
|
89
|
+
* from every message. The serialisers (anthropic/openai-responses) read this;
|
|
90
|
+
* these fields are NEVER part of the wire payload. Cheap because we only
|
|
91
|
+
* shallow-clone the messages that actually have private fields.
|
|
92
92
|
*
|
|
93
93
|
* @param {object[]} messages
|
|
94
94
|
* @returns {object[]}
|
|
@@ -97,9 +97,10 @@ export function stripMetaForWire(messages) {
|
|
|
97
97
|
if (!Array.isArray(messages)) return messages;
|
|
98
98
|
let mutated = false;
|
|
99
99
|
const out = messages.map(m => {
|
|
100
|
-
if (m && typeof m === 'object'
|
|
100
|
+
if (m && typeof m === 'object'
|
|
101
|
+
&& ('_meta' in m || '_runtimeTurnId' in m || '_partialTurn' in m)) {
|
|
101
102
|
mutated = true;
|
|
102
|
-
const { _meta, ...rest } = m;
|
|
103
|
+
const { _meta, _runtimeTurnId, _partialTurn, ...rest } = m;
|
|
103
104
|
return rest;
|
|
104
105
|
}
|
|
105
106
|
return m;
|
package/yeaft/web-bridge.js
CHANGED
|
@@ -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({
|
|
@@ -2577,6 +2610,23 @@ function handleEngineEvent(event, hctx) {
|
|
|
2577
2610
|
isError: !!event.isError,
|
|
2578
2611
|
});
|
|
2579
2612
|
}
|
|
2613
|
+
if (hctx.sessionId && hctx.turnId && !hctx.skipPartialHistory) {
|
|
2614
|
+
const appendedPrompts = Array.isArray(hctx.appendedUserPrompts) ? hctx.appendedUserPrompts : [];
|
|
2615
|
+
const prompts = hctx.includeInitialPrompt && typeof hctx.prompt === 'string'
|
|
2616
|
+
? [hctx.prompt, ...appendedPrompts]
|
|
2617
|
+
: appendedPrompts;
|
|
2618
|
+
appendTurnToSessionHistory(
|
|
2619
|
+
hctx.sessionId,
|
|
2620
|
+
hctx.threadId || event.threadId || 'main',
|
|
2621
|
+
hctx.vpId,
|
|
2622
|
+
prompts,
|
|
2623
|
+
hctx.assistantTextParts || [],
|
|
2624
|
+
hctx.toolCallsAccum || [],
|
|
2625
|
+
hctx.toolResultsAccum || [],
|
|
2626
|
+
hctx.thinkingBlocksAccum || [],
|
|
2627
|
+
{ turnId: hctx.turnId, partial: true },
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2580
2630
|
sendSessionOutputFrame({
|
|
2581
2631
|
type: 'user',
|
|
2582
2632
|
tool_use_result: [{
|
|
@@ -3358,6 +3408,7 @@ async function ensureSessionLoaded() {
|
|
|
3358
3408
|
|
|
3359
3409
|
yeaftConversationId = `yeaft-${Date.now()}`;
|
|
3360
3410
|
hydrateYeaftStatusFromSession(session, { reason: 'session_ready', emitEvent: true });
|
|
3411
|
+
broadcastSkillSlashCommands(session);
|
|
3361
3412
|
|
|
3362
3413
|
// Per-group history is hydrated lazily on first `getOrCreateSessionHistory`
|
|
3363
3414
|
// — there's no global "all conversations" tape any more.
|
|
@@ -3598,6 +3649,9 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3598
3649
|
vpEngine = getOrCreateVpEngine(sessionId, vpId, threadId);
|
|
3599
3650
|
if (thread) thread.engine = vpEngine;
|
|
3600
3651
|
|
|
3652
|
+
const inboundInjectedBy = inboundEnvelope?.msg?.meta?.injectedBy;
|
|
3653
|
+
const inboundIsInternal = inboundInjectedBy === 'route_forward' || inboundInjectedBy === 'task_result';
|
|
3654
|
+
|
|
3601
3655
|
handlerCtx = {
|
|
3602
3656
|
assistantTextParts,
|
|
3603
3657
|
toolCallsAccum,
|
|
@@ -3610,6 +3664,9 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3610
3664
|
threadId,
|
|
3611
3665
|
thread,
|
|
3612
3666
|
appendedUserPrompts,
|
|
3667
|
+
prompt,
|
|
3668
|
+
includeInitialPrompt: !inboundIsInternal,
|
|
3669
|
+
skipPartialHistory: false,
|
|
3613
3670
|
markTurnEnd,
|
|
3614
3671
|
};
|
|
3615
3672
|
// Always trim the snapshot before passing to engine.query. This is
|
|
@@ -3651,10 +3708,8 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3651
3708
|
// the source VP's tool action. Do not append it as a visible prompt for
|
|
3652
3709
|
// the target VP turn; otherwise UI replay can show a trailing handoff
|
|
3653
3710
|
// block after the target response.
|
|
3654
|
-
const inboundInjectedBy = inboundEnvelope?.msg?.meta?.injectedBy;
|
|
3655
|
-
const inboundIsInternal = inboundInjectedBy === 'route_forward' || inboundInjectedBy === 'task_result';
|
|
3656
3711
|
const visiblePrompts = inboundIsInternal ? appendedUserPrompts : [prompt, ...appendedUserPrompts];
|
|
3657
|
-
appendTurnToSessionHistory(sessionId, threadId, vpId, visiblePrompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum);
|
|
3712
|
+
appendTurnToSessionHistory(sessionId, threadId, vpId, visiblePrompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum, { turnId });
|
|
3658
3713
|
|
|
3659
3714
|
sendSessionOutputFrame({
|
|
3660
3715
|
type: 'assistant',
|
|
@@ -3766,8 +3821,9 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3766
3821
|
}
|
|
3767
3822
|
|
|
3768
3823
|
/**
|
|
3769
|
-
* Atomically append a completed VP-turn's messages to the GROUP'S
|
|
3770
|
-
* conversation history.
|
|
3824
|
+
* Atomically append a completed or partial VP-turn's messages to the GROUP'S
|
|
3825
|
+
* conversation history. Partial writes are replaced by the final write when
|
|
3826
|
+
* the same runtime turn completes.
|
|
3771
3827
|
*
|
|
3772
3828
|
* Note: this does NOT see the engine's collapsed form — it appends the
|
|
3773
3829
|
* raw user prompt(s) + the per-VP assistant text + tool results. Related
|
|
@@ -3779,15 +3835,20 @@ async function runVpTurn({ prompt, promptParts = null, sessionId, vpId, threadId
|
|
|
3779
3835
|
* a session, this in-memory tape carries the un-collapsed form — which
|
|
3780
3836
|
* is fine because each VP turn's `engine.query` re-collapses on the fly.
|
|
3781
3837
|
*/
|
|
3782
|
-
function
|
|
3783
|
-
|
|
3784
|
-
const
|
|
3838
|
+
function buildTurnHistoryEntries(threadId, vpId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum, opts = {}) {
|
|
3839
|
+
const entries = [];
|
|
3840
|
+
const runtimeTurnId = typeof opts.turnId === 'string' && opts.turnId ? opts.turnId : null;
|
|
3841
|
+
const markEntry = (entry) => {
|
|
3842
|
+
if (runtimeTurnId) entry._runtimeTurnId = runtimeTurnId;
|
|
3843
|
+
if (opts.partial) entry._partialTurn = true;
|
|
3844
|
+
return entry;
|
|
3845
|
+
};
|
|
3785
3846
|
const promptList = Array.isArray(prompts) ? prompts : [prompts];
|
|
3786
3847
|
for (const prompt of promptList) {
|
|
3787
3848
|
if (typeof prompt === 'string' && prompt.trim()) {
|
|
3788
3849
|
// user rows intentionally carry NO speakerVpId — every VP in the
|
|
3789
3850
|
// session should see the prompt in their history.
|
|
3790
|
-
|
|
3851
|
+
entries.push(markEntry({ role: 'user', content: prompt, threadId: threadId || 'main' }));
|
|
3791
3852
|
}
|
|
3792
3853
|
}
|
|
3793
3854
|
|
|
@@ -3821,7 +3882,7 @@ function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistan
|
|
|
3821
3882
|
: { thinking: tb.thinking, signature: tb.signature }
|
|
3822
3883
|
));
|
|
3823
3884
|
}
|
|
3824
|
-
|
|
3885
|
+
entries.push(markEntry(assistantMsg));
|
|
3825
3886
|
|
|
3826
3887
|
for (const tr of toolResultsAccum) {
|
|
3827
3888
|
const toolMsg = {
|
|
@@ -3832,8 +3893,30 @@ function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistan
|
|
|
3832
3893
|
threadId: threadId || 'main',
|
|
3833
3894
|
};
|
|
3834
3895
|
if (vpId) toolMsg.speakerVpId = vpId;
|
|
3835
|
-
|
|
3896
|
+
entries.push(markEntry(toolMsg));
|
|
3897
|
+
}
|
|
3898
|
+
}
|
|
3899
|
+
return entries;
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
function appendTurnToSessionHistory(sessionId, threadId, vpId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum, opts = {}) {
|
|
3903
|
+
if (!sessionId) return;
|
|
3904
|
+
const history = getOrCreateSessionHistory(sessionId);
|
|
3905
|
+
const nextEntries = buildTurnHistoryEntries(threadId, vpId, prompts, assistantTextParts, toolCallsAccum, toolResultsAccum, thinkingBlocksAccum, opts);
|
|
3906
|
+
if (nextEntries.length === 0) return;
|
|
3907
|
+
const runtimeTurnId = typeof opts.turnId === 'string' && opts.turnId ? opts.turnId : null;
|
|
3908
|
+
if (runtimeTurnId) {
|
|
3909
|
+
let insertAt = history.length;
|
|
3910
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
3911
|
+
if (history[i]?._runtimeTurnId === runtimeTurnId) {
|
|
3912
|
+
insertAt = i;
|
|
3913
|
+
history.splice(i, 1);
|
|
3914
|
+
}
|
|
3836
3915
|
}
|
|
3916
|
+
if (insertAt > history.length) insertAt = history.length;
|
|
3917
|
+
history.splice(insertAt, 0, ...nextEntries);
|
|
3918
|
+
} else {
|
|
3919
|
+
history.push(...nextEntries);
|
|
3837
3920
|
}
|
|
3838
3921
|
}
|
|
3839
3922
|
|