@danieltmn/tamnora 0.11.3 → 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 +39 -0
- package/README.md +7 -1
- package/harness/packages/cli/src/commands/repl.js +18 -5
- package/harness/packages/cli/src/commands/run.js +11 -2
- package/harness/packages/cli/src/commands/tui.js +13 -3
- package/harness/packages/cli/src/lib/runtime.js +2 -0
- package/harness/packages/cli/src/tui/app.js +22 -0
- package/harness/packages/core/src/agent/compact.js +38 -0
- package/harness/packages/core/src/agent/context.js +13 -8
- package/harness/packages/core/src/agent/runAgent.js +35 -4
- package/harness/packages/core/src/agent/scope.js +15 -0
- package/harness/packages/core/src/config/index.js +6 -0
- package/harness/packages/core/src/index.js +36 -1
- package/harness/packages/core/src/memory/index.js +220 -0
- package/harness/packages/core/src/providers/cache.js +29 -0
- package/harness/packages/core/src/providers/ollama.js +3 -2
- package/harness/packages/core/src/providers/openaiCompatible.js +9 -2
- package/harness/packages/core/src/providers/openrouter.js +6 -1
- package/harness/packages/core/src/providers/presets.js +7 -4
- package/harness/packages/core/src/providers/stream.js +45 -4
- package/harness/packages/core/src/rules/index.js +151 -0
- package/harness/packages/core/src/skills/index.js +21 -0
- package/harness/packages/core/src/tools/executors.js +21 -13
- package/harness/packages/core/src/tools/index.js +2 -2
- package/harness/packages/core/src/tools/registry.js +3 -2
- package/harness/packages/core/test/agent.test.js +38 -0
- package/harness/packages/core/test/compact.test.js +33 -0
- package/harness/packages/core/test/context.test.js +14 -0
- package/harness/packages/core/test/memory.test.js +96 -0
- package/harness/packages/core/test/providers.test.js +66 -0
- package/harness/packages/core/test/rules.test.js +64 -0
- package/harness/packages/core/test/skills.test.js +10 -1
- package/harness/packages/core/test/stream.test.js +20 -0
- package/harness/packages/core/test/tools.test.js +18 -0
- package/harness/packages/server/public/app.js +116 -5
- package/harness/packages/server/public/index.html +49 -1
- package/harness/packages/server/src/lib/agentRun.js +8 -8
- package/harness/packages/server/src/lib/agentTools.js +78 -8
- package/harness/packages/server/src/lib/approvals.js +7 -0
- package/harness/packages/server/src/lib/channels/telegram.js +12 -1
- package/harness/packages/server/src/lib/compact.js +77 -0
- package/harness/packages/server/src/lib/prompt.js +17 -3
- package/harness/packages/server/src/lib/rules.js +43 -0
- package/harness/packages/server/src/lib/skills.js +12 -1
- package/harness/packages/server/src/lib/tasks.js +2 -1
- package/harness/packages/server/src/routes/chat.js +24 -2
- package/harness/packages/server/src/routes/memory.js +66 -0
- package/harness/packages/server/src/routes/providers.js +4 -1
- package/harness/packages/server/src/routes/sessions.js +4 -50
- package/harness/packages/server/src/server.js +12 -2
- package/harness/packages/server/test/scope.test.js +68 -0
- package/harness/packages/server/test/server.test.js +291 -49
- package/harness/tamnora.example.json +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,45 @@ Todos los cambios relevantes de este proyecto se documentan en este archivo.
|
|
|
5
5
|
El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/)
|
|
6
6
|
y el proyecto usa [Versionado Semantico](https://semver.org/lang/es/).
|
|
7
7
|
|
|
8
|
+
## [0.13.0] - 2026-10-10
|
|
9
|
+
|
|
10
|
+
### Agregado
|
|
11
|
+
|
|
12
|
+
- **Fase 2 del roadmap del server (memoria y reglas)**:
|
|
13
|
+
- **Memoria durable por proyecto** (`.tamnora/memory/`): `MEMORY.md` como índice
|
|
14
|
+
y un archivo por tema. La maneja el agente con la tool `memory`
|
|
15
|
+
(`read | write | search | list`; `write` = `ask`) y se inyecta un resumen en el
|
|
16
|
+
system prompt de cada corrida. Nueva pestaña **Memoria** en la consola y
|
|
17
|
+
`GET/PUT/DELETE /api/memory`.
|
|
18
|
+
- **Reglas por path** (`rules: ['.tamnora/rules/*.md']`): archivos `.md` con
|
|
19
|
+
frontmatter `paths:` (globs). Las estáticas entran al system prompt; las
|
|
20
|
+
contextuales se inyectan en runtime cuando una tool toca un archivo que
|
|
21
|
+
matchea, de forma efímera (no ensucian el historial).
|
|
22
|
+
|
|
23
|
+
## [0.12.0] - 2026-10-10
|
|
24
|
+
|
|
25
|
+
### Agregado
|
|
26
|
+
|
|
27
|
+
- **Fase 1 del roadmap del server (contexto y costo)**:
|
|
28
|
+
- **Skills on-demand**: se inyecta un índice (`name` + `when`) y el modelo carga
|
|
29
|
+
el cuerpo con la tool `skill({ name })`. Config `skills.mode: lazy|eager`
|
|
30
|
+
(lazy por defecto). Ahorra tokens en cada turno.
|
|
31
|
+
- **`max_tokens` por ronda** (`maxOutputTokens`; 0 = default del proveedor), en
|
|
32
|
+
providers, `/api/settings` y CLI.
|
|
33
|
+
- **Prompt caching opt-in** (`promptCaching`): `cache_control` para Anthropic
|
|
34
|
+
(OpenRouter/Anthropic) y `prompt_cache_key` para OpenAI.
|
|
35
|
+
- **Auto-compactación** (`compact: { enabled, threshold: 0.75, keepLast: 8 }`):
|
|
36
|
+
resume lo viejo y lo reemplaza por `meta.summary` + `meta.summaryCursor` sin
|
|
37
|
+
borrar el historial. Evento `compacted`.
|
|
38
|
+
- **Seam de ciclo de vida** en `runAgent` (`run.start/end`, `tool.before/after`),
|
|
39
|
+
base para checkpoints y hooks.
|
|
40
|
+
|
|
41
|
+
### Arreglado
|
|
42
|
+
|
|
43
|
+
- `trimContext` ya no descarta el objetivo/resumen de la sesión al recortar
|
|
44
|
+
(protege los mensajes system iniciales).
|
|
45
|
+
- `effectiveStrictScope` no se exponía en `ctx`, lo que rompía `GET /api/settings`.
|
|
46
|
+
|
|
8
47
|
## [0.11.3] - 2026-10-09
|
|
9
48
|
|
|
10
49
|
### Cambios
|
package/README.md
CHANGED
|
@@ -52,6 +52,8 @@ Atajos: `enter` enviar · `↑/↓` historial · `←/→` cursor · `home/end`
|
|
|
52
52
|
`/limpiar`. El pegado de bloques multilinea entra como un solo texto y el header
|
|
53
53
|
muestra el contexto usado (`ctx <tokens>`). Las tools `write`/`edit` muestran un
|
|
54
54
|
**diff lado a lado** (viejo en rojo, nuevo en verde, con numeros de linea).
|
|
55
|
+
`/estricto` (o `--strict`) prende el modo **solo lo pedido**: el agente hace
|
|
56
|
+
exactamente lo que pedis, sin refactors ni cambios fuera del pedido.
|
|
55
57
|
|
|
56
58
|
### Server como servicio
|
|
57
59
|
|
|
@@ -106,7 +108,11 @@ menu "mas" (los tres puntitos). En el chat:
|
|
|
106
108
|
menu `⋮`; el indicador de "pensando/trabajando" vive en el pie (`#sb-activity`).
|
|
107
109
|
- **Runs durables**: cerrar el celular o cortar la red **no cancela** el turno; al
|
|
108
110
|
volver, la consola **reconecta** y reconstruye el progreso. Para cortarlo se usa
|
|
109
|
-
el boton de detener (o `POST /api/sessions/:id/stop`)
|
|
111
|
+
el boton de detener (o `POST /api/sessions/:id/stop`), que ademas corta el stream
|
|
112
|
+
del modelo y mata las tools de larga duracion (p. ej. `bash`) en curso.
|
|
113
|
+
- **Solo lo pedido (estricto)**: el toggle de la pestaña Agente (o `PUT
|
|
114
|
+
/api/settings` con `strictScope`) hace que el agente se limite a lo solicitado,
|
|
115
|
+
sin mejoras ni cambios extra; se combina con Plan o Build.
|
|
110
116
|
- **Audio**: un unico reproductor encola la narracion, el tool `speak`, la
|
|
111
117
|
respuesta final y los consejos del asesor, y reproduce de a uno con pausa y
|
|
112
118
|
detener (Ajustes -> Voz).
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
Store,
|
|
7
7
|
createTools,
|
|
8
8
|
buildMcpRegistry,
|
|
9
|
+
scopeDirective,
|
|
9
10
|
} from '../lib/core.js';
|
|
10
11
|
import { parse, databasePath, num } from '../lib/util.js';
|
|
11
12
|
import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider } from '../lib/runtime.js';
|
|
@@ -18,6 +19,8 @@ const OPTIONS = {
|
|
|
18
19
|
'max-steps': { type: 'string' },
|
|
19
20
|
tools: { type: 'string' },
|
|
20
21
|
'no-tools': { type: 'boolean' },
|
|
22
|
+
strict: { type: 'boolean' },
|
|
23
|
+
'no-strict': { type: 'boolean' },
|
|
21
24
|
yes: { type: 'boolean', short: 'y' },
|
|
22
25
|
};
|
|
23
26
|
|
|
@@ -26,9 +29,13 @@ export async function replCommand(argv) {
|
|
|
26
29
|
const cwd = values.dir ? path.resolve(values.dir) : process.cwd();
|
|
27
30
|
const config = await loadConfig(cwd);
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
const instructions = await loadInstructions(cwd, config.instructions);
|
|
33
|
+
let systemExtra = '';
|
|
30
34
|
const maxRounds = num(values['max-steps'], config.maxRounds);
|
|
31
35
|
const store = new Store(databasePath(cwd));
|
|
36
|
+
let strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
|
|
37
|
+
const buildPrompt = () =>
|
|
38
|
+
buildSystemPrompt(instructions, [systemExtra, scopeDirective(strict)].filter(Boolean).join('\n\n'));
|
|
32
39
|
const resolved = resolveCliProvider(store, config, { provider: values.provider, model: values.model });
|
|
33
40
|
let providerId = resolved.providerId;
|
|
34
41
|
let model = resolved.model;
|
|
@@ -108,7 +115,7 @@ export async function replCommand(argv) {
|
|
|
108
115
|
} else if (cmd === 'help') {
|
|
109
116
|
process.stdout.write(
|
|
110
117
|
`${color.dim(
|
|
111
|
-
'/model <id> /provider <id> /system <texto> /clear /session /exit'
|
|
118
|
+
'/model <id> /provider <id> /system <texto> /strict /clear /session /exit'
|
|
112
119
|
)}\n`
|
|
113
120
|
);
|
|
114
121
|
} else if (cmd === 'model') {
|
|
@@ -118,8 +125,13 @@ export async function replCommand(argv) {
|
|
|
118
125
|
if (arg) providerId = arg;
|
|
119
126
|
process.stdout.write(color.dim(`proveedor: ${providerId}\n`));
|
|
120
127
|
} else if (cmd === 'system') {
|
|
121
|
-
if (arg)
|
|
122
|
-
process.stdout.write(color.dim(`system prompt (${
|
|
128
|
+
if (arg) systemExtra = arg;
|
|
129
|
+
process.stdout.write(color.dim(`system prompt (${buildPrompt().length} chars)\n`));
|
|
130
|
+
} else if (cmd === 'strict' || cmd === 'estricto') {
|
|
131
|
+
if (arg) strict = /^(on|si|sí|1|true)$/i.test(arg);
|
|
132
|
+
else strict = !strict;
|
|
133
|
+
store.setSetting('strictScope', strict);
|
|
134
|
+
process.stdout.write(color.dim(`solo lo pedido: ${strict ? 'activado' : 'desactivado'}\n`));
|
|
123
135
|
} else if (cmd === 'clear') {
|
|
124
136
|
history = [];
|
|
125
137
|
process.stdout.write(color.dim('historial limpiado\n'));
|
|
@@ -142,12 +154,13 @@ export async function replCommand(argv) {
|
|
|
142
154
|
baseUrl,
|
|
143
155
|
apiKey,
|
|
144
156
|
customProviders,
|
|
145
|
-
systemPrompt,
|
|
157
|
+
systemPrompt: buildPrompt(),
|
|
146
158
|
history,
|
|
147
159
|
input: text,
|
|
148
160
|
maxRounds,
|
|
149
161
|
maxTokens: config.maxTokens,
|
|
150
162
|
maxDurationMs: config.maxDurationMs,
|
|
163
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
151
164
|
tools: toolDefs,
|
|
152
165
|
execute,
|
|
153
166
|
onEvent: printer.onEvent,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { loadConfig, loadInstructions, Store, createTools, buildMcpRegistry } from '../lib/core.js';
|
|
2
|
+
import { loadConfig, loadInstructions, Store, createTools, buildMcpRegistry, scopeDirective } from '../lib/core.js';
|
|
3
3
|
import { parse, databasePath, num } from '../lib/util.js';
|
|
4
4
|
import { buildSystemPrompt, runTurn, createPrinter, resolveCliProvider } from '../lib/runtime.js';
|
|
5
5
|
import { color } from '../ui/color.js';
|
|
@@ -15,6 +15,8 @@ const OPTIONS = {
|
|
|
15
15
|
system: { type: 'string' },
|
|
16
16
|
tools: { type: 'string' },
|
|
17
17
|
'no-tools': { type: 'boolean' },
|
|
18
|
+
strict: { type: 'boolean' },
|
|
19
|
+
'no-strict': { type: 'boolean' },
|
|
18
20
|
yes: { type: 'boolean', short: 'y' },
|
|
19
21
|
help: { type: 'boolean', short: 'h' },
|
|
20
22
|
};
|
|
@@ -95,6 +97,8 @@ export async function runCommand(argv) {
|
|
|
95
97
|
const onSigint = () => controller.abort();
|
|
96
98
|
process.once('SIGINT', onSigint);
|
|
97
99
|
|
|
100
|
+
const strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
|
|
101
|
+
|
|
98
102
|
try {
|
|
99
103
|
const { result, newMessages } = await runTurn({
|
|
100
104
|
providerId,
|
|
@@ -102,12 +106,16 @@ export async function runCommand(argv) {
|
|
|
102
106
|
baseUrl,
|
|
103
107
|
apiKey,
|
|
104
108
|
customProviders,
|
|
105
|
-
systemPrompt: buildSystemPrompt(
|
|
109
|
+
systemPrompt: buildSystemPrompt(
|
|
110
|
+
instructions,
|
|
111
|
+
[values.system, scopeDirective(strict)].filter(Boolean).join('\n\n')
|
|
112
|
+
),
|
|
106
113
|
history,
|
|
107
114
|
input: prompt,
|
|
108
115
|
maxRounds: num(values['max-steps'], config.maxRounds),
|
|
109
116
|
maxTokens: config.maxTokens,
|
|
110
117
|
maxDurationMs: config.maxDurationMs,
|
|
118
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
111
119
|
signal: controller.signal,
|
|
112
120
|
tools: toolDefs,
|
|
113
121
|
execute,
|
|
@@ -163,6 +171,7 @@ Opciones:
|
|
|
163
171
|
--max-steps <n> Techo de rondas de herramientas (0 = sin límite, default)
|
|
164
172
|
--tools <lista> Tools habilitadas (coma), ej. read,list,grep
|
|
165
173
|
--no-tools Deshabilitar todas las tools
|
|
174
|
+
--strict Hacer solo lo pedido (estricto) --no-strict
|
|
166
175
|
-y, --yes Aprobar automáticamente los permisos "ask"
|
|
167
176
|
--json Salida JSON (sin streaming)`);
|
|
168
177
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { loadConfig, loadInstructions, Store, buildMcpRegistry, resolveMode } from '../lib/core.js';
|
|
2
|
+
import { loadConfig, loadInstructions, Store, buildMcpRegistry, resolveMode, scopeDirective } from '../lib/core.js';
|
|
3
3
|
import { parse, databasePath, num } from '../lib/util.js';
|
|
4
4
|
import { buildSystemPrompt, resolveCliProvider } from '../lib/runtime.js';
|
|
5
5
|
import { runTui } from '../tui/app.js';
|
|
@@ -14,6 +14,8 @@ const OPTIONS = {
|
|
|
14
14
|
system: { type: 'string' },
|
|
15
15
|
tools: { type: 'string' },
|
|
16
16
|
'no-tools': { type: 'boolean' },
|
|
17
|
+
strict: { type: 'boolean' },
|
|
18
|
+
'no-strict': { type: 'boolean' },
|
|
17
19
|
yes: { type: 'boolean', short: 'y' },
|
|
18
20
|
help: { type: 'boolean', short: 'h' },
|
|
19
21
|
};
|
|
@@ -63,10 +65,11 @@ export async function tuiCommand(argv) {
|
|
|
63
65
|
: undefined;
|
|
64
66
|
|
|
65
67
|
const mode = resolveMode(values.mode || store.getSetting('activeMode') || 'agent');
|
|
68
|
+
let strict = values.strict ? true : values['no-strict'] ? false : store.getSetting('strictScope') === true;
|
|
66
69
|
const enabledFor = (m) =>
|
|
67
70
|
m.tools ? (baseEnabled ? baseEnabled.filter((tool) => m.tools.includes(tool)) : m.tools) : baseEnabled;
|
|
68
71
|
const promptFor = (m) =>
|
|
69
|
-
buildSystemPrompt(instructions, [values.system, m.systemPrompt].filter(Boolean).join('\n\n'));
|
|
72
|
+
buildSystemPrompt(instructions, [values.system, m.systemPrompt, scopeDirective(strict)].filter(Boolean).join('\n\n'));
|
|
70
73
|
const modeContext = (id) => {
|
|
71
74
|
const m = resolveMode(id);
|
|
72
75
|
return { label: m.label, enabled: enabledFor(m), systemPrompt: promptFor(m) };
|
|
@@ -83,10 +86,16 @@ export async function tuiCommand(argv) {
|
|
|
83
86
|
modeId: mode.id,
|
|
84
87
|
modeContext,
|
|
85
88
|
onModeChange: (id) => store.setSetting('activeMode', id),
|
|
89
|
+
strict,
|
|
90
|
+
onStrictChange: (value) => {
|
|
91
|
+
strict = value;
|
|
92
|
+
store.setSetting('strictScope', value);
|
|
93
|
+
},
|
|
86
94
|
systemPrompt: promptFor(mode),
|
|
87
95
|
maxRounds: num(values['max-steps'], config.maxRounds),
|
|
88
96
|
maxTokens: config.maxTokens,
|
|
89
97
|
maxDurationMs: config.maxDurationMs,
|
|
98
|
+
maxOutputTokens: config.maxOutputTokens,
|
|
90
99
|
toolOptions: {
|
|
91
100
|
root: cwd,
|
|
92
101
|
enabled: enabledFor(mode),
|
|
@@ -106,7 +115,7 @@ Consola full-screen (estilo opencode/codex). Atajos:
|
|
|
106
115
|
enter enviar ↑ / ↓ historial
|
|
107
116
|
← → mover cursor home/end inicio / fin
|
|
108
117
|
pgup/pgdn scroll ctrl+l limpiar
|
|
109
|
-
ctrl+. alternar Plan / Build /modo /salir /limpiar
|
|
118
|
+
ctrl+. alternar Plan / Build /modo /estricto /salir /limpiar
|
|
110
119
|
|
|
111
120
|
Opciones:
|
|
112
121
|
--provider <id> Proveedor (por defecto: el activo)
|
|
@@ -115,5 +124,6 @@ Opciones:
|
|
|
115
124
|
--dir <path> Directorio del proyecto
|
|
116
125
|
--max-steps <n> Techo de rondas de herramientas (0 = sin límite, default)
|
|
117
126
|
--tools <lista> Tools habilitadas (coma) --no-tools
|
|
127
|
+
--strict Hacer solo lo pedido (estricto) --no-strict
|
|
118
128
|
-y, --yes Auto-aprobar tools con permiso "ask"`);
|
|
119
129
|
}
|
|
@@ -54,6 +54,7 @@ export async function runTurn({
|
|
|
54
54
|
maxRounds,
|
|
55
55
|
maxTokens,
|
|
56
56
|
maxDurationMs,
|
|
57
|
+
maxOutputTokens,
|
|
57
58
|
stallWarnMs,
|
|
58
59
|
signal,
|
|
59
60
|
execute,
|
|
@@ -80,6 +81,7 @@ export async function runTurn({
|
|
|
80
81
|
maxRounds,
|
|
81
82
|
maxTokens,
|
|
82
83
|
maxDurationMs,
|
|
84
|
+
maxOutputTokens,
|
|
83
85
|
stallWarnMs,
|
|
84
86
|
signal,
|
|
85
87
|
onEvent,
|
|
@@ -17,10 +17,13 @@ export function runTui({
|
|
|
17
17
|
modeId = 'agent',
|
|
18
18
|
modeContext,
|
|
19
19
|
onModeChange,
|
|
20
|
+
strict = false,
|
|
21
|
+
onStrictChange,
|
|
20
22
|
systemPrompt,
|
|
21
23
|
maxRounds,
|
|
22
24
|
maxTokens,
|
|
23
25
|
maxDurationMs,
|
|
26
|
+
maxOutputTokens,
|
|
24
27
|
toolOptions,
|
|
25
28
|
store,
|
|
26
29
|
sessionId,
|
|
@@ -101,6 +104,18 @@ export function runTui({
|
|
|
101
104
|
draw();
|
|
102
105
|
}
|
|
103
106
|
|
|
107
|
+
function setStrict(value) {
|
|
108
|
+
strict = value;
|
|
109
|
+
onStrictChange?.(value);
|
|
110
|
+
setMode(mode);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function toggleStrict(value) {
|
|
114
|
+
setStrict(value == null ? !strict : value);
|
|
115
|
+
addMessage('assistant', `Solo lo pedido: ${strict ? 'activado' : 'desactivado'}.`);
|
|
116
|
+
draw();
|
|
117
|
+
}
|
|
118
|
+
|
|
104
119
|
function draw() {
|
|
105
120
|
if (closed) return;
|
|
106
121
|
const { frame, cursor } = render(state, {
|
|
@@ -153,6 +168,12 @@ export function runTui({
|
|
|
153
168
|
draw();
|
|
154
169
|
return;
|
|
155
170
|
}
|
|
171
|
+
if (text === '/estricto' || text === '/strict') return toggleStrict();
|
|
172
|
+
const strictArg = text.match(/^\/(?:estricto|strict)\s+(on|off|si|sí|no)$/i);
|
|
173
|
+
if (strictArg) {
|
|
174
|
+
toggleStrict(/^(on|si|sí)$/i.test(strictArg[1]));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
156
177
|
if (text === '/limpiar' || text === '/clear') {
|
|
157
178
|
state.input = '';
|
|
158
179
|
state.messages = [];
|
|
@@ -184,6 +205,7 @@ export function runTui({
|
|
|
184
205
|
maxRounds,
|
|
185
206
|
maxTokens,
|
|
186
207
|
maxDurationMs,
|
|
208
|
+
maxOutputTokens,
|
|
187
209
|
signal: controller.signal,
|
|
188
210
|
tools: toolset.definitions,
|
|
189
211
|
execute: toolset.execute,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Compactación de contexto (auto-compact). La lógica de decisión es pura y
|
|
2
|
+
// testeable; la llamada al modelo para resumir vive en el server.
|
|
3
|
+
|
|
4
|
+
import { estimateTokens } from './context.js';
|
|
5
|
+
|
|
6
|
+
export const COMPACT_INSTRUCTION =
|
|
7
|
+
'Resumí la conversación en un texto conciso que preserve decisiones tomadas, archivos tocados, hallazgos y el estado actual, para poder continuar el trabajo sin el historial completo.';
|
|
8
|
+
|
|
9
|
+
export const SUMMARY_INSTRUCTION =
|
|
10
|
+
'Resumí la conversación para la persona: qué se pidió, qué se hizo, decisiones tomadas, archivos o comandos relevantes y el estado actual. Sé claro y conciso, en español, sin markdown pesado.';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Historial vigente: sin los mensajes del asesor ni lo ya resumido (anterior al
|
|
14
|
+
* cursor de compactación). Si el cursor no está (historial truncado), devuelve
|
|
15
|
+
* todo para no perder contexto.
|
|
16
|
+
*/
|
|
17
|
+
export function liveHistory(history = [], cursor = null) {
|
|
18
|
+
const filtered = history.filter((message) => message.name !== 'advisor');
|
|
19
|
+
if (!cursor) return filtered;
|
|
20
|
+
const idx = filtered.findIndex((message) => message.id === cursor);
|
|
21
|
+
return idx === -1 ? filtered : filtered.slice(idx + 1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Decide si conviene compactar, sobre el historial vigente. Puro (no llama al
|
|
26
|
+
* modelo). Devuelve qué resumir, qué conservar y el cursor (id del último
|
|
27
|
+
* mensaje resumido), o `null` si no hace falta.
|
|
28
|
+
*/
|
|
29
|
+
export function planCompaction(live, { contextTokens = 0, threshold = 0.75, keepLast = 8 } = {}) {
|
|
30
|
+
if (!(contextTokens > 0)) return null;
|
|
31
|
+
const keep = Math.max(0, Number(keepLast) || 0);
|
|
32
|
+
if (!Array.isArray(live) || live.length <= keep + 2) return null;
|
|
33
|
+
if (estimateTokens(live) < contextTokens * threshold) return null;
|
|
34
|
+
const toSummarize = live.slice(0, live.length - keep);
|
|
35
|
+
const cursor = toSummarize.at(-1)?.id;
|
|
36
|
+
if (!cursor) return null;
|
|
37
|
+
return { toSummarize, keep: live.slice(live.length - keep), cursor };
|
|
38
|
+
}
|
|
@@ -40,9 +40,14 @@ export function trimContext(messages, { maxTokens, keepLast = 8 } = {}) {
|
|
|
40
40
|
if (estimateTokens(messages) <= maxTokens) return { messages, trimmed: 0, dropped: 0 };
|
|
41
41
|
|
|
42
42
|
const out = messages.map((message) => ({ ...message }));
|
|
43
|
+
// Prefijo protegido: los system iniciales (base + objetivo + resumen de
|
|
44
|
+
// compactación). Nunca se recortan ni se descartan: son contexto esencial.
|
|
45
|
+
let head = 1;
|
|
46
|
+
while (head < out.length && out[head].role === 'system') head += 1;
|
|
47
|
+
|
|
43
48
|
let trimmed = 0;
|
|
44
|
-
const cutoff = Math.max(
|
|
45
|
-
for (let i =
|
|
49
|
+
const cutoff = Math.max(head, out.length - keepLast);
|
|
50
|
+
for (let i = head; i < cutoff; i++) {
|
|
46
51
|
const message = out[i];
|
|
47
52
|
if (message.role === 'tool' && typeof message.content === 'string' && message.content.length > TOOL_KEEP_CHARS) {
|
|
48
53
|
out[i] = { ...message, content: `${message.content.slice(0, TOOL_KEEP_CHARS)}\n…[recortado por contexto]` };
|
|
@@ -52,15 +57,15 @@ export function trimContext(messages, { maxTokens, keepLast = 8 } = {}) {
|
|
|
52
57
|
if (estimateTokens(out) <= maxTokens) return { messages: out, trimmed, dropped: 0 };
|
|
53
58
|
|
|
54
59
|
let dropped = 0;
|
|
55
|
-
while (estimateTokens(out) > maxTokens && out.length > keepLast
|
|
56
|
-
const first = out[
|
|
57
|
-
let end =
|
|
60
|
+
while (estimateTokens(out) > maxTokens && out.length - head > keepLast) {
|
|
61
|
+
const first = out[head];
|
|
62
|
+
let end = head + 1;
|
|
58
63
|
if (first.role === 'assistant' && Array.isArray(first.tool_calls)) {
|
|
59
64
|
while (end < out.length && out[end].role === 'tool') end += 1;
|
|
60
65
|
}
|
|
61
|
-
if (out.length - (end -
|
|
62
|
-
out.splice(
|
|
63
|
-
dropped += end -
|
|
66
|
+
if (out.length - (end - head) < keepLast) break;
|
|
67
|
+
out.splice(head, end - head);
|
|
68
|
+
dropped += end - head;
|
|
64
69
|
}
|
|
65
70
|
return { messages: out, trimmed, dropped };
|
|
66
71
|
}
|
|
@@ -57,14 +57,29 @@ export async function runAgent({
|
|
|
57
57
|
maxRounds = MAX_TOOL_ROUNDS,
|
|
58
58
|
maxTokens = 0,
|
|
59
59
|
maxDurationMs = 0,
|
|
60
|
+
maxOutputTokens = 0,
|
|
61
|
+
promptCaching = false,
|
|
60
62
|
repeatLimit = 3,
|
|
61
63
|
wrapUp = true,
|
|
62
64
|
stallWarnMs = 90000,
|
|
63
65
|
retries = 2,
|
|
64
66
|
retryDelayMs = 500,
|
|
65
67
|
contextTokens = 0,
|
|
68
|
+
onLifecycle,
|
|
66
69
|
}) {
|
|
67
70
|
const working = [...messages];
|
|
71
|
+
// Mensajes efimeros (p. ej. reglas por path inyectadas en runtime): entran al
|
|
72
|
+
// contexto de las proximas rondas pero NO se persisten en el historial.
|
|
73
|
+
const transient = [];
|
|
74
|
+
const emitLifecycle = (type, payload) => {
|
|
75
|
+
if (!onLifecycle) return undefined;
|
|
76
|
+
try {
|
|
77
|
+
return onLifecycle({ type, ...payload });
|
|
78
|
+
} catch {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
emitLifecycle('run.start', { model });
|
|
68
83
|
let usageTotal = null;
|
|
69
84
|
const startedAt = Date.now();
|
|
70
85
|
const fingerprints = new Map();
|
|
@@ -105,9 +120,9 @@ export async function runAgent({
|
|
|
105
120
|
|
|
106
121
|
// Presupuesto de contexto: el recorte va sobre una copia; `working` sigue
|
|
107
122
|
// completo para persistir el historial con indices estables.
|
|
108
|
-
let contextMessages = working;
|
|
123
|
+
let contextMessages = transient.length ? [...working, ...transient] : working;
|
|
109
124
|
if (contextTokens > 0) {
|
|
110
|
-
const trimmedResult = trimContext(
|
|
125
|
+
const trimmedResult = trimContext(contextMessages, { maxTokens: contextTokens });
|
|
111
126
|
contextMessages = trimmedResult.messages;
|
|
112
127
|
if (trimmedResult.trimmed || trimmedResult.dropped) {
|
|
113
128
|
onEvent?.({
|
|
@@ -129,6 +144,8 @@ export async function runAgent({
|
|
|
129
144
|
messages: contextMessages,
|
|
130
145
|
tools,
|
|
131
146
|
signal,
|
|
147
|
+
maxOutputTokens,
|
|
148
|
+
promptCaching,
|
|
132
149
|
onEvent: (event) => {
|
|
133
150
|
if (event && (event.type === 'reasoning' || event.type === 'tool_calls')) emitted = true;
|
|
134
151
|
if (event?.type === 'text' && event.delta) emitted = true;
|
|
@@ -182,6 +199,7 @@ export async function runAgent({
|
|
|
182
199
|
content: 'Cancelado por el usuario.',
|
|
183
200
|
});
|
|
184
201
|
}
|
|
202
|
+
emitLifecycle('run.end', { stopReason: 'aborted', usage: usageTotal });
|
|
185
203
|
return {
|
|
186
204
|
content: result.content ?? '',
|
|
187
205
|
reasoning: result.reasoning ?? '',
|
|
@@ -208,6 +226,7 @@ export async function runAgent({
|
|
|
208
226
|
output = NO_PROGRESS_MESSAGE;
|
|
209
227
|
} else {
|
|
210
228
|
newCalls++;
|
|
229
|
+
await emitLifecycle('tool.before', { id: call.id, name, args });
|
|
211
230
|
try {
|
|
212
231
|
output = await execute(name, args, toolContext);
|
|
213
232
|
} catch (err) {
|
|
@@ -229,6 +248,15 @@ export async function runAgent({
|
|
|
229
248
|
name,
|
|
230
249
|
content: normalized.content,
|
|
231
250
|
});
|
|
251
|
+
|
|
252
|
+
if (!(repeatLimit > 0 && seen > repeatLimit)) {
|
|
253
|
+
const after = await emitLifecycle('tool.after', { id: call.id, name, args, result: output });
|
|
254
|
+
const inject = after && typeof after === 'object' ? after.inject : undefined;
|
|
255
|
+
if (typeof inject === 'string' && inject.trim()) {
|
|
256
|
+
transient.push({ role: 'system', content: inject });
|
|
257
|
+
if (transient.length > 6) transient.shift();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
232
260
|
}
|
|
233
261
|
|
|
234
262
|
noProgressStreak = newCalls === 0 ? noProgressStreak + 1 : 0;
|
|
@@ -244,7 +272,7 @@ export async function runAgent({
|
|
|
244
272
|
|
|
245
273
|
let wrappedUp = false;
|
|
246
274
|
if (wrapUp && (stopReason === 'max_rounds' || stopReason === 'no_progress' || stopReason === 'budget')) {
|
|
247
|
-
const wrap = await runFinalAnswer({ provider, config, model, working, signal, onEvent, maxRetries: retries, retryDelayMs });
|
|
275
|
+
const wrap = await runFinalAnswer({ provider, config, model, working, signal, onEvent, maxRetries: retries, retryDelayMs, maxOutputTokens, promptCaching });
|
|
248
276
|
if (wrap.usage) accumulateUsage(wrap.usage);
|
|
249
277
|
if (wrap.content) {
|
|
250
278
|
working.push({ role: 'assistant', content: wrap.content, tokens_in: wrap.usage?.prompt_tokens, tokens_out: wrap.usage?.completion_tokens });
|
|
@@ -253,6 +281,7 @@ export async function runAgent({
|
|
|
253
281
|
}
|
|
254
282
|
}
|
|
255
283
|
|
|
284
|
+
emitLifecycle('run.end', { stopReason, usage: usageTotal });
|
|
256
285
|
return {
|
|
257
286
|
content,
|
|
258
287
|
reasoning: '',
|
|
@@ -266,7 +295,7 @@ export async function runAgent({
|
|
|
266
295
|
}
|
|
267
296
|
|
|
268
297
|
/** Ronda final con tools deshabilitadas para forzar un cierre con texto. */
|
|
269
|
-
async function runFinalAnswer({ provider, config, model, working, signal, onEvent, maxRetries, retryDelayMs }) {
|
|
298
|
+
async function runFinalAnswer({ provider, config, model, working, signal, onEvent, maxRetries, retryDelayMs, maxOutputTokens, promptCaching }) {
|
|
270
299
|
try {
|
|
271
300
|
const result = await withRetry(
|
|
272
301
|
() =>
|
|
@@ -275,6 +304,8 @@ async function runFinalAnswer({ provider, config, model, working, signal, onEven
|
|
|
275
304
|
messages: [...working, { role: 'system', content: WRAP_UP_PROMPT }],
|
|
276
305
|
tools: [],
|
|
277
306
|
signal,
|
|
307
|
+
maxOutputTokens,
|
|
308
|
+
promptCaching,
|
|
278
309
|
onEvent,
|
|
279
310
|
}),
|
|
280
311
|
{ retries: maxRetries, baseDelayMs: retryDelayMs, signal }
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// Alcance estricto: cuando esta activo, el system prompt pide hacer exactamente
|
|
2
|
+
// lo solicitado, sin mejoras, refactors ni cambios fuera del pedido.
|
|
3
|
+
|
|
4
|
+
export const STRICT_SCOPE_PROMPT = [
|
|
5
|
+
'Modo estricto (solo lo pedido): hacé EXACTAMENTE lo que se te pide, nada más.',
|
|
6
|
+
'- No agregues mejoras, refactors, archivos, dependencias ni cambios que no se hayan pedido explícitamente.',
|
|
7
|
+
'- No amplíes el alcance: si el pedido menciona un archivo o una función, limitate a eso.',
|
|
8
|
+
'- Si para cumplir el pedido hiciera falta algo extra, preguntá antes de hacerlo.',
|
|
9
|
+
'- Preferí el cambio mínimo que resuelve el pedido y no toques código no relacionado.',
|
|
10
|
+
].join('\n');
|
|
11
|
+
|
|
12
|
+
/** Bloque de prompt del alcance estricto, o cadena vacía si está desactivado. */
|
|
13
|
+
export function scopeDirective(enabled) {
|
|
14
|
+
return enabled ? STRICT_SCOPE_PROMPT : '';
|
|
15
|
+
}
|
|
@@ -10,6 +10,12 @@ export const DEFAULT_CONFIG = {
|
|
|
10
10
|
maxRounds: 0,
|
|
11
11
|
maxTokens: 0,
|
|
12
12
|
maxDurationMs: 0,
|
|
13
|
+
maxOutputTokens: 0,
|
|
14
|
+
promptCaching: false,
|
|
15
|
+
skills: { mode: 'lazy' },
|
|
16
|
+
compact: { enabled: true, threshold: 0.75, keepLast: 8 },
|
|
17
|
+
rules: ['.tamnora/rules/*.md'],
|
|
18
|
+
memory: { enabled: true },
|
|
13
19
|
tools: [],
|
|
14
20
|
permissions: {},
|
|
15
21
|
mcp: [],
|
|
@@ -12,10 +12,12 @@ export { resolveApiKey, resolveEnvMap } from './config/secrets.js';
|
|
|
12
12
|
export { ensureUtf8Console } from './console.js';
|
|
13
13
|
export { MAX_TOOL_ROUNDS, runAgent, parseToolArguments, normalizeToolOutput } from './agent/runAgent.js';
|
|
14
14
|
export { estimateTokens, trimContext } from './agent/context.js';
|
|
15
|
+
export { liveHistory, planCompaction, COMPACT_INSTRUCTION, SUMMARY_INSTRUCTION } from './agent/compact.js';
|
|
15
16
|
export { diffLines, diffRows } from './tools/diff.js';
|
|
16
17
|
export { isGitRepo, gitStatus, gitDiff } from './git/index.js';
|
|
17
18
|
export { summarizeToolCall, previewToolResult } from './tools/labels.js';
|
|
18
19
|
export { MODES, MODE_IDS, resolveMode } from './agent/modes.js';
|
|
20
|
+
export { STRICT_SCOPE_PROMPT, scopeDirective } from './agent/scope.js';
|
|
19
21
|
export { withRetry, isRetryableError, isAbortError } from './providers/resilience.js';
|
|
20
22
|
export {
|
|
21
23
|
PROVIDERS,
|
|
@@ -86,8 +88,41 @@ export { summarize } from './eval/report.js';
|
|
|
86
88
|
export { runScorers, validateSchema, extractJson, SCORERS } from './eval/scorers.js';
|
|
87
89
|
export { createJudge } from './eval/judge.js';
|
|
88
90
|
export { createTaskWorkspace, normalizeExpect } from './eval/task.js';
|
|
89
|
-
export {
|
|
91
|
+
export {
|
|
92
|
+
parseSkillMarkdown,
|
|
93
|
+
loadSkills,
|
|
94
|
+
mergeSkills,
|
|
95
|
+
writeSkillFiles,
|
|
96
|
+
buildSkillsPrompt,
|
|
97
|
+
buildSkillsIndex,
|
|
98
|
+
} from './skills/index.js';
|
|
90
99
|
export { parseSkillRef, fetchSkillFromGithub } from './skills/remote.js';
|
|
100
|
+
export {
|
|
101
|
+
MEMORY_DIR,
|
|
102
|
+
MEMORY_INDEX_NAME,
|
|
103
|
+
memoryRoot,
|
|
104
|
+
memoryIndexPath,
|
|
105
|
+
memorySlug,
|
|
106
|
+
isMemoryTopic,
|
|
107
|
+
readMemoryIndex,
|
|
108
|
+
listMemoryTopics,
|
|
109
|
+
readMemoryTopic,
|
|
110
|
+
writeMemory,
|
|
111
|
+
writeIndex as writeMemoryIndex,
|
|
112
|
+
deleteMemory,
|
|
113
|
+
searchMemory,
|
|
114
|
+
memoryPrompt,
|
|
115
|
+
} from './memory/index.js';
|
|
116
|
+
export {
|
|
117
|
+
parseRuleMarkdown,
|
|
118
|
+
loadRules,
|
|
119
|
+
staticRules,
|
|
120
|
+
pathRules,
|
|
121
|
+
ruleMatchesPath,
|
|
122
|
+
rulesForPath,
|
|
123
|
+
buildStaticRulesBlock,
|
|
124
|
+
buildPathRulesBlock,
|
|
125
|
+
} from './rules/index.js';
|
|
91
126
|
export {
|
|
92
127
|
searchMcpRegistry,
|
|
93
128
|
getMcpRegistryServer,
|