@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
|
@@ -728,11 +728,15 @@ test('approving clears the permission instead of bringing it back', async () =>
|
|
|
728
728
|
body: JSON.stringify({ allow: true }),
|
|
729
729
|
});
|
|
730
730
|
assert.equal(decision.status, 200);
|
|
731
|
-
approved.push(ask.askId);
|
|
732
|
-
// Inmediatamente despues de resolver, no queda pendiente: si
|
|
733
|
-
// el cliente lo re-pintaria en bucle.
|
|
734
|
-
|
|
735
|
-
|
|
731
|
+
approved.push(ask.askId);
|
|
732
|
+
// Inmediatamente despues de resolver, ese permiso no queda pendiente: si
|
|
733
|
+
// volviera, el cliente lo re-pintaria en bucle. (Puede haber otro askId
|
|
734
|
+
// nuevo del siguiente paso, pero el resuelto no puede reaparecer.)
|
|
735
|
+
const after = await (await fetch(`${base}/api/state`)).json();
|
|
736
|
+
assert.ok(
|
|
737
|
+
!after.permissions.some((p) => p.askId === ask.askId),
|
|
738
|
+
'el permiso resuelto no debe reaparecer'
|
|
739
|
+
);
|
|
736
740
|
}
|
|
737
741
|
}
|
|
738
742
|
// Tres peticiones del agente -> tres askId distintos, ninguno revivido.
|
|
@@ -1056,6 +1060,167 @@ test('skills: indice lazy por defecto (sin body)', async () => {
|
|
|
1056
1060
|
}
|
|
1057
1061
|
});
|
|
1058
1062
|
|
|
1063
|
+
test('manages project memory from the console', async () => {
|
|
1064
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-'));
|
|
1065
|
+
const { instance, base } = await startServer({ cwd });
|
|
1066
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1067
|
+
try {
|
|
1068
|
+
let res = await fetch(`${base}/api/memory/arquitectura`, {
|
|
1069
|
+
method: 'PUT',
|
|
1070
|
+
headers: json,
|
|
1071
|
+
body: JSON.stringify({ title: 'Arquitectura', body: 'Usa SQLite.' }),
|
|
1072
|
+
});
|
|
1073
|
+
assert.equal(res.status, 200);
|
|
1074
|
+
let data = await res.json();
|
|
1075
|
+
assert.equal(data.topics.length, 1);
|
|
1076
|
+
assert.match(data.index, /arquitectura\.md/);
|
|
1077
|
+
|
|
1078
|
+
const detail = await (await fetch(`${base}/api/memory/arquitectura`)).json();
|
|
1079
|
+
assert.match(detail.topic.body, /Usa SQLite/);
|
|
1080
|
+
|
|
1081
|
+
await fetch(`${base}/api/memory`, {
|
|
1082
|
+
method: 'PUT',
|
|
1083
|
+
headers: json,
|
|
1084
|
+
body: JSON.stringify({ index: '# Memoria\n\n- propio' }),
|
|
1085
|
+
});
|
|
1086
|
+
data = await (await fetch(`${base}/api/memory`)).json();
|
|
1087
|
+
assert.match(data.index, /propio/);
|
|
1088
|
+
|
|
1089
|
+
res = await fetch(`${base}/api/memory/arquitectura`, { method: 'DELETE' });
|
|
1090
|
+
assert.equal(res.status, 200);
|
|
1091
|
+
data = await res.json();
|
|
1092
|
+
assert.equal(data.topics.length, 0);
|
|
1093
|
+
} finally {
|
|
1094
|
+
await instance.close();
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test('project memory is injected into the system prompt', async () => {
|
|
1099
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-prompt-'));
|
|
1100
|
+
await mkdir(path.join(cwd, '.tamnora', 'memory'), { recursive: true });
|
|
1101
|
+
await writeFile(path.join(cwd, '.tamnora', 'memory', 'MEMORY.md'), '# Memoria\n\n- [Dato](dato.md) — clave\n');
|
|
1102
|
+
let captured = '';
|
|
1103
|
+
const provider = {
|
|
1104
|
+
id: 'capture',
|
|
1105
|
+
async streamChat(_config, { messages }) {
|
|
1106
|
+
captured = messages?.[0]?.content ?? '';
|
|
1107
|
+
return { content: 'ok', reasoning: '', toolCalls: [] };
|
|
1108
|
+
},
|
|
1109
|
+
};
|
|
1110
|
+
const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
|
|
1111
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1112
|
+
try {
|
|
1113
|
+
const created = await (
|
|
1114
|
+
await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
|
|
1115
|
+
).json();
|
|
1116
|
+
await (
|
|
1117
|
+
await fetch(`${base}/api/chat`, {
|
|
1118
|
+
method: 'POST',
|
|
1119
|
+
headers: json,
|
|
1120
|
+
body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'hola' }),
|
|
1121
|
+
})
|
|
1122
|
+
).text();
|
|
1123
|
+
assert.match(captured, /Memoria del proyecto/);
|
|
1124
|
+
assert.match(captured, /dato\.md/);
|
|
1125
|
+
} finally {
|
|
1126
|
+
await instance.close();
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test('the memory tool writes project memory', async () => {
|
|
1131
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-mem-tool-'));
|
|
1132
|
+
await writeFile(path.join(cwd, 'tamnora.json'), JSON.stringify({ permissions: { memory: 'allow' } }));
|
|
1133
|
+
const provider = {
|
|
1134
|
+
id: 'memtool',
|
|
1135
|
+
async streamChat(_config, { messages }) {
|
|
1136
|
+
if (messages.some((m) => m.role === 'tool' && m.name === 'memory')) {
|
|
1137
|
+
return { content: 'listo', reasoning: '', toolCalls: [] };
|
|
1138
|
+
}
|
|
1139
|
+
return {
|
|
1140
|
+
content: '',
|
|
1141
|
+
reasoning: '',
|
|
1142
|
+
toolCalls: [
|
|
1143
|
+
{
|
|
1144
|
+
id: '1',
|
|
1145
|
+
type: 'function',
|
|
1146
|
+
function: { name: 'memory', arguments: JSON.stringify({ action: 'write', topic: 'nota', content: 'Hola memoria.' }) },
|
|
1147
|
+
},
|
|
1148
|
+
],
|
|
1149
|
+
};
|
|
1150
|
+
},
|
|
1151
|
+
};
|
|
1152
|
+
const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
|
|
1153
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1154
|
+
try {
|
|
1155
|
+
const created = await (
|
|
1156
|
+
await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
|
|
1157
|
+
).json();
|
|
1158
|
+
await (
|
|
1159
|
+
await fetch(`${base}/api/chat`, {
|
|
1160
|
+
method: 'POST',
|
|
1161
|
+
headers: json,
|
|
1162
|
+
body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'recordá esto' }),
|
|
1163
|
+
})
|
|
1164
|
+
).text();
|
|
1165
|
+
const file = await readFile(path.join(cwd, '.tamnora', 'memory', 'nota.md'), 'utf8');
|
|
1166
|
+
assert.match(file, /Hola memoria/);
|
|
1167
|
+
} finally {
|
|
1168
|
+
await instance.close();
|
|
1169
|
+
}
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
test('rules by path are injected (transiently) when a tool touches a matching file', async () => {
|
|
1173
|
+
const cwd = await mkdtemp(path.join(tmpdir(), 'tamnora-rules-inject-'));
|
|
1174
|
+
await mkdir(path.join(cwd, '.tamnora', 'rules'), { recursive: true });
|
|
1175
|
+
await writeFile(path.join(cwd, '.tamnora', 'rules', 'estilo.md'), '---\npaths: src/**\n---\nMARCADOR_ESTILO: usá 2 espacios.');
|
|
1176
|
+
await mkdir(path.join(cwd, 'src'), { recursive: true });
|
|
1177
|
+
await writeFile(path.join(cwd, 'src', 'index.js'), 'export const x = 1;\n');
|
|
1178
|
+
let captured = null;
|
|
1179
|
+
const provider = {
|
|
1180
|
+
id: 'rules',
|
|
1181
|
+
async streamChat(_config, { messages }) {
|
|
1182
|
+
if (messages.some((m) => m.role === 'tool' && m.name === 'read')) {
|
|
1183
|
+
captured = messages;
|
|
1184
|
+
return { content: 'ok', reasoning: '', toolCalls: [] };
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
content: '',
|
|
1188
|
+
reasoning: '',
|
|
1189
|
+
toolCalls: [
|
|
1190
|
+
{ id: 'r1', type: 'function', function: { name: 'read', arguments: JSON.stringify({ path: 'src/index.js' }) } },
|
|
1191
|
+
],
|
|
1192
|
+
};
|
|
1193
|
+
},
|
|
1194
|
+
};
|
|
1195
|
+
const { instance, base } = await startServer({ cwd, resolveProvider: () => ({ provider, config: {} }) });
|
|
1196
|
+
const json = { 'Content-Type': 'application/json' };
|
|
1197
|
+
try {
|
|
1198
|
+
const created = await (
|
|
1199
|
+
await fetch(`${base}/api/sessions`, { method: 'POST', headers: json, body: JSON.stringify({ title: 't', model: 'm', cwd }) })
|
|
1200
|
+
).json();
|
|
1201
|
+
await (
|
|
1202
|
+
await fetch(`${base}/api/chat`, {
|
|
1203
|
+
method: 'POST',
|
|
1204
|
+
headers: json,
|
|
1205
|
+
body: JSON.stringify({ sessionId: created.session.id, model: 'm', input: 'mirá src/index.js' }),
|
|
1206
|
+
})
|
|
1207
|
+
).text();
|
|
1208
|
+
assert.ok(captured, 'el provider no recibió una segunda ronda');
|
|
1209
|
+
assert.ok(
|
|
1210
|
+
captured.some((m) => m.role === 'system' && /MARCADOR_ESTILO/.test(m.content)),
|
|
1211
|
+
'no se inyectó la regla por path'
|
|
1212
|
+
);
|
|
1213
|
+
// No se persiste: el historial guardado no incluye la regla efímera.
|
|
1214
|
+
const session = await (await fetch(`${base}/api/sessions/${created.session.id}`)).json();
|
|
1215
|
+
assert.ok(
|
|
1216
|
+
session.messages.every((m) => !(m.role === 'system' && /MARCADOR_ESTILO/.test(String(m.content ?? '')))),
|
|
1217
|
+
'la regla efímera se persistió en el historial'
|
|
1218
|
+
);
|
|
1219
|
+
} finally {
|
|
1220
|
+
await instance.close();
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1059
1224
|
test('skills: modo eager inyecta el body en el system prompt', async () => {
|
|
1060
1225
|
let captured = '';
|
|
1061
1226
|
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