@yeaft/webchat-agent 0.1.860 → 0.1.864

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.
@@ -45,6 +45,15 @@ import {
45
45
  groupsRoot,
46
46
  } from './groups/group-crud.js';
47
47
  import { openGroup, loadGroupMeta } from './groups/group-store.js';
48
+ import {
49
+ createChat as createChatStore,
50
+ openChat,
51
+ listChats,
52
+ renameChat as renameChatStore,
53
+ archiveChat as archiveChatStore,
54
+ deleteChat as deleteChatStore,
55
+ touchChat,
56
+ } from './chats/chat-store.js';
48
57
  import { loadGroupConfig, resolveGroupConfig, GroupConfigError } from './groups/group-config.js';
49
58
  import { updateGroupConfig } from './groups/group-crud.js';
50
59
  import { createCoordinator } from './groups/coordinator.js';
@@ -1254,12 +1263,13 @@ function resolveGroupDefaultVpId(groupId) {
1254
1263
  }
1255
1264
  }
1256
1265
 
1257
- function sendYeaftOutput(data, { groupId, vpId, turnId, threadId } = {}) {
1258
- const resolvedVpId = vpId || resolveGroupDefaultVpId(groupId);
1266
+ function sendYeaftOutput(data, { groupId, chatId, vpId, turnId, threadId } = {}) {
1267
+ const resolvedVpId = vpId || (groupId ? resolveGroupDefaultVpId(groupId) : null);
1259
1268
  sendToServer({
1260
1269
  type: 'yeaft_output',
1261
1270
  conversationId: yeaftConversationId,
1262
1271
  ...(groupId ? { groupId } : {}),
1272
+ ...(chatId ? { chatId } : {}),
1263
1273
  ...(resolvedVpId ? { vpId: resolvedVpId } : {}),
1264
1274
  ...(turnId ? { turnId } : {}),
1265
1275
  ...(threadId ? { threadId } : {}),
@@ -1268,11 +1278,12 @@ function sendYeaftOutput(data, { groupId, vpId, turnId, threadId } = {}) {
1268
1278
  }
1269
1279
 
1270
1280
  /** Send a yeaft_output event (non-claude_output metadata). */
1271
- function sendYeaftEvent(event, { groupId, vpId, turnId, threadId } = {}) {
1281
+ function sendYeaftEvent(event, { groupId, chatId, vpId, turnId, threadId } = {}) {
1272
1282
  sendToServer({
1273
1283
  type: 'yeaft_output',
1274
1284
  conversationId: yeaftConversationId,
1275
1285
  ...(groupId ? { groupId } : {}),
1286
+ ...(chatId ? { chatId } : {}),
1276
1287
  ...(vpId ? { vpId } : {}),
1277
1288
  ...(turnId ? { turnId } : {}),
1278
1289
  ...(threadId ? { threadId } : {}),
@@ -1665,6 +1676,365 @@ export function handleYeaftSetDefaultVp(msg) {
1665
1676
  }
1666
1677
  }
1667
1678
 
1679
+ // ─── Yeaft Chat Mode ──────────────────────────────────────────
1680
+ //
1681
+ // 1:1 conversation between the user and a single VP. Reuses the engine,
1682
+ // memory, tool, and adapter stacks; bypasses the group coordinator and
1683
+ // fan-out machinery entirely. Persisted under `~/.yeaft/chats/<chatId>/`.
1684
+
1685
+ const chatEngines = new Map(); // key: `${chatId}::${vpId}` → Engine
1686
+ const chatAborts = new Map(); // key: chatId → AbortController for the in-flight turn
1687
+
1688
+ function chatsRootFor(yeaftDir) {
1689
+ return join(yeaftDir, 'chats');
1690
+ }
1691
+
1692
+ function snapshotChats(yeaftDir) {
1693
+ if (!yeaftDir) return [];
1694
+ try { return listChats(chatsRootFor(yeaftDir)); }
1695
+ catch { return []; }
1696
+ }
1697
+
1698
+ function sendChatCrudResult(payload) {
1699
+ sendYeaftEvent({ type: 'chat_crud_result', ...payload });
1700
+ }
1701
+
1702
+ function sendChatSnapshotBroadcast() {
1703
+ try {
1704
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1705
+ if (!yeaftDir) return;
1706
+ sendYeaftEvent({ type: 'chat_list_updated', chats: snapshotChats(yeaftDir) });
1707
+ } catch (err) {
1708
+ console.warn('[Yeaft] sendChatSnapshotBroadcast failed:', err?.message || err);
1709
+ }
1710
+ }
1711
+
1712
+ function chatErrorPayload(err) {
1713
+ return {
1714
+ code: (err && err.code) || 'unknown',
1715
+ chatId: err && err.chatId,
1716
+ message: err && err.message,
1717
+ };
1718
+ }
1719
+
1720
+ /**
1721
+ * Build the vpPersona payload threaded into engine.query so the worker
1722
+ * system prompt carries the VP's identity/role/persona/planInstruction.
1723
+ * Returns null on miss — callers treat that as "use generic prompt".
1724
+ * Shared by handleYeaftChatSend and the group fan-out path so the field
1725
+ * set stays in lockstep.
1726
+ */
1727
+ function buildVpPersona(vpId) {
1728
+ if (!vpId) return null;
1729
+ try {
1730
+ const vp = readVp(vpId);
1731
+ if (!vp) return null;
1732
+ return {
1733
+ vpId,
1734
+ displayName: vp.displayName || vpId,
1735
+ displayNameZh: vp.displayNameZh || '',
1736
+ role: vp.role || '',
1737
+ roleZh: vp.roleZh || '',
1738
+ persona: vp.persona || '',
1739
+ planInstruction: typeof vp.planInstruction === 'string' ? vp.planInstruction : '',
1740
+ };
1741
+ } catch {
1742
+ return null;
1743
+ }
1744
+ }
1745
+
1746
+ export function handleYeaftListChats(msg) {
1747
+ const requestId = msg && msg.requestId;
1748
+ try {
1749
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1750
+ sendChatCrudResult({ op: 'list', requestId, ok: true, chats: snapshotChats(yeaftDir) });
1751
+ } catch (err) {
1752
+ sendChatCrudResult({ op: 'list', requestId, ok: false, error: chatErrorPayload(err) });
1753
+ }
1754
+ }
1755
+
1756
+ export function handleYeaftCreateChat(msg) {
1757
+ const requestId = msg && msg.requestId;
1758
+ // Accept fields at top-level (current wire shape) AND inside `payload`
1759
+ // (legacy / future) — keeps backwards compatibility cheap.
1760
+ const top = msg || {};
1761
+ const payload = (msg && msg.payload) || {};
1762
+ const displayName = payload.displayName || top.displayName;
1763
+ const workDir = payload.workDir || top.workDir;
1764
+ const explicitId = payload.id || top.id;
1765
+ // Chat mode is 1:1 with the built-in Omni assistant by default. The
1766
+ // VP picker is gone from the UI; callers may still override vpId to
1767
+ // bind a chat to a specialist, but the default is omni.
1768
+ const vpId = payload.vpId || top.vpId || 'omni';
1769
+ try {
1770
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1771
+ if (!yeaftDir) throw new Error('no yeaft directory configured');
1772
+ // Fail loud at create time if the requested VP (default: omni) is
1773
+ // not installed — otherwise the chat would silently degrade to a
1774
+ // generic prompt at first send.
1775
+ if (!buildVpPersona(vpId)) {
1776
+ throw new Error(`VP '${vpId}' is not installed`);
1777
+ }
1778
+ const root = chatsRootFor(yeaftDir);
1779
+ const chatId = (explicitId && String(explicitId).trim())
1780
+ || `chat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
1781
+ const h = createChatStore(root, {
1782
+ id: chatId,
1783
+ vpId,
1784
+ displayName,
1785
+ workDir,
1786
+ });
1787
+ const meta = h.getMeta();
1788
+ h.close();
1789
+ sendChatCrudResult({ op: 'create', requestId, ok: true, chat: meta });
1790
+ sendChatSnapshotBroadcast();
1791
+ } catch (err) {
1792
+ sendChatCrudResult({ op: 'create', requestId, ok: false, error: chatErrorPayload(err) });
1793
+ }
1794
+ }
1795
+
1796
+ export function handleYeaftRenameChat(msg) {
1797
+ const requestId = msg && msg.requestId;
1798
+ const chatId = msg && msg.chatId;
1799
+ const displayName = msg && msg.displayName;
1800
+ try {
1801
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1802
+ const chat = renameChatStore(chatsRootFor(yeaftDir), chatId, displayName);
1803
+ sendChatCrudResult({ op: 'rename', requestId, ok: true, chat });
1804
+ sendChatSnapshotBroadcast();
1805
+ } catch (err) {
1806
+ sendChatCrudResult({ op: 'rename', requestId, ok: false, error: chatErrorPayload(err) });
1807
+ }
1808
+ }
1809
+
1810
+ export function handleYeaftArchiveChat(msg) {
1811
+ const requestId = msg && msg.requestId;
1812
+ const chatId = msg && msg.chatId;
1813
+ try {
1814
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1815
+ const ok = archiveChatStore(chatsRootFor(yeaftDir), chatId);
1816
+ if (!ok) throw new Error(`chat ${chatId} not found`);
1817
+ invalidateChatRuntime(chatId);
1818
+ sendChatCrudResult({ op: 'archive', requestId, ok: true, chatId });
1819
+ sendChatSnapshotBroadcast();
1820
+ } catch (err) {
1821
+ sendChatCrudResult({ op: 'archive', requestId, ok: false, error: chatErrorPayload(err) });
1822
+ }
1823
+ }
1824
+
1825
+ export function handleYeaftDeleteChat(msg) {
1826
+ const requestId = msg && msg.requestId;
1827
+ const chatId = msg && msg.chatId;
1828
+ try {
1829
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1830
+ const ok = deleteChatStore(chatsRootFor(yeaftDir), chatId);
1831
+ if (!ok) throw new Error(`chat ${chatId} not found`);
1832
+ invalidateChatRuntime(chatId);
1833
+ sendChatCrudResult({ op: 'delete', requestId, ok: true, chatId });
1834
+ sendChatSnapshotBroadcast();
1835
+ } catch (err) {
1836
+ sendChatCrudResult({ op: 'delete', requestId, ok: false, error: chatErrorPayload(err) });
1837
+ }
1838
+ }
1839
+
1840
+ function invalidateChatRuntime(chatId) {
1841
+ if (!chatId) return;
1842
+ const prefix = `${chatId}::`;
1843
+ for (const k of Array.from(chatEngines.keys())) {
1844
+ if (k.startsWith(prefix)) chatEngines.delete(k);
1845
+ }
1846
+ const ctrl = chatAborts.get(chatId);
1847
+ if (ctrl) {
1848
+ try { ctrl.abort(); } catch { /* best-effort */ }
1849
+ chatAborts.delete(chatId);
1850
+ }
1851
+ }
1852
+
1853
+ function getOrCreateChatEngine(chatId, vpId) {
1854
+ const key = `${chatId}::${vpId}`;
1855
+ let eng = chatEngines.get(key);
1856
+ if (eng) return eng;
1857
+ if (!session) throw new Error('getOrCreateChatEngine: session not loaded');
1858
+ eng = new Engine({
1859
+ adapter: session.adapter,
1860
+ trace: session.trace,
1861
+ config: session.config,
1862
+ conversationStore: session.conversationStore,
1863
+ memoryIndex: session.memoryIndex || null,
1864
+ amsRegistry: session.amsRegistry,
1865
+ toolRegistry: session.toolRegistry,
1866
+ skillManager: session.skillManager,
1867
+ mcpManager: session.mcpManager,
1868
+ yeaftDir: session.yeaftDir,
1869
+ toolStats: session.toolStats || null,
1870
+ chatId,
1871
+ vpId,
1872
+ });
1873
+ chatEngines.set(key, eng);
1874
+ return eng;
1875
+ }
1876
+
1877
+ /**
1878
+ * Send a single user turn in a chat session. 1:1 — no fan-out, no
1879
+ * coordinator, no @-mention dispatch. Engine events are forwarded as
1880
+ * yeaft_output with `chatId` instead of `groupId`.
1881
+ */
1882
+ export async function handleYeaftChatSend(msg) {
1883
+ if (!msg || typeof msg !== 'object') return;
1884
+ const chatId = typeof msg.chatId === 'string' ? msg.chatId.trim() : '';
1885
+ const text = typeof msg.text === 'string' ? msg.text : '';
1886
+ if (!chatId) return;
1887
+ if (!text.trim()) return;
1888
+
1889
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
1890
+ if (!yeaftDir) {
1891
+ sendYeaftOutput({
1892
+ type: 'assistant',
1893
+ message: { content: [{ type: 'text', text: '⚠️ Yeaft session error: no yeaft directory configured.' }] },
1894
+ }, { chatId });
1895
+ sendYeaftOutput({ type: 'result', result_text: '' }, { chatId });
1896
+ return;
1897
+ }
1898
+
1899
+ await ensureSessionLoaded();
1900
+
1901
+ const root = chatsRootFor(yeaftDir);
1902
+ let handle;
1903
+ try { handle = openChat(root, chatId); }
1904
+ catch (err) {
1905
+ sendYeaftOutput({
1906
+ type: 'assistant',
1907
+ message: { content: [{ type: 'text', text: `⚠️ ${err.message}` }] },
1908
+ }, { chatId });
1909
+ sendYeaftOutput({ type: 'result', result_text: '' }, { chatId });
1910
+ return;
1911
+ }
1912
+ const meta = handle.getMeta();
1913
+ if (!meta) {
1914
+ handle.close();
1915
+ sendYeaftOutput({
1916
+ type: 'assistant',
1917
+ message: { content: [{ type: 'text', text: `⚠️ Chat ${chatId} not found.` }] },
1918
+ }, { chatId });
1919
+ sendYeaftOutput({ type: 'result', result_text: '' }, { chatId });
1920
+ return;
1921
+ }
1922
+ const vpId = meta.vpId;
1923
+
1924
+ // Persist attachments + build LLM-side multimodal parts. Same flow as
1925
+ // the group path (handleYeaftGroupChat) so chat mode isn't a
1926
+ // second-class citizen for images / files.
1927
+ const inboundFiles = Array.isArray(msg.files) ? msg.files : [];
1928
+ let attachmentBundle = { promptAttachments: [], promptSuffix: '', promptParts: [], failed: [] };
1929
+ if (inboundFiles.length > 0) {
1930
+ try {
1931
+ attachmentBundle = persistYeaftAttachments(inboundFiles, { subdir: `chat-${chatId}` });
1932
+ } catch (err) {
1933
+ console.warn('[Yeaft] yeaft_chat_send: attachment persist failed', err?.message || err);
1934
+ }
1935
+ }
1936
+ if (Array.isArray(attachmentBundle.failed) && attachmentBundle.failed.length > 0) {
1937
+ const detail = attachmentBundle.failed.map((f) => ` - ${f.name}: ${f.error}`).join('\n');
1938
+ sendYeaftOutput({
1939
+ type: 'assistant',
1940
+ message: { content: [{ type: 'text', text: `⚠️ ${attachmentBundle.failed.length} file(s) could not be attached:\n${detail}` }] },
1941
+ }, { chatId });
1942
+ }
1943
+ const persistedAttachments = attachmentsForPersistence(attachmentBundle.promptAttachments);
1944
+
1945
+ // Append the user message to the chat log so subsequent reads see it.
1946
+ try {
1947
+ handle.appendMessage({ from: 'user', role: 'user', text, meta: { attachments: persistedAttachments } });
1948
+ } catch (err) {
1949
+ console.warn('[Yeaft] chat appendMessage failed:', err?.message || err);
1950
+ } finally {
1951
+ handle.close();
1952
+ }
1953
+
1954
+ const turnId = `t_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 6)}`;
1955
+ const envelope = { chatId, vpId, turnId };
1956
+
1957
+ // Cancel any in-flight turn on this chat — 1:1 has no parallelism.
1958
+ const prior = chatAborts.get(chatId);
1959
+ if (prior) { try { prior.abort(); } catch { /* ignore */ } }
1960
+ const abort = new AbortController();
1961
+ chatAborts.set(chatId, abort);
1962
+
1963
+ sendYeaftEvent({ type: 'vp_turn_start', vpId, threadId: 'main', turnId, chatId, title: meta.displayName || '' }, envelope);
1964
+
1965
+ let queryTimer = null;
1966
+ const resetQueryTimer = () => {
1967
+ if (queryTimer) clearTimeout(queryTimer);
1968
+ queryTimer = setTimeout(() => {
1969
+ if (!abort.signal.aborted) {
1970
+ try { abort.abort(); } catch { /* ignore */ }
1971
+ }
1972
+ }, QUERY_TIMEOUT_MS);
1973
+ };
1974
+ resetQueryTimer();
1975
+
1976
+ try {
1977
+ const assistantTextParts = [];
1978
+ const toolCallsAccum = [];
1979
+ const toolResultsAccum = [];
1980
+ const thinkingBlocksAccum = [];
1981
+ const appendedUserPrompts = [];
1982
+
1983
+ const eng = getOrCreateChatEngine(chatId, vpId);
1984
+ const handlerCtx = {
1985
+ assistantTextParts,
1986
+ toolCallsAccum,
1987
+ toolResultsAccum,
1988
+ thinkingBlocksAccum,
1989
+ resetQueryTimer,
1990
+ chatId,
1991
+ vpId,
1992
+ turnId,
1993
+ threadId: 'main',
1994
+ thread: null,
1995
+ appendedUserPrompts,
1996
+ };
1997
+
1998
+ // Load the VP persona (defaults to omni) so the engine's worker
1999
+ // prompt has the right identity/role/persona blocks. Without this,
2000
+ // chat mode runs with a generic system prompt instead of Omni.
2001
+ const vpPersona = buildVpPersona(vpId);
2002
+
2003
+ for await (const event of eng.query({
2004
+ prompt: text,
2005
+ promptParts: attachmentBundle.promptParts && attachmentBundle.promptParts.length > 0 ? attachmentBundle.promptParts : null,
2006
+ signal: abort.signal,
2007
+ userAlreadyPersisted: false,
2008
+ threadId: 'main',
2009
+ senderVpId: vpId,
2010
+ vpPersona,
2011
+ })) {
2012
+ resetQueryTimer();
2013
+ handleEngineEvent(event, handlerCtx);
2014
+ }
2015
+
2016
+ sendYeaftOutput({ type: 'assistant', message: { content: [] } }, envelope);
2017
+ sendYeaftOutput({ type: 'result', result_text: '' }, envelope);
2018
+
2019
+ try { touchChat(root, chatId); } catch { /* best-effort */ }
2020
+ } catch (err) {
2021
+ const isAbort = err && (err.name === 'AbortError' || err.name === 'LLMAbortError');
2022
+ if (isAbort) {
2023
+ sendYeaftOutput({ type: 'result', result_text: '', stopped: true }, envelope);
2024
+ } else {
2025
+ console.error('[Yeaft] chat-send error:', err);
2026
+ sendYeaftOutput({
2027
+ type: 'assistant',
2028
+ message: { content: [{ type: 'text', text: `⚠️ Chat error: ${err.message}` }] },
2029
+ }, envelope);
2030
+ sendYeaftOutput({ type: 'result', result_text: '' }, envelope);
2031
+ }
2032
+ } finally {
2033
+ if (queryTimer) clearTimeout(queryTimer);
2034
+ if (chatAborts.get(chatId) === abort) chatAborts.delete(chatId);
2035
+ }
2036
+ }
2037
+
1668
2038
  /**
1669
2039
  * Install the dream pipeline progress sink and runtime settings bridge.
1670
2040
  * Thread scheduling is owned by the group VP runtime below, not by mutable
@@ -2437,23 +2807,8 @@ export function buildVpQueryOpts({ vpId, groupCoordinator, groupId, envelope, th
2437
2807
  if (groupMeta && typeof groupMeta.workDir === 'string' && groupMeta.workDir.trim()) {
2438
2808
  out.workDir = groupMeta.workDir.trim();
2439
2809
  }
2440
- try {
2441
- const vp = readVp(resolvedVpId);
2442
- if (vp) {
2443
- out.vpPersona = {
2444
- vpId: resolvedVpId,
2445
- displayName: vp.displayName || resolvedVpId,
2446
- displayNameZh: vp.displayNameZh || '',
2447
- role: vp.role || '',
2448
- roleZh: vp.roleZh || '',
2449
- persona: vp.persona || '',
2450
- // Optional per-VP planning style for the `StartPlan` tool. Empty
2451
- // string means "fall back to the default template" — the tool
2452
- // handles the lookup so callers stay ignorant of the default.
2453
- planInstruction: typeof vp.planInstruction === 'string' ? vp.planInstruction : '',
2454
- };
2455
- }
2456
- } catch { /* persona load is best-effort */ }
2810
+ const persona = buildVpPersona(resolvedVpId);
2811
+ if (persona) out.vpPersona = persona;
2457
2812
  if (groupCoordinator && typeof groupCoordinator.ingest === 'function') {
2458
2813
  try {
2459
2814
  out.router = createRouter({ coordinator: groupCoordinator });