@danieltmn/tamnora 0.12.0 → 0.13.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/CHANGELOG.md +15 -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 +161 -0
- package/harness/tamnora.example.json +6 -0
- package/package.json +1 -1
|
@@ -1056,6 +1056,167 @@ test('skills: indice lazy por defecto (sin body)', async () => {
|
|
|
1056
1056
|
}
|
|
1057
1057
|
});
|
|
1058
1058
|
|
|
1059
|
+
test('manages project memory from the console', async () => {
|
|
1060
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-'));
|
|
1061
|
+
const { instance, base } = await startServer({ cwd });
|
|
1062
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1063
|
+
try {
|
|
1064
|
+
let res = await fetch(`${base}/api/memory/arquitectura`, {
|
|
1065
|
+
method: 'PUT',
|
|
1066
|
+
headers: json,
|
|
1067
|
+
body: JSON.stringify({ title: 'Arquitectura', body: 'Usa SQLite.' }),
|
|
1068
|
+
});
|
|
1069
|
+
assert.equal(res.status, 200);
|
|
1070
|
+
let data = await res.json();
|
|
1071
|
+
assert.equal(data.topics.length, 1);
|
|
1072
|
+
assert.match(data.index, /arquitectura\.md/);
|
|
1073
|
+
|
|
1074
|
+
const detail = await (await fetch(`${base}/api/memory/arquitectura`)).json();
|
|
1075
|
+
assert.match(detail.topic.body, /Usa SQLite/);
|
|
1076
|
+
|
|
1077
|
+
await fetch(`${base}/api/memory`, {
|
|
1078
|
+
method: 'PUT',
|
|
1079
|
+
headers: json,
|
|
1080
|
+
body: JSON.stringify({ index: '# Memoria\n\n- propio' }),
|
|
1081
|
+
});
|
|
1082
|
+
data = await (await fetch(`${base}/api/memory`)).json();
|
|
1083
|
+
assert.match(data.index, /propio/);
|
|
1084
|
+
|
|
1085
|
+
res = await fetch(`${base}/api/memory/arquitectura`, { method: 'DELETE' });
|
|
1086
|
+
assert.equal(res.status, 200);
|
|
1087
|
+
data = await res.json();
|
|
1088
|
+
assert.equal(data.topics.length, 0);
|
|
1089
|
+
} finally {
|
|
1090
|
+
await instance.close();
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
|
|
1094
|
+
test('project memory is injected into the system prompt', async () => {
|
|
1095
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-prompt-'));
|
|
1096
|
+
await mkdir(path.join(cwd, '.tamnora', 'memory'), { recursive: true });
|
|
1097
|
+
await writeFile(path.join(cwd, '.tamnora', 'memory', 'MEMORY.md'), '# Memoria\n\n- [Dato](dato.md) — clave\n');
|
|
1098
|
+
let captured = '';
|
|
1099
|
+
const provider = {
|
|
1100
|
+
id: 'capture',
|
|
1101
|
+
async streamChat(_config, { messages }) {
|
|
1102
|
+
captured = messages?.[0]?.content ?? '';
|
|
1103
|
+
return { content: 'ok', reasoning: '', toolCalls: [] };
|
|
1104
|
+
},
|
|
1105
|
+
};
|
|
1106
|
+
const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
|
|
1107
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1108
|
+
try {
|
|
1109
|
+
const created = await (
|
|
1110
|
+
await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
|
|
1111
|
+
).json();
|
|
1112
|
+
await (
|
|
1113
|
+
await fetch(`${base}/api/chat`, {
|
|
1114
|
+
method: 'POST',
|
|
1115
|
+
headers: json,
|
|
1116
|
+
body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'hola' }),
|
|
1117
|
+
})
|
|
1118
|
+
).text();
|
|
1119
|
+
assert.match(captured, /Memoria del proyecto/);
|
|
1120
|
+
assert.match(captured, /dato\.md/);
|
|
1121
|
+
} finally {
|
|
1122
|
+
await instance.close();
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
test('the memory tool writes project memory', async () => {
|
|
1127
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-tool-'));
|
|
1128
|
+
await writeFile(path.join(cwd, 'tamnora.json'), JSON.stringify({ permissions: { memory: 'allow' } }));
|
|
1129
|
+
const provider = {
|
|
1130
|
+
id: 'memtool',
|
|
1131
|
+
async streamChat(_config, { messages }) {
|
|
1132
|
+
if (messages.some((m) => m.role === 'tool' && m.name === 'memory')) {
|
|
1133
|
+
return { content: 'listo', reasoning: '', toolCalls: [] };
|
|
1134
|
+
}
|
|
1135
|
+
return {
|
|
1136
|
+
content: '',
|
|
1137
|
+
reasoning: '',
|
|
1138
|
+
toolCalls: [
|
|
1139
|
+
{
|
|
1140
|
+
id: '1',
|
|
1141
|
+
type: 'function',
|
|
1142
|
+
function: { name: 'memory', arguments: JSON.stringify({ action: 'write', topic: 'nota', content: 'Hola memoria.' }) },
|
|
1143
|
+
},
|
|
1144
|
+
],
|
|
1145
|
+
};
|
|
1146
|
+
},
|
|
1147
|
+
};
|
|
1148
|
+
const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
|
|
1149
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1150
|
+
try {
|
|
1151
|
+
const created = await (
|
|
1152
|
+
await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
|
|
1153
|
+
).json();
|
|
1154
|
+
await (
|
|
1155
|
+
await fetch(`${base}/api/chat`, {
|
|
1156
|
+
method: 'POST',
|
|
1157
|
+
headers: json,
|
|
1158
|
+
body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'recordá esto' }),
|
|
1159
|
+
})
|
|
1160
|
+
).text();
|
|
1161
|
+
const file = await readFile(path.join(cwd, '.tamnora', 'memory', 'nota.md'), 'utf8');
|
|
1162
|
+
assert.match(file, /Hola memoria/);
|
|
1163
|
+
} finally {
|
|
1164
|
+
await instance.close();
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
test('rules by path are injected (transiently) when a tool touches a matching file', async () => {
|
|
1169
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-rules-inject-'));
|
|
1170
|
+
await mkdir(path.join(cwd, '.tamnora', 'rules'), { recursive: true });
|
|
1171
|
+
await writeFile(path.join(cwd, '.tamnora', 'rules', 'estilo.md'), '---\npaths: src/**\n---\nMARCADOR_ESTILO: usá 2 espacios.');
|
|
1172
|
+
await mkdir(path.join(cwd, 'src'), { recursive: true });
|
|
1173
|
+
await writeFile(path.join(cwd, 'src', 'index.js'), 'export const x = 1;\n');
|
|
1174
|
+
let captured = null;
|
|
1175
|
+
const provider = {
|
|
1176
|
+
id: 'rules',
|
|
1177
|
+
async streamChat(_config, { messages }) {
|
|
1178
|
+
if (messages.some((m) => m.role === 'tool' && m.name === 'read')) {
|
|
1179
|
+
captured = messages;
|
|
1180
|
+
return { content: 'ok', reasoning: '', toolCalls: [] };
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
content: '',
|
|
1184
|
+
reasoning: '',
|
|
1185
|
+
toolCalls: [
|
|
1186
|
+
{ id: 'r1', type: 'function', function: { name: 'read', arguments: JSON.stringify({ path: 'src/index.js' }) } },
|
|
1187
|
+
],
|
|
1188
|
+
};
|
|
1189
|
+
},
|
|
1190
|
+
};
|
|
1191
|
+
const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
|
|
1192
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1193
|
+
try {
|
|
1194
|
+
const created = await (
|
|
1195
|
+
await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
|
|
1196
|
+
).json();
|
|
1197
|
+
await (
|
|
1198
|
+
await fetch(`${base}/api/chat`, {
|
|
1199
|
+
method: 'POST',
|
|
1200
|
+
headers: json,
|
|
1201
|
+
body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'mirá src/index.js' }),
|
|
1202
|
+
})
|
|
1203
|
+
).text();
|
|
1204
|
+
assert.ok(captured, 'el provider no recibió una segunda ronda');
|
|
1205
|
+
assert.ok(
|
|
1206
|
+
captured.some((m) => m.role === 'system' && /MARCADOR_ESTILO/.test(m.content)),
|
|
1207
|
+
'no se inyectó la regla por path'
|
|
1208
|
+
);
|
|
1209
|
+
// No se persiste: el historial guardado no incluye la regla efímera.
|
|
1210
|
+
const session = await (await fetch(`${base}/api/sessions/${created.session.id}`)).json();
|
|
1211
|
+
assert.ok(
|
|
1212
|
+
session.messages.every((m) => !(m.role === 'system' && /MARCADOR_ESTILO/.test(String(m.content ?? '')))),
|
|
1213
|
+
'la regla efímera se persistió en el historial'
|
|
1214
|
+
);
|
|
1215
|
+
} finally {
|
|
1216
|
+
await instance.close();
|
|
1217
|
+
}
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1059
1220
|
test('skills: modo eager inyecta el body en el system prompt', async () => {
|
|
1060
1221
|
let captured = '';
|
|
1061
1222
|
const captureProvider = {
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
"maxRounds": 0,
|
|
6
6
|
"maxTokens": 0,
|
|
7
7
|
"maxDurationMs": 0,
|
|
8
|
+
"maxOutputTokens": 0,
|
|
9
|
+
"promptCaching": false,
|
|
10
|
+
"skills": { "mode": "lazy" },
|
|
11
|
+
"compact": { "enabled": true, "threshold": 0.75, "keepLast": 8 },
|
|
12
|
+
"rules": [".tamnora/rules/*.md"],
|
|
13
|
+
"memory": { "enabled": true },
|
|
8
14
|
"tools": [],
|
|
9
15
|
"permissions": {},
|
|
10
16
|
"mcp": [
|
package/package.json
CHANGED