@blockrun/franklin 3.3.2 → 3.5.0
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 +58 -7
- package/dist/agent/commands.d.ts +1 -1
- package/dist/agent/commands.js +128 -17
- package/dist/agent/compact.d.ts +2 -2
- package/dist/agent/compact.js +148 -22
- package/dist/agent/context.d.ts +8 -3
- package/dist/agent/context.js +301 -108
- package/dist/agent/error-classifier.d.ts +11 -2
- package/dist/agent/error-classifier.js +64 -10
- package/dist/agent/llm.d.ts +8 -1
- package/dist/agent/llm.js +114 -19
- package/dist/agent/loop.d.ts +1 -2
- package/dist/agent/loop.js +509 -61
- package/dist/agent/optimize.d.ts +2 -2
- package/dist/agent/optimize.js +9 -7
- package/dist/agent/permissions.d.ts +1 -1
- package/dist/agent/permissions.js +1 -1
- package/dist/agent/planner.d.ts +42 -0
- package/dist/agent/planner.js +110 -0
- package/dist/agent/reduce.d.ts +7 -1
- package/dist/agent/reduce.js +85 -3
- package/dist/agent/streaming-executor.d.ts +6 -1
- package/dist/agent/streaming-executor.js +83 -5
- package/dist/agent/tokens.d.ts +11 -2
- package/dist/agent/tokens.js +38 -5
- package/dist/agent/tool-guard.d.ts +27 -0
- package/dist/agent/tool-guard.js +324 -0
- package/dist/agent/types.d.ts +7 -1
- package/dist/agent/types.js +1 -1
- package/dist/banner.js +27 -40
- package/dist/brain/extract.d.ts +11 -0
- package/dist/brain/extract.js +154 -0
- package/dist/brain/index.d.ts +3 -0
- package/dist/brain/index.js +2 -0
- package/dist/brain/store.d.ts +42 -0
- package/dist/brain/store.js +225 -0
- package/dist/brain/types.d.ts +45 -0
- package/dist/brain/types.js +5 -0
- package/dist/commands/daemon.js +2 -1
- package/dist/commands/start.js +16 -3
- package/dist/config.js +1 -1
- package/dist/index.js +27 -2
- package/dist/learnings/extractor.d.ts +13 -0
- package/dist/learnings/extractor.js +69 -8
- package/dist/learnings/index.d.ts +1 -1
- package/dist/learnings/index.js +1 -1
- package/dist/learnings/store.js +42 -13
- package/dist/learnings/types.d.ts +1 -1
- package/dist/mcp/client.d.ts +1 -1
- package/dist/mcp/client.js +5 -5
- package/dist/mcp/config.d.ts +1 -1
- package/dist/mcp/config.js +1 -1
- package/dist/panel/html.d.ts +2 -0
- package/dist/panel/html.js +409 -146
- package/dist/panel/server.js +19 -0
- package/dist/pricing.js +3 -2
- package/dist/proxy/fallback.d.ts +3 -1
- package/dist/proxy/fallback.js +4 -4
- package/dist/proxy/server.js +29 -11
- package/dist/proxy/sse-translator.js +1 -1
- package/dist/router/categories.d.ts +21 -0
- package/dist/router/categories.js +96 -0
- package/dist/router/index.d.ts +9 -2
- package/dist/router/index.js +106 -27
- package/dist/router/local-elo.d.ts +32 -0
- package/dist/router/local-elo.js +107 -0
- package/dist/router/selector.d.ts +46 -0
- package/dist/router/selector.js +106 -0
- package/dist/session/storage.d.ts +5 -1
- package/dist/session/storage.js +24 -2
- package/dist/social/a11y.d.ts +1 -1
- package/dist/social/a11y.js +5 -1
- package/dist/social/browser.d.ts +5 -0
- package/dist/social/browser.js +22 -0
- package/dist/social/preflight.d.ts +4 -0
- package/dist/social/preflight.js +42 -3
- package/dist/stats/failures.d.ts +20 -0
- package/dist/stats/failures.js +63 -0
- package/dist/stats/format.d.ts +6 -0
- package/dist/stats/format.js +23 -0
- package/dist/stats/insights.js +1 -21
- package/dist/stats/session-tracker.d.ts +21 -0
- package/dist/stats/session-tracker.js +28 -0
- package/dist/stats/tracker.d.ts +1 -1
- package/dist/stats/tracker.js +1 -1
- package/dist/tools/bash.d.ts +14 -1
- package/dist/tools/bash.js +132 -7
- package/dist/tools/edit.js +77 -14
- package/dist/tools/glob.js +13 -3
- package/dist/tools/grep.js +30 -12
- package/dist/tools/imagegen.js +3 -3
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +5 -1
- package/dist/tools/read.d.ts +16 -2
- package/dist/tools/read.js +36 -8
- package/dist/tools/searchx.d.ts +6 -2
- package/dist/tools/searchx.js +221 -44
- package/dist/tools/subagent.js +37 -3
- package/dist/tools/task.js +43 -7
- package/dist/tools/validate.d.ts +11 -0
- package/dist/tools/validate.js +42 -0
- package/dist/tools/webfetch.js +18 -7
- package/dist/tools/websearch.js +41 -7
- package/dist/tools/write.js +26 -6
- package/dist/ui/app.js +31 -6
- package/dist/ui/model-picker.d.ts +1 -1
- package/dist/ui/model-picker.js +1 -1
- package/dist/ui/terminal.d.ts +1 -1
- package/dist/ui/terminal.js +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { Entity, EntityType, Observation, Relation, BrainExtraction } from './types.js';
|
|
2
|
+
export { loadEntities, saveEntities, findEntity, upsertEntity, loadObservations, getEntityObservations, addObservation, loadRelations, getEntityRelations, upsertRelation, searchEntities, buildEntityContext, getBrainStats, } from './store.js';
|
|
3
|
+
export { extractBrainEntities } from './extract.js';
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { loadEntities, saveEntities, findEntity, upsertEntity, loadObservations, getEntityObservations, addObservation, loadRelations, getEntityRelations, upsertRelation, searchEntities, buildEntityContext, getBrainStats, } from './store.js';
|
|
2
|
+
export { extractBrainEntities } from './extract.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Franklin Brain — JSONL storage for entities, observations, relations.
|
|
3
|
+
* All in-memory with JSONL persistence. No database.
|
|
4
|
+
*/
|
|
5
|
+
import type { Entity, EntityType, Observation, Relation } from './types.js';
|
|
6
|
+
export declare function loadEntities(): Entity[];
|
|
7
|
+
export declare function saveEntities(entities: Entity[]): void;
|
|
8
|
+
/**
|
|
9
|
+
* Find entity by name or alias (case-insensitive).
|
|
10
|
+
*/
|
|
11
|
+
export declare function findEntity(entities: Entity[], nameOrAlias: string): Entity | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Create or update an entity. Returns the entity ID.
|
|
14
|
+
* If an entity with a matching name/alias exists, merges aliases and bumps reference_count.
|
|
15
|
+
*/
|
|
16
|
+
export declare function upsertEntity(entities: Entity[], name: string, type: EntityType, aliases?: string[]): string;
|
|
17
|
+
export declare function loadObservations(): Observation[];
|
|
18
|
+
export declare function getEntityObservations(entityId: string): Observation[];
|
|
19
|
+
/**
|
|
20
|
+
* Add an observation. Deduplicates by content similarity (exact match).
|
|
21
|
+
*/
|
|
22
|
+
export declare function addObservation(entityId: string, content: string, source: string, confidence?: number, tags?: string[]): void;
|
|
23
|
+
export declare function loadRelations(): Relation[];
|
|
24
|
+
export declare function getEntityRelations(entityId: string): Relation[];
|
|
25
|
+
/**
|
|
26
|
+
* Add or update a relation. If same from+to+type exists, bumps count.
|
|
27
|
+
*/
|
|
28
|
+
export declare function upsertRelation(fromId: string, toId: string, type: string, confidence?: number): void;
|
|
29
|
+
/**
|
|
30
|
+
* Search entities by name/alias substring match.
|
|
31
|
+
*/
|
|
32
|
+
export declare function searchEntities(query: string, limit?: number): Entity[];
|
|
33
|
+
/**
|
|
34
|
+
* Build context string for entities mentioned in the conversation.
|
|
35
|
+
* Returns empty string if no relevant entities found.
|
|
36
|
+
*/
|
|
37
|
+
export declare function buildEntityContext(mentionedNames: string[]): string;
|
|
38
|
+
export declare function getBrainStats(): {
|
|
39
|
+
entities: number;
|
|
40
|
+
observations: number;
|
|
41
|
+
relations: number;
|
|
42
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Franklin Brain — JSONL storage for entities, observations, relations.
|
|
3
|
+
* All in-memory with JSONL persistence. No database.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { BLOCKRUN_DIR } from '../config.js';
|
|
9
|
+
const BRAIN_DIR = path.join(BLOCKRUN_DIR, 'brain');
|
|
10
|
+
const ENTITIES_FILE = path.join(BRAIN_DIR, 'entities.jsonl');
|
|
11
|
+
const OBSERVATIONS_FILE = path.join(BRAIN_DIR, 'observations.jsonl');
|
|
12
|
+
const RELATIONS_FILE = path.join(BRAIN_DIR, 'relations.jsonl');
|
|
13
|
+
const MAX_ENTITIES = 200;
|
|
14
|
+
function uid() { return crypto.randomBytes(8).toString('hex'); }
|
|
15
|
+
function ensureDir() {
|
|
16
|
+
fs.mkdirSync(BRAIN_DIR, { recursive: true });
|
|
17
|
+
}
|
|
18
|
+
// ─── Generic JSONL helpers ────────────────────────────────────────────────
|
|
19
|
+
function loadJsonl(file) {
|
|
20
|
+
try {
|
|
21
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
22
|
+
const results = [];
|
|
23
|
+
for (const line of raw.split('\n')) {
|
|
24
|
+
if (!line.trim())
|
|
25
|
+
continue;
|
|
26
|
+
try {
|
|
27
|
+
results.push(JSON.parse(line));
|
|
28
|
+
}
|
|
29
|
+
catch { /* skip corrupt */ }
|
|
30
|
+
}
|
|
31
|
+
return results;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function saveJsonl(file, items) {
|
|
38
|
+
ensureDir();
|
|
39
|
+
const tmp = file + '.tmp';
|
|
40
|
+
fs.writeFileSync(tmp, items.map(i => JSON.stringify(i)).join('\n') + '\n');
|
|
41
|
+
fs.renameSync(tmp, file);
|
|
42
|
+
}
|
|
43
|
+
function appendJsonl(file, item) {
|
|
44
|
+
ensureDir();
|
|
45
|
+
fs.appendFileSync(file, JSON.stringify(item) + '\n');
|
|
46
|
+
}
|
|
47
|
+
// ─── Entities ─────────────────────────────────────────────────────────────
|
|
48
|
+
export function loadEntities() {
|
|
49
|
+
return loadJsonl(ENTITIES_FILE);
|
|
50
|
+
}
|
|
51
|
+
export function saveEntities(entities) {
|
|
52
|
+
saveJsonl(ENTITIES_FILE, entities);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Find entity by name or alias (case-insensitive).
|
|
56
|
+
*/
|
|
57
|
+
export function findEntity(entities, nameOrAlias) {
|
|
58
|
+
const lower = nameOrAlias.toLowerCase().trim();
|
|
59
|
+
return entities.find(e => e.name.toLowerCase() === lower ||
|
|
60
|
+
e.aliases.some(a => a.toLowerCase() === lower));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Create or update an entity. Returns the entity ID.
|
|
64
|
+
* If an entity with a matching name/alias exists, merges aliases and bumps reference_count.
|
|
65
|
+
*/
|
|
66
|
+
export function upsertEntity(entities, name, type, aliases = []) {
|
|
67
|
+
const existing = findEntity(entities, name) ||
|
|
68
|
+
aliases.map(a => findEntity(entities, a)).find(Boolean);
|
|
69
|
+
if (existing) {
|
|
70
|
+
// Merge aliases
|
|
71
|
+
const allAliases = new Set([...existing.aliases, ...aliases, name]);
|
|
72
|
+
allAliases.delete(existing.name); // Don't alias canonical name
|
|
73
|
+
existing.aliases = [...allAliases];
|
|
74
|
+
existing.reference_count++;
|
|
75
|
+
existing.updated_at = Date.now();
|
|
76
|
+
return existing.id;
|
|
77
|
+
}
|
|
78
|
+
// New entity
|
|
79
|
+
const entity = {
|
|
80
|
+
id: uid(),
|
|
81
|
+
type,
|
|
82
|
+
name,
|
|
83
|
+
aliases: aliases.filter(a => a.toLowerCase() !== name.toLowerCase()),
|
|
84
|
+
created_at: Date.now(),
|
|
85
|
+
updated_at: Date.now(),
|
|
86
|
+
reference_count: 1,
|
|
87
|
+
};
|
|
88
|
+
entities.push(entity);
|
|
89
|
+
// Cap at MAX_ENTITIES — prune least-referenced
|
|
90
|
+
if (entities.length > MAX_ENTITIES) {
|
|
91
|
+
entities.sort((a, b) => b.reference_count - a.reference_count);
|
|
92
|
+
entities.length = MAX_ENTITIES;
|
|
93
|
+
}
|
|
94
|
+
return entity.id;
|
|
95
|
+
}
|
|
96
|
+
// ─── Observations ─────────────────────────────────────────────────────────
|
|
97
|
+
export function loadObservations() {
|
|
98
|
+
return loadJsonl(OBSERVATIONS_FILE);
|
|
99
|
+
}
|
|
100
|
+
export function getEntityObservations(entityId) {
|
|
101
|
+
return loadObservations().filter(o => o.entity_id === entityId);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Add an observation. Deduplicates by content similarity (exact match).
|
|
105
|
+
*/
|
|
106
|
+
export function addObservation(entityId, content, source, confidence = 0.8, tags = ['fact']) {
|
|
107
|
+
const existing = loadObservations();
|
|
108
|
+
const contentLower = content.toLowerCase().trim();
|
|
109
|
+
// Skip exact duplicates for this entity
|
|
110
|
+
if (existing.some(o => o.entity_id === entityId && o.content.toLowerCase().trim() === contentLower)) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
appendJsonl(OBSERVATIONS_FILE, {
|
|
114
|
+
id: uid(),
|
|
115
|
+
entity_id: entityId,
|
|
116
|
+
content,
|
|
117
|
+
source,
|
|
118
|
+
confidence,
|
|
119
|
+
tags,
|
|
120
|
+
created_at: Date.now(),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
// ─── Relations ────────────────────────────────────────────────────────────
|
|
124
|
+
export function loadRelations() {
|
|
125
|
+
return loadJsonl(RELATIONS_FILE);
|
|
126
|
+
}
|
|
127
|
+
export function getEntityRelations(entityId) {
|
|
128
|
+
return loadRelations().filter(r => r.from_id === entityId || r.to_id === entityId);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Add or update a relation. If same from+to+type exists, bumps count.
|
|
132
|
+
*/
|
|
133
|
+
export function upsertRelation(fromId, toId, type, confidence = 0.8) {
|
|
134
|
+
const relations = loadRelations();
|
|
135
|
+
const existing = relations.find(r => r.from_id === fromId && r.to_id === toId && r.type === type);
|
|
136
|
+
if (existing) {
|
|
137
|
+
existing.count++;
|
|
138
|
+
existing.last_seen = Date.now();
|
|
139
|
+
existing.confidence = Math.min(existing.confidence + 0.05, 1.0);
|
|
140
|
+
saveJsonl(RELATIONS_FILE, relations);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
appendJsonl(RELATIONS_FILE, {
|
|
144
|
+
id: uid(),
|
|
145
|
+
from_id: fromId,
|
|
146
|
+
to_id: toId,
|
|
147
|
+
type,
|
|
148
|
+
confidence,
|
|
149
|
+
count: 1,
|
|
150
|
+
last_seen: Date.now(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ─── Search ───────────────────────────────────────────────────────────────
|
|
155
|
+
/**
|
|
156
|
+
* Search entities by name/alias substring match.
|
|
157
|
+
*/
|
|
158
|
+
export function searchEntities(query, limit = 10) {
|
|
159
|
+
const lower = query.toLowerCase().trim();
|
|
160
|
+
if (!lower)
|
|
161
|
+
return [];
|
|
162
|
+
return loadEntities()
|
|
163
|
+
.filter(e => e.name.toLowerCase().includes(lower) ||
|
|
164
|
+
e.aliases.some(a => a.toLowerCase().includes(lower)))
|
|
165
|
+
.sort((a, b) => b.reference_count - a.reference_count)
|
|
166
|
+
.slice(0, limit);
|
|
167
|
+
}
|
|
168
|
+
// ─── Context building (for system prompt injection) ───────────────────────
|
|
169
|
+
const MAX_BRAIN_CHARS = 1500;
|
|
170
|
+
/**
|
|
171
|
+
* Build context string for entities mentioned in the conversation.
|
|
172
|
+
* Returns empty string if no relevant entities found.
|
|
173
|
+
*/
|
|
174
|
+
export function buildEntityContext(mentionedNames) {
|
|
175
|
+
if (mentionedNames.length === 0)
|
|
176
|
+
return '';
|
|
177
|
+
const entities = loadEntities();
|
|
178
|
+
const matched = [];
|
|
179
|
+
for (const name of mentionedNames) {
|
|
180
|
+
const entity = findEntity(entities, name);
|
|
181
|
+
if (entity)
|
|
182
|
+
matched.push(entity);
|
|
183
|
+
}
|
|
184
|
+
if (matched.length === 0)
|
|
185
|
+
return '';
|
|
186
|
+
const lines = ['# Known Entities'];
|
|
187
|
+
let chars = lines[0].length;
|
|
188
|
+
for (const entity of matched) {
|
|
189
|
+
const observations = getEntityObservations(entity.id)
|
|
190
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
191
|
+
.slice(0, 5);
|
|
192
|
+
const relations = getEntityRelations(entity.id);
|
|
193
|
+
const header = `\n## ${entity.name} (${entity.type})`;
|
|
194
|
+
if (chars + header.length > MAX_BRAIN_CHARS)
|
|
195
|
+
break;
|
|
196
|
+
lines.push(header);
|
|
197
|
+
chars += header.length;
|
|
198
|
+
for (const obs of observations) {
|
|
199
|
+
const line = `- ${obs.content}`;
|
|
200
|
+
if (chars + line.length + 1 > MAX_BRAIN_CHARS)
|
|
201
|
+
break;
|
|
202
|
+
lines.push(line);
|
|
203
|
+
chars += line.length + 1;
|
|
204
|
+
}
|
|
205
|
+
for (const rel of relations.slice(0, 3)) {
|
|
206
|
+
const otherEntity = entities.find(e => e.id === (rel.from_id === entity.id ? rel.to_id : rel.from_id));
|
|
207
|
+
if (!otherEntity)
|
|
208
|
+
continue;
|
|
209
|
+
const line = `- ${rel.type} → ${otherEntity.name}`;
|
|
210
|
+
if (chars + line.length + 1 > MAX_BRAIN_CHARS)
|
|
211
|
+
break;
|
|
212
|
+
lines.push(line);
|
|
213
|
+
chars += line.length + 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return lines.length > 1 ? lines.join('\n') : '';
|
|
217
|
+
}
|
|
218
|
+
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
219
|
+
export function getBrainStats() {
|
|
220
|
+
return {
|
|
221
|
+
entities: loadEntities().length,
|
|
222
|
+
observations: loadObservations().length,
|
|
223
|
+
relations: loadRelations().length,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Franklin Brain — entity-based knowledge graph.
|
|
3
|
+
* Inspired by GBrain (Garry Tan). Lightweight JSONL, no database.
|
|
4
|
+
*/
|
|
5
|
+
export type EntityType = 'person' | 'project' | 'company' | 'product' | 'concept';
|
|
6
|
+
export interface Entity {
|
|
7
|
+
id: string;
|
|
8
|
+
type: EntityType;
|
|
9
|
+
name: string;
|
|
10
|
+
aliases: string[];
|
|
11
|
+
created_at: number;
|
|
12
|
+
updated_at: number;
|
|
13
|
+
reference_count: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Observation {
|
|
16
|
+
id: string;
|
|
17
|
+
entity_id: string;
|
|
18
|
+
content: string;
|
|
19
|
+
source: string;
|
|
20
|
+
confidence: number;
|
|
21
|
+
tags: string[];
|
|
22
|
+
created_at: number;
|
|
23
|
+
}
|
|
24
|
+
export interface Relation {
|
|
25
|
+
id: string;
|
|
26
|
+
from_id: string;
|
|
27
|
+
to_id: string;
|
|
28
|
+
type: string;
|
|
29
|
+
confidence: number;
|
|
30
|
+
count: number;
|
|
31
|
+
last_seen: number;
|
|
32
|
+
}
|
|
33
|
+
export interface BrainExtraction {
|
|
34
|
+
entities: Array<{
|
|
35
|
+
name: string;
|
|
36
|
+
type: EntityType;
|
|
37
|
+
aliases?: string[];
|
|
38
|
+
observations: string[];
|
|
39
|
+
}>;
|
|
40
|
+
relations: Array<{
|
|
41
|
+
from: string;
|
|
42
|
+
to: string;
|
|
43
|
+
type: string;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
package/dist/commands/daemon.js
CHANGED
|
@@ -50,7 +50,8 @@ export async function daemonCommand(action, options) {
|
|
|
50
50
|
fs.mkdirSync(BLOCKRUN_DIR, { recursive: true });
|
|
51
51
|
const child = spawn(runcodeBin, ['proxy', '--port', String(port)], {
|
|
52
52
|
detached: true,
|
|
53
|
-
|
|
53
|
+
// stdout → /dev/null (banner + startup messages), stderr → log file (debug/errors only)
|
|
54
|
+
stdio: ['ignore', 'ignore', fs.openSync(LOG_FILE, 'a')],
|
|
54
55
|
});
|
|
55
56
|
child.unref();
|
|
56
57
|
fs.writeFileSync(PID_FILE, String(child.pid));
|
package/dist/commands/start.js
CHANGED
|
@@ -7,6 +7,7 @@ import { printBanner } from '../banner.js';
|
|
|
7
7
|
import { assembleInstructions } from '../agent/context.js';
|
|
8
8
|
import { interactiveSession } from '../agent/loop.js';
|
|
9
9
|
import { allCapabilities, createSubAgentCapability } from '../tools/index.js';
|
|
10
|
+
import { validateToolDescriptions } from '../tools/validate.js';
|
|
10
11
|
import { launchInkUI } from '../ui/app.js';
|
|
11
12
|
import { pickModel, resolveModel } from '../ui/model-picker.js';
|
|
12
13
|
import { loadMcpConfig } from '../mcp/config.js';
|
|
@@ -112,7 +113,7 @@ export async function startCommand(options) {
|
|
|
112
113
|
onBalanceFetched?.(balStr);
|
|
113
114
|
})();
|
|
114
115
|
// Assemble system instructions
|
|
115
|
-
const systemInstructions = assembleInstructions(workDir);
|
|
116
|
+
const systemInstructions = assembleInstructions(workDir, model);
|
|
116
117
|
// Connect MCP servers (non-blocking — add tools if servers are available)
|
|
117
118
|
const mcpConfig = loadMcpConfig(workDir);
|
|
118
119
|
let mcpTools = [];
|
|
@@ -133,6 +134,13 @@ export async function startCommand(options) {
|
|
|
133
134
|
// Build capabilities (built-in + MCP + sub-agent)
|
|
134
135
|
const subAgent = createSubAgentCapability(apiUrl, chain, allCapabilities);
|
|
135
136
|
const capabilities = [...allCapabilities, ...mcpTools, subAgent];
|
|
137
|
+
// Validate tool descriptions (self-evolution: detect SearchX-style description bugs)
|
|
138
|
+
if (options.debug) {
|
|
139
|
+
const issues = validateToolDescriptions(capabilities);
|
|
140
|
+
for (const issue of issues) {
|
|
141
|
+
console.error(`[validate] ${issue.severity}: ${issue.toolName} — ${issue.issue}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
136
144
|
// Agent config
|
|
137
145
|
const agentConfig = {
|
|
138
146
|
model,
|
|
@@ -214,11 +222,16 @@ async function runWithInkUI(agentConfig, model, workDir, version, walletInfo, on
|
|
|
214
222
|
if (sessionHistory && sessionHistory.length >= 4) {
|
|
215
223
|
try {
|
|
216
224
|
const { extractLearnings } = await import('../learnings/extractor.js');
|
|
225
|
+
const { extractBrainEntities } = await import('../brain/extract.js');
|
|
217
226
|
const { ModelClient } = await import('../agent/llm.js');
|
|
218
227
|
const client = new ModelClient({ apiUrl: agentConfig.apiUrl, chain: agentConfig.chain });
|
|
228
|
+
const sid = `session-${new Date().toISOString()}`;
|
|
219
229
|
await Promise.race([
|
|
220
|
-
|
|
221
|
-
|
|
230
|
+
Promise.all([
|
|
231
|
+
extractLearnings(sessionHistory, sid, client),
|
|
232
|
+
extractBrainEntities(sessionHistory, sid, client),
|
|
233
|
+
]),
|
|
234
|
+
new Promise(resolve => setTimeout(resolve, 15_000)),
|
|
222
235
|
]);
|
|
223
236
|
}
|
|
224
237
|
catch { /* extraction is best-effort */ }
|
package/dist/config.js
CHANGED
|
@@ -11,7 +11,7 @@ try {
|
|
|
11
11
|
catch { /* use default */ }
|
|
12
12
|
export const VERSION = _version;
|
|
13
13
|
// Shared User-Agent string for all outbound HTTP requests
|
|
14
|
-
export const USER_AGENT = `
|
|
14
|
+
export const USER_AGENT = `franklin/${_version} (node/${process.versions.node}; ${process.platform}; ${process.arch})`;
|
|
15
15
|
export const BLOCKRUN_DIR = path.join(os.homedir(), '.blockrun');
|
|
16
16
|
export const CHAIN_FILE = path.join(BLOCKRUN_DIR, 'payment-chain');
|
|
17
17
|
export const API_URLS = {
|
package/dist/index.js
CHANGED
|
@@ -173,10 +173,25 @@ program
|
|
|
173
173
|
});
|
|
174
174
|
// Default action: if no subcommand given, run 'start'
|
|
175
175
|
const args = process.argv.slice(2);
|
|
176
|
-
const knownCommands = program.commands.map(c => c.name());
|
|
177
176
|
const firstArg = args[0];
|
|
177
|
+
const HELP_FLAGS = new Set(['-h', '--help']);
|
|
178
|
+
const VERSION_FLAGS = new Set(['-V', '--version']);
|
|
179
|
+
const START_ONLY_FLAGS = new Set(['--trust', '--debug', '-m', '--model']);
|
|
180
|
+
function hasAnyFlag(argv, flags) {
|
|
181
|
+
return argv.some(arg => flags.has(arg));
|
|
182
|
+
}
|
|
183
|
+
function hasStartOnlyFlag(argv) {
|
|
184
|
+
return argv.some(arg => START_ONLY_FLAGS.has(arg));
|
|
185
|
+
}
|
|
178
186
|
// Handle chain shortcuts: `runcode solana` or `runcode base`
|
|
179
187
|
if (firstArg === 'solana' || firstArg === 'base') {
|
|
188
|
+
if (hasAnyFlag(args, HELP_FLAGS)) {
|
|
189
|
+
program.parse(['node', 'franklin', 'start', '--help']);
|
|
190
|
+
}
|
|
191
|
+
if (hasAnyFlag(args, VERSION_FLAGS)) {
|
|
192
|
+
console.log(version);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
180
195
|
const { saveChain } = await import('./config.js');
|
|
181
196
|
saveChain(firstArg);
|
|
182
197
|
const startOpts = { version };
|
|
@@ -192,7 +207,17 @@ if (firstArg === 'solana' || firstArg === 'base') {
|
|
|
192
207
|
await startCommand(startOpts);
|
|
193
208
|
process.exit(0);
|
|
194
209
|
}
|
|
195
|
-
else if (!firstArg ||
|
|
210
|
+
else if (!firstArg || firstArg.startsWith('-')) {
|
|
211
|
+
if (hasAnyFlag(args, HELP_FLAGS) && hasStartOnlyFlag(args)) {
|
|
212
|
+
program.parse(['node', 'franklin', 'start', '--help']);
|
|
213
|
+
}
|
|
214
|
+
if (hasAnyFlag(args, VERSION_FLAGS) && hasStartOnlyFlag(args)) {
|
|
215
|
+
console.log(version);
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
if (hasAnyFlag(args, HELP_FLAGS) || hasAnyFlag(args, VERSION_FLAGS)) {
|
|
219
|
+
program.parse();
|
|
220
|
+
}
|
|
196
221
|
// No subcommand or only flags — treat as 'start' with flags
|
|
197
222
|
const startOpts = { version };
|
|
198
223
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -14,3 +14,16 @@ export declare function bootstrapFromClaudeConfig(client: ModelClient): Promise<
|
|
|
14
14
|
* Runs asynchronously — caller should fire-and-forget.
|
|
15
15
|
*/
|
|
16
16
|
export declare function extractLearnings(history: Dialogue[], sessionId: string, client: ModelClient): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Check if mid-session extraction should run, and if so, run it in background.
|
|
19
|
+
* Called from the agent loop after tool execution completes.
|
|
20
|
+
*
|
|
21
|
+
* Triggers when:
|
|
22
|
+
* 1. Token count exceeds init threshold (first extraction) OR update threshold (subsequent)
|
|
23
|
+
* 2. AND enough tool calls have happened since last extraction
|
|
24
|
+
* 3. AND we haven't hit the per-session cap
|
|
25
|
+
*
|
|
26
|
+
* Inspired by Claude Code's SessionMemory which runs a background subagent
|
|
27
|
+
* to extract conversation notes periodically.
|
|
28
|
+
*/
|
|
29
|
+
export declare function maybeMidSessionExtract(history: Dialogue[], estimatedTokens: number, totalToolCalls: number, sessionId: string, client: ModelClient): void;
|
|
@@ -6,17 +6,19 @@ import fs from 'node:fs';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import os from 'node:os';
|
|
8
8
|
import { loadLearnings, mergeLearning, saveLearnings } from './store.js';
|
|
9
|
-
//
|
|
9
|
+
// Free models for learning extraction — JSON extraction is simple enough.
|
|
10
|
+
// Ordered by reliability: try the best free model first, fall back to others.
|
|
10
11
|
const EXTRACTION_MODELS = [
|
|
11
|
-
'
|
|
12
|
-
'
|
|
13
|
-
'nvidia/
|
|
12
|
+
'nvidia/nemotron-ultra-253b', // Best free model for structured output
|
|
13
|
+
'nvidia/qwen3-coder-480b', // Strong at JSON tasks
|
|
14
|
+
'nvidia/devstral-2-123b', // Fallback
|
|
14
15
|
];
|
|
15
16
|
const VALID_CATEGORIES = new Set([
|
|
16
17
|
'language', 'model_preference', 'tool_pattern', 'coding_style',
|
|
17
|
-
'communication', 'domain', 'correction', '
|
|
18
|
+
'communication', 'domain', 'correction', 'negative', 'project_context',
|
|
19
|
+
'workflow', 'other',
|
|
18
20
|
]);
|
|
19
|
-
const EXTRACTION_PROMPT = `You are analyzing a conversation between a user and an AI coding agent. Extract user preferences
|
|
21
|
+
const EXTRACTION_PROMPT = `You are analyzing a conversation between a user and an AI coding agent. Extract user preferences, behavioral patterns, and project knowledge that would help personalize future interactions.
|
|
20
22
|
|
|
21
23
|
Analyze for:
|
|
22
24
|
1. Language — what language does the user write in? (English, Chinese, mixed?)
|
|
@@ -25,16 +27,21 @@ Analyze for:
|
|
|
25
27
|
4. Communication — are they terse or verbose? Do they want explanations or just code?
|
|
26
28
|
5. Domain — what tech stack, frameworks, or project type?
|
|
27
29
|
6. Corrections — did they repeatedly correct the same agent behavior?
|
|
28
|
-
7.
|
|
30
|
+
7. **Negative signals** — did the user say "don't do X", "stop doing Y", "never Z"? These are HIGH PRIORITY (confidence 0.9+). Use category "negative".
|
|
31
|
+
8. **Project context** — architecture decisions, key file locations, deployment patterns, team conventions. Use category "project_context".
|
|
32
|
+
9. Workflow — do they prefer short tasks or long planning sessions?
|
|
29
33
|
|
|
30
34
|
Rules:
|
|
31
35
|
- ONLY extract signals clearly supported by evidence in the conversation.
|
|
32
36
|
- Do NOT speculate. If evidence is weak, set confidence below 0.5.
|
|
37
|
+
- **Negative signals get HIGH confidence** (0.9+) — when a user says "don't" or "stop" or corrects the agent, that's a strong signal.
|
|
38
|
+
- **Project context gets MEDIUM confidence** (0.7) — architecture/tech decisions are usually deliberate.
|
|
33
39
|
- If the conversation is too short or generic, return an empty array.
|
|
34
40
|
- Each learning should be one clear, actionable sentence.
|
|
41
|
+
- For negative learnings, start with "NEVER" or "Do NOT" to make the instruction clear.
|
|
35
42
|
|
|
36
43
|
Respond with ONLY a JSON object (no markdown fences, no commentary):
|
|
37
|
-
{"learnings":[{"learning":"...","category":"language|model_preference|tool_pattern|coding_style|communication|domain|correction|workflow|other","confidence":0.5}]}`;
|
|
44
|
+
{"learnings":[{"learning":"...","category":"language|model_preference|tool_pattern|coding_style|communication|domain|correction|negative|project_context|workflow|other","confidence":0.5}]}`;
|
|
38
45
|
/**
|
|
39
46
|
* Condense session history into a compact text for extraction.
|
|
40
47
|
* Only includes user messages and assistant text — skips tool calls/results.
|
|
@@ -198,6 +205,9 @@ export async function extractLearnings(history, sessionId, client) {
|
|
|
198
205
|
const condensed = condenseHistory(history);
|
|
199
206
|
if (condensed.length < 100)
|
|
200
207
|
return; // Too little content
|
|
208
|
+
await runExtraction(condensed, sessionId, client);
|
|
209
|
+
}
|
|
210
|
+
async function runExtraction(condensed, sessionId, client) {
|
|
201
211
|
// Try each model until one succeeds
|
|
202
212
|
let result = null;
|
|
203
213
|
for (const model of EXTRACTION_MODELS) {
|
|
@@ -232,3 +242,54 @@ export async function extractLearnings(history, sessionId, client) {
|
|
|
232
242
|
}
|
|
233
243
|
saveLearnings(existing);
|
|
234
244
|
}
|
|
245
|
+
const midSessionState = {
|
|
246
|
+
lastExtractionTokens: 0,
|
|
247
|
+
lastExtractionToolCalls: 0,
|
|
248
|
+
extractionCount: 0,
|
|
249
|
+
};
|
|
250
|
+
/** Token threshold before first mid-session extraction */
|
|
251
|
+
const MID_SESSION_INIT_THRESHOLD = 30_000;
|
|
252
|
+
/** Token growth since last extraction to trigger another */
|
|
253
|
+
const MID_SESSION_UPDATE_THRESHOLD = 25_000;
|
|
254
|
+
/** Minimum tool calls since last extraction */
|
|
255
|
+
const MID_SESSION_TOOL_CALLS_THRESHOLD = 5;
|
|
256
|
+
/** Max mid-session extractions per session (don't spam) */
|
|
257
|
+
const MID_SESSION_MAX_EXTRACTIONS = 3;
|
|
258
|
+
/**
|
|
259
|
+
* Check if mid-session extraction should run, and if so, run it in background.
|
|
260
|
+
* Called from the agent loop after tool execution completes.
|
|
261
|
+
*
|
|
262
|
+
* Triggers when:
|
|
263
|
+
* 1. Token count exceeds init threshold (first extraction) OR update threshold (subsequent)
|
|
264
|
+
* 2. AND enough tool calls have happened since last extraction
|
|
265
|
+
* 3. AND we haven't hit the per-session cap
|
|
266
|
+
*
|
|
267
|
+
* Inspired by Claude Code's SessionMemory which runs a background subagent
|
|
268
|
+
* to extract conversation notes periodically.
|
|
269
|
+
*/
|
|
270
|
+
export function maybeMidSessionExtract(history, estimatedTokens, totalToolCalls, sessionId, client) {
|
|
271
|
+
// Cap reached — stop extracting
|
|
272
|
+
if (midSessionState.extractionCount >= MID_SESSION_MAX_EXTRACTIONS)
|
|
273
|
+
return;
|
|
274
|
+
// Check token threshold
|
|
275
|
+
const tokenGrowth = estimatedTokens - midSessionState.lastExtractionTokens;
|
|
276
|
+
const threshold = midSessionState.extractionCount === 0
|
|
277
|
+
? MID_SESSION_INIT_THRESHOLD
|
|
278
|
+
: MID_SESSION_UPDATE_THRESHOLD;
|
|
279
|
+
if (tokenGrowth < threshold)
|
|
280
|
+
return;
|
|
281
|
+
// Check tool calls threshold
|
|
282
|
+
const toolCallGrowth = totalToolCalls - midSessionState.lastExtractionToolCalls;
|
|
283
|
+
if (toolCallGrowth < MID_SESSION_TOOL_CALLS_THRESHOLD)
|
|
284
|
+
return;
|
|
285
|
+
// Trigger extraction — fire and forget (never blocks the conversation)
|
|
286
|
+
midSessionState.lastExtractionTokens = estimatedTokens;
|
|
287
|
+
midSessionState.lastExtractionToolCalls = totalToolCalls;
|
|
288
|
+
midSessionState.extractionCount++;
|
|
289
|
+
const condensed = condenseHistory(history);
|
|
290
|
+
if (condensed.length < 100)
|
|
291
|
+
return;
|
|
292
|
+
// Run in background — errors are silently swallowed
|
|
293
|
+
runExtraction(condensed, `${sessionId}:mid-${midSessionState.extractionCount}`, client)
|
|
294
|
+
.catch(() => { });
|
|
295
|
+
}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export type { Learning, LearningCategory, ExtractionResult } from './types.js';
|
|
2
2
|
export { loadLearnings, saveLearnings, mergeLearning, decayLearnings, formatForPrompt } from './store.js';
|
|
3
|
-
export { extractLearnings, bootstrapFromClaudeConfig } from './extractor.js';
|
|
3
|
+
export { extractLearnings, bootstrapFromClaudeConfig, maybeMidSessionExtract } from './extractor.js';
|
package/dist/learnings/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { loadLearnings, saveLearnings, mergeLearning, decayLearnings, formatForPrompt } from './store.js';
|
|
2
|
-
export { extractLearnings, bootstrapFromClaudeConfig } from './extractor.js';
|
|
2
|
+
export { extractLearnings, bootstrapFromClaudeConfig, maybeMidSessionExtract } from './extractor.js';
|
package/dist/learnings/store.js
CHANGED
|
@@ -111,20 +111,49 @@ const MAX_PROMPT_CHARS = 2000; // ~500 tokens
|
|
|
111
111
|
export function formatForPrompt(learnings) {
|
|
112
112
|
if (learnings.length === 0)
|
|
113
113
|
return '';
|
|
114
|
-
|
|
115
|
-
const
|
|
114
|
+
// Separate negative learnings (highest priority) from others
|
|
115
|
+
const negative = learnings.filter(l => l.category === 'negative');
|
|
116
|
+
const projectCtx = learnings.filter(l => l.category === 'project_context');
|
|
117
|
+
const preferences = learnings.filter(l => l.category !== 'negative' && l.category !== 'project_context');
|
|
118
|
+
const sections = [];
|
|
116
119
|
let chars = 0;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
// Negative learnings first (most important — prevents repeating mistakes)
|
|
121
|
+
if (negative.length > 0) {
|
|
122
|
+
const negSorted = [...negative].sort((a, b) => score(b) - score(a));
|
|
123
|
+
const negLines = negSorted
|
|
124
|
+
.filter(l => { if (chars + l.learning.length + 5 > MAX_PROMPT_CHARS)
|
|
125
|
+
return false; chars += l.learning.length + 5; return true; })
|
|
126
|
+
.map(l => `- ⛔ ${l.learning}`);
|
|
127
|
+
if (negLines.length > 0) {
|
|
128
|
+
sections.push('## Rules (from past corrections)\n' + negLines.join('\n'));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Project context
|
|
132
|
+
if (projectCtx.length > 0) {
|
|
133
|
+
const ctxSorted = [...projectCtx].sort((a, b) => score(b) - score(a));
|
|
134
|
+
const ctxLines = ctxSorted
|
|
135
|
+
.filter(l => { if (chars + l.learning.length + 5 > MAX_PROMPT_CHARS)
|
|
136
|
+
return false; chars += l.learning.length + 5; return true; })
|
|
137
|
+
.map(l => `- ${l.learning}`);
|
|
138
|
+
if (ctxLines.length > 0) {
|
|
139
|
+
sections.push('## Project Context\n' + ctxLines.join('\n'));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// General preferences
|
|
143
|
+
if (preferences.length > 0) {
|
|
144
|
+
const prefSorted = [...preferences].sort((a, b) => score(b) - score(a));
|
|
145
|
+
const prefLines = prefSorted
|
|
146
|
+
.filter(l => { if (chars + l.learning.length + 5 > MAX_PROMPT_CHARS)
|
|
147
|
+
return false; chars += l.learning.length + 5; return true; })
|
|
148
|
+
.map(l => {
|
|
149
|
+
const conf = l.confidence >= 0.8 ? '●' : l.confidence >= 0.5 ? '◐' : '○';
|
|
150
|
+
return `- ${conf} ${l.learning}`;
|
|
151
|
+
});
|
|
152
|
+
if (prefLines.length > 0) {
|
|
153
|
+
sections.push('## Preferences\n' + prefLines.join('\n'));
|
|
154
|
+
}
|
|
126
155
|
}
|
|
127
|
-
if (
|
|
156
|
+
if (sections.length === 0)
|
|
128
157
|
return '';
|
|
129
|
-
return
|
|
158
|
+
return '# Personal Context\nLearned from previous sessions:\n\n' + sections.join('\n\n');
|
|
130
159
|
}
|