@yeaft/webchat-agent 0.1.422 → 0.1.423

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.
@@ -25,6 +25,7 @@ import { sendToServer, flushMessageBuffer } from './buffer.js';
25
25
  import { handleRestartAgent, handleUpgradeAgent } from './upgrade.js';
26
26
  import { loadMcpServers, updateMcpConfig } from '../mcp.js';
27
27
  import { getLlmConfig, updateLlmConfig } from '../unify/config-api.js';
28
+ import { handleUnifyChat, handleUnifyModeSwitch, resetUnifySession } from '../unify/web-bridge.js';
28
29
 
29
30
  export async function handleMessage(msg) {
30
31
  switch (msg.type) {
@@ -318,5 +319,18 @@ export async function handleMessage(msg) {
318
319
  sendToServer({ type: 'llm_config_updated', ...result });
319
320
  break;
320
321
  }
322
+
323
+ // Unify — independent chat via Engine
324
+ case 'unify_chat':
325
+ await handleUnifyChat(msg);
326
+ break;
327
+
328
+ case 'unify_mode_switch':
329
+ handleUnifyModeSwitch(msg);
330
+ break;
331
+
332
+ case 'unify_reset':
333
+ await resetUnifySession();
334
+ break;
321
335
  }
322
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.422",
3
+ "version": "0.1.423",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -0,0 +1,296 @@
1
+ /**
2
+ * web-bridge.js — Bridge between web UI and Yeaft Unify Engine.
3
+ *
4
+ * Translates Engine events into claude_output-format messages so the
5
+ * frontend can fully reuse the standard Chat rendering pipeline
6
+ * (MessageList, AssistantTurn, ToolLine, AskCard, waiting cat, etc.).
7
+ *
8
+ * Architecture:
9
+ * 1. On first use, loadSession() initialises Engine with skills + MCP enabled.
10
+ * 2. A virtual conversationId ('unify-<ts>') is assigned per session.
11
+ * 3. Engine.query() yields events → translated into unify_output messages
12
+ * that carry { conversationId, data } in claude_output format.
13
+ * 4. The frontend's handleUnifyOutput dispatches them through handleClaudeOutput.
14
+ */
15
+
16
+ import { loadSession } from './session.js';
17
+ import { sendToServer } from '../connection/buffer.js';
18
+ import ctx from '../context.js';
19
+
20
+ /** @type {import('./session.js').Session | null} */
21
+ let session = null;
22
+
23
+ /** @type {AbortController | null} */
24
+ let currentAbort = null;
25
+
26
+ /** Virtual conversationId for the Unify session */
27
+ let unifyConversationId = null;
28
+
29
+ /** Current query mode: 'chat' or 'work' */
30
+ let currentMode = 'chat';
31
+
32
+ /**
33
+ * Send a unify_output message carrying claude_output-format data.
34
+ * The server forwards this as-is to the web client.
35
+ * The frontend's handleUnifyOutput will dispatch via handleClaudeOutput.
36
+ */
37
+ function sendUnifyOutput(data) {
38
+ sendToServer({
39
+ type: 'unify_output',
40
+ conversationId: unifyConversationId,
41
+ data,
42
+ });
43
+ }
44
+
45
+ /**
46
+ * Send a unify_output event (non-claude_output metadata).
47
+ */
48
+ function sendUnifyEvent(event) {
49
+ sendToServer({
50
+ type: 'unify_output',
51
+ conversationId: unifyConversationId,
52
+ event,
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Handle a unify_chat message from the web UI.
58
+ *
59
+ * @param {{ prompt: string, mode?: string, userId?: string, username?: string }} msg
60
+ */
61
+ export async function handleUnifyChat(msg) {
62
+ const { prompt, mode } = msg;
63
+ if (!prompt?.trim()) return;
64
+
65
+ // Update mode if provided
66
+ if (mode === 'chat' || mode === 'work') {
67
+ currentMode = mode;
68
+ }
69
+
70
+ try {
71
+ // ─── Lazy-init session (reuse across queries — Engine manages history) ──
72
+ if (!session) {
73
+ const yeaftDir = ctx.CONFIG?.yeaftDir;
74
+ session = await loadSession({
75
+ ...(yeaftDir && { dir: yeaftDir }),
76
+ // Enable all features — no lazy shortcuts
77
+ skipMCP: false,
78
+ skipSkills: false,
79
+ });
80
+
81
+ // Create a stable conversationId for the Unify session
82
+ unifyConversationId = `unify-${Date.now()}`;
83
+
84
+ // Notify UI: session is ready with model info + conversationId
85
+ sendUnifyEvent({
86
+ type: 'session_ready',
87
+ conversationId: unifyConversationId,
88
+ model: session.config.model,
89
+ skills: session.status.skills,
90
+ mcpServers: session.status.mcpServers,
91
+ tools: session.status.tools,
92
+ });
93
+ }
94
+
95
+ // ─── Cancel any in-flight query ──
96
+ if (currentAbort) {
97
+ currentAbort.abort();
98
+ currentAbort = null;
99
+ }
100
+
101
+ currentAbort = new AbortController();
102
+
103
+ // ─── Stream Engine events → claude_output format ──
104
+ for await (const event of session.engine.query({
105
+ prompt,
106
+ mode: currentMode,
107
+ signal: currentAbort.signal,
108
+ })) {
109
+ switch (event.type) {
110
+ // ── Text streaming ──
111
+ case 'text_delta':
112
+ sendUnifyOutput({
113
+ type: 'assistant',
114
+ message: {
115
+ content: [{ type: 'text', text: event.text }],
116
+ },
117
+ });
118
+ break;
119
+
120
+ // ── Thinking streaming (extended thinking) ──
121
+ case 'thinking_delta':
122
+ // Currently not rendered in UI, but forward for future use
123
+ sendUnifyEvent({ type: 'thinking_delta', text: event.text });
124
+ break;
125
+
126
+ // ── Tool call announced by LLM ──
127
+ case 'tool_call':
128
+ // Finish any in-progress text streaming so UI shows typing dots
129
+ sendUnifyOutput({
130
+ type: 'assistant',
131
+ message: { content: [] },
132
+ });
133
+ // Send tool_use block
134
+ sendUnifyOutput({
135
+ type: 'assistant',
136
+ message: {
137
+ content: [{
138
+ type: 'tool_use',
139
+ id: event.id,
140
+ name: event.name,
141
+ input: event.input,
142
+ }],
143
+ },
144
+ });
145
+ break;
146
+
147
+ // ── Tool execution started ──
148
+ case 'tool_start':
149
+ // Tool is running — the UI already shows it from tool_use block above
150
+ break;
151
+
152
+ // ── Tool execution completed ──
153
+ case 'tool_end':
154
+ // Send tool_result as a user message (matches Claude CLI format)
155
+ sendUnifyOutput({
156
+ type: 'user',
157
+ tool_use_result: [{
158
+ type: 'tool_result',
159
+ tool_use_id: event.id,
160
+ content: event.output || '',
161
+ is_error: event.isError || false,
162
+ }],
163
+ });
164
+ break;
165
+
166
+ // ── Turn boundaries ──
167
+ case 'turn_start':
168
+ // No UI action needed
169
+ break;
170
+
171
+ case 'turn_end':
172
+ // Don't send result/done here — wait for the outermost loop to finish
173
+ break;
174
+
175
+ // ── Token usage ──
176
+ case 'usage':
177
+ sendUnifyEvent({
178
+ type: 'context_usage',
179
+ inputTokens: event.inputTokens,
180
+ outputTokens: event.outputTokens,
181
+ });
182
+ break;
183
+
184
+ // ── Stop reason from LLM ──
185
+ case 'stop':
186
+ // Intermediate signal — final done is sent after the loop
187
+ break;
188
+
189
+ // ── Memory recall ──
190
+ case 'recall':
191
+ sendUnifyEvent({
192
+ type: 'recall',
193
+ entryCount: event.entryCount,
194
+ cached: event.cached,
195
+ });
196
+ break;
197
+
198
+ // ── Context consolidation ──
199
+ case 'consolidate':
200
+ sendUnifyEvent({
201
+ type: 'consolidate',
202
+ archivedCount: event.archivedCount,
203
+ extractedCount: event.extractedCount,
204
+ });
205
+ break;
206
+
207
+ // ── Model fallback ──
208
+ case 'fallback':
209
+ sendUnifyEvent({
210
+ type: 'fallback',
211
+ from: event.from,
212
+ to: event.to,
213
+ reason: event.reason,
214
+ });
215
+ break;
216
+
217
+ // ── Errors ──
218
+ case 'error':
219
+ sendUnifyOutput({
220
+ type: 'assistant',
221
+ message: {
222
+ content: [{
223
+ type: 'text',
224
+ text: `⚠️ Error: ${event.error?.message || 'Unknown error'}`,
225
+ }],
226
+ },
227
+ });
228
+ break;
229
+
230
+ default:
231
+ // Silently consume unknown events
232
+ break;
233
+ }
234
+ }
235
+
236
+ // ─── Query complete — signal turn end ──
237
+ // Finish any streaming text
238
+ sendUnifyOutput({
239
+ type: 'assistant',
240
+ message: { content: [] },
241
+ });
242
+ // Send result to clear processing state
243
+ sendUnifyOutput({
244
+ type: 'result',
245
+ result_text: '',
246
+ });
247
+
248
+ } catch (err) {
249
+ // Don't report abort errors
250
+ if (err.name === 'AbortError') return;
251
+
252
+ console.error('[Unify] query error:', err.message);
253
+ sendUnifyOutput({
254
+ type: 'assistant',
255
+ message: {
256
+ content: [{
257
+ type: 'text',
258
+ text: `⚠️ Session error: ${err.message}`,
259
+ }],
260
+ },
261
+ });
262
+ // Still send result to clear processing state
263
+ sendUnifyOutput({
264
+ type: 'result',
265
+ result_text: '',
266
+ });
267
+ } finally {
268
+ currentAbort = null;
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Handle mode switch from the web UI.
274
+ * @param {{ mode: 'chat' | 'work' }} msg
275
+ */
276
+ export function handleUnifyModeSwitch(msg) {
277
+ if (msg.mode === 'chat' || msg.mode === 'work') {
278
+ currentMode = msg.mode;
279
+ }
280
+ }
281
+
282
+ /**
283
+ * Reset Unify session (for clear messages).
284
+ */
285
+ export async function resetUnifySession() {
286
+ if (currentAbort) {
287
+ currentAbort.abort();
288
+ currentAbort = null;
289
+ }
290
+ if (session) {
291
+ await session.shutdown();
292
+ session = null;
293
+ }
294
+ unifyConversationId = null;
295
+ currentMode = 'chat';
296
+ }