@yeaft/webchat-agent 1.0.7 → 1.0.10

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.7",
3
+ "version": "1.0.10",
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",
@@ -59,6 +59,34 @@ function applyAnthropicThinking(body, model, effort, effortContext = {}) {
59
59
  const DEFAULT_BASE_URL = 'https://api.anthropic.com';
60
60
  const API_VERSION = '2023-06-01';
61
61
 
62
+ function hasNonEmptyText(value) {
63
+ return typeof value === 'string' && value.trim().length > 0;
64
+ }
65
+
66
+ function translateUserContent(content) {
67
+ if (hasNonEmptyText(content)) return content;
68
+
69
+ if (Array.isArray(content)) {
70
+ const parts = [];
71
+ for (const part of content) {
72
+ if (!part || typeof part !== 'object') {
73
+ if (hasNonEmptyText(part)) parts.push({ type: 'text', text: String(part) });
74
+ continue;
75
+ }
76
+ if (part.type === 'text') {
77
+ if (hasNonEmptyText(part.text)) parts.push(part);
78
+ continue;
79
+ }
80
+ // Non-text blocks (image/document/tool_result-like compatible payloads)
81
+ // are meaningful even without a text field. Preserve them verbatim.
82
+ parts.push(part);
83
+ }
84
+ return parts.length > 0 ? parts : null;
85
+ }
86
+
87
+ return null;
88
+ }
89
+
62
90
  /**
63
91
  * AnthropicAdapter — Talks to Anthropic Messages API.
64
92
  */
@@ -116,7 +144,8 @@ export class AnthropicAdapter extends LLMAdapter {
116
144
  for (const msg of messages) {
117
145
  if (msg.role === 'system') continue; // system goes separately
118
146
  if (msg.role === 'user') {
119
- result.push({ role: 'user', content: msg.content });
147
+ const content = translateUserContent(msg.content);
148
+ if (content) result.push({ role: 'user', content });
120
149
  } else if (msg.role === 'assistant') {
121
150
  const content = [];
122
151
  // task-327d: Anthropic requires thinking blocks to appear BEFORE
@@ -137,7 +166,7 @@ export class AnthropicAdapter extends LLMAdapter {
137
166
  }
138
167
  }
139
168
  }
140
- if (msg.content) {
169
+ if (hasNonEmptyText(msg.content)) {
141
170
  content.push({ type: 'text', text: msg.content });
142
171
  }
143
172
  if (msg.toolCalls) {
@@ -150,7 +179,7 @@ export class AnthropicAdapter extends LLMAdapter {
150
179
  });
151
180
  }
152
181
  }
153
- result.push({ role: 'assistant', content });
182
+ if (content.length > 0) result.push({ role: 'assistant', content });
154
183
  } else if (msg.role === 'tool') {
155
184
  // Anthropic requires all tool_results from the same turn in a single
156
185
  // user message. Merge consecutive tool messages into one.
@@ -12,16 +12,18 @@
12
12
  * Plus the D1 bootstrap helper:
13
13
  * ensureDefaultSessionIfEmpty(yeaftDir, {libDir}) — if NO session exists on
14
14
  * disk, seed `session_default` with roster = every VP in the library, and
15
- * defaultVpId = alphabetically first vpId. No-op when ≥1 session present.
15
+ * defaultVpId = `omni` when present, otherwise the alphabetically first vpId.
16
+ * No-op when ≥1 session present.
16
17
  *
17
18
  * Hard constraints (PM):
18
19
  * (a) We don't touch 334o storage primitives (storage/index.js) — we call
19
20
  * group-store.openSession / saveMeta which already go through openLog.
20
21
  * (b) We don't touch VP entity (vp-store.js / vp-loader.js) — only read
21
22
  * via scanVpLibrary to know which VPs exist at seed time.
22
- * (c) When `addMember` is called with an empty roster and no defaultVpId
23
- * resolvable, callers surface `no_default_vp` via `createSessionFromSpec`;
24
- * on `removeMember` we permit the empty state (UI nudges the user).
23
+ * (c) `createSessionFromSpec` seeds omitted/empty rosters with the default
24
+ * generalist VP when the library has one; truly empty VP libraries can
25
+ * still create empty sessions and surface `no_default_vp` on first send.
26
+ * On `removeMember` we permit the empty state (UI nudges the user).
25
27
  *
26
28
  * Error shape — every throw is a `SessionCrudError` with a stable `.code`:
27
29
  * 'not_found' — group id has no dir / meta
@@ -98,6 +100,7 @@ export class SessionCrudError extends Error {
98
100
  }
99
101
 
100
102
  const GROUP_WORKDIR_REGISTRY = 'group-workdirs.json';
103
+ const DEFAULT_VP_ID = 'omni';
101
104
 
102
105
  export function sessionsRoot(yeaftDir) {
103
106
  return join(yeaftDir, 'sessions');
@@ -260,11 +263,24 @@ export function makeSessionId(name) {
260
263
  return nextSessionId(slug);
261
264
  }
262
265
 
266
+ function preferDefaultVp(vpIds) {
267
+ if (!Array.isArray(vpIds) || vpIds.length === 0) return null;
268
+ return vpIds.includes(DEFAULT_VP_ID) ? DEFAULT_VP_ID : vpIds[0];
269
+ }
270
+
271
+ function scanSortedVpIds(libDir) {
272
+ const vpIds = scanVpLibrary({ dir: libDir })
273
+ .map(v => v && v.id)
274
+ .filter(v => typeof v === 'string' && v.length > 0);
275
+ vpIds.sort((a, b) => a.localeCompare(b));
276
+ return vpIds;
277
+ }
278
+
263
279
  /**
264
280
  * (B) D1 seed — called at boot (or when multi-VP is first enabled). Idempotent:
265
281
  * returns `{seeded:false}` if any session already exists on disk (including
266
282
  * `session_default`). When empty, seeds with roster = full VP library, sorted
267
- * alphabetically; defaultVpId = roster[0].
283
+ * alphabetically; defaultVpId = `omni` when present, otherwise roster[0].
268
284
  *
269
285
  * When the VP library is also empty, we still seed an empty-roster session so
270
286
  * the UI has somewhere to land — but defaultVpId is null and downstream
@@ -278,14 +294,12 @@ export function ensureDefaultSessionIfEmpty(yeaftDir, options = {}) {
278
294
  return { seeded: false, sessionId: existing[0].id };
279
295
  }
280
296
 
281
- // Sort VP ids alphabetically (stable for tests / deterministic UI).
282
- // NB: vp-store returns records with `.id` (not `.vpId`) keep this in sync.
283
- const vps = scanVpLibrary({ dir: libDir })
284
- .map(v => v && v.id)
285
- .filter(v => typeof v === 'string' && v.length > 0);
286
- vps.sort((a, b) => a.localeCompare(b));
297
+ // Sort VP ids alphabetically (stable for tests / deterministic UI), but
298
+ // prefer the generalist Omni VP as the default when present so first-run
299
+ // sessions land on a useful assistant instead of an arbitrary first id.
300
+ const vps = scanSortedVpIds(libDir);
287
301
 
288
- const defaultVpId = vps[0] || null;
302
+ const defaultVpId = preferDefaultVp(vps);
289
303
  const { group, created } = seedDefaultSession(yeaftDir, {
290
304
  name: options.name || 'Default',
291
305
  roster: vps,
@@ -301,21 +315,27 @@ export function ensureDefaultSessionIfEmpty(yeaftDir, options = {}) {
301
315
  }
302
316
 
303
317
  /**
304
- * (A.1) Create group from a wizard spec. `spec.roster` is authoritative
305
- * we do NOT auto-expand to the full VP library here. That's D1's job only.
318
+ * (A.1) Create session from a wizard spec. `spec.roster` is authoritative
319
+ * when non-empty. If the caller omits a roster, seed the session with the
320
+ * default generalist VP (`omni`) when it exists so a new Session is usable
321
+ * immediately instead of opening with an empty roster.
306
322
  *
307
323
  * @param {string} yeaftDir
308
324
  * @param {{name:string, roster?:string[], defaultVpId?:string|null, workDir?:string}} spec
309
325
  * @returns {{id:string, name:string, roster:string[], defaultVpId:string|null, workDir?:string}}
310
326
  */
311
327
  export function createSessionFromSpec(yeaftDir, spec, options = {}) {
312
- const normalizedWorkDir = normalizeWorkDir(spec && spec.workDir);
328
+ const input = spec || {};
329
+ const normalizedWorkDir = normalizeWorkDir(input.workDir);
313
330
  const groupYeaftDir = normalizedWorkDir ? yeaftDirForWorkDir(normalizedWorkDir) : yeaftDir;
314
331
  const memoryRoot = options.memoryRoot || (groupYeaftDir ? join(groupYeaftDir, 'memory') : DEFAULT_MEMORY_ROOT);
315
- const name = String(spec && spec.name || '').trim();
332
+ const libDir = options.libDir || DEFAULT_VP_LIB_DIR;
333
+ const name = String(input.name || '').trim();
316
334
  if (!name) throw new SessionCrudError('invalid_name', null, 'group name required');
317
335
 
318
- const roster = Array.isArray(spec.roster) ? spec.roster.slice() : [];
336
+ const callerRoster = Array.isArray(input.roster) ? input.roster.slice() : [];
337
+ const fallbackVpId = callerRoster.length > 0 ? null : preferDefaultVp(scanSortedVpIds(libDir));
338
+ const roster = callerRoster.length > 0 ? callerRoster : (fallbackVpId ? [fallbackVpId] : []);
319
339
  // Validate every member up-front so we fail before touching fs.
320
340
  for (const vpId of roster) {
321
341
  if (isReservedVpId(vpId)) {
@@ -325,10 +345,9 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
325
345
  if (!v.ok) throw new SessionCrudError(v.reason, null, `invalid vpId: ${vpId}`);
326
346
  }
327
347
 
328
- // defaultVpId resolution: explicit > roster[0] > null. Null is allowed at
329
- // create time (empty roster) the wizard modal warns the user downstream
330
- // (task-334m spec: `no_default_vp` surfaced on first send, not on create).
331
- let defaultVpId = spec.defaultVpId || null;
348
+ // defaultVpId resolution: explicit > roster[0] > null. Null is only
349
+ // possible when both caller roster and VP library are empty.
350
+ let defaultVpId = input.defaultVpId || null;
332
351
  if (defaultVpId && !roster.includes(defaultVpId)) {
333
352
  throw new SessionCrudError('default_not_in_roster', null, `${defaultVpId} not in roster`);
334
353
  }
@@ -354,8 +373,8 @@ export function createSessionFromSpec(yeaftDir, spec, options = {}) {
354
373
  // turn.
355
374
  try {
356
375
  ensureSessionConfigFile(yeaftDir, id);
357
- if (spec && spec.config && typeof spec.config === 'object') {
358
- saveSessionConfig(yeaftDir, id, spec.config);
376
+ if (input.config && typeof input.config === 'object') {
377
+ saveSessionConfig(yeaftDir, id, input.config);
359
378
  }
360
379
  } catch (err) {
361
380
  console.warn(`[session-crud] failed to seed config.json for ${id}:`, err?.message || err);