@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 +1 -1
- package/yeaft/llm/anthropic.js +32 -3
- package/yeaft/sessions/session-crud.js +42 -23
package/package.json
CHANGED
package/yeaft/llm/anthropic.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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)
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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
|
-
//
|
|
283
|
-
|
|
284
|
-
|
|
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
|
|
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
|
|
305
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
|
329
|
-
//
|
|
330
|
-
|
|
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 (
|
|
358
|
-
saveSessionConfig(yeaftDir, id,
|
|
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);
|