@danieltmn/tamnora 0.12.0 → 0.13.1
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/CHANGELOG.md +23 -0
- package/harness/packages/core/src/agent/runAgent.js +23 -5
- package/harness/packages/core/src/config/index.js +2 -0
- package/harness/packages/core/src/index.js +26 -0
- package/harness/packages/core/src/memory/index.js +220 -0
- package/harness/packages/core/src/rules/index.js +151 -0
- package/harness/packages/core/test/memory.test.js +96 -0
- package/harness/packages/core/test/rules.test.js +64 -0
- package/harness/packages/server/public/app.js +96 -1
- package/harness/packages/server/public/index.html +46 -1
- package/harness/packages/server/src/lib/agentRun.js +2 -2
- package/harness/packages/server/src/lib/agentTools.js +60 -5
- package/harness/packages/server/src/lib/channels/telegram.js +3 -0
- package/harness/packages/server/src/lib/prompt.js +14 -3
- package/harness/packages/server/src/lib/rules.js +43 -0
- package/harness/packages/server/src/routes/chat.js +3 -0
- package/harness/packages/server/src/routes/memory.js +66 -0
- package/harness/packages/server/src/server.js +4 -0
- package/harness/packages/server/test/server.test.js +170 -5
- package/harness/tamnora.example.json +6 -0
- package/package.json +1 -1
|
@@ -195,7 +195,7 @@ function activateTab(name) {
|
|
|
195
195
|
hideMoreMenu();
|
|
196
196
|
if (name === 'runs') refreshRuns();
|
|
197
197
|
if (name === 'providers') refreshProviders();
|
|
198
|
-
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshChannels(); refreshLimits(); refreshVoz(); refreshAsesor(); refreshTranscribe(); refreshProfile(); }
|
|
198
|
+
if (name === 'ajustes') { refreshMcp(); refreshSkills(); refreshMemory(); refreshChannels(); refreshLimits(); refreshVoz(); refreshAsesor(); refreshTranscribe(); refreshProfile(); }
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
document.querySelectorAll('.tab[data-tab]').forEach((t) =>
|
|
@@ -3701,6 +3701,10 @@ const mcpForm = $('#mcp-form');
|
|
|
3701
3701
|
const skillListEl = $('#skill-list');
|
|
3702
3702
|
const skillModal = $('#skill-modal');
|
|
3703
3703
|
const skillForm = $('#skill-form');
|
|
3704
|
+
const memoryListEl = $('#memory-list');
|
|
3705
|
+
const memoryIndexEl = $('#memory-index');
|
|
3706
|
+
const memoryModal = $('#memory-modal');
|
|
3707
|
+
const memoryForm = $('#memory-form');
|
|
3704
3708
|
const tgTokenEl = $('#tg-token');
|
|
3705
3709
|
const tgChatsEl = $('#tg-chats');
|
|
3706
3710
|
const tgTranscribeEl = $('#tg-transcribe');
|
|
@@ -3989,6 +3993,88 @@ async function submitSkill(event) {
|
|
|
3989
3993
|
refreshSkills();
|
|
3990
3994
|
}
|
|
3991
3995
|
|
|
3996
|
+
/* --- ajustes: Memoria ---------------------------------------------------- */
|
|
3997
|
+
|
|
3998
|
+
async function refreshMemory() {
|
|
3999
|
+
if (!memoryListEl) return;
|
|
4000
|
+
const data = await api('/api/memory').catch(() => null);
|
|
4001
|
+
if (!data) return;
|
|
4002
|
+
if (memoryIndexEl && document.activeElement !== memoryIndexEl) memoryIndexEl.value = data.index ?? '';
|
|
4003
|
+
clear(memoryListEl);
|
|
4004
|
+
if (!data.topics?.length) {
|
|
4005
|
+
memoryListEl.appendChild(el('li', 'muted', 'sin memoria todavía'));
|
|
4006
|
+
return;
|
|
4007
|
+
}
|
|
4008
|
+
for (const t of data.topics) {
|
|
4009
|
+
const li = el('li', 'active');
|
|
4010
|
+
const main = el('div', 'grow');
|
|
4011
|
+
main.append(
|
|
4012
|
+
el('div', '', t.title || t.name),
|
|
4013
|
+
el('div', 'pmeta', `${t.name}${t.bytes ? ` · ${Math.max(1, Math.round(t.bytes / 1024))} KB` : ''}`)
|
|
4014
|
+
);
|
|
4015
|
+
li.appendChild(main);
|
|
4016
|
+
const actions = el('div', 'pactions');
|
|
4017
|
+
actions.appendChild(button('editar', (event) => { event.stopPropagation(); openMemoryForm(t.name); }));
|
|
4018
|
+
actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeMemory(t.name); }, 'btn danger'));
|
|
4019
|
+
li.appendChild(actions);
|
|
4020
|
+
li.addEventListener('click', () => openMemoryForm(t.name));
|
|
4021
|
+
memoryListEl.appendChild(li);
|
|
4022
|
+
}
|
|
4023
|
+
}
|
|
4024
|
+
|
|
4025
|
+
async function openMemoryForm(name = null) {
|
|
4026
|
+
$('#memory-title').textContent = name ? `editar ${name}` : 'nuevo tema';
|
|
4027
|
+
$('#mem-name').value = name ? name.replace(/\.md$/i, '') : '';
|
|
4028
|
+
$('#mem-name').readOnly = Boolean(name);
|
|
4029
|
+
$('#mem-title').value = '';
|
|
4030
|
+
$('#mem-body').value = '';
|
|
4031
|
+
setStatus('#mem-status', '');
|
|
4032
|
+
if (name) {
|
|
4033
|
+
const data = await api(`/api/memory/${encodeURIComponent(name)}`).catch(() => null);
|
|
4034
|
+
if (data?.topic) {
|
|
4035
|
+
$('#mem-title').value = data.topic.title ?? '';
|
|
4036
|
+
$('#mem-body').value = data.topic.body ?? '';
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
memoryModal.showModal();
|
|
4040
|
+
if (!name) $('#mem-name').focus();
|
|
4041
|
+
}
|
|
4042
|
+
|
|
4043
|
+
function closeMemoryForm() {
|
|
4044
|
+
if (memoryModal.open) memoryModal.close();
|
|
4045
|
+
}
|
|
4046
|
+
|
|
4047
|
+
async function removeMemory(name) {
|
|
4048
|
+
await deleteEntity(`/api/memory/${encodeURIComponent(name)}`, {
|
|
4049
|
+
confirmText: `¿Borrar la memoria "${name}"?`,
|
|
4050
|
+
refresh: refreshMemory,
|
|
4051
|
+
});
|
|
4052
|
+
}
|
|
4053
|
+
|
|
4054
|
+
async function submitMemory(event) {
|
|
4055
|
+
event.preventDefault();
|
|
4056
|
+
const name = $('#mem-name').value.trim();
|
|
4057
|
+
if (!name) return setStatus('#mem-status', 'Falta el tema.', 'err');
|
|
4058
|
+
const body = { title: $('#mem-title').value.trim(), body: $('#mem-body').value };
|
|
4059
|
+
try {
|
|
4060
|
+
await api(`/api/memory/${encodeURIComponent(name)}`, { method: 'PUT', body });
|
|
4061
|
+
} catch (error) {
|
|
4062
|
+
return setStatus('#mem-status', error.message, 'err');
|
|
4063
|
+
}
|
|
4064
|
+
closeMemoryForm();
|
|
4065
|
+
refreshMemory();
|
|
4066
|
+
}
|
|
4067
|
+
|
|
4068
|
+
async function saveMemoryIndex() {
|
|
4069
|
+
try {
|
|
4070
|
+
await api('/api/memory', { method: 'PUT', body: { index: memoryIndexEl.value } });
|
|
4071
|
+
} catch (error) {
|
|
4072
|
+
return setStatus('#memory-status', error.message, 'err');
|
|
4073
|
+
}
|
|
4074
|
+
setStatus('#memory-status', 'guardado', 'ok');
|
|
4075
|
+
refreshMemory();
|
|
4076
|
+
}
|
|
4077
|
+
|
|
3992
4078
|
async function refreshChannels() {
|
|
3993
4079
|
if (!tgStatusEl) return;
|
|
3994
4080
|
const data = await api('/api/channels').catch(() => null);
|
|
@@ -4665,6 +4751,15 @@ $('#asesor-test')?.addEventListener('click', testAsesor);
|
|
|
4665
4751
|
skillModal.addEventListener('click', (event) => {
|
|
4666
4752
|
if (event.target === skillModal) closeSkillForm();
|
|
4667
4753
|
});
|
|
4754
|
+
|
|
4755
|
+
$('#memory-new')?.addEventListener('click', () => openMemoryForm());
|
|
4756
|
+
$('#memory-close')?.addEventListener('click', closeMemoryForm);
|
|
4757
|
+
$('#mem-cancel')?.addEventListener('click', closeMemoryForm);
|
|
4758
|
+
memoryForm?.addEventListener('submit', submitMemory);
|
|
4759
|
+
$('#memory-save-index')?.addEventListener('click', saveMemoryIndex);
|
|
4760
|
+
memoryModal?.addEventListener('click', (event) => {
|
|
4761
|
+
if (event.target === memoryModal) closeMemoryForm();
|
|
4762
|
+
});
|
|
4668
4763
|
}
|
|
4669
4764
|
|
|
4670
4765
|
/* --- eval ---------------------------------------------------------------- */
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
</head>
|
|
38
38
|
<body>
|
|
39
39
|
<header class="topbar">
|
|
40
|
-
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.
|
|
40
|
+
<div class="brand"><span class="logo"><img src="/icons/logo.png" alt="Tamnora" /></span> <span id="topbar-section" class="topbar-section">chat</span> <span class="ver">v0.13.1</span></div>
|
|
41
41
|
<nav class="tabs">
|
|
42
42
|
<button class="tab active" data-tab="chat">chat</button>
|
|
43
43
|
<button class="tab" data-tab="providers">proveedores</button>
|
|
@@ -461,6 +461,7 @@
|
|
|
461
461
|
<button type="button" class="ajustes-tab" data-panel="sesiones" role="tab">Sesiones</button>
|
|
462
462
|
<button type="button" class="ajustes-tab" data-panel="mcp" role="tab">MCP</button>
|
|
463
463
|
<button type="button" class="ajustes-tab" data-panel="skills" role="tab">Skills</button>
|
|
464
|
+
<button type="button" class="ajustes-tab" data-panel="memoria" role="tab">Memoria</button>
|
|
464
465
|
<button type="button" class="ajustes-tab" data-panel="telegram" role="tab">Telegram</button>
|
|
465
466
|
<button type="button" class="ajustes-tab" data-panel="voz" role="tab">Voz</button>
|
|
466
467
|
<button type="button" class="ajustes-tab" data-panel="asesor" role="tab">Asesor</button>
|
|
@@ -583,6 +584,25 @@
|
|
|
583
584
|
<ul id="skill-list" class="providers"></ul>
|
|
584
585
|
</div>
|
|
585
586
|
|
|
587
|
+
<div class="panel ajustes-panel" data-panel="memoria">
|
|
588
|
+
<div class="row between">
|
|
589
|
+
<h2>Memoria</h2>
|
|
590
|
+
<button id="memory-new" class="btn">+ tema</button>
|
|
591
|
+
</div>
|
|
592
|
+
<p class="muted verify-line">
|
|
593
|
+
Lo que el proyecto recuerda entre sesiones (<code>.tamnora/memory/</code>). El agente la mantiene con la
|
|
594
|
+
herramienta <code>memory</code>; acá la podés revisar y editar.
|
|
595
|
+
</p>
|
|
596
|
+
<label class="field"><span>índice (MEMORY.md)</span>
|
|
597
|
+
<textarea id="memory-index" rows="6" spellcheck="false" placeholder="# Memoria del proyecto"></textarea>
|
|
598
|
+
</label>
|
|
599
|
+
<div class="row between settings-actions">
|
|
600
|
+
<span id="memory-status" class="summary"></span>
|
|
601
|
+
<button id="memory-save-index" class="btn" type="button">guardar índice</button>
|
|
602
|
+
</div>
|
|
603
|
+
<ul id="memory-list" class="providers"></ul>
|
|
604
|
+
</div>
|
|
605
|
+
|
|
586
606
|
<div class="panel ajustes-panel" data-panel="telegram">
|
|
587
607
|
<div class="row between">
|
|
588
608
|
<h2>Telegram</h2>
|
|
@@ -956,6 +976,31 @@
|
|
|
956
976
|
</form>
|
|
957
977
|
</dialog>
|
|
958
978
|
|
|
979
|
+
<dialog id="memory-modal" class="modal">
|
|
980
|
+
<form id="memory-form" class="modal-card">
|
|
981
|
+
<div class="row between">
|
|
982
|
+
<h3 id="memory-title">nuevo tema</h3>
|
|
983
|
+
<button type="button" id="memory-close" class="btn icon" title="cerrar" aria-label="cerrar">
|
|
984
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
985
|
+
<path d="M18 6 6 18M6 6l12 12" />
|
|
986
|
+
</svg>
|
|
987
|
+
</button>
|
|
988
|
+
</div>
|
|
989
|
+
<div class="row">
|
|
990
|
+
<label class="field"><span>tema (slug)</span><input id="mem-name" spellcheck="false" placeholder="arquitectura" /></label>
|
|
991
|
+
<label class="field grow"><span>título</span><input id="mem-title" spellcheck="false" placeholder="Arquitectura" /></label>
|
|
992
|
+
</div>
|
|
993
|
+
<label class="field"><span>contenido (markdown)</span>
|
|
994
|
+
<textarea id="mem-body" rows="10" spellcheck="false" placeholder="Qué recordar del proyecto…"></textarea>
|
|
995
|
+
</label>
|
|
996
|
+
<div class="row between settings-actions">
|
|
997
|
+
<button type="button" id="mem-cancel" class="btn">cancelar</button>
|
|
998
|
+
<button type="submit" class="btn primary">guardar</button>
|
|
999
|
+
</div>
|
|
1000
|
+
<div id="mem-status" class="summary"></div>
|
|
1001
|
+
</form>
|
|
1002
|
+
</dialog>
|
|
1003
|
+
|
|
959
1004
|
<dialog id="palette-modal" class="modal palette-modal">
|
|
960
1005
|
<div class="palette">
|
|
961
1006
|
<div class="palette-row">
|
|
@@ -27,9 +27,9 @@ export function combineToolset({ local, admin = null, tasks = null, exposeAdmin
|
|
|
27
27
|
...(admin ? admin.definitions.filter((d) => adminAllowed(d.function.name)) : []),
|
|
28
28
|
...(tasks ? tasks.definitions : []),
|
|
29
29
|
];
|
|
30
|
-
const execute = (name, args, context) => {
|
|
30
|
+
const execute = (name, args, context = {}) => {
|
|
31
31
|
if (tasks?.handles(name)) return tasks.execute(name, args);
|
|
32
|
-
if (adminAllowed(name)) return admin.execute(name, args,
|
|
32
|
+
if (adminAllowed(name)) return admin.execute(name, args, { approve, ...context });
|
|
33
33
|
return local.execute(name, args, context);
|
|
34
34
|
};
|
|
35
35
|
return { definitions, execute };
|
|
@@ -12,6 +12,12 @@ import {
|
|
|
12
12
|
registryServerToConfig,
|
|
13
13
|
normalizeMcpConfig,
|
|
14
14
|
homeDir,
|
|
15
|
+
memorySlug,
|
|
16
|
+
listMemoryTopics,
|
|
17
|
+
readMemoryIndex,
|
|
18
|
+
readMemoryTopic,
|
|
19
|
+
writeMemory,
|
|
20
|
+
searchMemory,
|
|
15
21
|
} from './core.js';
|
|
16
22
|
import { SKILL_ID_PATTERN, skillsView, writeSkill, setSkillEnabled, findSkill } from './skills.js';
|
|
17
23
|
|
|
@@ -71,6 +77,18 @@ const DEFINITIONS = [
|
|
|
71
77
|
},
|
|
72
78
|
[]
|
|
73
79
|
),
|
|
80
|
+
definition(
|
|
81
|
+
'memory',
|
|
82
|
+
'Memoria durable del proyecto, persiste entre sesiones. Acciones: list (temas), read (índice o un tema), search (busca) y write (crea/actualiza un tema). Guardá ahí lo que valga la pena recordar del proyecto.',
|
|
83
|
+
{
|
|
84
|
+
action: { type: 'string', enum: ['list', 'read', 'search', 'write'], description: 'Acción a ejecutar' },
|
|
85
|
+
topic: { type: 'string', description: 'Tema (slug) para read/write' },
|
|
86
|
+
query: { type: 'string', description: 'Texto a buscar (action=search)' },
|
|
87
|
+
title: { type: 'string', description: 'Título del tema (action=write)' },
|
|
88
|
+
content: { type: 'string', description: 'Contenido markdown (action=write)' },
|
|
89
|
+
},
|
|
90
|
+
['action']
|
|
91
|
+
),
|
|
74
92
|
];
|
|
75
93
|
|
|
76
94
|
async function fetchText(url) {
|
|
@@ -90,24 +108,26 @@ async function fetchText(url) {
|
|
|
90
108
|
}
|
|
91
109
|
}
|
|
92
110
|
|
|
93
|
-
function decisionFor(ctx, name) {
|
|
111
|
+
function decisionFor(ctx, name, args) {
|
|
94
112
|
const perms = ctx.config.permissions ?? {};
|
|
95
113
|
const value = perms[name];
|
|
96
114
|
if (value === 'allow' || value === 'ask' || value === 'deny') return value;
|
|
115
|
+
if (name === 'memory') return String(args?.action ?? 'read').toLowerCase() === 'write' ? 'ask' : 'allow';
|
|
97
116
|
return name.startsWith('list_') || name === 'skill' ? 'allow' : 'ask';
|
|
98
117
|
}
|
|
99
118
|
|
|
100
119
|
/**
|
|
101
|
-
* Crea las tools de administracion. `execute(name, args, { approve })`,
|
|
102
|
-
* `approve(name, args)` resuelve true/false para los permisos "ask"
|
|
120
|
+
* Crea las tools de administracion. `execute(name, args, { approve, cwd })`,
|
|
121
|
+
* donde `approve(name, args)` resuelve true/false para los permisos "ask" y
|
|
122
|
+
* `cwd` es la carpeta de la sesion (para la memoria del proyecto).
|
|
103
123
|
*/
|
|
104
124
|
export function createAgentTools(ctx) {
|
|
105
125
|
function handles(name) {
|
|
106
126
|
return DEFINITIONS.some((d) => d.function.name === name);
|
|
107
127
|
}
|
|
108
128
|
|
|
109
|
-
async function execute(name, args = {}, { approve } = {}) {
|
|
110
|
-
const decision = decisionFor(ctx, name);
|
|
129
|
+
async function execute(name, args = {}, { approve, cwd } = {}) {
|
|
130
|
+
const decision = decisionFor(ctx, name, args);
|
|
111
131
|
if (decision === 'deny') return `Permiso denegado para "${name}".`;
|
|
112
132
|
if (decision === 'ask') {
|
|
113
133
|
const ok = approve ? await approve(name, args) : false;
|
|
@@ -248,6 +268,41 @@ export function createAgentTools(ctx) {
|
|
|
248
268
|
return `Skill "${id}" instalada.`;
|
|
249
269
|
}
|
|
250
270
|
|
|
271
|
+
case 'memory': {
|
|
272
|
+
const root = cwd ?? ctx.cwd;
|
|
273
|
+
const action = String(args.action ?? 'read').toLowerCase();
|
|
274
|
+
if (action === 'list') {
|
|
275
|
+
const topics = listMemoryTopics(root);
|
|
276
|
+
if (!topics.length) return 'No hay memoria guardada todavía para este proyecto.';
|
|
277
|
+
return JSON.stringify(topics.map(({ name, title, bytes }) => ({ name, title, bytes })), null, 2);
|
|
278
|
+
}
|
|
279
|
+
if (action === 'search') {
|
|
280
|
+
const query = String(args.query ?? '').trim();
|
|
281
|
+
if (!query) return 'Pasá "query" para buscar en la memoria.';
|
|
282
|
+
const hits = searchMemory(root, query);
|
|
283
|
+
if (!hits.length) return `Sin resultados para "${query}".`;
|
|
284
|
+
return hits.map((h) => `### ${h.title} (${h.name})\n${h.snippet}`).join('\n\n');
|
|
285
|
+
}
|
|
286
|
+
if (action === 'write') {
|
|
287
|
+
const content = String(args.content ?? '').trim();
|
|
288
|
+
if (!content) return 'Pasá "content" para guardar en memoria.';
|
|
289
|
+
try {
|
|
290
|
+
const saved = await writeMemory(root, { topic: args.topic ?? args.title, title: args.title, content });
|
|
291
|
+
return `Guardado en memoria: ${saved.name} (${saved.title}).`;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
return `No se pudo guardar la memoria: ${error.message}`;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const topic = String(args.topic ?? '').trim();
|
|
297
|
+
if (topic) {
|
|
298
|
+
const name = topic.endsWith('.md') ? topic : `${memorySlug(topic)}.md`;
|
|
299
|
+
const read = readMemoryTopic(root, name);
|
|
300
|
+
if (!read) return `No hay memoria del tema "${topic}".`;
|
|
301
|
+
return read.body;
|
|
302
|
+
}
|
|
303
|
+
return readMemoryIndex(root).trim() || 'No hay índice de memoria todavía.';
|
|
304
|
+
}
|
|
305
|
+
|
|
251
306
|
default:
|
|
252
307
|
return `Herramienta desconocida: ${name}`;
|
|
253
308
|
}
|
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
toolCallPayload,
|
|
26
26
|
} from '../agentRun.js';
|
|
27
27
|
import { mp3AOgg } from '../ffmpeg.js';
|
|
28
|
+
import { createRulesInjector } from '../rules.js';
|
|
28
29
|
import { resolveTts } from '../ttsSettings.js';
|
|
29
30
|
import { transcribeAudio } from '../transcribe.js';
|
|
30
31
|
|
|
@@ -505,6 +506,8 @@ export function startTelegramChannel(
|
|
|
505
506
|
messages,
|
|
506
507
|
tools: definitions,
|
|
507
508
|
execute,
|
|
509
|
+
toolContext: { cwd, sessionId },
|
|
510
|
+
onLifecycle: createRulesInjector({ cwd, patterns: ctx.config.rules ?? [] }) ?? undefined,
|
|
508
511
|
signal: controller.signal,
|
|
509
512
|
maxRounds: ctx.effectiveMaxRounds?.() ?? ctx.config.maxRounds,
|
|
510
513
|
maxTokens: ctx.effectiveMaxTokens?.() ?? 0,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadInstructions } from './core.js';
|
|
1
|
+
import { loadInstructions, loadRules, buildStaticRulesBlock, memoryPrompt } from './core.js';
|
|
2
2
|
|
|
3
3
|
export const DEFAULT_SYSTEM_PROMPT =
|
|
4
4
|
'Eres Tamnora, un asistente útil. Responde en el idioma del usuario y usa las herramientas cuando aporten valor.';
|
|
@@ -34,14 +34,21 @@ function workspacePreamble(cwd) {
|
|
|
34
34
|
* @param {() => string} [opts.profileText] - user profile block, re-read on every call
|
|
35
35
|
* @param {() => string} [opts.skillsText] - skills block, re-read on every call
|
|
36
36
|
* @param {() => string} [opts.scopeText] - strict-scope block, re-read on every call
|
|
37
|
+
* @param {string[]} [opts.ruleFiles] - glob patterns of project rule files
|
|
38
|
+
* @param {boolean} [opts.memory] - inject the project memory block
|
|
37
39
|
*/
|
|
38
|
-
export function createPromptBuilder({ cwd, files, profileText, skillsText, scopeText }) {
|
|
40
|
+
export function createPromptBuilder({ cwd, files, profileText, skillsText, scopeText, ruleFiles = [], memory = false }) {
|
|
39
41
|
const cache = new Map();
|
|
40
42
|
|
|
41
43
|
async function buildFor(workspace) {
|
|
42
44
|
const instructions = (await loadInstructions(workspace, files)).trim();
|
|
43
45
|
const base = workspacePreamble(workspace);
|
|
44
|
-
|
|
46
|
+
let text = instructions ? `${base}\n\n---\n\n${instructions}` : base;
|
|
47
|
+
if (ruleFiles.length) {
|
|
48
|
+
const block = buildStaticRulesBlock(await loadRules(workspace, ruleFiles).catch(() => []));
|
|
49
|
+
if (block) text = `${text}\n\n---\n\n${block}`;
|
|
50
|
+
}
|
|
51
|
+
return text;
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
return async function systemPromptFor(workspace = cwd) {
|
|
@@ -53,6 +60,10 @@ export function createPromptBuilder({ cwd, files, profileText, skillsText, scope
|
|
|
53
60
|
if (profile) parts.push(profile);
|
|
54
61
|
const extra = typeof skillsText === 'function' ? String(skillsText() ?? '').trim() : '';
|
|
55
62
|
if (extra) parts.push(extra);
|
|
63
|
+
if (memory) {
|
|
64
|
+
const mem = memoryPrompt(key);
|
|
65
|
+
if (mem) parts.push(mem);
|
|
66
|
+
}
|
|
56
67
|
const scope = typeof scopeText === 'function' ? String(scopeText() ?? '').trim() : '';
|
|
57
68
|
if (scope) parts.push(scope);
|
|
58
69
|
return parts.join('\n\n---\n\n');
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Reglas de proyecto por path inyectadas en runtime. Se engancha al seam de
|
|
2
|
+
// ciclo de vida del agente: cuando una tool toca un archivo que matchea el
|
|
3
|
+
// `paths:` de una regla, se agrega al contexto de las proximas rondas (efimera,
|
|
4
|
+
// no se persiste). Dedup: una instancia por run, cada regla una vez.
|
|
5
|
+
|
|
6
|
+
import { loadRules, rulesForPath, buildPathRulesBlock } from './core.js';
|
|
7
|
+
|
|
8
|
+
const PATH_TOOLS = new Set(['read', 'write', 'edit', 'list']);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Crea un handler `onLifecycle(event, payload)` para las reglas por path.
|
|
12
|
+
* Devuelve `null` si no hay patrones configurados.
|
|
13
|
+
*/
|
|
14
|
+
export function createRulesInjector({ cwd, patterns = [] }) {
|
|
15
|
+
if (!patterns.length) return null;
|
|
16
|
+
let cached = null;
|
|
17
|
+
let loading = null;
|
|
18
|
+
const injected = new Set();
|
|
19
|
+
|
|
20
|
+
async function all() {
|
|
21
|
+
if (cached) return cached;
|
|
22
|
+
if (!loading) {
|
|
23
|
+
loading = loadRules(cwd, patterns)
|
|
24
|
+
.catch(() => [])
|
|
25
|
+
.then((rules) => {
|
|
26
|
+
cached = rules;
|
|
27
|
+
return rules;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
return loading;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return async function onLifecycle(event) {
|
|
34
|
+
if (event?.type !== 'tool.after' || !PATH_TOOLS.has(event.name)) return undefined;
|
|
35
|
+
const rel = event.args?.path ?? event.args?.file;
|
|
36
|
+
if (!rel || typeof rel !== 'string') return undefined;
|
|
37
|
+
const rules = await all();
|
|
38
|
+
const matches = rulesForPath(rules, rel).filter((rule) => !injected.has(rule.id));
|
|
39
|
+
if (!matches.length) return undefined;
|
|
40
|
+
for (const rule of matches) injected.add(rule.id);
|
|
41
|
+
return { inject: buildPathRulesBlock(matches, rel) };
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
import { fraseInicio, fraseFin, fraseError, createFiltroTools } from '../lib/announcer.js';
|
|
20
20
|
import { resolveAdvisor, advisorShouldRun, consultAdvisor } from '../lib/advisor.js';
|
|
21
21
|
import { maybeAutoCompact } from '../lib/compact.js';
|
|
22
|
+
import { createRulesInjector } from '../lib/rules.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Emit streaming partials at most once per `ms` window, keeping only the
|
|
@@ -268,6 +269,8 @@ export function registerChat(route, ctx) {
|
|
|
268
269
|
messages,
|
|
269
270
|
tools: definitions,
|
|
270
271
|
execute,
|
|
272
|
+
toolContext: { cwd: sessionCwd, sessionId },
|
|
273
|
+
onLifecycle: createRulesInjector({ cwd: sessionCwd, patterns: ctx.config.rules ?? [] }) ?? undefined,
|
|
271
274
|
signal: ctx.runs.signal(sessionId),
|
|
272
275
|
maxRounds: Number(body.maxRounds) || ctx.effectiveMaxRounds?.() || MAX_TOOL_ROUNDS,
|
|
273
276
|
maxTokens: Number(body.maxTokens) || ctx.effectiveMaxTokens?.() || 0,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Memoria durable del proyecto (`<cwd>/.tamnora/memory`). El agente la usa con la
|
|
2
|
+
// tool `memory`; la consola la lista, edita y borra desde la pestaña Memoria.
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
memoryRoot,
|
|
6
|
+
isMemoryTopic,
|
|
7
|
+
readMemoryIndex,
|
|
8
|
+
listMemoryTopics,
|
|
9
|
+
readMemoryTopic,
|
|
10
|
+
writeMemory,
|
|
11
|
+
deleteMemory,
|
|
12
|
+
writeMemoryIndex,
|
|
13
|
+
} from '../lib/core.js';
|
|
14
|
+
import { readJson, sendJson, sendError } from '../lib/http.js';
|
|
15
|
+
|
|
16
|
+
/** Acepta el slug o el nombre de archivo (`x` o `x.md`). */
|
|
17
|
+
function topicFile(name) {
|
|
18
|
+
const raw = String(name ?? '');
|
|
19
|
+
return /\.md$/i.test(raw) ? raw : `${raw}.md`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function memoryView(cwd) {
|
|
23
|
+
return {
|
|
24
|
+
root: memoryRoot(cwd),
|
|
25
|
+
index: readMemoryIndex(cwd),
|
|
26
|
+
topics: listMemoryTopics(cwd),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerMemory(route, ctx) {
|
|
31
|
+
route('GET', '/api/memory', ({ res }) => {
|
|
32
|
+
sendJson(res, 200, memoryView(ctx.cwd));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
route('PUT', '/api/memory', async ({ req, res }) => {
|
|
36
|
+
const body = await readJson(req);
|
|
37
|
+
await writeMemoryIndex(ctx.cwd, String(body.index ?? ''));
|
|
38
|
+
sendJson(res, 200, memoryView(ctx.cwd));
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
route('GET', '/api/memory/:name', ({ res, params }) => {
|
|
42
|
+
const topic = readMemoryTopic(ctx.cwd, topicFile(params.name));
|
|
43
|
+
if (!topic) return sendError(res, 404, 'Memoria no encontrada.');
|
|
44
|
+
sendJson(res, 200, { topic });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
route('PUT', '/api/memory/:name', async ({ req, res, params }) => {
|
|
48
|
+
const file = topicFile(params.name);
|
|
49
|
+
if (!isMemoryTopic(file)) return sendError(res, 400, 'Nombre de memoria inválido.');
|
|
50
|
+
const body = await readJson(req);
|
|
51
|
+
const content = String(body.body ?? body.content ?? '').trim();
|
|
52
|
+
if (!content) return sendError(res, 400, 'La memoria necesita contenido.');
|
|
53
|
+
await writeMemory(ctx.cwd, {
|
|
54
|
+
topic: file.replace(/\.md$/i, ''),
|
|
55
|
+
title: body.title,
|
|
56
|
+
content,
|
|
57
|
+
});
|
|
58
|
+
sendJson(res, 200, memoryView(ctx.cwd));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
route('DELETE', '/api/memory/:name', async ({ res, params }) => {
|
|
62
|
+
const removed = await deleteMemory(ctx.cwd, topicFile(params.name));
|
|
63
|
+
if (!removed) return sendError(res, 404, 'Memoria no encontrada.');
|
|
64
|
+
sendJson(res, 200, memoryView(ctx.cwd));
|
|
65
|
+
});
|
|
66
|
+
}
|
|
@@ -36,6 +36,7 @@ import { registerModels } from './routes/models.js';
|
|
|
36
36
|
import { registerProviders } from './routes/providers.js';
|
|
37
37
|
import { registerMcp } from './routes/mcp.js';
|
|
38
38
|
import { registerSkills } from './routes/skills.js';
|
|
39
|
+
import { registerMemory } from './routes/memory.js';
|
|
39
40
|
import { registerChannels } from './routes/channels.js';
|
|
40
41
|
import { registerProfile } from './routes/profile.js';
|
|
41
42
|
import { registerTts } from './routes/tts.js';
|
|
@@ -208,6 +209,8 @@ export async function createTamnoraServer({
|
|
|
208
209
|
profileText: () => profilePrompt(resolveProfile(ctx.app)),
|
|
209
210
|
skillsText: () => skillsBlock(ctx),
|
|
210
211
|
scopeText: () => scopeDirective(ctx.effectiveStrictScope()),
|
|
212
|
+
ruleFiles: config.rules ?? [],
|
|
213
|
+
memory: config.memory?.enabled !== false,
|
|
211
214
|
});
|
|
212
215
|
ctx.systemPrompt = (await ctx.systemPromptFor(cwd)) || DEFAULT_SYSTEM_PROMPT;
|
|
213
216
|
ctx.agentTools = createAgentTools(ctx);
|
|
@@ -245,6 +248,7 @@ export async function createTamnoraServer({
|
|
|
245
248
|
registerProviders(route, ctx);
|
|
246
249
|
registerMcp(route, ctx);
|
|
247
250
|
registerSkills(route, ctx);
|
|
251
|
+
registerMemory(route, ctx);
|
|
248
252
|
registerChannels(route, ctx);
|
|
249
253
|
registerProfile(route, ctx);
|
|
250
254
|
registerTts(route, ctx);
|