@simonfestl/husky-cli 1.6.5 → 1.8.2
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/README.md +46 -0
- package/dist/commands/agent.js +43 -0
- package/dist/commands/biz/tickets.js +31 -1
- package/dist/commands/brain.js +279 -8
- package/dist/commands/chat.js +124 -1
- package/dist/commands/config.d.ts +4 -0
- package/dist/commands/config.js +9 -1
- package/dist/commands/e2e.js +361 -9
- package/dist/commands/image.d.ts +2 -0
- package/dist/commands/image.js +141 -0
- package/dist/commands/llm-context.js +69 -2
- package/dist/commands/task.js +109 -5
- package/dist/commands/vm.js +272 -0
- package/dist/commands/youtube.d.ts +2 -0
- package/dist/commands/youtube.js +178 -0
- package/dist/index.js +4 -0
- package/dist/lib/agent-identity.d.ts +25 -0
- package/dist/lib/agent-identity.js +73 -0
- package/dist/lib/biz/agent-brain.d.ts +63 -1
- package/dist/lib/biz/agent-brain.js +316 -4
- package/dist/lib/biz/learning-capture.d.ts +42 -0
- package/dist/lib/biz/learning-capture.js +107 -0
- package/dist/lib/biz/pii-filter.d.ts +34 -0
- package/dist/lib/biz/pii-filter.js +125 -0
- package/dist/lib/biz/qdrant.d.ts +5 -1
- package/dist/lib/biz/qdrant.js +20 -6
- package/dist/lib/biz/sop-generator.d.ts +39 -0
- package/dist/lib/biz/sop-generator.js +131 -0
- package/package.json +7 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Identity Resolution
|
|
3
|
+
*
|
|
4
|
+
* Resolves agent identity for learnings and brain operations.
|
|
5
|
+
* Priority: workerId (config) > HUSKY_AGENT_ID (env) > session-based ID (fallback)
|
|
6
|
+
*/
|
|
7
|
+
import { getConfig } from '../commands/config.js';
|
|
8
|
+
import { isValidAgentType } from './biz/agent-brain.js';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import * as os from 'os';
|
|
11
|
+
let sessionAgentId;
|
|
12
|
+
/**
|
|
13
|
+
* Get session-based agent ID (created once per CLI session)
|
|
14
|
+
*/
|
|
15
|
+
function getSessionAgentId() {
|
|
16
|
+
if (!sessionAgentId) {
|
|
17
|
+
const hostname = os.hostname();
|
|
18
|
+
const timestamp = Date.now().toString(36);
|
|
19
|
+
const random = randomUUID().split('-')[0];
|
|
20
|
+
sessionAgentId = `session-${hostname}-${timestamp}-${random}`;
|
|
21
|
+
}
|
|
22
|
+
return sessionAgentId;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Resolve agent identity from config, env, or generate session ID
|
|
26
|
+
*/
|
|
27
|
+
export function resolveAgentIdentity() {
|
|
28
|
+
const config = getConfig();
|
|
29
|
+
// Priority 1: workerId from config (persistent)
|
|
30
|
+
if (config.workerId) {
|
|
31
|
+
const agentType = config.agentType && isValidAgentType(config.agentType)
|
|
32
|
+
? config.agentType
|
|
33
|
+
: undefined;
|
|
34
|
+
return {
|
|
35
|
+
agentId: config.workerId,
|
|
36
|
+
agentType,
|
|
37
|
+
source: 'config',
|
|
38
|
+
workerId: config.workerId,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
// Priority 2: HUSKY_AGENT_ID from environment
|
|
42
|
+
const envAgentId = process.env.HUSKY_AGENT_ID;
|
|
43
|
+
if (envAgentId) {
|
|
44
|
+
const envAgentType = process.env.HUSKY_AGENT_TYPE;
|
|
45
|
+
const agentType = envAgentType && isValidAgentType(envAgentType)
|
|
46
|
+
? envAgentType
|
|
47
|
+
: undefined;
|
|
48
|
+
return {
|
|
49
|
+
agentId: envAgentId,
|
|
50
|
+
agentType,
|
|
51
|
+
source: 'env',
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Priority 3: Session-based ID (fallback)
|
|
55
|
+
const sessionId = getSessionAgentId();
|
|
56
|
+
return {
|
|
57
|
+
agentId: sessionId,
|
|
58
|
+
agentType: undefined,
|
|
59
|
+
source: 'session',
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Get agent ID (convenience method)
|
|
64
|
+
*/
|
|
65
|
+
export function getAgentId() {
|
|
66
|
+
return resolveAgentIdentity().agentId;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Get agent type (convenience method)
|
|
70
|
+
*/
|
|
71
|
+
export function getAgentType() {
|
|
72
|
+
return resolveAgentIdentity().agentType;
|
|
73
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const AGENT_TYPES: readonly ["support", "claude", "gotess", "supervisor", "worker"];
|
|
2
2
|
export type AgentType = typeof AGENT_TYPES[number];
|
|
3
|
+
export type MemoryVisibility = 'private' | 'team' | 'public';
|
|
3
4
|
export interface Memory {
|
|
4
5
|
id: string;
|
|
5
6
|
agent: string;
|
|
@@ -9,6 +10,17 @@ export interface Memory {
|
|
|
9
10
|
createdAt: Date;
|
|
10
11
|
updatedAt: Date;
|
|
11
12
|
metadata?: Record<string, unknown>;
|
|
13
|
+
visibility?: MemoryVisibility;
|
|
14
|
+
publishedBy?: string;
|
|
15
|
+
publishedAt?: string;
|
|
16
|
+
useCount?: number;
|
|
17
|
+
endorsements?: number;
|
|
18
|
+
recallCount?: number;
|
|
19
|
+
lastRecalledAt?: string;
|
|
20
|
+
boostCount?: number;
|
|
21
|
+
downvoteCount?: number;
|
|
22
|
+
qualityScore?: number;
|
|
23
|
+
status?: 'active' | 'archived' | 'deleted';
|
|
12
24
|
}
|
|
13
25
|
export interface RecallResult {
|
|
14
26
|
memory: Memory;
|
|
@@ -32,7 +44,7 @@ export declare class AgentBrain {
|
|
|
32
44
|
databaseName: string;
|
|
33
45
|
};
|
|
34
46
|
private ensureCollection;
|
|
35
|
-
remember(content: string, tags?: string[], metadata?: Record<string, unknown
|
|
47
|
+
remember(content: string, tags?: string[], metadata?: Record<string, unknown>, visibility?: MemoryVisibility, allowPii?: boolean): Promise<string>;
|
|
36
48
|
recall(query: string, limit?: number, minScore?: number): Promise<RecallResult[]>;
|
|
37
49
|
recallByTags(tags: string[], limit?: number): Promise<Memory[]>;
|
|
38
50
|
forget(memoryId: string): Promise<void>;
|
|
@@ -41,5 +53,55 @@ export declare class AgentBrain {
|
|
|
41
53
|
count: number;
|
|
42
54
|
tags: Record<string, number>;
|
|
43
55
|
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Publish a memory for sharing
|
|
58
|
+
*/
|
|
59
|
+
publish(memoryId: string, visibility: MemoryVisibility): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Unpublish a memory (set to private)
|
|
62
|
+
*/
|
|
63
|
+
unpublish(memoryId: string): Promise<void>;
|
|
64
|
+
/**
|
|
65
|
+
* Recall shared memories from other agents
|
|
66
|
+
*/
|
|
67
|
+
recallShared(query: string, limit?: number, minScore?: number, publicOnly?: boolean): Promise<RecallResult[]>;
|
|
68
|
+
/**
|
|
69
|
+
* List shared memories
|
|
70
|
+
*/
|
|
71
|
+
listShared(limit?: number, publicOnly?: boolean): Promise<Memory[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Boost a memory (positive feedback)
|
|
74
|
+
*/
|
|
75
|
+
boost(memoryId: string): Promise<void>;
|
|
76
|
+
/**
|
|
77
|
+
* Downvote a memory (negative feedback)
|
|
78
|
+
*/
|
|
79
|
+
downvote(memoryId: string): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Get quality metrics for a memory
|
|
82
|
+
*/
|
|
83
|
+
getQuality(memoryId: string): Promise<{
|
|
84
|
+
recallCount: number;
|
|
85
|
+
boostCount: number;
|
|
86
|
+
downvoteCount: number;
|
|
87
|
+
qualityScore: number;
|
|
88
|
+
status: string;
|
|
89
|
+
}>;
|
|
90
|
+
/**
|
|
91
|
+
* Calculate quality score based on feedback
|
|
92
|
+
*/
|
|
93
|
+
private calculateQualityScore;
|
|
94
|
+
/**
|
|
95
|
+
* Calculate effective score with decay
|
|
96
|
+
*/
|
|
97
|
+
private calculateEffectiveScore;
|
|
98
|
+
/**
|
|
99
|
+
* Archive low-quality memories
|
|
100
|
+
*/
|
|
101
|
+
cleanup(dryRun?: boolean, threshold?: number, minAgeDays?: number, tags?: string[]): Promise<Memory[]>;
|
|
102
|
+
/**
|
|
103
|
+
* Permanently delete archived memories
|
|
104
|
+
*/
|
|
105
|
+
purge(retentionDays?: number): Promise<number>;
|
|
44
106
|
}
|
|
45
107
|
export default AgentBrain;
|
|
@@ -2,6 +2,7 @@ import { QdrantClient } from './qdrant.js';
|
|
|
2
2
|
import { EmbeddingService } from './embeddings.js';
|
|
3
3
|
import { getConfig } from '../../commands/config.js';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
|
+
import { sanitizeForEmbedding } from './pii-filter.js';
|
|
5
6
|
const MEMORIES_COLLECTION = 'agent-memories';
|
|
6
7
|
const VECTOR_SIZE = 768;
|
|
7
8
|
export const AGENT_TYPES = ['support', 'claude', 'gotess', 'supervisor', 'worker'];
|
|
@@ -57,19 +58,42 @@ export class AgentBrain {
|
|
|
57
58
|
await this.qdrant.createCollection(MEMORIES_COLLECTION, VECTOR_SIZE);
|
|
58
59
|
}
|
|
59
60
|
}
|
|
60
|
-
async remember(content, tags = [], metadata) {
|
|
61
|
+
async remember(content, tags = [], metadata, visibility = 'private', allowPii = false) {
|
|
61
62
|
await this.ensureCollection();
|
|
62
|
-
|
|
63
|
+
// Phase 5: PII Filter (GDPR compliance)
|
|
64
|
+
const sanitizeResult = sanitizeForEmbedding(content, allowPii);
|
|
65
|
+
if (sanitizeResult.redactionCount > 0 && !allowPii) {
|
|
66
|
+
console.warn(` ⚠️ PII removed from memory content (${sanitizeResult.redactedTypes.join(', ')})`);
|
|
67
|
+
}
|
|
68
|
+
// Use sanitized content for embedding (privacy-safe)
|
|
69
|
+
const embedding = await this.embeddings.embed(sanitizeResult.sanitized);
|
|
63
70
|
const id = randomUUID();
|
|
64
71
|
const now = new Date().toISOString();
|
|
65
72
|
await this.qdrant.upsertOne(MEMORIES_COLLECTION, id, embedding, {
|
|
66
73
|
agent: this.agentId,
|
|
67
74
|
agentType: this.agentType || 'default',
|
|
68
|
-
content,
|
|
75
|
+
content: sanitizeResult.sanitized, // Store sanitized content
|
|
69
76
|
tags,
|
|
70
|
-
metadata:
|
|
77
|
+
metadata: {
|
|
78
|
+
...(metadata || {}),
|
|
79
|
+
// Store redaction info for transparency
|
|
80
|
+
piiRedacted: sanitizeResult.redactionCount > 0,
|
|
81
|
+
piiTypes: sanitizeResult.redactedTypes,
|
|
82
|
+
},
|
|
71
83
|
createdAt: now,
|
|
72
84
|
updatedAt: now,
|
|
85
|
+
// Phase 2: Visibility
|
|
86
|
+
visibility,
|
|
87
|
+
publishedBy: visibility !== 'private' ? this.agentId : undefined,
|
|
88
|
+
publishedAt: visibility !== 'private' ? now : undefined,
|
|
89
|
+
useCount: 0,
|
|
90
|
+
endorsements: 0,
|
|
91
|
+
// Phase 3: Quality
|
|
92
|
+
recallCount: 0,
|
|
93
|
+
boostCount: 0,
|
|
94
|
+
downvoteCount: 0,
|
|
95
|
+
qualityScore: 1.0,
|
|
96
|
+
status: 'active',
|
|
73
97
|
});
|
|
74
98
|
return id;
|
|
75
99
|
}
|
|
@@ -195,5 +219,293 @@ export class AgentBrain {
|
|
|
195
219
|
tags: tagCounts,
|
|
196
220
|
};
|
|
197
221
|
}
|
|
222
|
+
// =========================================================================
|
|
223
|
+
// Phase 2: Cross-Agent Sharing
|
|
224
|
+
// =========================================================================
|
|
225
|
+
/**
|
|
226
|
+
* Publish a memory for sharing
|
|
227
|
+
*/
|
|
228
|
+
async publish(memoryId, visibility) {
|
|
229
|
+
await this.ensureCollection();
|
|
230
|
+
const now = new Date().toISOString();
|
|
231
|
+
await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
|
|
232
|
+
visibility,
|
|
233
|
+
publishedBy: this.agentId,
|
|
234
|
+
publishedAt: now,
|
|
235
|
+
updatedAt: now,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Unpublish a memory (set to private)
|
|
240
|
+
*/
|
|
241
|
+
async unpublish(memoryId) {
|
|
242
|
+
await this.ensureCollection();
|
|
243
|
+
const now = new Date().toISOString();
|
|
244
|
+
await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
|
|
245
|
+
visibility: 'private',
|
|
246
|
+
publishedBy: undefined,
|
|
247
|
+
publishedAt: undefined,
|
|
248
|
+
updatedAt: now,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Recall shared memories from other agents
|
|
253
|
+
*/
|
|
254
|
+
async recallShared(query, limit = 5, minScore = 0.5, publicOnly = false) {
|
|
255
|
+
await this.ensureCollection();
|
|
256
|
+
const queryEmbedding = await this.embeddings.embed(query);
|
|
257
|
+
const filter = {
|
|
258
|
+
must: [
|
|
259
|
+
{
|
|
260
|
+
should: publicOnly
|
|
261
|
+
? [{ key: 'visibility', match: { value: 'public' } }]
|
|
262
|
+
: [
|
|
263
|
+
{ key: 'visibility', match: { value: 'public' } },
|
|
264
|
+
...(this.agentType
|
|
265
|
+
? [{ key: 'visibility', match: { value: 'team' } }, { key: 'agentType', match: { value: this.agentType } }]
|
|
266
|
+
: []),
|
|
267
|
+
],
|
|
268
|
+
},
|
|
269
|
+
{ key: 'status', match: { value: 'active' } },
|
|
270
|
+
],
|
|
271
|
+
};
|
|
272
|
+
const results = await this.qdrant.search(MEMORIES_COLLECTION, queryEmbedding, limit * 3, {
|
|
273
|
+
filter,
|
|
274
|
+
scoreThreshold: minScore,
|
|
275
|
+
});
|
|
276
|
+
return results.slice(0, limit).map(r => ({
|
|
277
|
+
memory: {
|
|
278
|
+
id: String(r.id),
|
|
279
|
+
agent: String(r.payload?.agent || ''),
|
|
280
|
+
agentType: String(r.payload?.agentType || ''),
|
|
281
|
+
content: String(r.payload?.content || ''),
|
|
282
|
+
tags: r.payload?.tags || [],
|
|
283
|
+
createdAt: new Date(String(r.payload?.createdAt || new Date().toISOString())),
|
|
284
|
+
updatedAt: new Date(String(r.payload?.updatedAt || new Date().toISOString())),
|
|
285
|
+
metadata: r.payload?.metadata,
|
|
286
|
+
visibility: r.payload?.visibility || 'private',
|
|
287
|
+
publishedBy: String(r.payload?.publishedBy || ''),
|
|
288
|
+
publishedAt: String(r.payload?.publishedAt || ''),
|
|
289
|
+
useCount: Number(r.payload?.useCount || 0),
|
|
290
|
+
endorsements: Number(r.payload?.endorsements || 0),
|
|
291
|
+
},
|
|
292
|
+
score: r.score,
|
|
293
|
+
}));
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* List shared memories
|
|
297
|
+
*/
|
|
298
|
+
async listShared(limit = 20, publicOnly = false) {
|
|
299
|
+
await this.ensureCollection();
|
|
300
|
+
const filter = {
|
|
301
|
+
must: [
|
|
302
|
+
{
|
|
303
|
+
should: publicOnly
|
|
304
|
+
? [{ key: 'visibility', match: { value: 'public' } }]
|
|
305
|
+
: [
|
|
306
|
+
{ key: 'visibility', match: { value: 'public' } },
|
|
307
|
+
...(this.agentType
|
|
308
|
+
? [{ key: 'visibility', match: { value: 'team' } }, { key: 'agentType', match: { value: this.agentType } }]
|
|
309
|
+
: []),
|
|
310
|
+
],
|
|
311
|
+
},
|
|
312
|
+
{ key: 'status', match: { value: 'active' } },
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
|
|
316
|
+
filter,
|
|
317
|
+
limit,
|
|
318
|
+
with_payload: true,
|
|
319
|
+
});
|
|
320
|
+
return results
|
|
321
|
+
.map(r => ({
|
|
322
|
+
id: String(r.id),
|
|
323
|
+
agent: String(r.payload?.agent || ''),
|
|
324
|
+
agentType: String(r.payload?.agentType || ''),
|
|
325
|
+
content: String(r.payload?.content || ''),
|
|
326
|
+
tags: r.payload?.tags || [],
|
|
327
|
+
createdAt: new Date(String(r.payload?.createdAt || new Date().toISOString())),
|
|
328
|
+
updatedAt: new Date(String(r.payload?.updatedAt || new Date().toISOString())),
|
|
329
|
+
metadata: r.payload?.metadata,
|
|
330
|
+
visibility: r.payload?.visibility || 'private',
|
|
331
|
+
endorsements: Number(r.payload?.endorsements || 0),
|
|
332
|
+
}))
|
|
333
|
+
.sort((a, b) => (b.endorsements || 0) - (a.endorsements || 0));
|
|
334
|
+
}
|
|
335
|
+
// =========================================================================
|
|
336
|
+
// Phase 3: Quality & Decay
|
|
337
|
+
// =========================================================================
|
|
338
|
+
/**
|
|
339
|
+
* Boost a memory (positive feedback)
|
|
340
|
+
*/
|
|
341
|
+
async boost(memoryId) {
|
|
342
|
+
await this.ensureCollection();
|
|
343
|
+
const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
|
|
344
|
+
if (!point)
|
|
345
|
+
throw new Error('Memory not found');
|
|
346
|
+
const boostCount = Number(point.payload?.boostCount || 0) + 1;
|
|
347
|
+
const downvoteCount = Number(point.payload?.downvoteCount || 0);
|
|
348
|
+
const qualityScore = this.calculateQualityScore(boostCount, downvoteCount);
|
|
349
|
+
await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
|
|
350
|
+
boostCount,
|
|
351
|
+
qualityScore,
|
|
352
|
+
updatedAt: new Date().toISOString(),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Downvote a memory (negative feedback)
|
|
357
|
+
*/
|
|
358
|
+
async downvote(memoryId) {
|
|
359
|
+
await this.ensureCollection();
|
|
360
|
+
const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
|
|
361
|
+
if (!point)
|
|
362
|
+
throw new Error('Memory not found');
|
|
363
|
+
const boostCount = Number(point.payload?.boostCount || 0);
|
|
364
|
+
const downvoteCount = Number(point.payload?.downvoteCount || 0) + 1;
|
|
365
|
+
const qualityScore = this.calculateQualityScore(boostCount, downvoteCount);
|
|
366
|
+
await this.qdrant.setPayload(MEMORIES_COLLECTION, memoryId, {
|
|
367
|
+
downvoteCount,
|
|
368
|
+
qualityScore,
|
|
369
|
+
updatedAt: new Date().toISOString(),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get quality metrics for a memory
|
|
374
|
+
*/
|
|
375
|
+
async getQuality(memoryId) {
|
|
376
|
+
await this.ensureCollection();
|
|
377
|
+
const point = await this.qdrant.getPoint(MEMORIES_COLLECTION, memoryId);
|
|
378
|
+
if (!point)
|
|
379
|
+
throw new Error('Memory not found');
|
|
380
|
+
return {
|
|
381
|
+
recallCount: Number(point.payload?.recallCount || 0),
|
|
382
|
+
boostCount: Number(point.payload?.boostCount || 0),
|
|
383
|
+
downvoteCount: Number(point.payload?.downvoteCount || 0),
|
|
384
|
+
qualityScore: Number(point.payload?.qualityScore || 1.0),
|
|
385
|
+
status: String(point.payload?.status || 'active'),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
/**
|
|
389
|
+
* Calculate quality score based on feedback
|
|
390
|
+
*/
|
|
391
|
+
calculateQualityScore(boostCount, downvoteCount) {
|
|
392
|
+
// Feedback adjustment: -0.2 to +0.2
|
|
393
|
+
const feedbackBoost = Math.tanh((boostCount - downvoteCount * 2) * 0.1) * 0.2;
|
|
394
|
+
return Math.max(0.1, Math.min(1.5, 1.0 + feedbackBoost));
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Calculate effective score with decay
|
|
398
|
+
*/
|
|
399
|
+
calculateEffectiveScore(semanticScore, createdAt, recallCount, boostCount, downvoteCount) {
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
const ageDays = (now - createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
402
|
+
// Exponential decay with agent-type-specific half-life
|
|
403
|
+
// Support agents: 14-day half-life (faster decay for time-sensitive support knowledge)
|
|
404
|
+
// Other agents: 30-day half-life (standard decay)
|
|
405
|
+
const halfLifeDays = this.agentType === 'support' ? 14 : 30;
|
|
406
|
+
const decayFactor = Math.pow(0.5, ageDays / halfLifeDays);
|
|
407
|
+
// Recall boost (logarithmic)
|
|
408
|
+
const recallBoost = Math.log2(recallCount + 1) * 0.1;
|
|
409
|
+
// Feedback adjustment
|
|
410
|
+
const feedbackBoost = Math.tanh((boostCount - downvoteCount * 2) * 0.1) * 0.2;
|
|
411
|
+
// Effective score
|
|
412
|
+
return semanticScore * Math.max(0.1, Math.min(1.5, decayFactor + recallBoost + feedbackBoost));
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Archive low-quality memories
|
|
416
|
+
*/
|
|
417
|
+
async cleanup(dryRun = true, threshold = 0.1, minAgeDays = 90, tags) {
|
|
418
|
+
await this.ensureCollection();
|
|
419
|
+
const filter = {
|
|
420
|
+
must: [
|
|
421
|
+
{ key: 'agent', match: { value: this.agentId } },
|
|
422
|
+
{ key: 'status', match: { value: 'active' } },
|
|
423
|
+
],
|
|
424
|
+
};
|
|
425
|
+
if (this.agentType) {
|
|
426
|
+
filter.must.push({ key: 'agentType', match: { value: this.agentType } });
|
|
427
|
+
}
|
|
428
|
+
// Add tag filter if specified
|
|
429
|
+
if (tags && tags.length > 0) {
|
|
430
|
+
filter.must.push({
|
|
431
|
+
should: tags.map(tag => ({
|
|
432
|
+
key: 'tags',
|
|
433
|
+
match: { any: [tag] }
|
|
434
|
+
}))
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
|
|
438
|
+
filter,
|
|
439
|
+
limit: 1000,
|
|
440
|
+
with_payload: true,
|
|
441
|
+
});
|
|
442
|
+
const toArchive = [];
|
|
443
|
+
const now = Date.now();
|
|
444
|
+
for (const r of results) {
|
|
445
|
+
const createdAt = new Date(String(r.payload?.createdAt || new Date().toISOString()));
|
|
446
|
+
const ageDays = (now - createdAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
447
|
+
const qualityScore = Number(r.payload?.qualityScore || 1.0);
|
|
448
|
+
const recallCount = Number(r.payload?.recallCount || 0);
|
|
449
|
+
const downvoteCount = Number(r.payload?.downvoteCount || 0);
|
|
450
|
+
const boostCount = Number(r.payload?.boostCount || 0);
|
|
451
|
+
// Cleanup criteria
|
|
452
|
+
const shouldArchive = (qualityScore < threshold && ageDays > minAgeDays) ||
|
|
453
|
+
(downvoteCount > 3 && boostCount === 0) ||
|
|
454
|
+
(recallCount === 0 && ageDays > 180);
|
|
455
|
+
if (shouldArchive) {
|
|
456
|
+
toArchive.push({
|
|
457
|
+
id: String(r.id),
|
|
458
|
+
agent: String(r.payload?.agent || ''),
|
|
459
|
+
agentType: String(r.payload?.agentType || ''),
|
|
460
|
+
content: String(r.payload?.content || ''),
|
|
461
|
+
tags: r.payload?.tags || [],
|
|
462
|
+
createdAt,
|
|
463
|
+
updatedAt: new Date(String(r.payload?.updatedAt || new Date().toISOString())),
|
|
464
|
+
metadata: r.payload?.metadata,
|
|
465
|
+
qualityScore,
|
|
466
|
+
recallCount,
|
|
467
|
+
downvoteCount,
|
|
468
|
+
boostCount,
|
|
469
|
+
});
|
|
470
|
+
if (!dryRun) {
|
|
471
|
+
await this.qdrant.setPayload(MEMORIES_COLLECTION, String(r.id), {
|
|
472
|
+
status: 'archived',
|
|
473
|
+
updatedAt: new Date().toISOString(),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return toArchive;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* Permanently delete archived memories
|
|
482
|
+
*/
|
|
483
|
+
async purge(retentionDays = 365) {
|
|
484
|
+
await this.ensureCollection();
|
|
485
|
+
const filter = {
|
|
486
|
+
must: [
|
|
487
|
+
{ key: 'agent', match: { value: this.agentId } },
|
|
488
|
+
{ key: 'status', match: { value: 'archived' } },
|
|
489
|
+
],
|
|
490
|
+
};
|
|
491
|
+
const results = await this.qdrant.scroll(MEMORIES_COLLECTION, {
|
|
492
|
+
filter,
|
|
493
|
+
limit: 1000,
|
|
494
|
+
with_payload: true,
|
|
495
|
+
});
|
|
496
|
+
const now = Date.now();
|
|
497
|
+
const toPurge = [];
|
|
498
|
+
for (const r of results) {
|
|
499
|
+
const updatedAt = new Date(String(r.payload?.updatedAt || new Date().toISOString()));
|
|
500
|
+
const daysSinceArchived = (now - updatedAt.getTime()) / (1000 * 60 * 60 * 24);
|
|
501
|
+
if (daysSinceArchived > retentionDays) {
|
|
502
|
+
toPurge.push(r.id);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (toPurge.length > 0) {
|
|
506
|
+
await this.qdrant.deletePoints(MEMORIES_COLLECTION, toPurge);
|
|
507
|
+
}
|
|
508
|
+
return toPurge.length;
|
|
509
|
+
}
|
|
198
510
|
}
|
|
199
511
|
export default AgentBrain;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning Capture Service
|
|
3
|
+
*
|
|
4
|
+
* Automatically captures learnings when a task is completed.
|
|
5
|
+
* Uses LLM (Vertex AI) to extract meaningful learnings from task context.
|
|
6
|
+
*/
|
|
7
|
+
import { AgentType } from './agent-brain.js';
|
|
8
|
+
export interface Task {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
status: string;
|
|
13
|
+
priority?: string;
|
|
14
|
+
projectId?: string;
|
|
15
|
+
metadata?: Record<string, unknown>;
|
|
16
|
+
}
|
|
17
|
+
export interface LearningCaptureInput {
|
|
18
|
+
taskId: string;
|
|
19
|
+
task: Task;
|
|
20
|
+
prUrl?: string;
|
|
21
|
+
explicitLearnings?: string;
|
|
22
|
+
agentId: string;
|
|
23
|
+
agentType?: AgentType;
|
|
24
|
+
}
|
|
25
|
+
export interface LearningResult {
|
|
26
|
+
id: string;
|
|
27
|
+
content: string;
|
|
28
|
+
tags: string[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Capture learnings from a completed task
|
|
32
|
+
*
|
|
33
|
+
* This function:
|
|
34
|
+
* 1. Generates learnings from task context (or uses explicit learnings)
|
|
35
|
+
* 2. Stores them in the agent's brain
|
|
36
|
+
* 3. Returns the learning IDs
|
|
37
|
+
*/
|
|
38
|
+
export declare function captureLearnings(input: LearningCaptureInput): Promise<LearningResult[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch task details from API
|
|
41
|
+
*/
|
|
42
|
+
export declare function fetchTask(taskId: string, apiUrl: string, apiKey?: string): Promise<Task | null>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Learning Capture Service
|
|
3
|
+
*
|
|
4
|
+
* Automatically captures learnings when a task is completed.
|
|
5
|
+
* Uses LLM (Vertex AI) to extract meaningful learnings from task context.
|
|
6
|
+
*/
|
|
7
|
+
import { AgentBrain } from './agent-brain.js';
|
|
8
|
+
/**
|
|
9
|
+
* Generate learnings from task context using LLM
|
|
10
|
+
*/
|
|
11
|
+
async function generateLearningsFromTask(task, prUrl) {
|
|
12
|
+
// For now, return basic task-based learnings
|
|
13
|
+
// TODO: In future, use Vertex AI to extract deeper insights from:
|
|
14
|
+
// - Task description
|
|
15
|
+
// - PR diff/content
|
|
16
|
+
// - Related comments/discussions
|
|
17
|
+
// - Error patterns encountered
|
|
18
|
+
const learnings = [];
|
|
19
|
+
// Basic learning from task completion
|
|
20
|
+
const basicTags = [
|
|
21
|
+
`task:${task.id}`,
|
|
22
|
+
...(task.projectId ? [`project:${task.projectId}`] : []),
|
|
23
|
+
'type:completion',
|
|
24
|
+
];
|
|
25
|
+
// Create a learning from the task
|
|
26
|
+
const taskLearning = {
|
|
27
|
+
content: `Completed task: ${task.title}${task.description ? ` - ${task.description}` : ''}${prUrl ? ` (PR: ${prUrl})` : ''}`,
|
|
28
|
+
tags: basicTags,
|
|
29
|
+
};
|
|
30
|
+
learnings.push(taskLearning);
|
|
31
|
+
return learnings;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Capture learnings from a completed task
|
|
35
|
+
*
|
|
36
|
+
* This function:
|
|
37
|
+
* 1. Generates learnings from task context (or uses explicit learnings)
|
|
38
|
+
* 2. Stores them in the agent's brain
|
|
39
|
+
* 3. Returns the learning IDs
|
|
40
|
+
*/
|
|
41
|
+
export async function captureLearnings(input) {
|
|
42
|
+
const { taskId, task, prUrl, explicitLearnings, agentId, agentType } = input;
|
|
43
|
+
// Initialize agent brain
|
|
44
|
+
const brain = new AgentBrain({ agentId, agentType });
|
|
45
|
+
const results = [];
|
|
46
|
+
// If explicit learnings provided, store them directly
|
|
47
|
+
if (explicitLearnings) {
|
|
48
|
+
const tags = [
|
|
49
|
+
`task:${taskId}`,
|
|
50
|
+
...(task.projectId ? [`project:${task.projectId}`] : []),
|
|
51
|
+
'type:explicit',
|
|
52
|
+
'source:manual',
|
|
53
|
+
];
|
|
54
|
+
const id = await brain.remember(explicitLearnings, tags, {
|
|
55
|
+
taskId,
|
|
56
|
+
taskTitle: task.title,
|
|
57
|
+
prUrl,
|
|
58
|
+
capturedAt: new Date().toISOString(),
|
|
59
|
+
});
|
|
60
|
+
results.push({
|
|
61
|
+
id,
|
|
62
|
+
content: explicitLearnings,
|
|
63
|
+
tags,
|
|
64
|
+
});
|
|
65
|
+
return results;
|
|
66
|
+
}
|
|
67
|
+
// Generate learnings from task context
|
|
68
|
+
try {
|
|
69
|
+
const generatedLearnings = await generateLearningsFromTask(task, prUrl);
|
|
70
|
+
for (const learning of generatedLearnings) {
|
|
71
|
+
const id = await brain.remember(learning.content, learning.tags, {
|
|
72
|
+
taskId,
|
|
73
|
+
taskTitle: task.title,
|
|
74
|
+
prUrl,
|
|
75
|
+
capturedAt: new Date().toISOString(),
|
|
76
|
+
source: 'auto-generated',
|
|
77
|
+
});
|
|
78
|
+
results.push({
|
|
79
|
+
id,
|
|
80
|
+
content: learning.content,
|
|
81
|
+
tags: learning.tags,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.warn(' ⚠️ Failed to generate learnings:', error instanceof Error ? error.message : error);
|
|
87
|
+
// Don't fail task completion if learning capture fails
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Fetch task details from API
|
|
93
|
+
*/
|
|
94
|
+
export async function fetchTask(taskId, apiUrl, apiKey) {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(`${apiUrl}/api/tasks/${taskId}`, {
|
|
97
|
+
headers: apiKey ? { 'x-api-key': apiKey } : {},
|
|
98
|
+
});
|
|
99
|
+
if (!res.ok) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
return await res.json();
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII Filter for GDPR-Compliant Embeddings
|
|
3
|
+
*
|
|
4
|
+
* Sanitizes content before embedding to remove personally identifiable information (PII).
|
|
5
|
+
* This enables:
|
|
6
|
+
* - DSGVO/GDPR compliance
|
|
7
|
+
* - Use of US-hosted models (OpenAI, Anthropic US)
|
|
8
|
+
* - Automatic SOP generation from learnings
|
|
9
|
+
* - Unlimited retention (no Löschrecht)
|
|
10
|
+
*/
|
|
11
|
+
export interface SanitizeResult {
|
|
12
|
+
sanitized: string;
|
|
13
|
+
redactedTypes: string[];
|
|
14
|
+
redactionCount: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Sanitize content by removing PII
|
|
18
|
+
*/
|
|
19
|
+
export declare function sanitizeForEmbedding(content: string, allowPii?: boolean): SanitizeResult;
|
|
20
|
+
/**
|
|
21
|
+
* Check if content contains PII
|
|
22
|
+
*/
|
|
23
|
+
export declare function containsPII(content: string): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Preview what would be redacted (for debugging)
|
|
26
|
+
*/
|
|
27
|
+
export declare function previewRedaction(content: string): {
|
|
28
|
+
original: string;
|
|
29
|
+
sanitized: string;
|
|
30
|
+
changes: Array<{
|
|
31
|
+
type: string;
|
|
32
|
+
count: number;
|
|
33
|
+
}>;
|
|
34
|
+
};
|