@yeaft/webchat-agent 0.1.409 → 0.1.411
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/unify/config.js +36 -0
- package/unify/engine.js +124 -16
- package/unify/index.js +14 -1
- package/unify/mcp.js +433 -0
- package/unify/memory/dream-prompt.js +272 -0
- package/unify/memory/dream.js +468 -0
- package/unify/memory/scan.js +273 -0
- package/unify/memory/types.js +139 -0
- package/unify/prompts.js +6 -0
- package/unify/session.js +191 -0
- package/unify/skills.js +315 -0
- package/unify/stop-hooks.js +146 -0
- package/unify/tools/enter-worktree.js +97 -0
- package/unify/tools/exit-worktree.js +131 -0
- package/unify/tools/mcp-tools.js +133 -0
- package/unify/tools/registry.js +146 -0
- package/unify/tools/skill.js +107 -0
- package/unify/tools/types.js +71 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scan.js — Memory header scanning and scope/tag matching
|
|
3
|
+
*
|
|
4
|
+
* Fast in-memory scanning of entry frontmatter for:
|
|
5
|
+
* - Scope tree traversal
|
|
6
|
+
* - Tag overlap scoring
|
|
7
|
+
* - Kind-based filtering
|
|
8
|
+
* - Stale entry detection (for Dream)
|
|
9
|
+
*
|
|
10
|
+
* Reference: yeaft-unify-core-systems.md §3.3, yeaft-unify-design.md §5.1
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { KINDS, KIND_PRIORITY, IMPORTANCE_WEIGHT, getAncestorScopes } from './types.js';
|
|
14
|
+
|
|
15
|
+
// ─── Scan Results ──────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} ScanResult
|
|
19
|
+
* @property {object[]} entries — all parsed entries
|
|
20
|
+
* @property {Map<string, number>} scopeCount — scope → entry count
|
|
21
|
+
* @property {Map<string, number>} kindCount — kind → entry count
|
|
22
|
+
* @property {Map<string, Set<string>>} tagIndex — tag → set of entry names
|
|
23
|
+
* @property {number} totalEntries — total count
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Scan all entries from a MemoryStore and build indexes.
|
|
28
|
+
*
|
|
29
|
+
* @param {import('./store.js').MemoryStore} memoryStore
|
|
30
|
+
* @returns {ScanResult}
|
|
31
|
+
*/
|
|
32
|
+
export function scanEntries(memoryStore) {
|
|
33
|
+
const entries = memoryStore.listEntries();
|
|
34
|
+
|
|
35
|
+
const scopeCount = new Map();
|
|
36
|
+
const kindCount = new Map();
|
|
37
|
+
const tagIndex = new Map();
|
|
38
|
+
|
|
39
|
+
for (const entry of entries) {
|
|
40
|
+
// Scope count
|
|
41
|
+
const scope = entry.scope || 'global';
|
|
42
|
+
scopeCount.set(scope, (scopeCount.get(scope) || 0) + 1);
|
|
43
|
+
|
|
44
|
+
// Kind count
|
|
45
|
+
const kind = entry.kind || 'fact';
|
|
46
|
+
kindCount.set(kind, (kindCount.get(kind) || 0) + 1);
|
|
47
|
+
|
|
48
|
+
// Tag index
|
|
49
|
+
const tags = entry.tags || [];
|
|
50
|
+
for (const tag of tags) {
|
|
51
|
+
const lowerTag = tag.toLowerCase();
|
|
52
|
+
if (!tagIndex.has(lowerTag)) tagIndex.set(lowerTag, new Set());
|
|
53
|
+
tagIndex.get(lowerTag).add(entry.name);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
entries,
|
|
59
|
+
scopeCount,
|
|
60
|
+
kindCount,
|
|
61
|
+
tagIndex,
|
|
62
|
+
totalEntries: entries.length,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Scoring Functions ─────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Score an entry for relevance to a query context.
|
|
70
|
+
*
|
|
71
|
+
* Scoring factors:
|
|
72
|
+
* - Scope match: exact=5, parent/child=3, global=1
|
|
73
|
+
* - Tag overlap: 2 per matching tag
|
|
74
|
+
* - Kind priority: see KIND_PRIORITY
|
|
75
|
+
* - Importance weight: see IMPORTANCE_WEIGHT
|
|
76
|
+
* - Frequency bonus: log2(frequency)
|
|
77
|
+
* - Recency bonus: entries updated in last 7 days get +2
|
|
78
|
+
*
|
|
79
|
+
* @param {object} entry — memory entry
|
|
80
|
+
* @param {{ scope?: string, tags?: string[], preferKinds?: string[] }} context
|
|
81
|
+
* @returns {number} — relevance score
|
|
82
|
+
*/
|
|
83
|
+
export function scoreEntry(entry, context = {}) {
|
|
84
|
+
let score = 0;
|
|
85
|
+
|
|
86
|
+
// Scope match
|
|
87
|
+
if (context.scope && entry.scope) {
|
|
88
|
+
if (entry.scope === context.scope) {
|
|
89
|
+
score += 5; // exact match
|
|
90
|
+
} else {
|
|
91
|
+
const ancestors = getAncestorScopes(context.scope);
|
|
92
|
+
if (ancestors.includes(entry.scope)) {
|
|
93
|
+
score += 3; // ancestor match
|
|
94
|
+
} else if (entry.scope.startsWith(context.scope + '/')) {
|
|
95
|
+
score += 3; // descendant match
|
|
96
|
+
} else if (entry.scope === 'global') {
|
|
97
|
+
score += 1; // global fallback
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Tag overlap
|
|
103
|
+
if (context.tags && context.tags.length > 0 && entry.tags) {
|
|
104
|
+
const entryTags = new Set(entry.tags.map(t => t.toLowerCase()));
|
|
105
|
+
for (const tag of context.tags) {
|
|
106
|
+
if (entryTags.has(tag.toLowerCase())) {
|
|
107
|
+
score += 2;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Kind priority
|
|
113
|
+
const kindPriority = KIND_PRIORITY[entry.kind] || 0;
|
|
114
|
+
score += kindPriority * 0.5;
|
|
115
|
+
|
|
116
|
+
// Preferred kinds bonus
|
|
117
|
+
if (context.preferKinds && context.preferKinds.includes(entry.kind)) {
|
|
118
|
+
score += 2;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Importance weight
|
|
122
|
+
const impWeight = IMPORTANCE_WEIGHT[entry.importance] || IMPORTANCE_WEIGHT.normal;
|
|
123
|
+
score += impWeight * 0.5;
|
|
124
|
+
|
|
125
|
+
// Frequency bonus (logarithmic)
|
|
126
|
+
const freq = entry.frequency || 1;
|
|
127
|
+
score += Math.log2(Math.max(freq, 1));
|
|
128
|
+
|
|
129
|
+
// Recency bonus
|
|
130
|
+
if (entry.updated_at) {
|
|
131
|
+
const daysSince = (Date.now() - new Date(entry.updated_at).getTime()) / (1000 * 60 * 60 * 24);
|
|
132
|
+
if (daysSince <= 7) score += 2;
|
|
133
|
+
else if (daysSince <= 30) score += 1;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return score;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── Stale Detection (for Dream) ────────────────────────────
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Find entries that are potentially stale.
|
|
143
|
+
*
|
|
144
|
+
* Stale criteria:
|
|
145
|
+
* - context entries older than 30 days
|
|
146
|
+
* - entries never recalled (frequency = 1) and older than 60 days
|
|
147
|
+
* - relation entries older than 90 days
|
|
148
|
+
*
|
|
149
|
+
* @param {object[]} entries
|
|
150
|
+
* @returns {object[]} — stale entries
|
|
151
|
+
*/
|
|
152
|
+
export function findStaleEntries(entries) {
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const stale = [];
|
|
155
|
+
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
const updatedAt = entry.updated_at ? new Date(entry.updated_at).getTime() : 0;
|
|
158
|
+
const daysSince = (now - updatedAt) / (1000 * 60 * 60 * 24);
|
|
159
|
+
|
|
160
|
+
let isStale = false;
|
|
161
|
+
|
|
162
|
+
// Context entries become stale fast
|
|
163
|
+
if (entry.kind === 'context' && daysSince > 30) {
|
|
164
|
+
isStale = true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Entries never recalled and old
|
|
168
|
+
if ((entry.frequency || 1) <= 1 && daysSince > 60) {
|
|
169
|
+
isStale = true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Relations are volatile
|
|
173
|
+
if (entry.kind === 'relation' && daysSince > 90) {
|
|
174
|
+
isStale = true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (isStale) {
|
|
178
|
+
stale.push({ ...entry, _daysSinceUpdate: Math.round(daysSince) });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return stale;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Duplicate Detection (for Dream Merge) ──────────────────
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Find groups of entries that are potentially duplicates.
|
|
189
|
+
* Entries are grouped if they share ≥2 tags AND the same kind.
|
|
190
|
+
*
|
|
191
|
+
* @param {object[]} entries
|
|
192
|
+
* @returns {object[][]} — groups of potentially duplicate entries
|
|
193
|
+
*/
|
|
194
|
+
export function findDuplicateGroups(entries) {
|
|
195
|
+
const groups = [];
|
|
196
|
+
const visited = new Set();
|
|
197
|
+
|
|
198
|
+
for (let i = 0; i < entries.length; i++) {
|
|
199
|
+
if (visited.has(i)) continue;
|
|
200
|
+
|
|
201
|
+
const group = [entries[i]];
|
|
202
|
+
const eTags = new Set((entries[i].tags || []).map(t => t.toLowerCase()));
|
|
203
|
+
|
|
204
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
205
|
+
if (visited.has(j)) continue;
|
|
206
|
+
if (entries[i].kind !== entries[j].kind) continue;
|
|
207
|
+
|
|
208
|
+
const jTags = new Set((entries[j].tags || []).map(t => t.toLowerCase()));
|
|
209
|
+
let overlap = 0;
|
|
210
|
+
for (const tag of eTags) {
|
|
211
|
+
if (jTags.has(tag)) overlap++;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (overlap >= 2) {
|
|
215
|
+
group.push(entries[j]);
|
|
216
|
+
visited.add(j);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (group.length > 1) {
|
|
221
|
+
visited.add(i);
|
|
222
|
+
groups.push(group);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return groups;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ─── Stats Summary ──────────────────────────────────────────
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Generate a text summary of memory state (for Dream prompts).
|
|
233
|
+
*
|
|
234
|
+
* @param {ScanResult} scan
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
export function summarizeScan(scan) {
|
|
238
|
+
const lines = [];
|
|
239
|
+
|
|
240
|
+
lines.push(`Total entries: ${scan.totalEntries}`);
|
|
241
|
+
|
|
242
|
+
// Kind breakdown
|
|
243
|
+
const kindLines = [];
|
|
244
|
+
for (const kind of KINDS) {
|
|
245
|
+
const count = scan.kindCount.get(kind) || 0;
|
|
246
|
+
if (count > 0) kindLines.push(`${kind}: ${count}`);
|
|
247
|
+
}
|
|
248
|
+
if (kindLines.length > 0) {
|
|
249
|
+
lines.push(`Kinds: ${kindLines.join(', ')}`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Scope breakdown (top 10)
|
|
253
|
+
const scopeEntries = [...scan.scopeCount.entries()]
|
|
254
|
+
.sort((a, b) => b[1] - a[1])
|
|
255
|
+
.slice(0, 10);
|
|
256
|
+
if (scopeEntries.length > 0) {
|
|
257
|
+
lines.push('Top scopes:');
|
|
258
|
+
for (const [scope, count] of scopeEntries) {
|
|
259
|
+
lines.push(` ${scope}: ${count}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Tag cloud (top 20)
|
|
264
|
+
const tagEntries = [...scan.tagIndex.entries()]
|
|
265
|
+
.map(([tag, names]) => [tag, names.size])
|
|
266
|
+
.sort((a, b) => b[1] - a[1])
|
|
267
|
+
.slice(0, 20);
|
|
268
|
+
if (tagEntries.length > 0) {
|
|
269
|
+
lines.push(`Top tags: ${tagEntries.map(([t, c]) => `${t}(${c})`).join(', ')}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return lines.join('\n');
|
|
273
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* types.js — Memory type definitions and constants
|
|
3
|
+
*
|
|
4
|
+
* Defines the 3D memory model:
|
|
5
|
+
* Kind = WHAT — 6 types: fact, preference, skill, lesson, context, relation
|
|
6
|
+
* Scope = WHERE — dynamic tree path: global / work/project / tech/typescript
|
|
7
|
+
* Tags = HOW — free keywords for retrieval
|
|
8
|
+
*
|
|
9
|
+
* Reference: yeaft-unify-core-systems.md §2.2, yeaft-unify-brainstorm-v3.md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ─── Kind ────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/** All valid memory kinds. */
|
|
15
|
+
export const KINDS = ['fact', 'preference', 'skill', 'lesson', 'context', 'relation'];
|
|
16
|
+
|
|
17
|
+
/** Kind descriptions for prompt context. */
|
|
18
|
+
export const KIND_DESCRIPTIONS = {
|
|
19
|
+
fact: 'Objective facts (project structure, tech stack, verified information)',
|
|
20
|
+
preference: 'User preferences (coding style, tools, communication style)',
|
|
21
|
+
skill: 'How to do something (patterns, techniques, workflows, commands)',
|
|
22
|
+
lesson: 'Lessons learned (bugs, pitfalls, effective alternatives)',
|
|
23
|
+
context: 'Temporal context (current OKR, project progress, deadlines)',
|
|
24
|
+
relation: 'People and relationships (teammates, roles, responsibilities)',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Kind priority for dream consolidation (higher = more important). */
|
|
28
|
+
export const KIND_PRIORITY = {
|
|
29
|
+
fact: 6,
|
|
30
|
+
preference: 5,
|
|
31
|
+
skill: 4,
|
|
32
|
+
lesson: 3,
|
|
33
|
+
context: 2,
|
|
34
|
+
relation: 1,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// ─── Scope ──────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parse a scope path into segments.
|
|
41
|
+
* @param {string} scope — e.g. "work/claude-web-chat/auth"
|
|
42
|
+
* @returns {string[]} — e.g. ["work", "claude-web-chat", "auth"]
|
|
43
|
+
*/
|
|
44
|
+
export function parseScopePath(scope) {
|
|
45
|
+
if (!scope) return ['global'];
|
|
46
|
+
return scope.split('/').filter(Boolean);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get all ancestor scopes (including the scope itself and 'global').
|
|
51
|
+
* @param {string} scope — e.g. "work/claude-web-chat/auth"
|
|
52
|
+
* @returns {string[]} — e.g. ["global", "work", "work/claude-web-chat", "work/claude-web-chat/auth"]
|
|
53
|
+
*/
|
|
54
|
+
export function getAncestorScopes(scope) {
|
|
55
|
+
if (!scope || scope === 'global') return ['global'];
|
|
56
|
+
|
|
57
|
+
const segments = parseScopePath(scope);
|
|
58
|
+
const ancestors = ['global'];
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < segments.length; i++) {
|
|
61
|
+
ancestors.push(segments.slice(0, i + 1).join('/'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return ancestors;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if two scopes are related (one is ancestor/descendant of the other).
|
|
69
|
+
* @param {string} a
|
|
70
|
+
* @param {string} b
|
|
71
|
+
* @returns {boolean}
|
|
72
|
+
*/
|
|
73
|
+
export function areScopesRelated(a, b) {
|
|
74
|
+
if (!a || !b || a === 'global' || b === 'global') return true;
|
|
75
|
+
return a.startsWith(b + '/') || b.startsWith(a + '/') || a === b;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Importance ─────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
/** Valid importance levels. */
|
|
81
|
+
export const IMPORTANCE_LEVELS = ['high', 'normal', 'low'];
|
|
82
|
+
|
|
83
|
+
/** Importance weight for scoring. */
|
|
84
|
+
export const IMPORTANCE_WEIGHT = {
|
|
85
|
+
high: 3,
|
|
86
|
+
normal: 2,
|
|
87
|
+
low: 1,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// ─── Entry Schema ──────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @typedef {Object} MemoryEntry
|
|
94
|
+
* @property {string} name — unique slug name
|
|
95
|
+
* @property {string} kind — one of KINDS
|
|
96
|
+
* @property {string} scope — tree path (e.g. "global", "tech/typescript")
|
|
97
|
+
* @property {string[]} tags — free keywords
|
|
98
|
+
* @property {string} importance — "high" | "normal" | "low"
|
|
99
|
+
* @property {number} frequency — how often this entry is recalled
|
|
100
|
+
* @property {string} content — the actual memory content
|
|
101
|
+
* @property {string[]} [related] — related entry names
|
|
102
|
+
* @property {string} [created_at] — ISO timestamp
|
|
103
|
+
* @property {string} [updated_at] — ISO timestamp
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate a memory entry object.
|
|
108
|
+
* @param {object} entry
|
|
109
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
110
|
+
*/
|
|
111
|
+
export function validateEntry(entry) {
|
|
112
|
+
const errors = [];
|
|
113
|
+
|
|
114
|
+
if (!entry || typeof entry !== 'object') {
|
|
115
|
+
return { valid: false, errors: ['Entry must be an object'] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (!entry.name || typeof entry.name !== 'string') {
|
|
119
|
+
errors.push('Entry must have a string "name"');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (entry.kind && !KINDS.includes(entry.kind)) {
|
|
123
|
+
errors.push(`Invalid kind "${entry.kind}". Must be one of: ${KINDS.join(', ')}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (entry.importance && !IMPORTANCE_LEVELS.includes(entry.importance)) {
|
|
127
|
+
errors.push(`Invalid importance "${entry.importance}". Must be one of: ${IMPORTANCE_LEVELS.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!entry.content || typeof entry.content !== 'string') {
|
|
131
|
+
errors.push('Entry must have string "content"');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (entry.tags && !Array.isArray(entry.tags)) {
|
|
135
|
+
errors.push('"tags" must be an array');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { valid: errors.length === 0, errors };
|
|
139
|
+
}
|
package/unify/prompts.js
CHANGED
|
@@ -61,6 +61,7 @@ export function buildSystemPrompt({
|
|
|
61
61
|
toolNames = [],
|
|
62
62
|
memory,
|
|
63
63
|
compactSummary,
|
|
64
|
+
skillContent,
|
|
64
65
|
} = {}) {
|
|
65
66
|
// Fallback to English for unknown languages
|
|
66
67
|
const lang = PROMPTS[language] || PROMPTS.en;
|
|
@@ -81,6 +82,11 @@ export function buildSystemPrompt({
|
|
|
81
82
|
parts.push(lang.tools(toolNames.join(', ')));
|
|
82
83
|
}
|
|
83
84
|
|
|
85
|
+
// ─── Skills Section ─────────────────────────────────────
|
|
86
|
+
if (skillContent) {
|
|
87
|
+
parts.push(skillContent);
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
// ─── Memory Section ─────────────────────────────────────
|
|
85
91
|
if (memory && (memory.profile || (memory.entries && memory.entries.length > 0))) {
|
|
86
92
|
const memoryParts = [lang.memoryHeader];
|
package/unify/session.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session.js — Session orchestrator for Yeaft Unify
|
|
3
|
+
*
|
|
4
|
+
* Single entry point: loadSession(options?) → Session
|
|
5
|
+
*
|
|
6
|
+
* Wires all subsystems together:
|
|
7
|
+
* initYeaftDir → loadConfig → createTrace → createLLMAdapter →
|
|
8
|
+
* ConversationStore → MemoryStore → SkillManager → MCPManager →
|
|
9
|
+
* ToolRegistry → Engine → Session
|
|
10
|
+
*
|
|
11
|
+
* The ~/.yeaft/ directory is the agent's persistent workspace.
|
|
12
|
+
* loadSession() loads (or initializes) this workspace and returns
|
|
13
|
+
* a fully wired Session ready for queries.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { initYeaftDir } from './init.js';
|
|
17
|
+
import { loadConfig, loadMCPConfig } from './config.js';
|
|
18
|
+
import { createTrace } from './debug-trace.js';
|
|
19
|
+
import { createLLMAdapter } from './llm/adapter.js';
|
|
20
|
+
import { ConversationStore } from './conversation/persist.js';
|
|
21
|
+
import { MemoryStore } from './memory/store.js';
|
|
22
|
+
import { SkillManager, createSkillManager } from './skills.js';
|
|
23
|
+
import { MCPManager } from './mcp.js';
|
|
24
|
+
import { createEmptyRegistry } from './tools/registry.js';
|
|
25
|
+
import { Engine } from './engine.js';
|
|
26
|
+
import { join } from 'path';
|
|
27
|
+
|
|
28
|
+
// Built-in tools
|
|
29
|
+
import mcpTools from './tools/mcp-tools.js';
|
|
30
|
+
import skillTool from './tools/skill.js';
|
|
31
|
+
import enterWorktree from './tools/enter-worktree.js';
|
|
32
|
+
import exitWorktree from './tools/exit-worktree.js';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} SessionOptions
|
|
36
|
+
* @property {string} [dir] — Yeaft data directory override (default: ~/.yeaft)
|
|
37
|
+
* @property {string} [model] — Model override
|
|
38
|
+
* @property {string} [language] — Language override ('en' | 'zh')
|
|
39
|
+
* @property {boolean} [debug] — Debug mode override
|
|
40
|
+
* @property {boolean} [skipMCP] — Skip MCP server connections (faster startup)
|
|
41
|
+
* @property {boolean} [skipSkills] — Skip skill loading
|
|
42
|
+
* @property {object[]} [extraTools] — Additional ToolDef objects to register
|
|
43
|
+
* @property {object} [configOverrides] — Additional config overrides
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {Object} Session
|
|
48
|
+
* @property {Engine} engine — The wired engine, ready for .query()
|
|
49
|
+
* @property {object} config — Resolved configuration
|
|
50
|
+
* @property {ConversationStore} conversationStore — Conversation persistence
|
|
51
|
+
* @property {MemoryStore} memoryStore — Memory persistence
|
|
52
|
+
* @property {SkillManager} skillManager — Skill manager
|
|
53
|
+
* @property {MCPManager} mcpManager — MCP manager
|
|
54
|
+
* @property {import('./tools/registry.js').ToolRegistry} toolRegistry — Tool registry
|
|
55
|
+
* @property {import('./debug-trace.js').DebugTrace|import('./debug-trace.js').NullTrace} trace
|
|
56
|
+
* @property {string} yeaftDir — Resolved data directory path
|
|
57
|
+
* @property {{ skills: number, mcpServers: string[], mcpFailed: object[], tools: number }} status
|
|
58
|
+
* @property {() => Promise<void>} shutdown — Graceful shutdown
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Load (or initialize) a Yeaft session.
|
|
63
|
+
*
|
|
64
|
+
* This is the main entry point for using Yeaft programmatically.
|
|
65
|
+
* It creates the directory structure if needed, loads config, connects
|
|
66
|
+
* to services, registers tools, and returns a ready-to-use Session.
|
|
67
|
+
*
|
|
68
|
+
* @param {SessionOptions} [options={}]
|
|
69
|
+
* @returns {Promise<Session>}
|
|
70
|
+
*/
|
|
71
|
+
export async function loadSession(options = {}) {
|
|
72
|
+
const {
|
|
73
|
+
dir,
|
|
74
|
+
model,
|
|
75
|
+
language,
|
|
76
|
+
debug,
|
|
77
|
+
skipMCP = false,
|
|
78
|
+
skipSkills = false,
|
|
79
|
+
extraTools = [],
|
|
80
|
+
configOverrides = {},
|
|
81
|
+
} = options;
|
|
82
|
+
|
|
83
|
+
// ─── 1. Load config (determines yeaftDir) ──────────────
|
|
84
|
+
const overrides = { ...configOverrides };
|
|
85
|
+
if (dir) overrides.dir = dir;
|
|
86
|
+
if (model) overrides.model = model;
|
|
87
|
+
if (language) overrides.language = language;
|
|
88
|
+
if (debug !== undefined) overrides.debug = debug;
|
|
89
|
+
|
|
90
|
+
const config = loadConfig(overrides);
|
|
91
|
+
const yeaftDir = config.dir;
|
|
92
|
+
|
|
93
|
+
// ─── 2. Ensure directory structure ─────────────────────
|
|
94
|
+
initYeaftDir(yeaftDir);
|
|
95
|
+
|
|
96
|
+
// ─── 3. Create debug trace ─────────────────────────────
|
|
97
|
+
const trace = createTrace({
|
|
98
|
+
enabled: config.debug,
|
|
99
|
+
dbPath: join(yeaftDir, 'debug.db'),
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ─── 4. Create LLM adapter ────────────────────────────
|
|
103
|
+
const adapter = await createLLMAdapter(config);
|
|
104
|
+
|
|
105
|
+
// ─── 5. Create stores ──────────────────────────────────
|
|
106
|
+
const conversationStore = new ConversationStore(yeaftDir);
|
|
107
|
+
const memoryStore = new MemoryStore(yeaftDir);
|
|
108
|
+
|
|
109
|
+
// ─── 6. Load skills ────────────────────────────────────
|
|
110
|
+
let skillManager;
|
|
111
|
+
if (skipSkills) {
|
|
112
|
+
skillManager = new SkillManager(yeaftDir);
|
|
113
|
+
// Don't call .load() — empty skill manager
|
|
114
|
+
} else {
|
|
115
|
+
skillManager = createSkillManager(yeaftDir);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── 7. Connect MCP servers ────────────────────────────
|
|
119
|
+
const mcpConfig = loadMCPConfig(yeaftDir);
|
|
120
|
+
const mcpManager = new MCPManager();
|
|
121
|
+
let mcpStatus = { connected: [], failed: [] };
|
|
122
|
+
|
|
123
|
+
if (!skipMCP && mcpConfig.servers.length > 0) {
|
|
124
|
+
mcpStatus = await mcpManager.connectAll(mcpConfig.servers);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ─── 8. Build tool registry ────────────────────────────
|
|
128
|
+
const toolRegistry = createEmptyRegistry();
|
|
129
|
+
|
|
130
|
+
// Register built-in tools
|
|
131
|
+
for (const tool of mcpTools) {
|
|
132
|
+
toolRegistry.register(tool);
|
|
133
|
+
}
|
|
134
|
+
toolRegistry.register(skillTool);
|
|
135
|
+
toolRegistry.register(enterWorktree);
|
|
136
|
+
toolRegistry.register(exitWorktree);
|
|
137
|
+
|
|
138
|
+
// Register any extra tools from caller
|
|
139
|
+
for (const tool of extraTools) {
|
|
140
|
+
toolRegistry.register(tool);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ─── 9. Create engine (wires everything) ───────────────
|
|
144
|
+
const engine = new Engine({
|
|
145
|
+
adapter,
|
|
146
|
+
trace,
|
|
147
|
+
config,
|
|
148
|
+
conversationStore,
|
|
149
|
+
memoryStore,
|
|
150
|
+
toolRegistry,
|
|
151
|
+
skillManager,
|
|
152
|
+
mcpManager,
|
|
153
|
+
yeaftDir,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// ─── 10. Build session ─────────────────────────────────
|
|
157
|
+
const status = {
|
|
158
|
+
skills: skillManager.size,
|
|
159
|
+
mcpServers: mcpStatus.connected,
|
|
160
|
+
mcpFailed: mcpStatus.failed,
|
|
161
|
+
tools: toolRegistry.size,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** Graceful shutdown: disconnect MCP, close trace DB. */
|
|
165
|
+
async function shutdown() {
|
|
166
|
+
try {
|
|
167
|
+
await mcpManager.disconnectAll();
|
|
168
|
+
} catch {
|
|
169
|
+
// Best-effort cleanup
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
trace.close();
|
|
173
|
+
} catch {
|
|
174
|
+
// Trace might not have close() (NullTrace)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
engine,
|
|
180
|
+
config,
|
|
181
|
+
conversationStore,
|
|
182
|
+
memoryStore,
|
|
183
|
+
skillManager,
|
|
184
|
+
mcpManager,
|
|
185
|
+
toolRegistry,
|
|
186
|
+
trace,
|
|
187
|
+
yeaftDir,
|
|
188
|
+
status,
|
|
189
|
+
shutdown,
|
|
190
|
+
};
|
|
191
|
+
}
|