@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/harness/packages/cli/src/commands/repl.js +18 -2
  3. package/harness/packages/cli/src/commands/run.js +5 -1
  4. package/harness/packages/cli/src/commands/tui.js +5 -1
  5. package/harness/packages/cli/src/lib/runtime.js +2 -0
  6. package/harness/packages/cli/src/tui/app.js +22 -2
  7. package/harness/packages/cli/src/tui/layout.js +1 -1
  8. package/harness/packages/core/src/agent/runAgent.js +5 -2
  9. package/harness/packages/core/src/agents/index.js +141 -0
  10. package/harness/packages/core/src/config/index.js +1 -0
  11. package/harness/packages/core/src/index.js +10 -0
  12. package/harness/packages/core/src/providers/index.js +3 -1
  13. package/harness/packages/core/src/providers/ollama.js +4 -1
  14. package/harness/packages/core/src/providers/openaiCompatible.js +6 -1
  15. package/harness/packages/core/src/providers/openrouter.js +5 -1
  16. package/harness/packages/core/src/providers/presets.js +4 -3
  17. package/harness/packages/core/src/providers/reasoning.js +58 -0
  18. package/harness/packages/core/test/agents.test.js +111 -0
  19. package/harness/packages/core/test/reasoning.test.js +43 -0
  20. package/harness/packages/server/public/app.js +37 -0
  21. package/harness/packages/server/public/chat.js +1 -0
  22. package/harness/packages/server/public/chrome.js +94 -1
  23. package/harness/packages/server/public/core.js +1 -1
  24. package/harness/packages/server/public/icons (1).zip +0 -0
  25. package/harness/packages/server/public/index.html +90 -1
  26. package/harness/packages/server/public/sessions.js +1 -0
  27. package/harness/packages/server/public/settings.js +167 -0
  28. package/harness/packages/server/public/settingsNav.js +1 -0
  29. package/harness/packages/server/src/lib/agents.js +74 -0
  30. package/harness/packages/server/src/lib/channels/telegram/run.js +1 -0
  31. package/harness/packages/server/src/lib/prompt.js +11 -3
  32. package/harness/packages/server/src/lib/scheduler.js +4 -0
  33. package/harness/packages/server/src/lib/sessionView.js +1 -0
  34. package/harness/packages/server/src/lib/skills.js +13 -0
  35. package/harness/packages/server/src/lib/tasks.js +71 -23
  36. package/harness/packages/server/src/routes/agents.js +88 -0
  37. package/harness/packages/server/src/routes/chat.js +9 -1
  38. package/harness/packages/server/src/routes/providers.js +7 -0
  39. package/harness/packages/server/src/routes/sessions.js +3 -1
  40. package/harness/packages/server/src/server.js +7 -0
  41. package/harness/packages/server/test/agents.test.js +194 -0
  42. package/harness/packages/server/test/consoleTabs.test.js +2 -2
  43. package/harness/packages/server/test/settingsNav.test.js +2 -2
  44. package/harness/tamnora.example.json +1 -0
  45. 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
- const extra = typeof skillsText === 'function' ? String(skillsText() ?? '').trim() : '';
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 (solo lectura por defecto). Devolvé un prompt claro y autocontenido; el subagente responde con un resumen.',
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 agentId = ctx.store.createAgent({ sessionId, kind: 'task', title: description, model });
80
- const mode = clampMode(resolveMode(args.mode ?? 'plan'), parentMode);
81
- emit('agent_start', { agentId, description, mode: mode.id, model }, agentId);
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
- const basePrompt = await ctx.systemPromptFor(cwd);
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: `${basePrompt}\n\n${SUBAGENT_PROMPT}\n\nTarea: ${description}` },
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(providerId),
106
- apiKey: ctx.apiKeyFor(providerId),
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 ?? 6,
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
+ }