@yeaft/webchat-agent 0.1.430 → 0.1.432

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.
@@ -6,6 +6,33 @@ import { dispatchToRole } from './routing.js';
6
6
  import { sendStatusUpdate } from './ui-messages.js';
7
7
  import { debouncedSaveSessionMeta } from './persistence.js';
8
8
 
9
+ /**
10
+ * Resolve @role mention from message content.
11
+ * Returns { target, message } if a valid role is found, null otherwise.
12
+ */
13
+ export function resolveAtMention(content, session) {
14
+ const atMatch = content.match(/^@(\S+)\s*([\s\S]*)/);
15
+ if (!atMatch) return null;
16
+
17
+ const atTarget = atMatch[1];
18
+ const message = atMatch[2].trim() || content;
19
+
20
+ for (const [name, role] of session.roles) {
21
+ if (name === atTarget.toLowerCase()) {
22
+ return { target: name, message };
23
+ }
24
+ if (role.displayName === atTarget) {
25
+ return { target: name, message };
26
+ }
27
+ // Fuzzy: compound "name-displayName" pattern (e.g. "dev-1-托瓦兹" or partial displayName)
28
+ const compound = `${name}-${role.displayName}`;
29
+ if (compound.toLowerCase() === atTarget.toLowerCase()) {
30
+ return { target: name, message };
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+
9
36
  /**
10
37
  * 处理人的输入
11
38
  */
@@ -38,6 +65,13 @@ export async function handleCrewHumanInput(msg) {
38
65
  debouncedSaveSessionMeta(session);
39
66
  }
40
67
 
68
+ // Resolve @role mention from content (works regardless of targetRole from frontend)
69
+ const atMention = resolveAtMention(content, session);
70
+ // Effective target: explicit targetRole from frontend > @mention from content > null
71
+ const effectiveTargetRole = targetRole || (atMention ? atMention.target : null);
72
+ // If @mention found, use the stripped message (without @prefix); otherwise use original content
73
+ const effectiveContent = atMention ? atMention.message : content;
74
+
41
75
  // Build dispatch content (supports image attachments)
42
76
  function buildHumanContent(prefix, text) {
43
77
  if (files && files.length > 0) {
@@ -72,55 +106,35 @@ export async function handleCrewHumanInput(msg) {
72
106
  // Status changed + new human message — persist (debounced, dispatch will follow)
73
107
  debouncedSaveSessionMeta(session);
74
108
 
75
- const target = targetRole || waitingContext?.fromRole || session.decisionMaker;
76
- await dispatchToRole(session, target, buildHumanContent('人工回复:', content), 'human');
109
+ const target = effectiveTargetRole || waitingContext?.fromRole || session.decisionMaker;
110
+ if (effectiveTargetRole) {
111
+ console.log(`[Crew] @mention override in waiting_human: routing to ${target} instead of ${waitingContext?.fromRole || session.decisionMaker}`);
112
+ }
113
+ await dispatchToRole(session, target, buildHumanContent('人工回复:', effectiveContent), 'human');
77
114
  return;
78
115
  }
79
116
 
80
- // 解析 @role 指令
81
- const atMatch = content.match(/^@(\S+)\s*([\s\S]*)/);
82
- if (atMatch) {
83
- const atTarget = atMatch[1];
84
- const message = atMatch[2].trim() || content;
117
+ // @role 指令 — effectiveTargetRole already resolved above
118
+ if (atMention && effectiveTargetRole) {
119
+ const target = effectiveTargetRole;
120
+ const message = effectiveContent;
85
121
 
86
- let target = null;
87
- for (const [name, role] of session.roles) {
88
- if (name === atTarget.toLowerCase()) {
89
- target = name;
90
- break;
91
- }
92
- if (role.displayName === atTarget) {
93
- target = name;
94
- break;
122
+ // 检测纯 skill 命令(如 /context, /simplify),直接发送不加前缀
123
+ if (/^\/[a-zA-Z0-9_-]+(?:\s+.*)?$/s.test(message)) {
124
+ let roleState = session.roleStates.get(target);
125
+ if (!roleState || !roleState.query || !roleState.inputStream) {
126
+ const { createRoleQuery } = await import('./role-query.js');
127
+ roleState = await createRoleQuery(session, target);
95
128
  }
96
- }
97
-
98
- if (target) {
99
- // 检测纯 skill 命令(如 /context, /simplify),直接发送不加前缀
100
- if (/^\/[a-zA-Z0-9_-]+(?:\s+.*)?$/s.test(message)) {
101
- let roleState = session.roleStates.get(target);
102
- if (!roleState || !roleState.query || !roleState.inputStream) {
103
- const { createRoleQuery } = await import('./role-query.js');
104
- roleState = await createRoleQuery(session, target);
105
- }
106
- // P1-4: 守卫 stream.enqueue
107
- try {
108
- if (roleState.inputStream && !roleState.inputStream.isDone) {
109
- roleState.inputStream.enqueue({
110
- type: 'user',
111
- message: { role: 'user', content: message }
112
- });
113
- } else {
114
- console.warn(`[Crew] Skill dispatch: stream closed for ${target}, recreating`);
115
- const { createRoleQuery } = await import('./role-query.js');
116
- roleState = await createRoleQuery(session, target);
117
- roleState.inputStream.enqueue({
118
- type: 'user',
119
- message: { role: 'user', content: message }
120
- });
121
- }
122
- } catch (enqueueErr) {
123
- console.error(`[Crew] Skill dispatch enqueue failed for ${target}:`, enqueueErr.message);
129
+ // P1-4: 守卫 stream.enqueue
130
+ try {
131
+ if (roleState.inputStream && !roleState.inputStream.isDone) {
132
+ roleState.inputStream.enqueue({
133
+ type: 'user',
134
+ message: { role: 'user', content: message }
135
+ });
136
+ } else {
137
+ console.warn(`[Crew] Skill dispatch: stream closed for ${target}, recreating`);
124
138
  const { createRoleQuery } = await import('./role-query.js');
125
139
  roleState = await createRoleQuery(session, target);
126
140
  roleState.inputStream.enqueue({
@@ -128,18 +142,27 @@ export async function handleCrewHumanInput(msg) {
128
142
  message: { role: 'user', content: message }
129
143
  });
130
144
  }
131
- sendStatusUpdate(session);
132
- console.log(`[Crew] Skill command dispatched to ${target}: ${message}`);
133
- return;
145
+ } catch (enqueueErr) {
146
+ console.error(`[Crew] Skill dispatch enqueue failed for ${target}:`, enqueueErr.message);
147
+ const { createRoleQuery } = await import('./role-query.js');
148
+ roleState = await createRoleQuery(session, target);
149
+ roleState.inputStream.enqueue({
150
+ type: 'user',
151
+ message: { role: 'user', content: message }
152
+ });
134
153
  }
135
- await dispatchToRole(session, target, buildHumanContent('人工消息:', message), 'human');
154
+ sendStatusUpdate(session);
155
+ console.log(`[Crew] Skill command dispatched to ${target}: ${message}`);
136
156
  return;
137
157
  }
158
+ console.log(`[Crew] @mention routing to ${target}: ${message.substring(0, 50)}...`);
159
+ await dispatchToRole(session, target, buildHumanContent('人工消息:', message), 'human');
160
+ return;
138
161
  }
139
162
 
140
163
  // 默认发给决策者
141
- const target = targetRole || session.decisionMaker;
142
- await dispatchToRole(session, target, buildHumanContent('人工消息:', content), 'human');
164
+ const target = effectiveTargetRole || session.decisionMaker;
165
+ await dispatchToRole(session, target, buildHumanContent('人工消息:', effectiveContent), 'human');
143
166
  }
144
167
 
145
168
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "0.1.430",
3
+ "version": "0.1.432",
4
4
  "description": "Remote agent for Yeaft WebChat — connects worker machines to the central server",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/unify/engine.js CHANGED
@@ -424,6 +424,20 @@ export class Engine {
424
424
  responseText,
425
425
  });
426
426
 
427
+ // Emit debug_turn for error path too
428
+ yield {
429
+ type: 'debug_turn',
430
+ turnNumber,
431
+ model: currentModel,
432
+ systemPrompt,
433
+ messages: conversationMessages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 50000) : m.content })),
434
+ response: responseText || `Error: ${err.message}`,
435
+ toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
436
+ usage: { inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens },
437
+ latencyMs,
438
+ stopReason: 'error',
439
+ };
440
+
427
441
  // ─── LLMContextError → force compact → retry ──────
428
442
  if (err instanceof LLMContextError && this.#conversationStore && this.#memoryStore) {
429
443
  const consolidated = await this.#maybeConsolidate();
@@ -465,6 +479,21 @@ export class Engine {
465
479
  responseText,
466
480
  });
467
481
 
482
+ // Emit debug_turn event for web UI debug panel
483
+ // (conversationMessages does NOT yet include the assistant response at this point)
484
+ yield {
485
+ type: 'debug_turn',
486
+ turnNumber,
487
+ model: currentModel,
488
+ systemPrompt,
489
+ messages: conversationMessages.map(m => ({ role: m.role, content: typeof m.content === 'string' ? m.content.slice(0, 50000) : m.content })),
490
+ response: responseText,
491
+ toolCalls: toolCalls.map(tc => ({ id: tc.id, name: tc.name, input: tc.input })),
492
+ usage: { inputTokens: totalUsage.inputTokens, outputTokens: totalUsage.outputTokens },
493
+ latencyMs,
494
+ stopReason,
495
+ };
496
+
468
497
  // Append assistant message to conversation
469
498
  const assistantMsg = { role: 'assistant', content: responseText };
470
499
  if (toolCalls.length > 0) {
@@ -214,6 +214,22 @@ export async function handleUnifyChat(msg) {
214
214
  });
215
215
  break;
216
216
 
217
+ // ── Debug turn data for web debug panel ──
218
+ case 'debug_turn':
219
+ sendUnifyEvent({
220
+ type: 'debug_turn',
221
+ turnNumber: event.turnNumber,
222
+ model: event.model,
223
+ systemPrompt: event.systemPrompt,
224
+ messages: event.messages,
225
+ response: event.response,
226
+ toolCalls: event.toolCalls,
227
+ usage: event.usage,
228
+ latencyMs: event.latencyMs,
229
+ stopReason: event.stopReason,
230
+ });
231
+ break;
232
+
217
233
  // ── Errors ──
218
234
  case 'error':
219
235
  sendUnifyOutput({