@danieltmn/tamnora 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/harness/packages/core/src/config/index.js +1 -0
- package/harness/packages/core/src/index.js +1 -0
- package/harness/packages/core/src/storage/sqlite.js +108 -2
- package/harness/packages/core/src/tools/labels.js +83 -0
- package/harness/packages/core/test/labels.test.js +34 -0
- package/harness/packages/server/public/app.js +436 -110
- package/harness/packages/server/public/index.html +1 -1
- package/harness/packages/server/public/styles.css +95 -4
- package/harness/packages/server/src/lib/channels/index.js +43 -0
- package/harness/packages/server/src/lib/{telegram.js → channels/telegram.js} +157 -26
- package/harness/packages/server/src/lib/http.js +3 -2
- package/harness/packages/server/src/lib/runManager.js +102 -0
- package/harness/packages/server/src/lib/tasks.js +126 -0
- package/harness/packages/server/src/routes/chat.js +161 -77
- package/harness/packages/server/src/routes/sessions.js +17 -0
- package/harness/packages/server/src/server.js +12 -16
- package/harness/packages/server/test/server.test.js +129 -1
- package/harness/tamnora.example.json +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,13 @@ Todos los cambios relevantes de este proyecto se documentan en este archivo.
|
|
|
5
5
|
El formato sigue [Keep a Changelog](https://keepachangelog.com/es-ES/1.1.0/)
|
|
6
6
|
y el proyecto usa [Versionado Semantico](https://semver.org/lang/es/).
|
|
7
7
|
|
|
8
|
+
## [0.6.0] - 2026-10-04
|
|
9
|
+
|
|
10
|
+
### Cambios
|
|
11
|
+
|
|
12
|
+
- docs: actividad colapsable, runs durables, subagentes y canales
|
|
13
|
+
- feat(server): experiencia remota con actividad colapsable, runs durables, subagentes y canales
|
|
14
|
+
|
|
8
15
|
## [0.5.1] - 2026-10-02
|
|
9
16
|
|
|
10
17
|
### Cambios
|
|
@@ -15,6 +15,7 @@ export const DEFAULT_CONFIG = {
|
|
|
15
15
|
processes: { enabled: true, allow: [] },
|
|
16
16
|
preview: { provider: 'tunnelmole' },
|
|
17
17
|
telegram: { token: '', allowedChatIds: [] },
|
|
18
|
+
agents: { maxRounds: 6 },
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
/** Directory (relative to the project) where harness state lived historically. */
|
|
@@ -3,6 +3,7 @@ export { ensureUtf8Console } from './console.js';
|
|
|
3
3
|
export { MAX_TOOL_ROUNDS, runAgent, parseToolArguments } from './agent/runAgent.js';
|
|
4
4
|
export { estimateTokens, trimContext } from './agent/context.js';
|
|
5
5
|
export { diffLines } from './tools/diff.js';
|
|
6
|
+
export { summarizeToolCall, previewToolResult } from './tools/labels.js';
|
|
6
7
|
export { MODES, MODE_IDS, resolveMode } from './agent/modes.js';
|
|
7
8
|
export { withRetry, isRetryableError, isAbortError } from './providers/resilience.js';
|
|
8
9
|
export {
|
|
@@ -28,12 +28,37 @@ CREATE TABLE IF NOT EXISTS messages (
|
|
|
28
28
|
tool_call_id TEXT,
|
|
29
29
|
name TEXT,
|
|
30
30
|
mode TEXT,
|
|
31
|
+
agent_id TEXT,
|
|
31
32
|
tokens_in INTEGER,
|
|
32
33
|
tokens_out INTEGER,
|
|
33
34
|
created_at INTEGER
|
|
34
35
|
);
|
|
35
36
|
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id, created_at);
|
|
36
37
|
|
|
38
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
session_id TEXT NOT NULL,
|
|
41
|
+
parent_agent_id TEXT,
|
|
42
|
+
kind TEXT,
|
|
43
|
+
title TEXT,
|
|
44
|
+
status TEXT,
|
|
45
|
+
model TEXT,
|
|
46
|
+
summary TEXT,
|
|
47
|
+
created_at INTEGER,
|
|
48
|
+
updated_at INTEGER
|
|
49
|
+
);
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_agents_session ON agents(session_id, created_at);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS session_events (
|
|
53
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
54
|
+
session_id TEXT NOT NULL,
|
|
55
|
+
agent_id TEXT,
|
|
56
|
+
type TEXT NOT NULL,
|
|
57
|
+
data TEXT,
|
|
58
|
+
created_at INTEGER
|
|
59
|
+
);
|
|
60
|
+
CREATE INDEX IF NOT EXISTS idx_session_events ON session_events(session_id, id);
|
|
61
|
+
|
|
37
62
|
CREATE TABLE IF NOT EXISTS runs (
|
|
38
63
|
id TEXT PRIMARY KEY,
|
|
39
64
|
dataset TEXT,
|
|
@@ -107,6 +132,7 @@ export function openDatabase(file) {
|
|
|
107
132
|
function migrate(db) {
|
|
108
133
|
const columns = db.prepare('PRAGMA table_info(messages)').all().map((c) => c.name);
|
|
109
134
|
if (!columns.includes('mode')) db.exec('ALTER TABLE messages ADD COLUMN mode TEXT');
|
|
135
|
+
if (!columns.includes('agent_id')) db.exec('ALTER TABLE messages ADD COLUMN agent_id TEXT');
|
|
110
136
|
}
|
|
111
137
|
|
|
112
138
|
const now = () => Date.now();
|
|
@@ -261,8 +287,8 @@ export class Store {
|
|
|
261
287
|
const id = randomUUID();
|
|
262
288
|
this.db
|
|
263
289
|
.prepare(
|
|
264
|
-
`INSERT INTO messages (id, session_id, role, content, tool_calls, tool_call_id, name, mode, tokens_in, tokens_out, created_at)
|
|
265
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
290
|
+
`INSERT INTO messages (id, session_id, role, content, tool_calls, tool_call_id, name, mode, agent_id, tokens_in, tokens_out, created_at)
|
|
291
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
266
292
|
)
|
|
267
293
|
.run(
|
|
268
294
|
id,
|
|
@@ -273,6 +299,7 @@ export class Store {
|
|
|
273
299
|
message.tool_call_id ?? null,
|
|
274
300
|
message.name ?? null,
|
|
275
301
|
message.mode ?? null,
|
|
302
|
+
message.agent_id ?? null,
|
|
276
303
|
message.tokens_in ?? null,
|
|
277
304
|
message.tokens_out ?? null,
|
|
278
305
|
now()
|
|
@@ -298,10 +325,74 @@ export class Store {
|
|
|
298
325
|
if (row.tool_call_id) message.tool_call_id = row.tool_call_id;
|
|
299
326
|
if (row.name) message.name = row.name;
|
|
300
327
|
if (row.mode) message.mode = row.mode;
|
|
328
|
+
if (row.agent_id) message.agent_id = row.agent_id;
|
|
301
329
|
return message;
|
|
302
330
|
});
|
|
303
331
|
}
|
|
304
332
|
|
|
333
|
+
// --- Agents (subagentes) ------------------------------------------------
|
|
334
|
+
|
|
335
|
+
createAgent({ sessionId, parentAgentId = null, kind = 'task', title, model } = {}) {
|
|
336
|
+
const id = randomUUID();
|
|
337
|
+
const ts = now();
|
|
338
|
+
this.db
|
|
339
|
+
.prepare(
|
|
340
|
+
`INSERT INTO agents (id, session_id, parent_agent_id, kind, title, status, model, created_at, updated_at)
|
|
341
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
342
|
+
)
|
|
343
|
+
.run(id, sessionId, parentAgentId, kind, title ?? null, 'running', model ?? null, ts, ts);
|
|
344
|
+
return id;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
finishAgent(id, { status = 'done', summary } = {}) {
|
|
348
|
+
this.db
|
|
349
|
+
.prepare(`UPDATE agents SET status = ?, summary = ?, updated_at = ? WHERE id = ?`)
|
|
350
|
+
.run(status, summary ?? null, now(), id);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
getAgent(id) {
|
|
354
|
+
const row = this.db.prepare(`SELECT * FROM agents WHERE id = ?`).get(id);
|
|
355
|
+
return row ? toAgent(row) : null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
listAgents(sessionId) {
|
|
359
|
+
return this.db
|
|
360
|
+
.prepare(`SELECT * FROM agents WHERE session_id = ? ORDER BY created_at ASC`)
|
|
361
|
+
.all(sessionId)
|
|
362
|
+
.map(toAgent);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// --- Session events (run log for reconnection) --------------------------
|
|
366
|
+
|
|
367
|
+
addSessionEvent(sessionId, { agentId = null, type, data } = {}) {
|
|
368
|
+
const info = this.db
|
|
369
|
+
.prepare(`INSERT INTO session_events (session_id, agent_id, type, data, created_at) VALUES (?, ?, ?, ?, ?)`)
|
|
370
|
+
.run(sessionId, agentId, type, json(data), now());
|
|
371
|
+
return Number(info.lastInsertRowid);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
updateSessionEvent(id, data) {
|
|
375
|
+
this.db.prepare(`UPDATE session_events SET data = ? WHERE id = ?`).run(json(data), id);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
getSessionEvents(sessionId, { from = 0, limit = 5000 } = {}) {
|
|
379
|
+
return this.db
|
|
380
|
+
.prepare(`SELECT * FROM session_events WHERE session_id = ? AND id > ? ORDER BY id ASC LIMIT ?`)
|
|
381
|
+
.all(sessionId, from, limit)
|
|
382
|
+
.map((row) => ({ id: row.id, agentId: row.agent_id, type: row.type, data: parseJson(row.data), createdAt: row.created_at }));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
lastSessionEventId(sessionId) {
|
|
386
|
+
const row = this.db
|
|
387
|
+
.prepare(`SELECT COALESCE(MAX(id), 0) AS id FROM session_events WHERE session_id = ?`)
|
|
388
|
+
.get(sessionId);
|
|
389
|
+
return row?.id ?? 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
clearSessionEvents(sessionId) {
|
|
393
|
+
this.db.prepare(`DELETE FROM session_events WHERE session_id = ?`).run(sessionId);
|
|
394
|
+
}
|
|
395
|
+
|
|
305
396
|
// --- Runs (evaluation) --------------------------------------------------
|
|
306
397
|
|
|
307
398
|
createRun({ dataset, config } = {}) {
|
|
@@ -468,6 +559,21 @@ export class Store {
|
|
|
468
559
|
}
|
|
469
560
|
}
|
|
470
561
|
|
|
562
|
+
function toAgent(row) {
|
|
563
|
+
return {
|
|
564
|
+
id: row.id,
|
|
565
|
+
sessionId: row.session_id,
|
|
566
|
+
parentAgentId: row.parent_agent_id ?? null,
|
|
567
|
+
kind: row.kind ?? 'task',
|
|
568
|
+
title: row.title ?? '',
|
|
569
|
+
status: row.status ?? 'running',
|
|
570
|
+
model: row.model ?? null,
|
|
571
|
+
summary: row.summary ?? null,
|
|
572
|
+
createdAt: row.created_at,
|
|
573
|
+
updatedAt: row.updated_at,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
471
577
|
function toProvider(row) {
|
|
472
578
|
return {
|
|
473
579
|
id: row.id,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Etiquetas legibles para tool-calls y previews de resultados. Las usa la
|
|
2
|
+
// consola (y cualquier canal) para mostrar la actividad del agente de forma
|
|
3
|
+
// compacta, colapsable y apta para pantallas chicas.
|
|
4
|
+
|
|
5
|
+
function text(value) {
|
|
6
|
+
return String(value ?? '').replace(/\s+/g, ' ').trim();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function shorten(value, max = 80) {
|
|
10
|
+
const raw = text(value);
|
|
11
|
+
return raw.length > max ? `${raw.slice(0, max - 1)}…` : raw;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Una linea con la esencia de un tool-call: `read src/app.js`,
|
|
16
|
+
* `grep "foo" *.js`, `bash npm test`. Devuelve '' si no hay nada util.
|
|
17
|
+
*/
|
|
18
|
+
export function summarizeToolCall(name, args = {}) {
|
|
19
|
+
const a = args && typeof args === 'object' ? args : {};
|
|
20
|
+
const tool = String(name ?? '');
|
|
21
|
+
const path = (value) => (value ? shorten(value, 60) : '');
|
|
22
|
+
|
|
23
|
+
switch (tool) {
|
|
24
|
+
case 'read': {
|
|
25
|
+
const base = path(a.path) || '.';
|
|
26
|
+
const offset = Number(a.offset) || 0;
|
|
27
|
+
const limit = Number(a.limit) || 0;
|
|
28
|
+
if (offset && limit) return `${base}:${offset}-${offset + limit - 1}`;
|
|
29
|
+
if (offset) return `${base}:${offset}+`;
|
|
30
|
+
return base;
|
|
31
|
+
}
|
|
32
|
+
case 'write':
|
|
33
|
+
case 'edit':
|
|
34
|
+
return path(a.path) || 'archivo';
|
|
35
|
+
case 'list':
|
|
36
|
+
return path(a.path) || '.';
|
|
37
|
+
case 'glob':
|
|
38
|
+
return [a.pattern ? `"${shorten(a.pattern, 50)}"` : '', a.path ? `en ${path(a.path)}` : ''].filter(Boolean).join(' ');
|
|
39
|
+
case 'grep':
|
|
40
|
+
return [
|
|
41
|
+
a.pattern ? `"${shorten(a.pattern, 50)}"` : '',
|
|
42
|
+
a.include ? `${shorten(a.include, 30)}` : '',
|
|
43
|
+
a.path ? `en ${path(a.path)}` : '',
|
|
44
|
+
]
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.join(' ');
|
|
47
|
+
case 'bash': {
|
|
48
|
+
const line = text(a.command).split('\n')[0];
|
|
49
|
+
return shorten(line, 90);
|
|
50
|
+
}
|
|
51
|
+
case 'fetch_url':
|
|
52
|
+
return shorten(a.url, 90);
|
|
53
|
+
case 'git_diff':
|
|
54
|
+
return [a.ref ? `vs ${shorten(a.ref, 30)}` : a.staged ? 'staged' : 'unstaged', a.path ? path(a.path) : '']
|
|
55
|
+
.filter(Boolean)
|
|
56
|
+
.join(' ');
|
|
57
|
+
case 'git_log':
|
|
58
|
+
return a.limit ? `${a.limit} commits` : '';
|
|
59
|
+
case 'git_commit':
|
|
60
|
+
return shorten(a.message, 70);
|
|
61
|
+
case 'task':
|
|
62
|
+
case 'spawn_agent':
|
|
63
|
+
return shorten(a.description || a.prompt, 80);
|
|
64
|
+
default:
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Recorta un resultado para mostrarlo como preview. Devuelve el texto y si
|
|
71
|
+
* quedo truncado (para ofrecer "ver completo" bajo demanda).
|
|
72
|
+
*/
|
|
73
|
+
export function previewToolResult(_name, result, { maxLines = 24, maxChars = 4000 } = {}) {
|
|
74
|
+
const value = typeof result === 'string' ? result : JSON.stringify(result ?? '', null, 2) ?? '';
|
|
75
|
+
const lines = value.split('\n');
|
|
76
|
+
let preview = lines.slice(0, maxLines).join('\n');
|
|
77
|
+
let truncated = lines.length > maxLines;
|
|
78
|
+
if (preview.length > maxChars) {
|
|
79
|
+
preview = preview.slice(0, maxChars);
|
|
80
|
+
truncated = true;
|
|
81
|
+
}
|
|
82
|
+
return { preview, truncated, totalLines: lines.length };
|
|
83
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { summarizeToolCall, previewToolResult } from '../src/index.js';
|
|
4
|
+
|
|
5
|
+
test('summarizeToolCall describe las tools de exploracion', () => {
|
|
6
|
+
assert.equal(summarizeToolCall('read', { path: 'src/app.js' }), 'src/app.js');
|
|
7
|
+
assert.equal(summarizeToolCall('read', { path: 'src/app.js', offset: 10, limit: 5 }), 'src/app.js:10-14');
|
|
8
|
+
assert.equal(summarizeToolCall('list', {}), '.');
|
|
9
|
+
assert.equal(summarizeToolCall('glob', { pattern: 'src/**/*.js' }), '"src/**/*.js"');
|
|
10
|
+
assert.equal(summarizeToolCall('grep', { pattern: 'foo', include: '*.js' }), '"foo" *.js');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('summarizeToolCall describe acciones y comandos', () => {
|
|
14
|
+
assert.equal(summarizeToolCall('bash', { command: 'npm test\n--watch' }), 'npm test --watch');
|
|
15
|
+
assert.equal(summarizeToolCall('write', { path: 'a/b.txt' }), 'a/b.txt');
|
|
16
|
+
assert.equal(summarizeToolCall('git_commit', { message: 'feat: algo' }), 'feat: algo');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('summarizeToolCall tolera args invalidos y recorta', () => {
|
|
20
|
+
assert.equal(summarizeToolCall('read', null), '.');
|
|
21
|
+
assert.equal(summarizeToolCall('desconocida', { a: 1 }), '');
|
|
22
|
+
assert.ok(summarizeToolCall('bash', { command: 'x'.repeat(500) }).length <= 90);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('previewToolResult recorta por lineas y marca truncado', () => {
|
|
26
|
+
const short = previewToolResult('read', 'uno\ndos\ntres');
|
|
27
|
+
assert.equal(short.preview, 'uno\ndos\ntres');
|
|
28
|
+
assert.equal(short.truncated, false);
|
|
29
|
+
assert.equal(short.totalLines, 3);
|
|
30
|
+
|
|
31
|
+
const long = previewToolResult('read', Array.from({ length: 100 }, (_, i) => `linea ${i}`).join('\n'));
|
|
32
|
+
assert.equal(long.truncated, true);
|
|
33
|
+
assert.equal(long.preview.split('\n').length, 24);
|
|
34
|
+
});
|