@danieltmn/tamnora 0.25.0 → 0.25.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 +6 -0
- package/harness/packages/cli/src/commands/repl.js +18 -2
- package/harness/packages/cli/src/commands/run.js +5 -1
- package/harness/packages/cli/src/commands/tui.js +5 -1
- package/harness/packages/cli/src/lib/runtime.js +2 -0
- package/harness/packages/cli/src/tui/app.js +22 -2
- package/harness/packages/cli/src/tui/layout.js +1 -1
- package/harness/packages/core/src/agent/runAgent.js +5 -2
- package/harness/packages/core/src/agents/index.js +141 -0
- package/harness/packages/core/src/config/index.js +1 -0
- package/harness/packages/core/src/index.js +10 -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/test/agents.test.js +111 -0
- package/harness/packages/core/test/reasoning.test.js +43 -0
- package/harness/packages/server/public/app.js +37 -0
- package/harness/packages/server/public/chat.js +1 -0
- package/harness/packages/server/public/chrome.js +94 -1
- package/harness/packages/server/public/core.js +1 -1
- package/harness/packages/server/public/icons (1).zip +0 -0
- package/harness/packages/server/public/index.html +90 -1
- package/harness/packages/server/public/sessions.js +1 -0
- package/harness/packages/server/public/settings.js +167 -0
- package/harness/packages/server/public/settingsNav.js +1 -0
- package/harness/packages/server/src/lib/agents.js +74 -0
- package/harness/packages/server/src/lib/channels/telegram/run.js +1 -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 +13 -0
- package/harness/packages/server/src/lib/tasks.js +71 -23
- package/harness/packages/server/src/routes/agents.js +88 -0
- package/harness/packages/server/src/routes/chat.js +9 -1
- 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/server.js +7 -0
- package/harness/packages/server/test/agents.test.js +194 -0
- package/harness/packages/server/test/consoleTabs.test.js +2 -2
- package/harness/packages/server/test/settingsNav.test.js +2 -2
- package/harness/tamnora.example.json +1 -0
- package/package.json +1 -1
|
@@ -185,6 +185,9 @@ export const skillSearchResultsEl = $('#skill-search-results');
|
|
|
185
185
|
export const skillCatalogEl = $('#skill-catalog');
|
|
186
186
|
export const skillModal = $('#skill-modal');
|
|
187
187
|
export const skillForm = $('#skill-form');
|
|
188
|
+
export const agentListEl = $('#agent-list');
|
|
189
|
+
export const agentModal = $('#agent-modal');
|
|
190
|
+
export const agentForm = $('#agent-form');
|
|
188
191
|
export const memoryListEl = $('#memory-list');
|
|
189
192
|
export const memoryIndexEl = $('#memory-index');
|
|
190
193
|
export const memoryModal = $('#memory-modal');
|
|
@@ -556,6 +559,170 @@ export async function submitSkill(event) {
|
|
|
556
559
|
refreshSkills();
|
|
557
560
|
}
|
|
558
561
|
|
|
562
|
+
/* --- ajustes: Subagentes ------------------------------------------------- */
|
|
563
|
+
|
|
564
|
+
export async function refreshAgents() {
|
|
565
|
+
if (!agentListEl) return;
|
|
566
|
+
const data = await api('/api/agents').catch(() => null);
|
|
567
|
+
if (!data) return;
|
|
568
|
+
clear(agentListEl);
|
|
569
|
+
if (!data.agents.length) {
|
|
570
|
+
agentListEl.appendChild(
|
|
571
|
+
emptyState(
|
|
572
|
+
'Sin subagentes todavía',
|
|
573
|
+
'Tocá «+ nuevo» para crear uno: el agente principal va a verlo en su catálogo y podrá delegarle tareas con task.'
|
|
574
|
+
)
|
|
575
|
+
);
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
for (const agent of data.agents) {
|
|
579
|
+
const li = el('li', agent.enabled ? 'active' : '');
|
|
580
|
+
li.appendChild(el('span', `pdot${agent.enabled ? ' on' : ''}`));
|
|
581
|
+
const main = el('div', 'grow');
|
|
582
|
+
const details = [
|
|
583
|
+
agent.provider || 'proveedor heredado',
|
|
584
|
+
agent.model || 'modelo heredado',
|
|
585
|
+
agent.mode || 'modo heredado',
|
|
586
|
+
agent.effort ? `esfuerzo ${agent.effort}` : '',
|
|
587
|
+
Array.isArray(agent.skills) ? `${agent.skills.length} skill(s)` : 'todas las skills',
|
|
588
|
+
].filter(Boolean);
|
|
589
|
+
main.append(
|
|
590
|
+
el('div', '', `${agent.name || agent.id} · ${agent.id}`),
|
|
591
|
+
el('div', 'pmeta', agent.description || details.join(' · '))
|
|
592
|
+
);
|
|
593
|
+
li.appendChild(main);
|
|
594
|
+
const actions = el('div', 'pactions');
|
|
595
|
+
actions.appendChild(button(agent.enabled ? 'deshabilitar' : 'habilitar', (event) => { event.stopPropagation(); toggleAgent(agent.id); }));
|
|
596
|
+
actions.appendChild(button('editar', (event) => { event.stopPropagation(); openAgentForm(agent.id); }));
|
|
597
|
+
if (agent.removable) {
|
|
598
|
+
actions.appendChild(button('borrar', (event) => { event.stopPropagation(); removeAgent(agent.id); }, 'btn danger'));
|
|
599
|
+
}
|
|
600
|
+
li.appendChild(actions);
|
|
601
|
+
li.addEventListener('click', () => openAgentForm(agent.id));
|
|
602
|
+
agentListEl.appendChild(li);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function fillAgentProviders() {
|
|
607
|
+
const select = $('#af-provider');
|
|
608
|
+
if (!select) return;
|
|
609
|
+
const current = select.value;
|
|
610
|
+
clear(select);
|
|
611
|
+
const inherit = document.createElement('option');
|
|
612
|
+
inherit.value = '';
|
|
613
|
+
inherit.textContent = 'heredar';
|
|
614
|
+
select.appendChild(inherit);
|
|
615
|
+
for (const provider of state.providers ?? []) {
|
|
616
|
+
const option = document.createElement('option');
|
|
617
|
+
option.value = provider.id;
|
|
618
|
+
option.textContent = provider.label && provider.label !== provider.id ? `${provider.label} (${provider.id})` : provider.id;
|
|
619
|
+
select.appendChild(option);
|
|
620
|
+
}
|
|
621
|
+
select.value = current;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function fillAgentSkills(selected = null) {
|
|
625
|
+
const select = $('#af-skills');
|
|
626
|
+
if (!select) return;
|
|
627
|
+
clear(select);
|
|
628
|
+
for (const skill of state.skills ?? []) {
|
|
629
|
+
const option = document.createElement('option');
|
|
630
|
+
option.value = skill.id;
|
|
631
|
+
option.textContent = `${skill.name || skill.id}${skill.enabled ? '' : ' (deshabilitada)'}`;
|
|
632
|
+
option.disabled = !skill.enabled;
|
|
633
|
+
select.appendChild(option);
|
|
634
|
+
}
|
|
635
|
+
const wanted = new Set(Array.isArray(selected) ? selected : []);
|
|
636
|
+
for (const option of select.options) option.selected = wanted.has(option.value);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export async function openAgentForm(id = null) {
|
|
640
|
+
$('#agent-title').textContent = id ? `editar ${id}` : 'nuevo subagente';
|
|
641
|
+
$('#af-id').value = id ?? '';
|
|
642
|
+
$('#af-id').readOnly = Boolean(id);
|
|
643
|
+
$('#af-name').value = '';
|
|
644
|
+
$('#af-description').value = '';
|
|
645
|
+
$('#af-model').value = '';
|
|
646
|
+
$('#af-rounds').value = '';
|
|
647
|
+
$('#af-tools').value = '';
|
|
648
|
+
$('#af-body').value = '';
|
|
649
|
+
setStatus('#af-status', '');
|
|
650
|
+
const providers = await api('/api/providers').catch(() => null);
|
|
651
|
+
if (providers?.providers) state.providers = providers.providers;
|
|
652
|
+
const skills = await api('/api/skills').catch(() => null);
|
|
653
|
+
state.skills = skills?.skills ?? [];
|
|
654
|
+
const effort = $('#af-effort');
|
|
655
|
+
if (effort) effort.value = '';
|
|
656
|
+
fillAgentProviders();
|
|
657
|
+
$('#af-provider').value = '';
|
|
658
|
+
$('#af-mode').value = '';
|
|
659
|
+
fillAgentSkills([]);
|
|
660
|
+
if (id) {
|
|
661
|
+
const data = await api(`/api/agents/${encodeURIComponent(id)}`).catch(() => null);
|
|
662
|
+
if (data?.agent) {
|
|
663
|
+
const agent = data.agent;
|
|
664
|
+
$('#af-name').value = agent.name ?? '';
|
|
665
|
+
$('#af-description').value = agent.description ?? '';
|
|
666
|
+
$('#af-model').value = agent.model ?? '';
|
|
667
|
+
$('#af-provider').value = agent.provider ?? '';
|
|
668
|
+
$('#af-mode').value = agent.mode ?? '';
|
|
669
|
+
$('#af-effort').value = agent.effort ?? '';
|
|
670
|
+
$('#af-rounds').value = agent.maxRounds || '';
|
|
671
|
+
$('#af-tools').value = (agent.tools ?? []).join(', ');
|
|
672
|
+
fillAgentSkills(agent.skills ?? null);
|
|
673
|
+
$('#af-body').value = agent.body ?? '';
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
agentModal.showModal();
|
|
677
|
+
if (!id) $('#af-id').focus();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
export function closeAgentForm() {
|
|
681
|
+
if (agentModal?.open) agentModal.close();
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export async function toggleAgent(id) {
|
|
685
|
+
await toggleEntity(`/api/agents/${encodeURIComponent(id)}/toggle`, {
|
|
686
|
+
method: 'POST',
|
|
687
|
+
errorLabel: 'cambiar el subagente',
|
|
688
|
+
refresh: refreshAgents,
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export async function removeAgent(id) {
|
|
693
|
+
await deleteEntity(`/api/agents/${encodeURIComponent(id)}`, {
|
|
694
|
+
confirmText: `¿Eliminar el subagente "${id}"?`,
|
|
695
|
+
refresh: refreshAgents,
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
export async function submitAgent(event) {
|
|
700
|
+
event.preventDefault();
|
|
701
|
+
const id = $('#af-id').value.trim();
|
|
702
|
+
if (!id) return setStatus('#af-status', 'Falta el id.', 'err');
|
|
703
|
+
const skillsSelect = $('#af-skills');
|
|
704
|
+
const selectedSkills = skillsSelect ? [...skillsSelect.selectedOptions].map((option) => option.value) : [];
|
|
705
|
+
const body = {
|
|
706
|
+
name: $('#af-name').value.trim() || id,
|
|
707
|
+
description: $('#af-description').value.trim(),
|
|
708
|
+
provider: $('#af-provider').value.trim(),
|
|
709
|
+
model: $('#af-model').value.trim(),
|
|
710
|
+
mode: $('#af-mode').value,
|
|
711
|
+
effort: $('#af-effort').value,
|
|
712
|
+
maxRounds: Number($('#af-rounds').value) || 0,
|
|
713
|
+
tools: $('#af-tools').value.split(',').map((item) => item.trim()).filter(Boolean),
|
|
714
|
+
skills: selectedSkills.length ? selectedSkills : null,
|
|
715
|
+
body: $('#af-body').value,
|
|
716
|
+
};
|
|
717
|
+
try {
|
|
718
|
+
await api(`/api/agents/${encodeURIComponent(id)}`, { method: 'PUT', body });
|
|
719
|
+
} catch (error) {
|
|
720
|
+
return setStatus('#af-status', error.message, 'err');
|
|
721
|
+
}
|
|
722
|
+
closeAgentForm();
|
|
723
|
+
refreshAgents();
|
|
724
|
+
}
|
|
725
|
+
|
|
559
726
|
/* --- ajustes: Memoria ---------------------------------------------------- */
|
|
560
727
|
|
|
561
728
|
export async function refreshMemory() {
|
|
@@ -20,6 +20,7 @@ export const SETTINGS_GROUPS = [
|
|
|
20
20
|
{ id: 'perfil', label: 'Perfil', keywords: 'nombre idioma notas como llamarte usuario datos' },
|
|
21
21
|
{ id: 'memoria', label: 'Memoria', keywords: 'recuerdos proyecto notas durable indice' },
|
|
22
22
|
{ id: 'skills', label: 'Skills', keywords: 'habilidades instrucciones skill catalogo' },
|
|
23
|
+
{ id: 'agentes', label: 'Subagentes', keywords: 'agentes subagentes delegar task modelo skills esfuerzo' },
|
|
23
24
|
{ id: 'mcp', label: 'MCP', keywords: 'servidores herramientas tools conectores catalogo playwright' },
|
|
24
25
|
],
|
|
25
26
|
},
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { rm } from 'node:fs/promises';
|
|
3
|
+
import {
|
|
4
|
+
homeDir,
|
|
5
|
+
loadAgents,
|
|
6
|
+
mergeAgents,
|
|
7
|
+
buildAgentsIndex,
|
|
8
|
+
writeAgentFile,
|
|
9
|
+
} from './core.js';
|
|
10
|
+
|
|
11
|
+
export const AGENT_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,39}$/i;
|
|
12
|
+
|
|
13
|
+
/** Agent roots, lowest priority first: casa agents, then the workspace's. */
|
|
14
|
+
export function agentRoots(cwd) {
|
|
15
|
+
return [path.join(homeDir(), 'agents'), path.join(cwd, '.tamnora', 'agents')];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function allAgents(cwd) {
|
|
19
|
+
const home = path.join(homeDir(), 'agents');
|
|
20
|
+
return mergeAgents(agentRoots(cwd).map((root) => loadAgents(root).map((a) => ({ ...a, root, home: root === home }))));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function disabledIds(store) {
|
|
24
|
+
const raw = store.getSetting('disabledAgents');
|
|
25
|
+
return new Set(Array.isArray(raw) ? raw.map(String) : []);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function agentsView(ctx) {
|
|
29
|
+
const disabled = disabledIds(ctx.store);
|
|
30
|
+
return allAgents(ctx.cwd).map((agent) => ({
|
|
31
|
+
id: agent.id,
|
|
32
|
+
name: agent.name || agent.id,
|
|
33
|
+
description: agent.description || '',
|
|
34
|
+
model: agent.model || '',
|
|
35
|
+
provider: agent.provider || '',
|
|
36
|
+
mode: agent.mode || '',
|
|
37
|
+
effort: agent.effort || '',
|
|
38
|
+
maxRounds: agent.maxRounds || 0,
|
|
39
|
+
tools: agent.tools,
|
|
40
|
+
skills: agent.skills,
|
|
41
|
+
enabled: !disabled.has(agent.id),
|
|
42
|
+
removable: Boolean(agent.home),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function findAgent(ctx, id) {
|
|
47
|
+
return allAgents(ctx.cwd).find((agent) => agent.id === id) ?? null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function enabledAgents(ctx) {
|
|
51
|
+
const disabled = disabledIds(ctx.store);
|
|
52
|
+
return allAgents(ctx.cwd).filter((agent) => !disabled.has(agent.id));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function agentsIndexPrompt(ctx) {
|
|
56
|
+
return buildAgentsIndex(enabledAgents(ctx));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function writeAgent(id, data) {
|
|
60
|
+
const dir = path.join(homeDir(), 'agents', String(id));
|
|
61
|
+
await writeAgentFile(dir, { ...data, id });
|
|
62
|
+
return dir;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function removeAgent(id) {
|
|
66
|
+
await rm(path.join(homeDir(), 'agents', String(id)), { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function setAgentEnabled(store, id, enabled) {
|
|
70
|
+
const current = new Set(Array.isArray(store.getSetting('disabledAgents')) ? store.getSetting('disabledAgents') : []);
|
|
71
|
+
if (enabled) current.delete(id);
|
|
72
|
+
else current.add(id);
|
|
73
|
+
store.setSetting('disabledAgents', [...current]);
|
|
74
|
+
}
|
|
@@ -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;
|
|
@@ -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),
|
|
@@ -57,6 +57,19 @@ export function skillsBlock(ctx) {
|
|
|
57
57
|
return eager ? skillsPrompt(ctx) : skillsIndexPrompt(ctx);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Bloque de skills acotado a las que declara un subagente. `ids === null`
|
|
62
|
+
* significa "sin restricción" (todas las habilitadas); un array vacío no
|
|
63
|
+
* inyecta skills.
|
|
64
|
+
*/
|
|
65
|
+
export function skillsBlockFor(ctx, ids = null) {
|
|
66
|
+
if (!Array.isArray(ids)) return skillsBlock(ctx);
|
|
67
|
+
const wanted = new Set(ids.map(String));
|
|
68
|
+
const selected = enabledSkills(ctx).filter((skill) => wanted.has(skill.id));
|
|
69
|
+
if (!selected.length) return '';
|
|
70
|
+
return ctx.config?.skills?.mode === 'eager' ? buildSkillsPrompt(selected) : buildSkillsIndex(selected);
|
|
71
|
+
}
|
|
72
|
+
|
|
60
73
|
export async function writeSkill(id, { name, description, when, body }) {
|
|
61
74
|
const dir = path.join(homeDir(), 'skills', id);
|
|
62
75
|
await mkdir(dir, { recursive: true });
|
|
@@ -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);
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { readJson, sendJson, sendError } from '../lib/http.js';
|
|
2
|
+
import { normalizeEffort, resolveMode } from '../lib/core.js';
|
|
3
|
+
import {
|
|
4
|
+
AGENT_ID_PATTERN,
|
|
5
|
+
agentsView,
|
|
6
|
+
findAgent,
|
|
7
|
+
writeAgent,
|
|
8
|
+
removeAgent,
|
|
9
|
+
setAgentEnabled,
|
|
10
|
+
} from '../lib/agents.js';
|
|
11
|
+
|
|
12
|
+
function cleanList(value) {
|
|
13
|
+
if (value === undefined || value === null) return null;
|
|
14
|
+
if (!Array.isArray(value)) return null;
|
|
15
|
+
return value.map((item) => String(item).trim()).filter(Boolean);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerAgents(route, ctx) {
|
|
19
|
+
route('GET', '/api/agents', ({ res }) => {
|
|
20
|
+
sendJson(res, 200, { agents: agentsView(ctx) });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
route('GET', '/api/agents/:id', ({ res, params }) => {
|
|
24
|
+
const agent = findAgent(ctx, params.id);
|
|
25
|
+
if (!agent) return sendError(res, 404, 'Subagente no encontrado.');
|
|
26
|
+
sendJson(res, 200, {
|
|
27
|
+
agent: {
|
|
28
|
+
id: agent.id,
|
|
29
|
+
name: agent.name || agent.id,
|
|
30
|
+
description: agent.description || '',
|
|
31
|
+
model: agent.model || '',
|
|
32
|
+
provider: agent.provider || '',
|
|
33
|
+
mode: agent.mode || '',
|
|
34
|
+
effort: agent.effort || '',
|
|
35
|
+
maxRounds: agent.maxRounds || 0,
|
|
36
|
+
tools: agent.tools,
|
|
37
|
+
skills: agent.skills,
|
|
38
|
+
body: agent.body,
|
|
39
|
+
removable: Boolean(agent.home),
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
route('PUT', '/api/agents/:id', async ({ req, res, params }) => {
|
|
45
|
+
const id = String(params.id);
|
|
46
|
+
if (!AGENT_ID_PATTERN.test(id)) return sendError(res, 400, 'Id de subagente inválido (a-z, 0-9, - y _).');
|
|
47
|
+
const body = await readJson(req);
|
|
48
|
+
if (!String(body.body ?? '').trim() && !String(body.description ?? '').trim()) {
|
|
49
|
+
return sendError(res, 400, 'El subagente necesita un cuerpo o una descripción.');
|
|
50
|
+
}
|
|
51
|
+
const mode = body.mode ? resolveMode(String(body.mode)).id : '';
|
|
52
|
+
const maxRounds = Number(body.maxRounds);
|
|
53
|
+
try {
|
|
54
|
+
await writeAgent(id, {
|
|
55
|
+
name: String(body.name ?? id).trim() || id,
|
|
56
|
+
description: String(body.description ?? '').trim(),
|
|
57
|
+
model: String(body.model ?? '').trim(),
|
|
58
|
+
provider: String(body.provider ?? '').trim(),
|
|
59
|
+
mode,
|
|
60
|
+
effort: normalizeEffort(body.effort),
|
|
61
|
+
maxRounds: Number.isFinite(maxRounds) && maxRounds > 0 ? Math.round(maxRounds) : 0,
|
|
62
|
+
tools: cleanList(body.tools),
|
|
63
|
+
skills: cleanList(body.skills),
|
|
64
|
+
body: String(body.body ?? ''),
|
|
65
|
+
});
|
|
66
|
+
} catch (error) {
|
|
67
|
+
return sendError(res, 500, `No se pudo guardar el subagente: ${error.message}`);
|
|
68
|
+
}
|
|
69
|
+
sendJson(res, 200, { agents: agentsView(ctx) });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
route('DELETE', '/api/agents/:id', async ({ res, params }) => {
|
|
73
|
+
const agent = findAgent(ctx, params.id);
|
|
74
|
+
if (!agent) return sendError(res, 404, 'Subagente no encontrado.');
|
|
75
|
+
if (!agent.home) return sendError(res, 400, 'Ese subagente vive en el workspace y no se borra desde acá.');
|
|
76
|
+
await removeAgent(agent.id);
|
|
77
|
+
setAgentEnabled(ctx.store, agent.id, true);
|
|
78
|
+
sendJson(res, 200, { agents: agentsView(ctx) });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
route('POST', '/api/agents/:id/toggle', async ({ res, params }) => {
|
|
82
|
+
const agent = findAgent(ctx, params.id);
|
|
83
|
+
if (!agent) return sendError(res, 404, 'Subagente no encontrado.');
|
|
84
|
+
const enabled = !agentsView(ctx).find((item) => item.id === agent.id)?.enabled;
|
|
85
|
+
setAgentEnabled(ctx.store, agent.id, enabled);
|
|
86
|
+
sendJson(res, 200, { id: agent.id, enabled, agents: agentsView(ctx) });
|
|
87
|
+
});
|
|
88
|
+
}
|