@yeaft/webchat-agent 0.1.399 → 0.1.409
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/crew/role-query.js +10 -6
- package/package.json +3 -1
- package/sdk/query.js +3 -1
- package/unify/cli.js +735 -0
- package/unify/config.js +269 -0
- package/unify/conversation/persist.js +436 -0
- package/unify/conversation/search.js +65 -0
- package/unify/debug-trace.js +398 -0
- package/unify/engine.js +511 -0
- package/unify/index.js +27 -0
- package/unify/init.js +147 -0
- package/unify/llm/adapter.js +186 -0
- package/unify/llm/anthropic.js +322 -0
- package/unify/llm/chat-completions.js +315 -0
- package/unify/memory/consolidate.js +187 -0
- package/unify/memory/extract.js +97 -0
- package/unify/memory/recall.js +243 -0
- package/unify/memory/store.js +507 -0
- package/unify/models.js +167 -0
- package/unify/prompts.js +109 -0
package/unify/engine.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* engine.js — Yeaft query loop
|
|
3
|
+
*
|
|
4
|
+
* The engine is the core orchestrator:
|
|
5
|
+
* 1. Before first turn: recall memories → inject into system prompt
|
|
6
|
+
* 2. Build messages array (with compact summary if available)
|
|
7
|
+
* 3. Call adapter.stream()
|
|
8
|
+
* 4. Collect text + tool_calls from stream events
|
|
9
|
+
* 5. If tool_calls → execute tools → append results → goto 3
|
|
10
|
+
* 6. If end_turn → persist messages → check consolidation → done
|
|
11
|
+
* 7. If max_tokens → auto-continue (up to maxContinueTurns)
|
|
12
|
+
* 8. On LLMContextError → force compact → retry
|
|
13
|
+
* 9. On retryable error with fallbackModel → switch model → retry
|
|
14
|
+
*
|
|
15
|
+
* Pattern derived from Claude Code's query loop (src/query.ts).
|
|
16
|
+
*
|
|
17
|
+
* Reference: yeaft-unify-implementation-plan.md §3.1, §4 (Phase 2)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { randomUUID } from 'crypto';
|
|
21
|
+
import { buildSystemPrompt } from './prompts.js';
|
|
22
|
+
import { LLMContextError } from './llm/adapter.js';
|
|
23
|
+
import { recall } from './memory/recall.js';
|
|
24
|
+
import { shouldConsolidate, consolidate } from './memory/consolidate.js';
|
|
25
|
+
|
|
26
|
+
/** Maximum number of turns before the engine stops to prevent infinite loops. */
|
|
27
|
+
const MAX_TURNS = 25;
|
|
28
|
+
|
|
29
|
+
/** Maximum auto-continue turns when stopReason is 'max_tokens'. */
|
|
30
|
+
const MAX_CONTINUE_TURNS = 3;
|
|
31
|
+
|
|
32
|
+
// ─── Engine Events (superset of adapter events) ──────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {{ type: 'turn_start', turnNumber: number }} TurnStartEvent
|
|
36
|
+
* @typedef {{ type: 'turn_end', turnNumber: number, stopReason: string }} TurnEndEvent
|
|
37
|
+
* @typedef {{ type: 'tool_start', id: string, name: string, input: object }} ToolStartEvent
|
|
38
|
+
* @typedef {{ type: 'tool_end', id: string, name: string, output: string, isError: boolean }} ToolEndEvent
|
|
39
|
+
* @typedef {{ type: 'consolidate', archivedCount: number, extractedCount: number }} ConsolidateEvent
|
|
40
|
+
* @typedef {{ type: 'recall', entryCount: number, cached: boolean }} RecallEvent
|
|
41
|
+
* @typedef {{ type: 'fallback', from: string, to: string, reason: string }} FallbackEvent
|
|
42
|
+
*
|
|
43
|
+
* @typedef {import('./llm/adapter.js').StreamEvent | TurnStartEvent | TurnEndEvent | ToolStartEvent | ToolEndEvent | ConsolidateEvent | RecallEvent | FallbackEvent} EngineEvent
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
// ─── Engine ──────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export class Engine {
|
|
49
|
+
/** @type {import('./llm/adapter.js').LLMAdapter} */
|
|
50
|
+
#adapter;
|
|
51
|
+
|
|
52
|
+
/** @type {import('./debug-trace.js').DebugTrace | import('./debug-trace.js').NullTrace} */
|
|
53
|
+
#trace;
|
|
54
|
+
|
|
55
|
+
/** @type {object} */
|
|
56
|
+
#config;
|
|
57
|
+
|
|
58
|
+
/** @type {Map<string, { name: string, description: string, parameters: object, execute: function }>} */
|
|
59
|
+
#tools;
|
|
60
|
+
|
|
61
|
+
/** @type {string} */
|
|
62
|
+
#traceId;
|
|
63
|
+
|
|
64
|
+
/** @type {import('./conversation/persist.js').ConversationStore|null} */
|
|
65
|
+
#conversationStore;
|
|
66
|
+
|
|
67
|
+
/** @type {import('./memory/store.js').MemoryStore|null} */
|
|
68
|
+
#memoryStore;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @param {{
|
|
72
|
+
* adapter: import('./llm/adapter.js').LLMAdapter,
|
|
73
|
+
* trace: object,
|
|
74
|
+
* config: object,
|
|
75
|
+
* conversationStore?: import('./conversation/persist.js').ConversationStore,
|
|
76
|
+
* memoryStore?: import('./memory/store.js').MemoryStore
|
|
77
|
+
* }} params
|
|
78
|
+
*/
|
|
79
|
+
constructor({ adapter, trace, config, conversationStore, memoryStore }) {
|
|
80
|
+
this.#adapter = adapter;
|
|
81
|
+
this.#trace = trace;
|
|
82
|
+
this.#config = config;
|
|
83
|
+
this.#tools = new Map();
|
|
84
|
+
this.#traceId = randomUUID();
|
|
85
|
+
this.#conversationStore = conversationStore || null;
|
|
86
|
+
this.#memoryStore = memoryStore || null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Register a tool that the LLM can call.
|
|
91
|
+
*
|
|
92
|
+
* @param {{ name: string, description: string, parameters: object, execute: (input: object, ctx?: { signal?: AbortSignal }) => Promise<string> }} tool
|
|
93
|
+
*/
|
|
94
|
+
registerTool(tool) {
|
|
95
|
+
this.#tools.set(tool.name, tool);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Unregister a tool.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} name
|
|
102
|
+
*/
|
|
103
|
+
unregisterTool(name) {
|
|
104
|
+
this.#tools.delete(name);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get the list of registered tool definitions (for passing to the adapter).
|
|
109
|
+
*
|
|
110
|
+
* @returns {import('./llm/adapter.js').UnifiedToolDef[]}
|
|
111
|
+
*/
|
|
112
|
+
#getToolDefs() {
|
|
113
|
+
const defs = [];
|
|
114
|
+
for (const [, tool] of this.#tools) {
|
|
115
|
+
defs.push({
|
|
116
|
+
name: tool.name,
|
|
117
|
+
description: tool.description,
|
|
118
|
+
parameters: tool.parameters,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return defs;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build the system prompt with memory and compact summary.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} mode
|
|
128
|
+
* @param {{ profile?: string, entries?: object[] }} [memory]
|
|
129
|
+
* @param {string} [compactSummary]
|
|
130
|
+
* @returns {string}
|
|
131
|
+
*/
|
|
132
|
+
#buildSystemPrompt(mode, memory, compactSummary) {
|
|
133
|
+
return buildSystemPrompt({
|
|
134
|
+
language: this.#config.language || 'en',
|
|
135
|
+
mode,
|
|
136
|
+
toolNames: Array.from(this.#tools.keys()),
|
|
137
|
+
memory,
|
|
138
|
+
compactSummary,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Perform memory recall for a given prompt.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} prompt
|
|
146
|
+
* @returns {Promise<{ profile: string, entries: object[] }|null>}
|
|
147
|
+
*/
|
|
148
|
+
async #recallMemory(prompt) {
|
|
149
|
+
if (!this.#memoryStore) return null;
|
|
150
|
+
|
|
151
|
+
const memory = { profile: '', entries: [] };
|
|
152
|
+
|
|
153
|
+
// Read user profile
|
|
154
|
+
memory.profile = this.#memoryStore.readProfile();
|
|
155
|
+
|
|
156
|
+
// Recall relevant entries
|
|
157
|
+
try {
|
|
158
|
+
const result = await recall({
|
|
159
|
+
prompt,
|
|
160
|
+
adapter: this.#adapter,
|
|
161
|
+
config: this.#config,
|
|
162
|
+
memoryStore: this.#memoryStore,
|
|
163
|
+
});
|
|
164
|
+
memory.entries = result.entries;
|
|
165
|
+
} catch {
|
|
166
|
+
// Recall failure is non-critical
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return memory;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Read compact summary from conversation store.
|
|
174
|
+
*
|
|
175
|
+
* @returns {string}
|
|
176
|
+
*/
|
|
177
|
+
#getCompactSummary() {
|
|
178
|
+
if (!this.#conversationStore) return '';
|
|
179
|
+
return this.#conversationStore.readCompactSummary();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Persist user message and assistant response to conversation store.
|
|
184
|
+
*
|
|
185
|
+
* @param {string} userContent
|
|
186
|
+
* @param {string} assistantContent
|
|
187
|
+
* @param {string} mode
|
|
188
|
+
* @param {object[]} [toolCalls]
|
|
189
|
+
*/
|
|
190
|
+
#persistMessages(userContent, assistantContent, mode, toolCalls) {
|
|
191
|
+
if (!this.#conversationStore) return;
|
|
192
|
+
|
|
193
|
+
// Persist user message
|
|
194
|
+
this.#conversationStore.append({
|
|
195
|
+
role: 'user',
|
|
196
|
+
content: userContent,
|
|
197
|
+
mode,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Persist assistant message
|
|
201
|
+
const assistantMsg = {
|
|
202
|
+
role: 'assistant',
|
|
203
|
+
content: assistantContent,
|
|
204
|
+
mode,
|
|
205
|
+
model: this.#config.model,
|
|
206
|
+
};
|
|
207
|
+
if (toolCalls && toolCalls.length > 0) {
|
|
208
|
+
assistantMsg.toolCalls = toolCalls;
|
|
209
|
+
}
|
|
210
|
+
this.#conversationStore.append(assistantMsg);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Check and trigger consolidation if needed.
|
|
215
|
+
*
|
|
216
|
+
* @returns {Promise<{ archivedCount: number, extractedCount: number }|null>}
|
|
217
|
+
*/
|
|
218
|
+
async #maybeConsolidate() {
|
|
219
|
+
if (!this.#conversationStore || !this.#memoryStore) return null;
|
|
220
|
+
|
|
221
|
+
const budget = this.#config.messageTokenBudget || 8192;
|
|
222
|
+
if (!shouldConsolidate(this.#conversationStore, budget)) return null;
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const result = await consolidate({
|
|
226
|
+
conversationStore: this.#conversationStore,
|
|
227
|
+
memoryStore: this.#memoryStore,
|
|
228
|
+
adapter: this.#adapter,
|
|
229
|
+
config: this.#config,
|
|
230
|
+
budget,
|
|
231
|
+
});
|
|
232
|
+
return { archivedCount: result.archivedCount, extractedCount: result.extractedEntries.length };
|
|
233
|
+
} catch {
|
|
234
|
+
// Consolidation failure is non-critical
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Run a query — the main loop.
|
|
241
|
+
*
|
|
242
|
+
* Yields EngineEvent objects that the caller (CLI, web) can consume
|
|
243
|
+
* to render output in real-time.
|
|
244
|
+
*
|
|
245
|
+
* @param {{ prompt: string, mode?: string, messages?: Array, signal?: AbortSignal }} params
|
|
246
|
+
* @yields {EngineEvent}
|
|
247
|
+
*/
|
|
248
|
+
async *query({ prompt, mode = 'chat', messages = [], signal }) {
|
|
249
|
+
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
250
|
+
yield {
|
|
251
|
+
type: 'error',
|
|
252
|
+
error: new Error('prompt is required and must be a non-empty string'),
|
|
253
|
+
retryable: false,
|
|
254
|
+
};
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ─── Pre-query: Recall + Compact Summary ────────────────
|
|
259
|
+
const memory = await this.#recallMemory(prompt);
|
|
260
|
+
if (memory && memory.entries.length > 0) {
|
|
261
|
+
yield { type: 'recall', entryCount: memory.entries.length, cached: false };
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const compactSummary = this.#getCompactSummary();
|
|
265
|
+
const systemPrompt = this.#buildSystemPrompt(mode, memory, compactSummary);
|
|
266
|
+
|
|
267
|
+
// Build conversation: existing messages + new user message
|
|
268
|
+
const conversationMessages = [
|
|
269
|
+
...messages,
|
|
270
|
+
{ role: 'user', content: prompt },
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
const toolDefs = this.#getToolDefs();
|
|
274
|
+
let turnNumber = 0;
|
|
275
|
+
let continueTurns = 0; // auto-continue counter
|
|
276
|
+
let fullResponseText = '';
|
|
277
|
+
let currentModel = this.#config.model;
|
|
278
|
+
|
|
279
|
+
while (true) {
|
|
280
|
+
turnNumber++;
|
|
281
|
+
|
|
282
|
+
// Safety: prevent infinite loops
|
|
283
|
+
if (turnNumber > MAX_TURNS) {
|
|
284
|
+
yield {
|
|
285
|
+
type: 'error',
|
|
286
|
+
error: new Error(`Max turns (${MAX_TURNS}) reached — stopping to prevent infinite loop`),
|
|
287
|
+
retryable: false,
|
|
288
|
+
};
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const turnId = this.#trace.startTurn({
|
|
293
|
+
traceId: this.#traceId,
|
|
294
|
+
mode,
|
|
295
|
+
turnNumber,
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const startTime = Date.now();
|
|
299
|
+
let responseText = '';
|
|
300
|
+
const toolCalls = [];
|
|
301
|
+
let stopReason = 'end_turn';
|
|
302
|
+
const totalUsage = { inputTokens: 0, outputTokens: 0 };
|
|
303
|
+
|
|
304
|
+
yield { type: 'turn_start', turnNumber };
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
// Stream from adapter
|
|
308
|
+
for await (const event of this.#adapter.stream({
|
|
309
|
+
model: currentModel,
|
|
310
|
+
system: systemPrompt,
|
|
311
|
+
messages: [...conversationMessages],
|
|
312
|
+
tools: toolDefs.length > 0 ? toolDefs : undefined,
|
|
313
|
+
maxTokens: this.#config.maxOutputTokens || 16384,
|
|
314
|
+
signal,
|
|
315
|
+
})) {
|
|
316
|
+
switch (event.type) {
|
|
317
|
+
case 'text_delta':
|
|
318
|
+
responseText += event.text;
|
|
319
|
+
yield event;
|
|
320
|
+
break;
|
|
321
|
+
case 'thinking_delta':
|
|
322
|
+
yield event;
|
|
323
|
+
break;
|
|
324
|
+
case 'tool_call':
|
|
325
|
+
toolCalls.push(event);
|
|
326
|
+
yield event;
|
|
327
|
+
break;
|
|
328
|
+
case 'usage':
|
|
329
|
+
totalUsage.inputTokens += event.inputTokens;
|
|
330
|
+
totalUsage.outputTokens += event.outputTokens;
|
|
331
|
+
yield event;
|
|
332
|
+
break;
|
|
333
|
+
case 'stop':
|
|
334
|
+
stopReason = event.stopReason;
|
|
335
|
+
yield event;
|
|
336
|
+
break;
|
|
337
|
+
case 'error':
|
|
338
|
+
yield event;
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
} catch (err) {
|
|
343
|
+
const latencyMs = Date.now() - startTime;
|
|
344
|
+
this.#trace.endTurn(turnId, {
|
|
345
|
+
model: currentModel,
|
|
346
|
+
inputTokens: totalUsage.inputTokens,
|
|
347
|
+
outputTokens: totalUsage.outputTokens,
|
|
348
|
+
stopReason: 'error',
|
|
349
|
+
latencyMs,
|
|
350
|
+
responseText,
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ─── LLMContextError → force compact → retry ──────
|
|
354
|
+
if (err instanceof LLMContextError && this.#conversationStore && this.#memoryStore) {
|
|
355
|
+
const consolidated = await this.#maybeConsolidate();
|
|
356
|
+
if (consolidated && consolidated.archivedCount > 0) {
|
|
357
|
+
yield { type: 'consolidate', archivedCount: consolidated.archivedCount, extractedCount: consolidated.extractedCount };
|
|
358
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'context_overflow_retry' };
|
|
359
|
+
continue; // retry with fewer messages
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ─── Fallback model ──────────────────────────────
|
|
364
|
+
const fallbackModel = this.#config.fallbackModel;
|
|
365
|
+
if (fallbackModel && fallbackModel !== currentModel &&
|
|
366
|
+
(err.name === 'LLMRateLimitError' || err.name === 'LLMServerError')) {
|
|
367
|
+
yield { type: 'fallback', from: currentModel, to: fallbackModel, reason: err.message };
|
|
368
|
+
currentModel = fallbackModel;
|
|
369
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'fallback_retry' };
|
|
370
|
+
continue; // retry with fallback model
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
yield {
|
|
374
|
+
type: 'error',
|
|
375
|
+
error: err,
|
|
376
|
+
retryable: err.name === 'LLMRateLimitError' || err.name === 'LLMServerError',
|
|
377
|
+
};
|
|
378
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'error' };
|
|
379
|
+
break;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const latencyMs = Date.now() - startTime;
|
|
383
|
+
|
|
384
|
+
// Record turn in debug trace
|
|
385
|
+
this.#trace.endTurn(turnId, {
|
|
386
|
+
model: currentModel,
|
|
387
|
+
inputTokens: totalUsage.inputTokens,
|
|
388
|
+
outputTokens: totalUsage.outputTokens,
|
|
389
|
+
stopReason,
|
|
390
|
+
latencyMs,
|
|
391
|
+
responseText,
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
// Append assistant message to conversation
|
|
395
|
+
const assistantMsg = { role: 'assistant', content: responseText };
|
|
396
|
+
if (toolCalls.length > 0) {
|
|
397
|
+
assistantMsg.toolCalls = toolCalls.map(tc => ({
|
|
398
|
+
id: tc.id,
|
|
399
|
+
name: tc.name,
|
|
400
|
+
input: tc.input,
|
|
401
|
+
}));
|
|
402
|
+
}
|
|
403
|
+
conversationMessages.push(assistantMsg);
|
|
404
|
+
fullResponseText += responseText;
|
|
405
|
+
|
|
406
|
+
// ─── Handle max_tokens → auto-continue ────────────
|
|
407
|
+
if (stopReason === 'max_tokens' && continueTurns < MAX_CONTINUE_TURNS) {
|
|
408
|
+
continueTurns++;
|
|
409
|
+
// Append a "Continue" user message
|
|
410
|
+
conversationMessages.push({ role: 'user', content: 'Continue' });
|
|
411
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'max_tokens_continue' };
|
|
412
|
+
continue; // loop back to call adapter again
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// If no tool calls, we're done
|
|
416
|
+
if (stopReason !== 'tool_use' || toolCalls.length === 0) {
|
|
417
|
+
yield { type: 'turn_end', turnNumber, stopReason };
|
|
418
|
+
|
|
419
|
+
// ─── Post-query: Persist + Consolidate ────────────
|
|
420
|
+
this.#persistMessages(prompt, fullResponseText, mode, assistantMsg.toolCalls);
|
|
421
|
+
|
|
422
|
+
const consolidated = await this.#maybeConsolidate();
|
|
423
|
+
if (consolidated && consolidated.archivedCount > 0) {
|
|
424
|
+
yield { type: 'consolidate', archivedCount: consolidated.archivedCount, extractedCount: consolidated.extractedCount };
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Execute tool calls and feed results back
|
|
431
|
+
for (const tc of toolCalls) {
|
|
432
|
+
const tool = this.#tools.get(tc.name);
|
|
433
|
+
const toolStartTime = Date.now();
|
|
434
|
+
|
|
435
|
+
let output;
|
|
436
|
+
let isError = false;
|
|
437
|
+
|
|
438
|
+
if (!tool) {
|
|
439
|
+
output = `Error: unknown tool "${tc.name}"`;
|
|
440
|
+
isError = true;
|
|
441
|
+
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true };
|
|
442
|
+
} else {
|
|
443
|
+
try {
|
|
444
|
+
yield { type: 'tool_start', id: tc.id, name: tc.name, input: tc.input };
|
|
445
|
+
output = await tool.execute(tc.input, { signal });
|
|
446
|
+
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: false };
|
|
447
|
+
} catch (err) {
|
|
448
|
+
output = `Error: ${err.message}`;
|
|
449
|
+
isError = true;
|
|
450
|
+
yield { type: 'tool_end', id: tc.id, name: tc.name, output, isError: true };
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const toolDurationMs = Date.now() - toolStartTime;
|
|
455
|
+
|
|
456
|
+
// Log tool to debug trace
|
|
457
|
+
this.#trace.logTool(turnId, {
|
|
458
|
+
toolName: tc.name,
|
|
459
|
+
toolInput: JSON.stringify(tc.input),
|
|
460
|
+
toolOutput: output,
|
|
461
|
+
durationMs: toolDurationMs,
|
|
462
|
+
isError,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Append tool result to conversation
|
|
466
|
+
conversationMessages.push({
|
|
467
|
+
role: 'tool',
|
|
468
|
+
toolCallId: tc.id,
|
|
469
|
+
content: output,
|
|
470
|
+
isError,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
yield { type: 'turn_end', turnNumber, stopReason: 'tool_use' };
|
|
475
|
+
|
|
476
|
+
// Loop back to call adapter again with tool results
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Get the trace ID for this engine instance.
|
|
482
|
+
* @returns {string}
|
|
483
|
+
*/
|
|
484
|
+
get traceId() {
|
|
485
|
+
return this.#traceId;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Get registered tool names.
|
|
490
|
+
* @returns {string[]}
|
|
491
|
+
*/
|
|
492
|
+
get toolNames() {
|
|
493
|
+
return Array.from(this.#tools.keys());
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Get the conversation store (for external access, e.g., CLI commands).
|
|
498
|
+
* @returns {import('./conversation/persist.js').ConversationStore|null}
|
|
499
|
+
*/
|
|
500
|
+
get conversationStore() {
|
|
501
|
+
return this.#conversationStore;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Get the memory store (for external access, e.g., CLI commands).
|
|
506
|
+
* @returns {import('./memory/store.js').MemoryStore|null}
|
|
507
|
+
*/
|
|
508
|
+
get memoryStore() {
|
|
509
|
+
return this.#memoryStore;
|
|
510
|
+
}
|
|
511
|
+
}
|
package/unify/index.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.js — Yeaft Unify module entry point
|
|
3
|
+
*
|
|
4
|
+
* Re-exports all public APIs for external consumption.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { initYeaftDir, DEFAULT_YEAFT_DIR } from './init.js';
|
|
8
|
+
export { loadConfig, parseFrontmatter } from './config.js';
|
|
9
|
+
export { DebugTrace, NullTrace, createTrace } from './debug-trace.js';
|
|
10
|
+
export {
|
|
11
|
+
LLMAdapter,
|
|
12
|
+
LLMRateLimitError,
|
|
13
|
+
LLMAuthError,
|
|
14
|
+
LLMContextError,
|
|
15
|
+
LLMServerError,
|
|
16
|
+
LLMAbortError,
|
|
17
|
+
createLLMAdapter,
|
|
18
|
+
} from './llm/adapter.js';
|
|
19
|
+
export { MODEL_REGISTRY, resolveModel, listModels, isKnownModel } from './models.js';
|
|
20
|
+
export { buildSystemPrompt, SUPPORTED_LANGUAGES } from './prompts.js';
|
|
21
|
+
export { Engine } from './engine.js';
|
|
22
|
+
export { ConversationStore, parseMessage, estimateTokens } from './conversation/persist.js';
|
|
23
|
+
export { searchMessages } from './conversation/search.js';
|
|
24
|
+
export { MemoryStore, parseEntry, serializeEntry, MEMORY_KINDS } from './memory/store.js';
|
|
25
|
+
export { recall, extractKeywords, computeFingerprint, clearRecallCache } from './memory/recall.js';
|
|
26
|
+
export { extractMemories } from './memory/extract.js';
|
|
27
|
+
export { consolidate, shouldConsolidate } from './memory/consolidate.js';
|
package/unify/init.js
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init.js — Yeaft directory structure initialization
|
|
3
|
+
*
|
|
4
|
+
* Ensures ~/.yeaft/ and all required subdirectories exist.
|
|
5
|
+
* Creates default config.md, MEMORY.md, and conversation/index.md if missing.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, writeFileSync } from 'fs';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { homedir } from 'os';
|
|
11
|
+
|
|
12
|
+
/** Default directory for Yeaft data. */
|
|
13
|
+
export const DEFAULT_YEAFT_DIR = join(homedir(), '.yeaft');
|
|
14
|
+
|
|
15
|
+
/** Subdirectories that must exist inside the Yeaft data directory. */
|
|
16
|
+
const SUBDIRS = [
|
|
17
|
+
'conversation/messages',
|
|
18
|
+
'conversation/cold',
|
|
19
|
+
'conversation/blobs',
|
|
20
|
+
'memory/entries',
|
|
21
|
+
'tasks',
|
|
22
|
+
'dream',
|
|
23
|
+
'skills',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/** Default config.md content (YAML frontmatter + markdown body). */
|
|
27
|
+
const DEFAULT_CONFIG = `---
|
|
28
|
+
model: claude-sonnet-4-20250514
|
|
29
|
+
language: en
|
|
30
|
+
debug: false
|
|
31
|
+
maxContextTokens: 200000
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
# Yeaft Config
|
|
35
|
+
|
|
36
|
+
Edit the YAML frontmatter above to change settings.
|
|
37
|
+
The \`model\` field is a model ID (e.g. \`gpt-5\`, \`claude-sonnet-4-20250514\`).
|
|
38
|
+
Yeaft auto-detects the correct API adapter and endpoint from the model ID.
|
|
39
|
+
|
|
40
|
+
## Language
|
|
41
|
+
|
|
42
|
+
Set \`language\` in frontmatter or \`YEAFT_LANGUAGE\` env var:
|
|
43
|
+
- \`en\` — English system prompts (default)
|
|
44
|
+
- \`zh\` — Chinese system prompts (中文系统提示)
|
|
45
|
+
|
|
46
|
+
## Model IDs
|
|
47
|
+
|
|
48
|
+
- \`claude-sonnet-4-20250514\` (default)
|
|
49
|
+
- \`claude-opus-4-20250514\`
|
|
50
|
+
- \`gpt-5\`, \`gpt-5.4\`, \`gpt-4.1\`, \`gpt-4.1-mini\`
|
|
51
|
+
- \`o3\`, \`o4-mini\`
|
|
52
|
+
- \`deepseek-chat\`, \`deepseek-reasoner\`
|
|
53
|
+
- \`gemini-2.5-pro\`, \`gemini-2.5-flash\`
|
|
54
|
+
|
|
55
|
+
## API Keys
|
|
56
|
+
|
|
57
|
+
Store API keys in \`~/.yeaft/.env\` (recommended) or export as env vars:
|
|
58
|
+
|
|
59
|
+
\`\`\`bash
|
|
60
|
+
# ~/.yeaft/.env
|
|
61
|
+
YEAFT_API_KEY=sk-ant-... # Anthropic
|
|
62
|
+
YEAFT_OPENAI_API_KEY=sk-... # OpenAI / DeepSeek / Gemini
|
|
63
|
+
\`\`\`
|
|
64
|
+
|
|
65
|
+
## Environment Variables
|
|
66
|
+
|
|
67
|
+
Shell env vars take precedence over .env and config.md:
|
|
68
|
+
|
|
69
|
+
- \`YEAFT_MODEL\` — override model ID
|
|
70
|
+
- \`YEAFT_LANGUAGE\` — language for system prompts (en/zh)
|
|
71
|
+
- \`YEAFT_API_KEY\` — Anthropic API key
|
|
72
|
+
- \`YEAFT_OPENAI_API_KEY\` — OpenAI-compatible API key
|
|
73
|
+
- \`YEAFT_PROXY_URL\` — CopilotProxy URL (default: http://localhost:6628)
|
|
74
|
+
- \`YEAFT_DEBUG\` — enable debug mode (1/true)
|
|
75
|
+
- \`YEAFT_DIR\` — data directory (default: ~/.yeaft)
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
/** Default MEMORY.md content. */
|
|
79
|
+
const DEFAULT_MEMORY = `# Yeaft Memory
|
|
80
|
+
|
|
81
|
+
This file stores persistent memory entries. The agent will read and update this file.
|
|
82
|
+
|
|
83
|
+
## Facts
|
|
84
|
+
|
|
85
|
+
## Preferences
|
|
86
|
+
|
|
87
|
+
## Project Context
|
|
88
|
+
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
/** Default conversation/index.md content. */
|
|
92
|
+
const DEFAULT_CONVERSATION_INDEX = `---
|
|
93
|
+
lastMessageId: null
|
|
94
|
+
totalMessages: 0
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
# Conversation Index
|
|
98
|
+
|
|
99
|
+
This file tracks the conversation state for the "one eternal conversation" model.
|
|
100
|
+
`;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Initialize the Yeaft data directory structure.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} [dir] — Root directory path. Defaults to ~/.yeaft/
|
|
106
|
+
* @returns {{ dir: string, created: string[] }} — The root dir and list of created paths
|
|
107
|
+
*/
|
|
108
|
+
export function initYeaftDir(dir) {
|
|
109
|
+
const root = dir || DEFAULT_YEAFT_DIR;
|
|
110
|
+
const created = [];
|
|
111
|
+
|
|
112
|
+
// Ensure root exists
|
|
113
|
+
if (!existsSync(root)) {
|
|
114
|
+
mkdirSync(root, { recursive: true });
|
|
115
|
+
created.push(root);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Ensure all subdirectories exist
|
|
119
|
+
for (const sub of SUBDIRS) {
|
|
120
|
+
const fullPath = join(root, sub);
|
|
121
|
+
if (!existsSync(fullPath)) {
|
|
122
|
+
mkdirSync(fullPath, { recursive: true });
|
|
123
|
+
created.push(fullPath);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Create default files if they don't exist
|
|
128
|
+
const configPath = join(root, 'config.md');
|
|
129
|
+
if (!existsSync(configPath)) {
|
|
130
|
+
writeFileSync(configPath, DEFAULT_CONFIG, 'utf8');
|
|
131
|
+
created.push(configPath);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const memoryPath = join(root, 'memory', 'MEMORY.md');
|
|
135
|
+
if (!existsSync(memoryPath)) {
|
|
136
|
+
writeFileSync(memoryPath, DEFAULT_MEMORY, 'utf8');
|
|
137
|
+
created.push(memoryPath);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const indexPath = join(root, 'conversation', 'index.md');
|
|
141
|
+
if (!existsSync(indexPath)) {
|
|
142
|
+
writeFileSync(indexPath, DEFAULT_CONVERSATION_INDEX, 'utf8');
|
|
143
|
+
created.push(indexPath);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { dir: root, created };
|
|
147
|
+
}
|