@danieltmn/tamnora 0.25.0 → 0.26.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 +18 -0
- package/README.md +13 -25
- package/SECURITY.md +3 -4
- package/harness/packages/cli/src/cli.js +0 -3
- package/harness/packages/cli/src/commands/bridge.js +0 -1
- package/harness/packages/cli/src/commands/repl.js +20 -4
- package/harness/packages/cli/src/commands/run.js +6 -2
- package/harness/packages/cli/src/commands/server.js +1 -1
- package/harness/packages/cli/src/commands/tui.js +6 -2
- package/harness/packages/cli/src/lib/runtime.js +2 -0
- package/harness/packages/cli/src/lib/util.js +2 -2
- package/harness/packages/cli/src/tui/app.js +24 -4
- package/harness/packages/cli/src/tui/layout.js +1 -1
- package/harness/packages/cli/test/tunnel.test.js +5 -32
- package/harness/packages/core/src/agent/runAgent.js +5 -2
- package/harness/packages/core/src/agents/index.js +113 -0
- package/harness/packages/core/src/config/app.js +0 -4
- package/harness/packages/core/src/config/home.js +1 -20
- package/harness/packages/core/src/config/index.js +6 -8
- package/harness/packages/core/src/extensions/load.js +22 -0
- package/harness/packages/core/src/frontmatter.js +32 -0
- package/harness/packages/core/src/hooks/index.js +2 -16
- package/harness/packages/core/src/index.js +12 -14
- package/harness/packages/core/src/plugins/index.js +2 -16
- package/harness/packages/core/src/procs/project.js +68 -0
- package/harness/packages/core/src/procs/static.js +17 -0
- package/harness/packages/core/src/providers/index.js +3 -1
- package/harness/packages/core/src/providers/ollama.js +4 -1
- package/harness/packages/core/src/providers/openaiCompatible.js +6 -1
- package/harness/packages/core/src/providers/openrouter.js +5 -1
- package/harness/packages/core/src/providers/presets.js +4 -3
- package/harness/packages/core/src/providers/reasoning.js +58 -0
- package/harness/packages/core/src/rules/index.js +8 -27
- package/harness/packages/core/src/skills/index.js +8 -19
- package/harness/packages/core/src/storage/sqlite.js +595 -690
- package/harness/packages/core/src/tunnel.js +9 -147
- package/harness/packages/core/test/agents.test.js +111 -0
- package/harness/packages/core/test/home.test.js +2 -35
- package/harness/packages/core/test/procs.test.js +3 -64
- package/harness/packages/core/test/reasoning.test.js +43 -0
- package/harness/packages/core/test/storage.test.js +1 -12
- package/harness/packages/server/public/app.js +39 -19
- package/harness/packages/server/public/appearance.js +2 -56
- package/harness/packages/server/public/boot.js +2 -2
- package/harness/packages/server/public/cards.js +2 -21
- package/harness/packages/server/public/chat.js +1 -16
- package/harness/packages/server/public/chatFormat.js +0 -11
- package/harness/packages/server/public/chrome.js +95 -5
- package/harness/packages/server/public/commands.js +1 -2
- package/harness/packages/server/public/core.js +1 -1
- package/harness/packages/server/public/explorer.js +7 -10
- package/harness/packages/server/public/icons (1).zip +0 -0
- package/harness/packages/server/public/index.html +1228 -1237
- package/harness/packages/server/public/landing/boot.js +2 -3
- package/harness/packages/server/public/landing/index.html +3 -367
- package/harness/packages/server/public/sessions.js +1 -0
- package/harness/packages/server/public/settings.js +1255 -1155
- package/harness/packages/server/public/settingsNav.js +1 -1
- package/harness/packages/server/public/styles.css +2341 -2867
- package/harness/packages/server/public/system.js +356 -495
- package/harness/packages/server/public/voice.js +6 -22
- package/harness/packages/server/src/auth.js +4 -15
- package/harness/packages/server/src/cli.js +1 -1
- package/harness/packages/server/src/lib/agents.js +60 -0
- package/harness/packages/server/src/lib/appearance.js +3 -22
- package/harness/packages/server/src/lib/channels/telegram/commands.js +1 -1
- package/harness/packages/server/src/lib/channels/telegram/run.js +1 -0
- package/harness/packages/server/src/lib/definitions.js +40 -0
- package/harness/packages/server/src/lib/prompt.js +11 -3
- package/harness/packages/server/src/lib/scheduler.js +4 -0
- package/harness/packages/server/src/lib/sessionView.js +1 -0
- package/harness/packages/server/src/lib/skills.js +27 -21
- package/harness/packages/server/src/lib/tasks.js +71 -23
- package/harness/packages/server/src/lib/ttsSettings.js +2 -2
- package/harness/packages/server/src/lib/workspace.js +9 -11
- package/harness/packages/server/src/routes/agents.js +88 -0
- package/harness/packages/server/src/routes/chat.js +9 -44
- package/harness/packages/server/src/routes/preview.js +5 -22
- package/harness/packages/server/src/routes/procs.js +2 -4
- package/harness/packages/server/src/routes/providers.js +7 -0
- package/harness/packages/server/src/routes/sessions.js +3 -1
- package/harness/packages/server/src/routes/tts.js +1 -1
- package/harness/packages/server/src/server.js +8 -5
- package/harness/packages/server/test/agents.test.js +194 -0
- package/harness/packages/server/test/appearance-route.test.js +3 -3
- package/harness/packages/server/test/appearance.test.js +12 -38
- package/harness/packages/server/test/chatFormat.test.js +0 -8
- package/harness/packages/server/test/consoleTabs.test.js +2 -2
- package/harness/packages/server/test/ops.test.js +8 -56
- package/harness/packages/server/test/preview.test.js +2 -42
- package/harness/packages/server/test/security.test.js +194 -370
- package/harness/packages/server/test/settingsNav.test.js +2 -2
- package/harness/packages/server/test/tts.test.js +4 -4
- package/harness/tamnora.example.json +2 -1
- package/package.json +2 -2
- package/harness/packages/cli/src/commands/eval.js +0 -186
- package/harness/packages/core/src/eval/dataset.js +0 -47
- package/harness/packages/core/src/eval/judge.js +0 -39
- package/harness/packages/core/src/eval/report.js +0 -48
- package/harness/packages/core/src/eval/runTask.js +0 -62
- package/harness/packages/core/src/eval/runner.js +0 -153
- package/harness/packages/core/src/eval/scorers.js +0 -217
- package/harness/packages/core/src/eval/task.js +0 -59
- package/harness/packages/core/src/procs/detect.js +0 -149
- package/harness/packages/core/test/eval.test.js +0 -144
- package/harness/packages/server/src/lib/advisor.js +0 -198
- package/harness/packages/server/src/routes/advisor.js +0 -79
- package/harness/packages/server/src/routes/eval.js +0 -100
- package/harness/packages/server/test/advisor.test.js +0 -249
- package/harness/scripts/example-dataset.jsonl +0 -4
|
@@ -6,11 +6,9 @@ import {
|
|
|
6
6
|
setStatus,
|
|
7
7
|
trModelEl,
|
|
8
8
|
trProviderEl,
|
|
9
|
-
vozAnnAdviceEl,
|
|
10
9
|
vozAnnAnswerEl,
|
|
11
10
|
vozAnnEndEl,
|
|
12
11
|
vozAnnStartEl,
|
|
13
|
-
vozAnnSummaryEl,
|
|
14
12
|
vozAnnToolsEl,
|
|
15
13
|
vozEnabledEl,
|
|
16
14
|
vozFfmpegEl,
|
|
@@ -573,7 +571,7 @@ export const voz = {
|
|
|
573
571
|
voice: 'es-AR-ElenaNeural',
|
|
574
572
|
rate: '+0%',
|
|
575
573
|
pitch: '+0Hz',
|
|
576
|
-
announce: { start: true, tools: true, end: true,
|
|
574
|
+
announce: { start: true, tools: true, end: true, answer: false },
|
|
577
575
|
},
|
|
578
576
|
};
|
|
579
577
|
|
|
@@ -772,11 +770,9 @@ export function narrar(event, data, turn) {
|
|
|
772
770
|
const { enabled, announce } = voz.settings;
|
|
773
771
|
if (!enabled && !handsFreeOn) return;
|
|
774
772
|
// Manos libres: conversacion, sin narracion de arranque/herramientas; lee la
|
|
775
|
-
// respuesta final
|
|
773
|
+
// respuesta final aunque la narracion este apagada en Ajustes.
|
|
776
774
|
if (handsFreeOn) {
|
|
777
|
-
if (event === '
|
|
778
|
-
if (data?.say) hablar(data.say, { label: 'asesor', maxAgeMs: 0, force: true });
|
|
779
|
-
} else if (event === 'done') {
|
|
775
|
+
if (event === 'done') {
|
|
780
776
|
const spoken = speechText(data?.content);
|
|
781
777
|
if (spoken) hablar(spoken, { label: 'respuesta', maxAgeMs: 0, force: true });
|
|
782
778
|
else if (data?.say) hablar(data.say, { maxAgeMs: 0, force: true });
|
|
@@ -788,15 +784,9 @@ export function narrar(event, data, turn) {
|
|
|
788
784
|
if (!enabled) return;
|
|
789
785
|
if (event === 'start' && announce.start) hablar(data?.say);
|
|
790
786
|
else if (event === 'tool_call' && announce.tools) hablar(data?.say);
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
else if (event === '
|
|
794
|
-
if (data?.kind === 'summary') {
|
|
795
|
-
if (announce.summary) hablar(data?.say, { label: 'resumen', maxAgeMs: 0 });
|
|
796
|
-
} else if (announce.advice) {
|
|
797
|
-
hablar(data?.say, { label: 'asesor', maxAgeMs: 0 });
|
|
798
|
-
}
|
|
799
|
-
} else if (event === 'done' && announce.answer) {
|
|
787
|
+
// La respuesta final es el cierre del turno: si queda en cola detras de un
|
|
788
|
+
// audio largo, igual hay que leerla (sin vencimiento).
|
|
789
|
+
else if (event === 'done' && announce.answer) {
|
|
800
790
|
// Lee la respuesta final limpia; si era solo codigo, cae al cierre.
|
|
801
791
|
const spoken = speechText(data?.content);
|
|
802
792
|
if (spoken) hablar(spoken, { label: 'respuesta', maxAgeMs: 0 });
|
|
@@ -828,9 +818,7 @@ export async function aplicarVoz(data) {
|
|
|
828
818
|
start: data.announce?.start !== false,
|
|
829
819
|
tools: data.announce?.tools !== false,
|
|
830
820
|
end: data.announce?.end !== false,
|
|
831
|
-
advice: data.announce?.advice !== false,
|
|
832
821
|
answer: data.announce?.answer === true,
|
|
833
|
-
summary: data.announce?.summary === true,
|
|
834
822
|
},
|
|
835
823
|
};
|
|
836
824
|
if (vozEnabledEl) vozEnabledEl.checked = voz.settings.enabled;
|
|
@@ -850,8 +838,6 @@ export async function aplicarVoz(data) {
|
|
|
850
838
|
if (vozAnnStartEl) vozAnnStartEl.checked = voz.settings.announce.start;
|
|
851
839
|
if (vozAnnToolsEl) vozAnnToolsEl.checked = voz.settings.announce.tools;
|
|
852
840
|
if (vozAnnEndEl) vozAnnEndEl.checked = voz.settings.announce.end;
|
|
853
|
-
if (vozAnnAdviceEl) vozAnnAdviceEl.checked = voz.settings.announce.advice;
|
|
854
|
-
if (vozAnnSummaryEl) vozAnnSummaryEl.checked = voz.settings.announce.summary;
|
|
855
841
|
if (vozAnnAnswerEl) vozAnnAnswerEl.checked = voz.settings.announce.answer;
|
|
856
842
|
const estado = $('#voz-status');
|
|
857
843
|
if (estado) {
|
|
@@ -877,9 +863,7 @@ export async function submitVoz() {
|
|
|
877
863
|
start: vozAnnStartEl?.checked !== false,
|
|
878
864
|
tools: vozAnnToolsEl?.checked !== false,
|
|
879
865
|
end: vozAnnEndEl?.checked !== false,
|
|
880
|
-
advice: vozAnnAdviceEl?.checked !== false,
|
|
881
866
|
answer: vozAnnAnswerEl?.checked === true,
|
|
882
|
-
summary: vozAnnSummaryEl?.checked === true,
|
|
883
867
|
},
|
|
884
868
|
};
|
|
885
869
|
const data = await api('/api/tts/settings', { method: 'PUT', body }).catch((e) => ({ error: e.message }));
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import { sendError } from './lib/http.js';
|
|
3
|
-
import { tunnelState } from './lib/core.js';
|
|
4
3
|
import { isLoopback } from './lib/workspace.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
@@ -38,26 +37,16 @@ export function parseCookies(header) {
|
|
|
38
37
|
return out;
|
|
39
38
|
}
|
|
40
39
|
|
|
41
|
-
// Detras del
|
|
42
|
-
// X-Forwarded-Proto para marcar la cookie Secure.
|
|
43
|
-
//
|
|
44
|
-
// local por direccion: usamos el Host (localhost/127.0.0.1 = local) y el
|
|
45
|
-
// estado del bridge para no mandar la cookie sin Secure por el tunel.
|
|
46
|
-
function isLocalHost(req) {
|
|
47
|
-
const host = String(req.headers.host || '').split(':')[0].toLowerCase();
|
|
48
|
-
return !host || host === 'localhost' || host === '127.0.0.1' || host === '::1' || host === '[::1]';
|
|
49
|
-
}
|
|
50
|
-
|
|
40
|
+
// Detras del bridge de tailscale el server recibe desde loopback: solo ahi
|
|
41
|
+
// confiamos en X-Forwarded-Proto para marcar la cookie Secure. La URL del
|
|
42
|
+
// tailnet es HTTPS aunque el salto interno sea http.
|
|
51
43
|
function isSecure(req) {
|
|
52
44
|
if (req.socket && req.socket.encrypted) return true;
|
|
53
45
|
const remote = req.socket && req.socket.remoteAddress;
|
|
54
46
|
const loopback = !remote || isLoopback(remote);
|
|
55
47
|
if (!loopback) return false;
|
|
56
48
|
const proto = String(req.headers['x-forwarded-proto'] || '').split(',')[0].trim().toLowerCase();
|
|
57
|
-
|
|
58
|
-
// Peticion por el tunel (host publico + bridge activo): la URL publica es
|
|
59
|
-
// HTTPS, la cookie no tiene que poder viajar por la variante http.
|
|
60
|
-
return Boolean(tunnelState()) && !isLocalHost(req);
|
|
49
|
+
return proto === 'https';
|
|
61
50
|
}
|
|
62
51
|
|
|
63
52
|
function serializeCookie(value, req, maxAge) {
|
|
@@ -52,7 +52,7 @@ export async function serveCommand(argv) {
|
|
|
52
52
|
function printHelp() {
|
|
53
53
|
console.log(`Uso: tamnora serve [opciones]
|
|
54
54
|
|
|
55
|
-
Levanta la consola del harness (chat
|
|
55
|
+
Levanta la consola del harness (chat) en localhost.
|
|
56
56
|
|
|
57
57
|
Opciones:
|
|
58
58
|
--port <n> Puerto (default: 4517)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { homeDir, loadAgents, mergeAgents, buildAgentsIndex, writeAgentFile } from './core.js';
|
|
3
|
+
import {
|
|
4
|
+
DEFINITION_ID_PATTERN,
|
|
5
|
+
allDefinitions,
|
|
6
|
+
disabledDefinitionIds,
|
|
7
|
+
removeDefinition,
|
|
8
|
+
setDefinitionEnabled,
|
|
9
|
+
} from './definitions.js';
|
|
10
|
+
|
|
11
|
+
export const AGENT_ID_PATTERN = DEFINITION_ID_PATTERN;
|
|
12
|
+
|
|
13
|
+
export function allAgents(cwd) {
|
|
14
|
+
return allDefinitions(cwd, { kind: 'agents', load: loadAgents, merge: mergeAgents });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function agentsView(ctx) {
|
|
18
|
+
const disabled = disabledDefinitionIds(ctx.store, 'agents');
|
|
19
|
+
return allAgents(ctx.cwd).map((agent) => ({
|
|
20
|
+
id: agent.id,
|
|
21
|
+
name: agent.name || agent.id,
|
|
22
|
+
description: agent.description || '',
|
|
23
|
+
model: agent.model || '',
|
|
24
|
+
provider: agent.provider || '',
|
|
25
|
+
mode: agent.mode || '',
|
|
26
|
+
effort: agent.effort || '',
|
|
27
|
+
maxRounds: agent.maxRounds || 0,
|
|
28
|
+
tools: agent.tools,
|
|
29
|
+
skills: agent.skills,
|
|
30
|
+
enabled: !disabled.has(agent.id),
|
|
31
|
+
removable: Boolean(agent.home),
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function findAgent(ctx, id) {
|
|
36
|
+
return allAgents(ctx.cwd).find((agent) => agent.id === id) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function enabledAgents(ctx) {
|
|
40
|
+
const disabled = disabledDefinitionIds(ctx.store, 'agents');
|
|
41
|
+
return allAgents(ctx.cwd).filter((agent) => !disabled.has(agent.id));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function agentsIndexPrompt(ctx) {
|
|
45
|
+
return buildAgentsIndex(enabledAgents(ctx));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function writeAgent(id, data) {
|
|
49
|
+
const dir = path.join(homeDir(), 'agents', String(id));
|
|
50
|
+
await writeAgentFile(dir, { ...data, id });
|
|
51
|
+
return dir;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function removeAgent(id) {
|
|
55
|
+
await removeDefinition('agents', id);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function setAgentEnabled(store, id, enabled) {
|
|
59
|
+
setDefinitionEnabled(store, 'agents', id, enabled);
|
|
60
|
+
}
|
|
@@ -1,27 +1,9 @@
|
|
|
1
1
|
// Apariencia de la consola (app.json -> appearance): tema, tipografía y tamaño.
|
|
2
2
|
// Los ids deben coincidir con public/appearance.js; acá solo se valida y acota.
|
|
3
3
|
|
|
4
|
-
export const THEME_IDS = [
|
|
5
|
-
'paper',
|
|
6
|
-
'dark',
|
|
7
|
-
'midnight',
|
|
8
|
-
'nord',
|
|
9
|
-
'dracula',
|
|
10
|
-
'solarized',
|
|
11
|
-
'solarized-dark',
|
|
12
|
-
'bosque',
|
|
13
|
-
'vino',
|
|
14
|
-
'arena',
|
|
15
|
-
'hielo',
|
|
16
|
-
'vercel',
|
|
17
|
-
'dark-plus',
|
|
18
|
-
'github',
|
|
19
|
-
'github-dark',
|
|
20
|
-
'mocha',
|
|
21
|
-
'ebano',
|
|
22
|
-
];
|
|
4
|
+
export const THEME_IDS = ['paper', 'github', 'dark', 'mocha'];
|
|
23
5
|
|
|
24
|
-
export const FONT_IDS = ['clasico', 'sistema'
|
|
6
|
+
export const FONT_IDS = ['clasico', 'sistema'];
|
|
25
7
|
|
|
26
8
|
export const DEFAULT_APPEARANCE = { theme: 'paper', font: 'clasico', chatFont: 14.5, scale: 1 };
|
|
27
9
|
|
|
@@ -31,8 +13,7 @@ const MIN_SCALE = 0.8;
|
|
|
31
13
|
const MAX_SCALE = 1.3;
|
|
32
14
|
|
|
33
15
|
export function resolveThemeId(id) {
|
|
34
|
-
|
|
35
|
-
return THEME_IDS.includes(legacy) ? legacy : DEFAULT_APPEARANCE.theme;
|
|
16
|
+
return THEME_IDS.includes(id) ? id : DEFAULT_APPEARANCE.theme;
|
|
36
17
|
}
|
|
37
18
|
|
|
38
19
|
export function resolveFontId(id) {
|
|
@@ -179,7 +179,7 @@ export async function handleCommand(T, chatId, text) {
|
|
|
179
179
|
T.controllers.get(String(chatId))?.abort();
|
|
180
180
|
return send(T, chatId, 'Cancelando.');
|
|
181
181
|
}
|
|
182
|
-
if (cmd === 'estricto'
|
|
182
|
+
if (cmd === 'estricto') {
|
|
183
183
|
const current = T.ctx.store.getSetting('strictScope') === true;
|
|
184
184
|
const value = arg ? /^(on|si|sí|1|true)$/i.test(arg) : !current;
|
|
185
185
|
T.ctx.store.setSetting('strictScope', value);
|
|
@@ -140,6 +140,7 @@ export async function run(T, chatId, input, images = []) {
|
|
|
140
140
|
maxDurationMs: T.ctx.effectiveMaxDurationMs?.() ?? 0,
|
|
141
141
|
maxOutputTokens: T.ctx.effectiveMaxOutputTokens?.() ?? T.ctx.config.maxOutputTokens ?? 0,
|
|
142
142
|
promptCaching: T.ctx.effectivePromptCaching?.() ?? T.ctx.config.promptCaching === true,
|
|
143
|
+
reasoningEffort: T.ctx.effectiveEffort?.() ?? '',
|
|
143
144
|
onEvent(event) {
|
|
144
145
|
if (event.type === 'text') {
|
|
145
146
|
full = event.full;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Registro comun de definiciones en markdown (skills y subagentes): raices
|
|
2
|
+
// casa + workspace, merge con prioridad al workspace y estado on/off en un
|
|
3
|
+
// setting (`disabledSkills` / `disabledAgents`).
|
|
4
|
+
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { rm } from 'node:fs/promises';
|
|
7
|
+
import { homeDir } from './core.js';
|
|
8
|
+
|
|
9
|
+
export const DEFINITION_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,39}$/i;
|
|
10
|
+
|
|
11
|
+
/** Raices, de menor a mayor prioridad: casa y despues workspace. */
|
|
12
|
+
export function definitionRoots(cwd, kind) {
|
|
13
|
+
return [path.join(homeDir(), kind), path.join(cwd, '.tamnora', kind)];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Carga y mergea las definiciones, marcando `root` y si viven en la casa. */
|
|
17
|
+
export function allDefinitions(cwd, { kind, load, merge }) {
|
|
18
|
+
const home = path.join(homeDir(), kind);
|
|
19
|
+
return merge(definitionRoots(cwd, kind).map((root) => load(root).map((item) => ({ ...item, root, home: root === home }))));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function disabledKey(kind) {
|
|
23
|
+
return `disabled${kind[0].toUpperCase()}${kind.slice(1)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function disabledDefinitionIds(store, kind) {
|
|
27
|
+
const raw = store.getSetting(disabledKey(kind));
|
|
28
|
+
return new Set(Array.isArray(raw) ? raw.map(String) : []);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function setDefinitionEnabled(store, kind, id, enabled) {
|
|
32
|
+
const current = disabledDefinitionIds(store, kind);
|
|
33
|
+
if (enabled) current.delete(id);
|
|
34
|
+
else current.add(id);
|
|
35
|
+
store.setSetting(disabledKey(kind), [...current]);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function removeDefinition(kind, id) {
|
|
39
|
+
return rm(path.join(homeDir(), kind, String(id)), { recursive: true, force: true });
|
|
40
|
+
}
|
|
@@ -33,12 +33,13 @@ function workspacePreamble(cwd) {
|
|
|
33
33
|
* @param {string[]} opts.files - instruction file names (e.g. ['AGENTS.md'])
|
|
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
|
+
* @param {() => string} [opts.agentsText] - subagent catalog block, re-read on every call
|
|
36
37
|
* @param {() => string} [opts.mcpText] - MCP tool index block, re-read on every call
|
|
37
38
|
* @param {() => string} [opts.scopeText] - strict-scope block, re-read on every call
|
|
38
39
|
* @param {string[]} [opts.ruleFiles] - glob patterns of project rule files
|
|
39
40
|
* @param {boolean} [opts.memory] - inject the project memory block
|
|
40
41
|
*/
|
|
41
|
-
export function createPromptBuilder({ cwd, files, profileText, skillsText, mcpText, scopeText, ruleFiles = [], memory = false }) {
|
|
42
|
+
export function createPromptBuilder({ cwd, files, profileText, skillsText, agentsText, mcpText, scopeText, ruleFiles = [], memory = false }) {
|
|
42
43
|
const cache = new Map();
|
|
43
44
|
|
|
44
45
|
async function buildFor(workspace) {
|
|
@@ -52,15 +53,22 @@ export function createPromptBuilder({ cwd, files, profileText, skillsText, mcpTe
|
|
|
52
53
|
return text;
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
return async function systemPromptFor(workspace = cwd) {
|
|
56
|
+
return async function systemPromptFor(workspace = cwd, overrides = {}) {
|
|
56
57
|
const key = workspace || cwd;
|
|
57
58
|
if (!cache.has(key)) cache.set(key, buildFor(key));
|
|
58
59
|
const base = await cache.get(key);
|
|
59
60
|
const parts = [base];
|
|
60
61
|
const profile = typeof profileText === 'function' ? String(profileText() ?? '').trim() : '';
|
|
61
62
|
if (profile) parts.push(profile);
|
|
62
|
-
|
|
63
|
+
// Un subagente puede reemplazar (o vaciar) el bloque de skills y el catálogo
|
|
64
|
+
// de otros subagentes: las tools no se heredan automáticamente.
|
|
65
|
+
const skillsSource = overrides.skillsText !== undefined ? overrides.skillsText : skillsText;
|
|
66
|
+
const extra =
|
|
67
|
+
typeof skillsSource === 'function' ? String(skillsSource() ?? '').trim() : String(skillsSource ?? '').trim();
|
|
63
68
|
if (extra) parts.push(extra);
|
|
69
|
+
const agentsSource = overrides.agentsText !== undefined ? overrides.agentsText : agentsText;
|
|
70
|
+
const agents = typeof agentsSource === 'function' ? String(agentsSource() ?? '').trim() : String(agentsSource ?? '').trim();
|
|
71
|
+
if (agents) parts.push(agents);
|
|
64
72
|
const mcp = typeof mcpText === 'function' ? String(mcpText() ?? '').trim() : '';
|
|
65
73
|
if (mcp) parts.push(mcp);
|
|
66
74
|
if (memory) {
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from './agentRun.js';
|
|
14
14
|
import { createSubagentTool } from './tasks.js';
|
|
15
15
|
import { createRulesInjector } from './rules.js';
|
|
16
|
+
import { enabledAgents } from './agents.js';
|
|
16
17
|
|
|
17
18
|
function scheduleSessionId(ctx, schedule) {
|
|
18
19
|
const payload = schedule.payload ?? {};
|
|
@@ -74,6 +75,8 @@ async function runAgentSchedule(ctx, schedule) {
|
|
|
74
75
|
providerId,
|
|
75
76
|
model,
|
|
76
77
|
parentMode: mode,
|
|
78
|
+
effort: ctx.effectiveEffort?.() ?? '',
|
|
79
|
+
agents: enabledAgents(ctx),
|
|
77
80
|
permissions: ctx.config.permissions,
|
|
78
81
|
mcp,
|
|
79
82
|
mcpMode: ctx.effectiveMcpMode(),
|
|
@@ -109,6 +112,7 @@ async function runAgentSchedule(ctx, schedule) {
|
|
|
109
112
|
maxDurationMs: ctx.effectiveMaxDurationMs?.() || 0,
|
|
110
113
|
maxOutputTokens: ctx.effectiveMaxOutputTokens?.() || 0,
|
|
111
114
|
promptCaching: ctx.effectivePromptCaching?.() ?? ctx.config.promptCaching === true,
|
|
115
|
+
reasoningEffort: ctx.effectiveEffort?.() ?? '',
|
|
112
116
|
onEvent(event) {
|
|
113
117
|
if (event.type === 'text') emit('text', { full: event.full });
|
|
114
118
|
else if (event.type === 'reasoning') emit('reasoning', { full: event.full });
|
|
@@ -12,6 +12,7 @@ export function sessionView(session) {
|
|
|
12
12
|
provider: session.provider,
|
|
13
13
|
model: session.model,
|
|
14
14
|
mode: session.meta?.mode ?? null,
|
|
15
|
+
effort: session.meta?.effort ?? null,
|
|
15
16
|
cwd: session.cwd,
|
|
16
17
|
goal: session.meta?.goal ?? null,
|
|
17
18
|
hasSummary: Boolean(session.meta?.summary),
|
|
@@ -1,26 +1,22 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { mkdir,
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { homeDir, loadSkills, mergeSkills, buildSkillsPrompt, buildSkillsIndex } from './core.js';
|
|
4
|
+
import {
|
|
5
|
+
DEFINITION_ID_PATTERN,
|
|
6
|
+
allDefinitions,
|
|
7
|
+
disabledDefinitionIds,
|
|
8
|
+
removeDefinition,
|
|
9
|
+
setDefinitionEnabled,
|
|
10
|
+
} from './definitions.js';
|
|
4
11
|
|
|
5
|
-
export const SKILL_ID_PATTERN =
|
|
6
|
-
|
|
7
|
-
/** Skill roots, lowest priority first: casa skills, then the workspace's. */
|
|
8
|
-
export function skillRoots(cwd) {
|
|
9
|
-
return [path.join(homeDir(), 'skills'), path.join(cwd, '.tamnora', 'skills')];
|
|
10
|
-
}
|
|
12
|
+
export const SKILL_ID_PATTERN = DEFINITION_ID_PATTERN;
|
|
11
13
|
|
|
12
14
|
export function allSkills(cwd) {
|
|
13
|
-
|
|
14
|
-
return mergeSkills(skillRoots(cwd).map((root) => loadSkills(root).map((s) => ({ ...s, root, home: root === home }))));
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function disabledIds(store) {
|
|
18
|
-
const raw = store.getSetting('disabledSkills');
|
|
19
|
-
return new Set(Array.isArray(raw) ? raw.map(String) : []);
|
|
15
|
+
return allDefinitions(cwd, { kind: 'skills', load: loadSkills, merge: mergeSkills });
|
|
20
16
|
}
|
|
21
17
|
|
|
22
18
|
export function skillsView(ctx) {
|
|
23
|
-
const disabled =
|
|
19
|
+
const disabled = disabledDefinitionIds(ctx.store, 'skills');
|
|
24
20
|
return allSkills(ctx.cwd).map((s) => ({
|
|
25
21
|
id: s.id,
|
|
26
22
|
name: s.name || s.id,
|
|
@@ -36,7 +32,7 @@ export function findSkill(ctx, id) {
|
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
export function enabledSkills(ctx) {
|
|
39
|
-
const disabled =
|
|
35
|
+
const disabled = disabledDefinitionIds(ctx.store, 'skills');
|
|
40
36
|
return allSkills(ctx.cwd)
|
|
41
37
|
.filter((s) => !disabled.has(s.id))
|
|
42
38
|
.map((s) => ({ id: s.id, name: s.name || s.id, description: s.description, when: s.when, body: s.body }));
|
|
@@ -57,6 +53,19 @@ export function skillsBlock(ctx) {
|
|
|
57
53
|
return eager ? skillsPrompt(ctx) : skillsIndexPrompt(ctx);
|
|
58
54
|
}
|
|
59
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Bloque de skills acotado a las que declara un subagente. `ids === null`
|
|
58
|
+
* significa "sin restricción" (todas las habilitadas); un array vacío no
|
|
59
|
+
* inyecta skills.
|
|
60
|
+
*/
|
|
61
|
+
export function skillsBlockFor(ctx, ids = null) {
|
|
62
|
+
if (!Array.isArray(ids)) return skillsBlock(ctx);
|
|
63
|
+
const wanted = new Set(ids.map(String));
|
|
64
|
+
const selected = enabledSkills(ctx).filter((skill) => wanted.has(skill.id));
|
|
65
|
+
if (!selected.length) return '';
|
|
66
|
+
return ctx.config?.skills?.mode === 'eager' ? buildSkillsPrompt(selected) : buildSkillsIndex(selected);
|
|
67
|
+
}
|
|
68
|
+
|
|
60
69
|
export async function writeSkill(id, { name, description, when, body }) {
|
|
61
70
|
const dir = path.join(homeDir(), 'skills', id);
|
|
62
71
|
await mkdir(dir, { recursive: true });
|
|
@@ -69,12 +78,9 @@ export async function writeSkill(id, { name, description, when, body }) {
|
|
|
69
78
|
}
|
|
70
79
|
|
|
71
80
|
export async function removeSkill(id) {
|
|
72
|
-
await
|
|
81
|
+
await removeDefinition('skills', id);
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
export function setSkillEnabled(store, id, enabled) {
|
|
76
|
-
|
|
77
|
-
if (enabled) current.delete(id);
|
|
78
|
-
else current.add(id);
|
|
79
|
-
store.setSetting('disabledSkills', [...current]);
|
|
85
|
+
setDefinitionEnabled(store, 'skills', id, enabled);
|
|
80
86
|
}
|
|
@@ -2,16 +2,13 @@
|
|
|
2
2
|
// contexto propio y un toolset acotado (solo lectura por defecto). Los eventos
|
|
3
3
|
// del subagente se emiten con `agentId` para que la consola los anide, y su
|
|
4
4
|
// resumen final se devuelve como resultado de la tool.
|
|
5
|
+
//
|
|
6
|
+
// Un subagente puede venir de una definicion del proyecto (`agents/*.md`): en
|
|
7
|
+
// ese caso aporta su propio modelo/proveedor, modo, tools, skills y esfuerzo.
|
|
5
8
|
|
|
6
|
-
import { runAgent, createTools, resolveMode } from './core.js';
|
|
9
|
+
import { runAgent, createTools, resolveMode, normalizeEffort, buildSubagentPrompt } from './core.js';
|
|
7
10
|
import { toolCallPayload, toolResultPayload } from './agentRun.js';
|
|
8
|
-
|
|
9
|
-
const SUBAGENT_PROMPT = [
|
|
10
|
-
'Sos un subagente de Tamnora. Ejecutá de forma autónoma la tarea encomendada',
|
|
11
|
-
'usando las herramientas disponibles y devolvé un resumen conciso y concreto de',
|
|
12
|
-
'lo que encontraste o hiciste (archivos, hallazgos, decisiones). No pidas',
|
|
13
|
-
'confirmaciones: si algo no es posible, explicalo en el resumen.',
|
|
14
|
-
].join(' ');
|
|
11
|
+
import { skillsBlockFor } from './skills.js';
|
|
15
12
|
|
|
16
13
|
function definition(name, description, parameters, required = []) {
|
|
17
14
|
return {
|
|
@@ -34,9 +31,16 @@ function clampMode(requested, parentMode) {
|
|
|
34
31
|
return { id: requested.id, tools, writeExtensions };
|
|
35
32
|
}
|
|
36
33
|
|
|
34
|
+
function intersect(list, allowed) {
|
|
35
|
+
if (!Array.isArray(list) || !list.length) return null;
|
|
36
|
+
const set = new Set(allowed);
|
|
37
|
+
return list.filter((item) => set.has(item));
|
|
38
|
+
}
|
|
39
|
+
|
|
37
40
|
/**
|
|
38
41
|
* Crea la tool `task` para una sesión/run y la función `execute`.
|
|
39
42
|
* `emit(type, data, agentId)` persiste y difunde el evento (via RunManager).
|
|
43
|
+
* `agents` es el catálogo de subagentes habilitados del workspace.
|
|
40
44
|
*/
|
|
41
45
|
export function createSubagentTool(ctx, options) {
|
|
42
46
|
const {
|
|
@@ -44,6 +48,7 @@ export function createSubagentTool(ctx, options) {
|
|
|
44
48
|
cwd,
|
|
45
49
|
providerId,
|
|
46
50
|
model,
|
|
51
|
+
effort = '',
|
|
47
52
|
permissions = {},
|
|
48
53
|
mcp = null,
|
|
49
54
|
mcpMode = 'auto',
|
|
@@ -52,15 +57,26 @@ export function createSubagentTool(ctx, options) {
|
|
|
52
57
|
exclude = [],
|
|
53
58
|
emit,
|
|
54
59
|
parentMode = null,
|
|
60
|
+
agents = [],
|
|
55
61
|
} = options;
|
|
56
62
|
|
|
63
|
+
const available = new Map(agents.map((agent) => [agent.id, agent]));
|
|
64
|
+
const agentParam = {
|
|
65
|
+
type: 'string',
|
|
66
|
+
description: available.size
|
|
67
|
+
? `Id del subagente (ver el catálogo del system prompt): ${[...available.keys()].join(', ')}`
|
|
68
|
+
: 'Id del subagente (no hay subagentes definidos)',
|
|
69
|
+
};
|
|
70
|
+
if (available.size) agentParam.enum = [...available.keys()];
|
|
71
|
+
|
|
57
72
|
const definitions = [
|
|
58
73
|
definition(
|
|
59
74
|
'task',
|
|
60
|
-
'Delega una subtarea a un subagente con su propio contexto
|
|
75
|
+
'Delega una subtarea a un subagente con su propio contexto. Si hay subagentes definidos, elegí uno con "agent"; si no, se crea uno ad-hoc de solo lectura. Devolvé un prompt claro y autocontenido; el subagente responde con un resumen.',
|
|
61
76
|
{
|
|
62
77
|
description: { type: 'string', description: 'Título corto de la subtarea' },
|
|
63
78
|
prompt: { type: 'string', description: 'Instrucción completa y autocontenida para el subagente' },
|
|
79
|
+
agent: agentParam,
|
|
64
80
|
mode: { type: 'string', enum: ['plan', 'agent'], description: 'plan (solo lectura, default) o agent' },
|
|
65
81
|
tools: { type: 'array', items: { type: 'string' }, description: 'Limitar las tools del subagente' },
|
|
66
82
|
},
|
|
@@ -76,15 +92,31 @@ export function createSubagentTool(ctx, options) {
|
|
|
76
92
|
const description = String(args.description ?? prompt).slice(0, 80).trim() || 'subtarea';
|
|
77
93
|
if (!prompt) return 'La subtarea necesita "prompt".';
|
|
78
94
|
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
|
|
95
|
+
const wanted = String(args.agent ?? '').trim();
|
|
96
|
+
const spec = wanted ? available.get(wanted) : null;
|
|
97
|
+
if (wanted && !spec) {
|
|
98
|
+
const ids = [...available.keys()].join(', ');
|
|
99
|
+
return ids ? `No existe el subagente "${wanted}". Disponibles: ${ids}.` : 'No hay subagentes definidos.';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const subProviderId = spec?.provider || providerId;
|
|
103
|
+
const subModel = spec?.model || model;
|
|
104
|
+
const subEffort = normalizeEffort(spec?.effort) || normalizeEffort(effort);
|
|
105
|
+
const rawMode = resolveMode(spec?.mode || args.mode || 'plan');
|
|
106
|
+
const mode = clampMode(rawMode, parentMode);
|
|
107
|
+
|
|
108
|
+
// Precedencia: tools de la definicion > tools pedidas > tools del modo.
|
|
109
|
+
const declared = Array.isArray(spec?.tools) && spec.tools.length ? spec.tools : null;
|
|
110
|
+
const requested = declared ?? (Array.isArray(args.tools) && args.tools.length ? args.tools : (mode.tools ?? undefined));
|
|
111
|
+
let enabled = requested ? requested.filter((tool) => !exclude.includes(tool)) : undefined;
|
|
112
|
+
if (enabled && mode.tools) enabled = intersect(enabled, mode.tools) ?? [];
|
|
113
|
+
else if (!enabled && mode.tools) enabled = [...mode.tools];
|
|
114
|
+
if (!enabled) enabled = undefined;
|
|
115
|
+
|
|
116
|
+
const title = spec ? spec.name || spec.id : description;
|
|
117
|
+
const agentId = ctx.store.createAgent({ sessionId, kind: 'task', title, model: subModel });
|
|
118
|
+
emit('agent_start', { agentId, description, mode: mode.id, model: subModel, agent: spec?.id ?? null }, agentId);
|
|
82
119
|
|
|
83
|
-
const requested =
|
|
84
|
-
Array.isArray(args.tools) && args.tools.length
|
|
85
|
-
? args.tools.filter((tool) => !exclude.includes(tool))
|
|
86
|
-
: (mode.tools ?? undefined);
|
|
87
|
-
const enabled = mode.tools ? requested.filter((tool) => mode.tools.includes(tool)) : requested;
|
|
88
120
|
const local = createTools({
|
|
89
121
|
root: cwd,
|
|
90
122
|
enabled,
|
|
@@ -96,29 +128,45 @@ export function createSubagentTool(ctx, options) {
|
|
|
96
128
|
mcpPinned,
|
|
97
129
|
signal,
|
|
98
130
|
});
|
|
99
|
-
|
|
131
|
+
// Un subagente con skills declaradas ve solo esas skills (arbol de contexto
|
|
132
|
+
// acotado); sin declaracion hereda el bloque general.
|
|
133
|
+
const skillsText = spec ? skillsBlockFor(ctx, spec.skills) : undefined;
|
|
134
|
+
const basePrompt = await ctx.systemPromptFor(cwd, {
|
|
135
|
+
skillsText: spec ? skillsText : undefined,
|
|
136
|
+
agentsText: '',
|
|
137
|
+
});
|
|
138
|
+
const systemPrompt = buildSubagentPrompt(
|
|
139
|
+
{
|
|
140
|
+
id: spec?.id ?? 'task',
|
|
141
|
+
name: spec?.name ?? description,
|
|
142
|
+
description: spec?.description ?? '',
|
|
143
|
+
body: spec?.body ?? '',
|
|
144
|
+
},
|
|
145
|
+
{ basePrompt }
|
|
146
|
+
);
|
|
100
147
|
const messages = [
|
|
101
|
-
{ role: 'system', content: `${
|
|
148
|
+
{ role: 'system', content: `${systemPrompt}\n\nTarea: ${description}` },
|
|
102
149
|
{ role: 'user', content: prompt },
|
|
103
150
|
];
|
|
104
151
|
const { provider, config } = ctx.resolveProvider({
|
|
105
|
-
...ctx.providerSettings(
|
|
106
|
-
apiKey: ctx.apiKeyFor(
|
|
152
|
+
...ctx.providerSettings(subProviderId),
|
|
153
|
+
apiKey: ctx.apiKeyFor(subProviderId),
|
|
107
154
|
});
|
|
108
155
|
|
|
109
156
|
try {
|
|
110
157
|
const result = await runAgent({
|
|
111
158
|
provider,
|
|
112
159
|
config,
|
|
113
|
-
model,
|
|
160
|
+
model: subModel,
|
|
114
161
|
messages,
|
|
115
162
|
tools: local.definitions,
|
|
116
163
|
execute: local.execute,
|
|
117
164
|
redactExtra: ctx.secretExtra?.() ?? [],
|
|
118
165
|
signal,
|
|
119
|
-
maxRounds: ctx.config.agents?.maxRounds
|
|
166
|
+
maxRounds: spec?.maxRounds || ctx.config.agents?.maxRounds || 6,
|
|
120
167
|
maxOutputTokens: ctx.config.agents?.maxOutputTokens ?? ctx.config.maxOutputTokens ?? 0,
|
|
121
168
|
promptCaching: ctx.effectivePromptCaching?.() ?? ctx.config.promptCaching === true,
|
|
169
|
+
reasoningEffort: subEffort,
|
|
122
170
|
onEvent(event) {
|
|
123
171
|
if (event.type === 'text') emit('text', { full: event.full, agentId }, agentId);
|
|
124
172
|
else if (event.type === 'reasoning') emit('reasoning', { full: event.full, agentId }, agentId);
|
|
@@ -9,14 +9,14 @@ export const DEFAULT_TTS = {
|
|
|
9
9
|
voice: VOZ_POR_DEFECTO,
|
|
10
10
|
rate: '+0%',
|
|
11
11
|
pitch: '+0Hz',
|
|
12
|
-
announce: { start: true, tools: true, end: true,
|
|
12
|
+
announce: { start: true, tools: true, end: true, answer: false },
|
|
13
13
|
};
|
|
14
14
|
|
|
15
15
|
/** Merge de defaults + lo guardado (con `announce` aplanado a booleans). */
|
|
16
16
|
export function resolveTts(app) {
|
|
17
17
|
const saved = app?.tts && typeof app.tts === 'object' ? app.tts : {};
|
|
18
18
|
const announce = { ...DEFAULT_TTS.announce, ...(saved.announce ?? {}) };
|
|
19
|
-
for (const key of ['start', 'tools', 'end', '
|
|
19
|
+
for (const key of ['start', 'tools', 'end', 'answer']) announce[key] = announce[key] === true;
|
|
20
20
|
return {
|
|
21
21
|
...DEFAULT_TTS,
|
|
22
22
|
...saved,
|