@danieltmn/tamnora 0.24.2 → 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 +20 -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 +868 -834
- package/harness/packages/server/public/appearance.js +4 -0
- package/harness/packages/server/public/boot.js +49 -0
- package/harness/packages/server/public/chat.js +1 -0
- package/harness/packages/server/public/chrome.js +946 -852
- package/harness/packages/server/public/commands.js +448 -405
- package/harness/packages/server/public/core.js +229 -216
- package/harness/packages/server/public/favicon.ico +0 -0
- package/harness/packages/server/public/icons/icon-128x128.png +0 -0
- package/harness/packages/server/public/icons/icon-144x144.png +0 -0
- package/harness/packages/server/public/icons/icon-152x152.png +0 -0
- package/harness/packages/server/public/icons/icon-192x192.png +0 -0
- package/harness/packages/server/public/icons/icon-384x384.png +0 -0
- package/harness/packages/server/public/icons/icon-512x512.png +0 -0
- package/harness/packages/server/public/icons/icon-72x72.png +0 -0
- package/harness/packages/server/public/icons/icon-96x96.png +0 -0
- package/harness/packages/server/public/icons/logo.png +0 -0
- package/harness/packages/server/public/icons/logo_dark.png +0 -0
- package/harness/packages/server/public/icons/logo_light.png +0 -0
- package/harness/packages/server/public/icons/tamnora.ico +0 -0
- package/harness/packages/server/public/icons (1).zip +0 -0
- package/harness/packages/server/public/index.html +161 -101
- package/harness/packages/server/public/landing/boot.js +16 -0
- package/harness/packages/server/public/landing/index.html +123 -59
- package/harness/packages/server/public/landing/landing.js +11 -9
- package/harness/packages/server/public/logo.png +0 -0
- package/harness/packages/server/public/markdown.js +1 -1
- package/harness/packages/server/public/sessions.js +1 -0
- package/harness/packages/server/public/settings.js +1322 -1080
- package/harness/packages/server/public/settingsNav.js +82 -0
- package/harness/packages/server/public/styles.css +2807 -2525
- package/harness/packages/server/public/system.js +2 -2
- package/harness/packages/server/src/lib/agents.js +74 -0
- package/harness/packages/server/src/lib/appearance.js +4 -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/restart.js +20 -0
- package/harness/packages/server/src/lib/scheduler.js +4 -0
- package/harness/packages/server/src/lib/securityHeaders.js +53 -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/reload.js +46 -0
- package/harness/packages/server/src/routes/sessions.js +3 -1
- package/harness/packages/server/src/server.js +13 -1
- package/harness/packages/server/test/accessibility.test.js +72 -0
- package/harness/packages/server/test/agents.test.js +194 -0
- package/harness/packages/server/test/appearance.test.js +4 -0
- package/harness/packages/server/test/consoleModules.test.js +133 -57
- package/harness/packages/server/test/consoleTabs.test.js +228 -0
- package/harness/packages/server/test/docsSync.test.js +73 -0
- package/harness/packages/server/test/landingLogin.test.js +89 -0
- package/harness/packages/server/test/markdownSecurity.test.js +80 -0
- package/harness/packages/server/test/reload.test.js +95 -0
- package/harness/packages/server/test/securityHeaders.test.js +109 -0
- package/harness/packages/server/test/settingsNav.test.js +90 -0
- package/harness/packages/server/test/themeCss.js +110 -0
- package/harness/packages/server/test/themes.test.js +72 -0
- package/harness/scripts/restart.mjs +101 -0
- package/harness/tamnora.example.json +1 -0
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { livePermission, reloadPane, renderSessions, resolvePermissionCard, sendPresence } from './chat.js';
|
|
2
2
|
import { providerSelect, setSummaryText, yesInput } from './chrome.js';
|
|
3
3
|
import { toast } from './commands.js';
|
|
4
|
-
import { $, api, clear, el, openStream, showConfirm, state, storage, streamPost } from './core.js';
|
|
4
|
+
import { $, api, clear, el, emptyState, openStream, showConfirm, state, storage, streamPost } from './core.js';
|
|
5
5
|
import { diffState, localState, pollDelayMs } from './reconcile.js';
|
|
6
6
|
import { followRun } from './runStream.js';
|
|
7
7
|
import { closeTab, renderTabs, showStandalonePermission } from './sessions.js';
|
|
@@ -116,7 +116,7 @@ export async function refreshRuns() {
|
|
|
116
116
|
});
|
|
117
117
|
runsList.appendChild(li);
|
|
118
118
|
}
|
|
119
|
-
if (!data.runs.length) runsList.appendChild(
|
|
119
|
+
if (!data.runs.length) runsList.appendChild(emptyState('Sin runs todavía', 'Las evaluaciones que corras desde la pestaña eval quedan guardadas acá.'));
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
export async function openRun(id) {
|
|
@@ -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
|
+
}
|
|
@@ -11,10 +11,14 @@ export const THEME_IDS = [
|
|
|
11
11
|
'solarized-dark',
|
|
12
12
|
'bosque',
|
|
13
13
|
'vino',
|
|
14
|
+
'arena',
|
|
15
|
+
'hielo',
|
|
14
16
|
'vercel',
|
|
15
17
|
'dark-plus',
|
|
16
18
|
'github',
|
|
17
19
|
'github-dark',
|
|
20
|
+
'mocha',
|
|
21
|
+
'ebano',
|
|
18
22
|
];
|
|
19
23
|
|
|
20
24
|
export const FONT_IDS = ['clasico', 'sistema', 'plex', 'inter', 'jetbrains', 'fira', 'source'];
|
|
@@ -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) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Lanza el script detached que espera la muerte de este server y lo relanza con
|
|
2
|
+
// el mismo comando con que arranco. Devolver rapido es el punto: el server tiene
|
|
3
|
+
// que soltar el puerto antes de que el nuevo intente entrar.
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { packageRoot, homeDir } from './core.js';
|
|
7
|
+
|
|
8
|
+
export function spawnRestart({ pid = process.pid, argv = process.argv.slice(1), home } = {}) {
|
|
9
|
+
const root = packageRoot();
|
|
10
|
+
if (!root) return { ok: false, error: 'no encontre el paquete del harness' };
|
|
11
|
+
const script = path.join(root, 'harness', 'scripts', 'restart.mjs');
|
|
12
|
+
const args = [script, '--pid', String(pid), '--argv', JSON.stringify(argv), '--home', home || homeDir()];
|
|
13
|
+
try {
|
|
14
|
+
const child = spawn(process.execPath, args, { detached: true, stdio: 'ignore', windowsHide: true });
|
|
15
|
+
child.unref();
|
|
16
|
+
return { ok: true, pid: child.pid };
|
|
17
|
+
} catch (error) {
|
|
18
|
+
return { ok: false, error: String(error?.message ?? error) };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -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 });
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Cabeceras de seguridad de la consola. La consola es una SPA sin build: sus
|
|
2
|
+
// scripts son modulos propios, asi que `script-src 'self'` alcanza y un XSS por
|
|
3
|
+
// un hueco del renderer de markdown no puede cargar ni ejecutar codigo ajeno.
|
|
4
|
+
|
|
5
|
+
const FONTS_CSS = 'https://fonts.googleapis.com';
|
|
6
|
+
const FONTS_FILES = 'https://fonts.gstatic.com';
|
|
7
|
+
|
|
8
|
+
export const CSP_DIRECTIVES = {
|
|
9
|
+
'default-src': ["'self'"],
|
|
10
|
+
'script-src': ["'self'"],
|
|
11
|
+
'style-src': ["'self'", "'unsafe-inline'", FONTS_CSS],
|
|
12
|
+
'font-src': ["'self'", FONTS_FILES, 'data:'],
|
|
13
|
+
'img-src': ["'self'", 'data:', 'blob:'],
|
|
14
|
+
'media-src': ["'self'", 'blob:', 'data:'],
|
|
15
|
+
'connect-src': ["'self'"],
|
|
16
|
+
// La pestaña preview embebe el dev server del usuario (local o por tunel).
|
|
17
|
+
'frame-src': ['http:', 'https:'],
|
|
18
|
+
'frame-ancestors': ["'self'"],
|
|
19
|
+
'manifest-src': ["'self'"],
|
|
20
|
+
'worker-src': ["'self'", 'blob:'],
|
|
21
|
+
'object-src': ["'none'"],
|
|
22
|
+
'base-uri': ["'self'"],
|
|
23
|
+
'form-action': ["'self'"],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function buildCsp(directives = CSP_DIRECTIVES) {
|
|
27
|
+
return Object.entries(directives)
|
|
28
|
+
.map(([name, sources]) => `${name} ${sources.join(' ')}`)
|
|
29
|
+
.join('; ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const CSP_VALUE = buildCsp();
|
|
33
|
+
|
|
34
|
+
export const CSP_MODES = ['enforce', 'report-only', 'off'];
|
|
35
|
+
|
|
36
|
+
export function cspMode(config) {
|
|
37
|
+
const mode = config?.server?.csp;
|
|
38
|
+
return CSP_MODES.includes(mode) ? mode : 'enforce';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Setea las cabeceras en `res` antes de `writeHead` (que las mezcla). La CSP solo
|
|
43
|
+
* va en lo que no es API: en un JSON o un SSE no protege nada.
|
|
44
|
+
*/
|
|
45
|
+
export function applySecurityHeaders(res, { api = false, csp = 'enforce' } = {}) {
|
|
46
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
47
|
+
res.setHeader('Referrer-Policy', 'same-origin');
|
|
48
|
+
res.setHeader('X-Frame-Options', 'SAMEORIGIN');
|
|
49
|
+
res.setHeader('Permissions-Policy', 'camera=(), geolocation=(), payment=(), usb=()');
|
|
50
|
+
if (api || csp === 'off') return;
|
|
51
|
+
const name = csp === 'report-only' ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
|
|
52
|
+
res.setHeader(name, CSP_VALUE);
|
|
53
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -3,11 +3,13 @@ import {
|
|
|
3
3
|
runAgent,
|
|
4
4
|
createTools,
|
|
5
5
|
resolveMode,
|
|
6
|
+
normalizeEffort,
|
|
6
7
|
MAX_TOOL_ROUNDS,
|
|
7
8
|
} from '../lib/core.js';
|
|
8
9
|
import { openSse, readJson, sendJson, sendError } from '../lib/http.js';
|
|
9
10
|
import { workspaceForRequest, resolveExistingDir, localOk } from '../lib/workspace.js';
|
|
10
11
|
import { createSubagentTool } from '../lib/tasks.js';
|
|
12
|
+
import { enabledAgents } from '../lib/agents.js';
|
|
11
13
|
import {
|
|
12
14
|
combineToolset,
|
|
13
15
|
combineLifecycle,
|
|
@@ -162,6 +164,8 @@ export function registerChat(route, ctx) {
|
|
|
162
164
|
const model = body.model || session?.model || ctx.effectiveModel();
|
|
163
165
|
if (!model) return sendError(res, 400, 'Falta "model" (en el cuerpo, en Ajustes o en tamnora.json).');
|
|
164
166
|
const mode = resolveMode(body.mode ?? session?.meta?.mode ?? ctx.store.getSetting('activeMode'));
|
|
167
|
+
// Esfuerzo de razonamiento: cuerpo > sesión > default global/config.
|
|
168
|
+
const effort = normalizeEffort(body.effort ?? session?.meta?.effort ?? ctx.effectiveEffort?.());
|
|
165
169
|
|
|
166
170
|
if (!sessionId) {
|
|
167
171
|
try {
|
|
@@ -174,7 +178,7 @@ export function registerChat(route, ctx) {
|
|
|
174
178
|
provider: providerId,
|
|
175
179
|
model,
|
|
176
180
|
cwd: sessionCwd,
|
|
177
|
-
meta: { mode: mode.id },
|
|
181
|
+
meta: { mode: mode.id, effort },
|
|
178
182
|
});
|
|
179
183
|
}
|
|
180
184
|
|
|
@@ -209,6 +213,7 @@ export function registerChat(route, ctx) {
|
|
|
209
213
|
provider: providerId,
|
|
210
214
|
model,
|
|
211
215
|
mode: mode.id,
|
|
216
|
+
effort,
|
|
212
217
|
cwd: sessionCwd,
|
|
213
218
|
say: fraseInicio(),
|
|
214
219
|
});
|
|
@@ -271,6 +276,8 @@ export function registerChat(route, ctx) {
|
|
|
271
276
|
providerId,
|
|
272
277
|
model,
|
|
273
278
|
parentMode: mode,
|
|
279
|
+
effort,
|
|
280
|
+
agents: enabledAgents(ctx),
|
|
274
281
|
permissions: ctx.config.permissions,
|
|
275
282
|
mcp,
|
|
276
283
|
mcpMode: ctx.effectiveMcpMode(),
|
|
@@ -347,6 +354,7 @@ export function registerChat(route, ctx) {
|
|
|
347
354
|
maxDurationMs: Number(body.maxDurationMs) || ctx.effectiveMaxDurationMs?.() || 0,
|
|
348
355
|
maxOutputTokens: Number(body.maxOutputTokens) || ctx.effectiveMaxOutputTokens?.() || 0,
|
|
349
356
|
promptCaching: ctx.effectivePromptCaching?.() ?? ctx.config.promptCaching === true,
|
|
357
|
+
reasoningEffort: effort,
|
|
350
358
|
stallWarnMs: body.stallWarnMs ?? ctx.config.stallWarnMs,
|
|
351
359
|
contextTokens,
|
|
352
360
|
onEvent(event) {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
readApp,
|
|
8
8
|
writeApp,
|
|
9
9
|
maskSecret,
|
|
10
|
+
normalizeEffort,
|
|
10
11
|
} from '../lib/core.js';
|
|
11
12
|
import { resolveAppearance, sanitizeAppearance } from '../lib/appearance.js';
|
|
12
13
|
import { readJson, sendJson, sendError } from '../lib/http.js';
|
|
@@ -26,6 +27,7 @@ export function providerList(ctx) {
|
|
|
26
27
|
baseUrl: row?.baseUrl || '',
|
|
27
28
|
defaultBaseUrl: meta.defaultBaseUrl,
|
|
28
29
|
requiresKey: row?.requiresKey ?? meta.requiresKey,
|
|
30
|
+
reasoning: meta.reasoning ?? 'none',
|
|
29
31
|
custom: false,
|
|
30
32
|
builtin: true,
|
|
31
33
|
stored: Boolean(row),
|
|
@@ -44,6 +46,7 @@ export function providerList(ctx) {
|
|
|
44
46
|
baseUrl: row.baseUrl,
|
|
45
47
|
defaultBaseUrl: '',
|
|
46
48
|
requiresKey: row.requiresKey ?? true,
|
|
49
|
+
reasoning: 'none',
|
|
47
50
|
custom: true,
|
|
48
51
|
builtin: false,
|
|
49
52
|
stored: true,
|
|
@@ -83,6 +86,7 @@ export function registerProviders(route, ctx) {
|
|
|
83
86
|
maxDurationMs: ctx.effectiveMaxDurationMs(),
|
|
84
87
|
maxOutputTokens: ctx.effectiveMaxOutputTokens(),
|
|
85
88
|
promptCaching: ctx.effectivePromptCaching(),
|
|
89
|
+
effort: ctx.effectiveEffort(),
|
|
86
90
|
defaultCwd: ctx.store.getSetting('defaultCwd') || '',
|
|
87
91
|
strictScope: ctx.effectiveStrictScope(),
|
|
88
92
|
mcpMode: ctx.effectiveMcpMode(),
|
|
@@ -101,6 +105,9 @@ export function registerProviders(route, ctx) {
|
|
|
101
105
|
if (body.mode !== undefined) ctx.store.setSetting('activeMode', resolveMode(String(body.mode)).id);
|
|
102
106
|
if (body.defaultCwd !== undefined) ctx.store.setSetting('defaultCwd', String(body.defaultCwd ?? '').trim() || null);
|
|
103
107
|
if (body.strictScope !== undefined) ctx.store.setSetting('strictScope', Boolean(body.strictScope));
|
|
108
|
+
if (body.effort !== undefined) {
|
|
109
|
+
ctx.store.setSetting('activeEffort', normalizeEffort(body.effort) || null);
|
|
110
|
+
}
|
|
104
111
|
if (body.promptCaching !== undefined) ctx.store.setSetting('promptCaching', Boolean(body.promptCaching));
|
|
105
112
|
if (body.mcpMode !== undefined) {
|
|
106
113
|
const mode = String(body.mcpMode);
|