@johpaz/hive-sdk 0.2.0 → 0.3.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 +306 -0
- package/README.md +11 -3
- package/package.json +10 -4
- package/packages/core/src/agent/agent-catalog.ts +81 -24
- package/packages/core/src/agent/compaction.ts +20 -1
- package/packages/core/src/agent/context-compiler.ts +6 -3
- package/packages/core/src/agent/conversation-store.ts +136 -2
- package/packages/core/src/agent/curator.ts +12 -3
- package/packages/core/src/agent/llm-providers/nvidia.ts +39 -0
- package/packages/core/src/agent/llm-providers/openai-compat-base.ts +38 -2
- package/packages/core/src/agent/playbook-selector.ts +18 -3
- package/packages/core/src/agent/prompt-builder.ts +2 -2
- package/packages/core/src/agent/providers/index.ts +36 -1
- package/packages/core/src/agent/reflector.ts +32 -9
- package/packages/core/src/agent/skill-selector.ts +2 -2
- package/packages/core/src/agent/thread-store.ts +43 -0
- package/packages/core/src/agent/tool-selector.ts +2 -0
- package/packages/core/src/api/createAgent.ts +66 -1
- package/packages/core/src/artifacts/index.ts +15 -0
- package/packages/core/src/artifacts/store.ts +77 -2
- package/packages/core/src/canvas/index.ts +9 -0
- package/packages/core/src/ethics/EthicsGuard.ts +7 -1
- package/packages/core/src/events/index.ts +18 -0
- package/packages/core/src/events/tool-narration.ts +4 -0
- package/packages/core/src/gateway/channel-notify.ts +103 -6
- package/packages/core/src/gateway/durable-queue.ts +13 -1
- package/packages/core/src/gateway/index.ts +3 -0
- package/packages/core/src/gateway/job-store.ts +6 -0
- package/packages/core/src/harness/executors.ts +493 -0
- package/packages/core/src/harness/index.ts +12 -2
- package/packages/core/src/hooks/index.ts +203 -0
- package/packages/core/src/images/index.ts +161 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/multimodal/vision-service.ts +45 -13
- package/packages/core/src/resilience/index.ts +13 -0
- package/packages/core/src/scheduler/CronScheduler.ts +48 -21
- package/packages/core/src/scheduler/cron/expression.ts +165 -0
- package/packages/core/src/scheduler/cron/index.ts +10 -0
- package/packages/core/src/scheduler/cron/job.ts +339 -0
- package/packages/core/src/scheduler/cron/next-run.ts +121 -0
- package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
- package/packages/core/src/scheduler/index.ts +21 -3
- package/packages/core/src/scheduler/integration.ts +16 -5
- package/packages/core/src/scheduler/types.ts +3 -18
- package/packages/core/src/services/agents.ts +268 -0
- package/packages/core/src/services/cron.ts +257 -0
- package/packages/core/src/services/endpoints.ts +289 -0
- package/packages/core/src/services/ethics.ts +107 -0
- package/packages/core/src/services/images.ts +212 -0
- package/packages/core/src/services/index.ts +112 -0
- package/packages/core/src/services/mcp.ts +201 -0
- package/packages/core/src/services/memory.ts +133 -0
- package/packages/core/src/services/models.ts +179 -0
- package/packages/core/src/services/providers.ts +152 -0
- package/packages/core/src/services/setup.ts +222 -0
- package/packages/core/src/services/skills.ts +241 -0
- package/packages/core/src/services/swarms.ts +307 -0
- package/packages/core/src/services/tools.ts +106 -0
- package/packages/core/src/sessions/index.ts +5 -3
- package/packages/core/src/sessions/resolve.ts +108 -0
- package/packages/core/src/skills/SkillLoader.ts +8 -1
- package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
- package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
- package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
- package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
- package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
- package/packages/core/src/skills/bundled-data.generated.ts +110 -12
- package/packages/core/src/storage/bootstrap.ts +74 -5
- package/packages/core/src/storage/collections.ts +106 -1
- package/packages/core/src/storage/crypto.ts +24 -7
- package/packages/core/src/storage/index.ts +2 -1
- package/packages/core/src/storage/onboarding.ts +59 -43
- package/packages/core/src/storage/reconcile.ts +6 -1
- package/packages/core/src/storage/seed.ts +89 -11
- package/packages/core/src/swarm/types.ts +3 -18
- package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
- package/packages/core/src/tool-runtime/index.ts +129 -14
- package/packages/core/src/tools/agents/index.ts +18 -60
- package/packages/core/src/tools/cli/index.ts +55 -0
- package/packages/core/src/tools/core/index.ts +50 -2
- package/packages/core/src/tools/cron/index.ts +4 -4
- package/packages/core/src/tools/images/index.ts +130 -0
- package/packages/core/src/tools/index.ts +14 -1
- package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
- package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
- package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reloj de pared ↔ instante, en una zona horaria IANA.
|
|
3
|
+
*
|
|
4
|
+
* Es la parte difícil de un cron con zona horaria y la razón por la que no
|
|
5
|
+
* alcanza con `Bun.cron.parse()`, que sólo trabaja en UTC. "Todos los días a
|
|
6
|
+
* las 9" significa las 9 **del reloj de la pared en Bogotá**, y ese instante se
|
|
7
|
+
* corre una hora dos veces al año en las zonas con horario de verano. Calcular
|
|
8
|
+
* el offset una sola vez y sumarlo produce un cron que se desfasa un día al año
|
|
9
|
+
* en marzo y otro en octubre — el tipo de error que aparece un domingo a las
|
|
10
|
+
* 2am y nadie sabe de dónde salió.
|
|
11
|
+
*
|
|
12
|
+
* Acá no se guarda ningún offset: se le pregunta a `Intl` en cada conversión,
|
|
13
|
+
* que es la única fuente que conoce las reglas de cada zona y sus cambios.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Los campos que muestra un reloj colgado en la pared de esa zona. */
|
|
17
|
+
export interface WallClock {
|
|
18
|
+
year: number
|
|
19
|
+
/** 1-12, no 0-11: acá no hay meses de base cero. */
|
|
20
|
+
month: number
|
|
21
|
+
day: number
|
|
22
|
+
hour: number
|
|
23
|
+
minute: number
|
|
24
|
+
second: number
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Día de la semana, 0 = domingo. */
|
|
28
|
+
export function wallClockWeekday(wall: WallClock): number {
|
|
29
|
+
return new Date(Date.UTC(wall.year, wall.month - 1, wall.day)).getUTCDay()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const formatters = new Map<string, Intl.DateTimeFormat>()
|
|
33
|
+
|
|
34
|
+
function formatter(timeZone: string): Intl.DateTimeFormat {
|
|
35
|
+
let f = formatters.get(timeZone)
|
|
36
|
+
if (!f) {
|
|
37
|
+
f = new Intl.DateTimeFormat("en-US", {
|
|
38
|
+
timeZone,
|
|
39
|
+
hourCycle: "h23",
|
|
40
|
+
year: "numeric",
|
|
41
|
+
month: "2-digit",
|
|
42
|
+
day: "2-digit",
|
|
43
|
+
hour: "2-digit",
|
|
44
|
+
minute: "2-digit",
|
|
45
|
+
second: "2-digit",
|
|
46
|
+
})
|
|
47
|
+
formatters.set(timeZone, f)
|
|
48
|
+
}
|
|
49
|
+
return f
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Valida una zona IANA. Lanza con un mensaje que dice cuál falló. */
|
|
53
|
+
export function assertTimeZone(timeZone: string): void {
|
|
54
|
+
try {
|
|
55
|
+
formatter(timeZone)
|
|
56
|
+
} catch {
|
|
57
|
+
throw new Error(`Zona horaria desconocida: "${timeZone}"`)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Qué marca el reloj de esa zona en ese instante. */
|
|
62
|
+
export function toWallClock(instant: Date, timeZone: string): WallClock {
|
|
63
|
+
const parts = formatter(timeZone).formatToParts(instant)
|
|
64
|
+
const campo = (tipo: string) => {
|
|
65
|
+
const p = parts.find((x) => x.type === tipo)
|
|
66
|
+
return p ? Number(p.value) : 0
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
year: campo("year"),
|
|
70
|
+
month: campo("month"),
|
|
71
|
+
day: campo("day"),
|
|
72
|
+
hour: campo("hour"),
|
|
73
|
+
minute: campo("minute"),
|
|
74
|
+
second: campo("second"),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function asUTCMillis(wall: WallClock): number {
|
|
79
|
+
return Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function mismoReloj(a: WallClock, b: WallClock): boolean {
|
|
83
|
+
return (
|
|
84
|
+
a.year === b.year && a.month === b.month && a.day === b.day &&
|
|
85
|
+
a.hour === b.hour && a.minute === b.minute && a.second === b.second
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* El instante en que el reloj de esa zona marca `wall`.
|
|
91
|
+
*
|
|
92
|
+
* Devuelve `null` cuando ese momento **no existe**: al adelantar el horario de
|
|
93
|
+
* verano el reloj salta de 01:59 a 03:00, así que "las 2:30" no ocurre ese día.
|
|
94
|
+
* Un cron agendado ahí no debe correr a una hora inventada; el que llama decide
|
|
95
|
+
* si lo salta o lo corre al día siguiente.
|
|
96
|
+
*
|
|
97
|
+
* Cuando el reloj se atrasa, la misma hora ocurre dos veces y se devuelve la
|
|
98
|
+
* primera — correr una vez es lo que espera quien agendó, no dos.
|
|
99
|
+
*/
|
|
100
|
+
export function toInstant(wall: WallClock, timeZone: string): Date | null {
|
|
101
|
+
const objetivo = asUTCMillis(wall)
|
|
102
|
+
|
|
103
|
+
// Punto de partida: tratar el reloj de pared como si fuera UTC y corregir con
|
|
104
|
+
// la diferencia que reporte la zona. Dos pasadas alcanzan siempre — la primera
|
|
105
|
+
// acerca al offset correcto, la segunda absorbe el caso en que la corrección
|
|
106
|
+
// cruzó un cambio de horario y el offset de destino era otro.
|
|
107
|
+
let ts = objetivo
|
|
108
|
+
for (let i = 0; i < 3; i++) {
|
|
109
|
+
const diferencia = objetivo - asUTCMillis(toWallClock(new Date(ts), timeZone))
|
|
110
|
+
if (diferencia === 0) break
|
|
111
|
+
ts += diferencia
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// La comprobación no es defensiva: es cómo se detecta el hueco del cambio de
|
|
115
|
+
// horario. Si la hora pedida no existe, ninguna corrección converge y el
|
|
116
|
+
// reloj de `ts` marca otra cosa.
|
|
117
|
+
if (!mismoReloj(toWallClock(new Date(ts), timeZone), wall)) return null
|
|
118
|
+
|
|
119
|
+
// Cuando el reloj se atrasa, ese mismo reloj de pared ocurre dos veces y la
|
|
120
|
+
// convergencia de arriba aterriza en la segunda. Se busca hacia atrás la
|
|
121
|
+
// primera: correr al instante más temprano es lo que espera quien agendó "a
|
|
122
|
+
// las 2:30", y así el job no se corre una hora ese día del año.
|
|
123
|
+
for (const atras of DESFASES_POSIBLES) {
|
|
124
|
+
const antes = ts - atras
|
|
125
|
+
if (mismoReloj(toWallClock(new Date(antes), timeZone), wall)) return new Date(antes)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return new Date(ts)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Cuánto puede saltar un reloj al cambiar de horario, de mayor a menor.
|
|
133
|
+
*
|
|
134
|
+
* Casi todas las zonas mueven una hora, pero no todas: Lord Howe mueve media, y
|
|
135
|
+
* hubo zonas con saltos de dos. Se prueban de mayor a menor para quedarse con
|
|
136
|
+
* la ocurrencia más temprana.
|
|
137
|
+
*/
|
|
138
|
+
const DESFASES_POSIBLES = [2 * 3_600_000, 3_600_000, 1_800_000]
|
|
@@ -1,11 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Scheduler — cron con
|
|
2
|
+
* Scheduler — cron sobre HiveDB, con motor propio y sin dependencias.
|
|
3
3
|
*
|
|
4
4
|
* Soporta jobs recurrentes y de una sola vez. La persistencia pasó de SQLite a
|
|
5
|
-
* las colecciones `cronJobs` / `taskRuns` de HiveDB en 0.1.5
|
|
5
|
+
* las colecciones `cronJobs` / `taskRuns` de HiveDB en 0.1.5, y el motor pasó de
|
|
6
|
+
* `croner` a `./cron` —sólo `setTimeout` e `Intl` del runtime— en 0.3.0.
|
|
6
7
|
*/
|
|
7
8
|
|
|
8
9
|
export { CronScheduler } from "./CronScheduler.ts";
|
|
10
|
+
|
|
11
|
+
// El motor por separado, para quien quiera calcular o validar sin montar un
|
|
12
|
+
// scheduler: una UI que muestra "próximas corridas" mientras se escribe la
|
|
13
|
+
// expresión, por ejemplo.
|
|
14
|
+
export {
|
|
15
|
+
Cron,
|
|
16
|
+
parseCronExpression,
|
|
17
|
+
isValidCronExpression,
|
|
18
|
+
nextOccurrence,
|
|
19
|
+
toWallClock,
|
|
20
|
+
toInstant,
|
|
21
|
+
assertTimeZone,
|
|
22
|
+
type CronOptions,
|
|
23
|
+
type CronFunction,
|
|
24
|
+
type CronFields,
|
|
25
|
+
type NextOccurrenceOptions,
|
|
26
|
+
type WallClock,
|
|
27
|
+
} from "./cron/index.ts";
|
|
9
28
|
// `executeScheduledTask` dejó de ser público: la ejecución entra por
|
|
10
29
|
// `createTaskHandler()`, que es lo que el scheduler engancha.
|
|
11
30
|
export { createTaskHandler, notifyTaskCompletion, setSchedulerForCleanup } from "./integration.ts";
|
|
@@ -20,5 +39,4 @@ export type {
|
|
|
20
39
|
TaskType,
|
|
21
40
|
TaskStatus,
|
|
22
41
|
TaskRunStatus,
|
|
23
|
-
CronerOptions,
|
|
24
42
|
} from "./types.ts";
|
|
@@ -12,6 +12,8 @@ import { resolveAgentId } from "../storage/onboarding.ts";
|
|
|
12
12
|
import { sendToUserChannel } from "../gateway/channel-notify.ts";
|
|
13
13
|
import { getNarration } from "../events/tool-narration.ts";
|
|
14
14
|
import { addMessage } from "../agent/conversation-store.ts";
|
|
15
|
+
import { makeThreadId, CRON_CHANNEL } from "../agent/thread-id.ts";
|
|
16
|
+
import { threadForChannel } from "../agent/thread-store.ts";
|
|
15
17
|
import { resolveBestChannel } from "../tools/cron/index.ts";
|
|
16
18
|
import { col } from "../storage/hive.ts";
|
|
17
19
|
import type { UserDoc, CronJobDoc } from "../storage/collections.ts";
|
|
@@ -123,7 +125,11 @@ ${prompt || `Execute tool: ${job.tool_name}`}`;
|
|
|
123
125
|
try {
|
|
124
126
|
const agentLoop = buildAgentLoop({ mcpManager: undefined });
|
|
125
127
|
|
|
126
|
-
|
|
128
|
+
// Un hilo estable por tarea programada. Antes era
|
|
129
|
+
// `sched_${job.id}_${Date.now()}`: cada disparo estrenaba un hilo, así que
|
|
130
|
+
// la tarea nunca recordaba sus ejecuciones anteriores y cada una dejaba
|
|
131
|
+
// mensajes huérfanos en `conversations` que ya nadie volvía a leer.
|
|
132
|
+
const sessionId = makeThreadId(user?.id || "default", CRON_CHANNEL, job.id);
|
|
127
133
|
|
|
128
134
|
const agentChannel = (job.channel && job.channel !== "system")
|
|
129
135
|
? job.channel
|
|
@@ -136,10 +142,10 @@ ${prompt || `Execute tool: ${job.tool_name}`}`;
|
|
|
136
142
|
if (!agentChannel) return;
|
|
137
143
|
try {
|
|
138
144
|
if (step.type === "tool_call" && step.toolName) {
|
|
139
|
-
await sendToUserChannel(agentChannel, user?.id || "", getNarration(step.toolName));
|
|
145
|
+
await sendToUserChannel(agentChannel, user?.id || "", getNarration(step.toolName), { threadId: sessionId });
|
|
140
146
|
} else if (step.type === "text" && step.message) {
|
|
141
147
|
const trimmed = step.message.trim();
|
|
142
|
-
if (trimmed) await sendToUserChannel(agentChannel, user?.id || "", trimmed);
|
|
148
|
+
if (trimmed) await sendToUserChannel(agentChannel, user?.id || "", trimmed, { threadId: sessionId });
|
|
143
149
|
}
|
|
144
150
|
} catch (err) {
|
|
145
151
|
log.warn(`[execute] Narration send failed: ${(err as Error).message}`);
|
|
@@ -239,13 +245,18 @@ export async function notifyTaskCompletion(
|
|
|
239
245
|
|
|
240
246
|
log.info(`[notify] Sending notification to ${notifyChannel}: "${message.slice(0, 50)}..."`);
|
|
241
247
|
|
|
248
|
+
// El aviso se guarda en el hilo donde el usuario lo va a leer —su conversación
|
|
249
|
+
// activa en la web, o el chat del canal— y no en el hilo del usuario, que desde
|
|
250
|
+
// la separación por canal ya no es donde mira nadie.
|
|
251
|
+
const notifyThreadId = (await threadForChannel(userId, notifyChannel)) ?? userId;
|
|
252
|
+
|
|
242
253
|
try {
|
|
243
|
-
await addMessage(
|
|
254
|
+
await addMessage(notifyThreadId, "assistant", message, { channel: notifyChannel });
|
|
244
255
|
} catch (e) {
|
|
245
256
|
log.warn(`[notify] Failed to persist notification to DB: ${(e as Error).message}`);
|
|
246
257
|
}
|
|
247
258
|
|
|
248
|
-
await sendToUserChannel(notifyChannel, userId, message);
|
|
259
|
+
await sendToUserChannel(notifyChannel, userId, message, { threadId: notifyThreadId });
|
|
249
260
|
log.info(`[notify] Notification sent to ${notifyChannel}`);
|
|
250
261
|
}
|
|
251
262
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hive Scheduler - Type Definitions
|
|
3
3
|
*
|
|
4
|
-
* Type interfaces for the
|
|
4
|
+
* Type interfaces for the scheduling system.
|
|
5
5
|
* All names use "CronJob" terminology (formerly ScheduledTask).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Cron } from "
|
|
8
|
+
import type { Cron } from "./cron/index.ts";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Task type: recurring uses cron expression, one_shot uses fire_at
|
|
@@ -146,24 +146,9 @@ export interface CronJobExecutionResult {
|
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/**
|
|
149
|
-
* Internal job wrapper holding
|
|
149
|
+
* Internal job wrapper holding the scheduled job and its metadata
|
|
150
150
|
*/
|
|
151
151
|
export interface CronJobEntry {
|
|
152
152
|
job: CronJob;
|
|
153
153
|
cron: Cron;
|
|
154
154
|
}
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Options for Croner job creation
|
|
158
|
-
*/
|
|
159
|
-
export interface CronerOptions {
|
|
160
|
-
timezone: string;
|
|
161
|
-
protect: boolean;
|
|
162
|
-
catch: boolean | ((error: Error) => void);
|
|
163
|
-
name: string;
|
|
164
|
-
maxRuns?: number;
|
|
165
|
-
interval?: number;
|
|
166
|
-
startAt?: string;
|
|
167
|
-
stopAt?: string;
|
|
168
|
-
domAndDow?: boolean;
|
|
169
|
-
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agentes — la API, no la tool.
|
|
3
|
+
*
|
|
4
|
+
* Crear y editar agentes sólo se podía por dos caminos, y ninguno servía para
|
|
5
|
+
* una UI: `agentCreateTool` (argumentos con forma de LLM) o consultas crudas a
|
|
6
|
+
* la colección `agents`. En hive el CRUD vive inline en `gateway/routes/agents.ts`.
|
|
7
|
+
*
|
|
8
|
+
* Una diferencia deliberada con hive: **acá se valida que las referencias
|
|
9
|
+
* existan**. Su ruta guarda `tools_json`, `skills_json` y `mcp_server_ids_json`
|
|
10
|
+
* tal cual llegan, sin comprobar nada, así que un id mal escrito no falla al
|
|
11
|
+
* guardar sino más tarde, cuando el agente intenta usar una capacidad que no
|
|
12
|
+
* existe — lejos de donde está el error. Validar al escribir cuesta tres
|
|
13
|
+
* consultas y ahorra ese viaje.
|
|
14
|
+
*
|
|
15
|
+
* Lo que NO hace, a propósito: tocar las filas de `tools`/`skills` al crear o
|
|
16
|
+
* borrar un agente. Son colecciones globales compartidas entre todos los
|
|
17
|
+
* agentes; borrar las de uno rompería a otro que las usa.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { col, toIndexable, fromIndexable, NO_PARENT } from "../storage/hive.ts";
|
|
21
|
+
import type { AgentDoc, ToolDoc, SkillDoc, McpServerDoc } from "../storage/collections.ts";
|
|
22
|
+
import { deleteAgentSecrets } from "../storage/crypto.ts";
|
|
23
|
+
import { expandToolAllowlist } from "../agent/delegation-runtime.ts";
|
|
24
|
+
import { logger } from "../utils/logger.ts";
|
|
25
|
+
|
|
26
|
+
const log = logger.child("services/agents");
|
|
27
|
+
|
|
28
|
+
export interface AgentSummary {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
description: string | null;
|
|
32
|
+
role: AgentDoc["role"];
|
|
33
|
+
status: string;
|
|
34
|
+
enabled: boolean;
|
|
35
|
+
providerId: string | null;
|
|
36
|
+
modelId: string | null;
|
|
37
|
+
/** Patrones declarados (`fs_*`), no la expansión. */
|
|
38
|
+
toolPatterns: string[];
|
|
39
|
+
skills: string[];
|
|
40
|
+
mcpServerIds: string[];
|
|
41
|
+
/** `"catalog"` = persona sembrada; `"user"` = creada por alguien. */
|
|
42
|
+
source: "user" | "catalog";
|
|
43
|
+
systemPrompt: string | null;
|
|
44
|
+
createdAt: number;
|
|
45
|
+
updatedAt: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface CreateAgentInput {
|
|
49
|
+
id?: string;
|
|
50
|
+
name: string;
|
|
51
|
+
description?: string | null;
|
|
52
|
+
systemPrompt?: string | null;
|
|
53
|
+
role?: AgentDoc["role"];
|
|
54
|
+
providerId?: string | null;
|
|
55
|
+
modelId?: string | null;
|
|
56
|
+
/** Acepta globs (`fs_*`); se validan tras expandir. */
|
|
57
|
+
toolPatterns?: string[];
|
|
58
|
+
skills?: string[];
|
|
59
|
+
mcpServerIds?: string[];
|
|
60
|
+
userId?: string;
|
|
61
|
+
maxIterations?: number;
|
|
62
|
+
enabled?: boolean;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type UpdateAgentInput = Partial<Omit<CreateAgentInput, "id">>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convierte un nombre en id.
|
|
69
|
+
*
|
|
70
|
+
* Normaliza los acentos antes de filtrar: sin eso "Efímero" queda como
|
|
71
|
+
* `ef_mero` y "Diseño" como `dise_o`, porque la í y la ñ no son `[a-z0-9]`.
|
|
72
|
+
* Para un producto en español eso no es un detalle cosmético.
|
|
73
|
+
*/
|
|
74
|
+
export function slugify(name: string): string {
|
|
75
|
+
return name
|
|
76
|
+
.normalize("NFD")
|
|
77
|
+
.replace(/[\u0300-\u036f]/g, "")
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
80
|
+
.replace(/^_|_$/g, "");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseList(json: string | null | undefined): string[] {
|
|
84
|
+
try {
|
|
85
|
+
const v = json ? JSON.parse(json) : [];
|
|
86
|
+
return Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : [];
|
|
87
|
+
} catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function toSummary(doc: AgentDoc): AgentSummary {
|
|
93
|
+
return {
|
|
94
|
+
id: doc.id,
|
|
95
|
+
name: doc.name,
|
|
96
|
+
description: doc.description,
|
|
97
|
+
role: doc.role,
|
|
98
|
+
status: doc.status,
|
|
99
|
+
enabled: doc.enabled,
|
|
100
|
+
providerId: fromIndexable(doc.provider_id),
|
|
101
|
+
modelId: fromIndexable(doc.model_id),
|
|
102
|
+
toolPatterns: parseList(doc.tool_allowlist_json ?? doc.tools_json),
|
|
103
|
+
skills: parseList(doc.skills_json),
|
|
104
|
+
mcpServerIds: parseList(doc.mcp_server_ids_json),
|
|
105
|
+
source: doc.source ?? "user",
|
|
106
|
+
systemPrompt: doc.system_prompt,
|
|
107
|
+
createdAt: doc.created_at,
|
|
108
|
+
updatedAt: doc.updated_at,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function agentsCol() {
|
|
113
|
+
return col<AgentDoc>("agents");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Comprueba que lo que se le asigna al agente exista de verdad.
|
|
118
|
+
*
|
|
119
|
+
* Los patrones de tools se expanden primero contra el registro vivo
|
|
120
|
+
* (`expandToolAllowlist`): un `fs_*` que no case con ninguna tool es un error
|
|
121
|
+
* de quien lo escribió, no una lista vacía silenciosa.
|
|
122
|
+
*/
|
|
123
|
+
async function validateReferences(input: {
|
|
124
|
+
toolPatterns?: string[];
|
|
125
|
+
skills?: string[];
|
|
126
|
+
mcpServerIds?: string[];
|
|
127
|
+
}): Promise<void> {
|
|
128
|
+
const problemas: string[] = [];
|
|
129
|
+
|
|
130
|
+
if (input.toolPatterns?.length) {
|
|
131
|
+
const expandidas = expandToolAllowlist(input.toolPatterns);
|
|
132
|
+
if (expandidas.length === 0) {
|
|
133
|
+
problemas.push(`ninguna tool coincide con ${JSON.stringify(input.toolPatterns)}`);
|
|
134
|
+
} else {
|
|
135
|
+
const c = await col<ToolDoc>("tools");
|
|
136
|
+
const faltan: string[] = [];
|
|
137
|
+
for (const name of expandidas) if (!(await c.get(name))) faltan.push(name);
|
|
138
|
+
if (faltan.length) problemas.push(`tools inexistentes: ${faltan.join(", ")}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (input.skills?.length) {
|
|
143
|
+
const c = await col<SkillDoc>("skills");
|
|
144
|
+
const faltan: string[] = [];
|
|
145
|
+
for (const id of input.skills) if (!(await c.get(id))) faltan.push(id);
|
|
146
|
+
if (faltan.length) problemas.push(`skills inexistentes: ${faltan.join(", ")}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (input.mcpServerIds?.length) {
|
|
150
|
+
const c = await col<McpServerDoc>("mcpServers");
|
|
151
|
+
const faltan: string[] = [];
|
|
152
|
+
for (const id of input.mcpServerIds) if (!(await c.get(id))) faltan.push(id);
|
|
153
|
+
if (faltan.length) problemas.push(`servidores MCP inexistentes: ${faltan.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (problemas.length) throw new Error(problemas.join("; "));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function createAgent(input: CreateAgentInput): Promise<AgentSummary> {
|
|
160
|
+
if (!input.name?.trim()) throw new Error("El agente necesita un nombre");
|
|
161
|
+
await validateReferences(input);
|
|
162
|
+
|
|
163
|
+
const c = await agentsCol();
|
|
164
|
+
const id = input.id ?? slugify(input.name);
|
|
165
|
+
if (await c.get(id)) throw new Error(`Ya existe un agente con id "${id}"`);
|
|
166
|
+
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const doc: AgentDoc = {
|
|
169
|
+
id,
|
|
170
|
+
user_id: input.userId ?? "",
|
|
171
|
+
name: input.name,
|
|
172
|
+
description: input.description ?? null,
|
|
173
|
+
system_prompt: input.systemPrompt ?? null,
|
|
174
|
+
tone: null,
|
|
175
|
+
role: input.role ?? "worker",
|
|
176
|
+
status: "idle",
|
|
177
|
+
enabled: input.enabled ?? true,
|
|
178
|
+
provider_id: toIndexable(input.providerId ?? null),
|
|
179
|
+
model_id: toIndexable(input.modelId ?? null),
|
|
180
|
+
tools_json: input.toolPatterns ? JSON.stringify(expandToolAllowlist(input.toolPatterns)) : null,
|
|
181
|
+
skills_json: input.skills ? JSON.stringify(input.skills) : null,
|
|
182
|
+
parent_id: NO_PARENT,
|
|
183
|
+
max_iterations: input.maxIterations ?? 10,
|
|
184
|
+
workspace: null,
|
|
185
|
+
lastTraceAt: null,
|
|
186
|
+
created_at: now,
|
|
187
|
+
updated_at: now,
|
|
188
|
+
source: "user",
|
|
189
|
+
// Los patrones se guardan sin expandir: `task_delegate` los vuelve a
|
|
190
|
+
// expandir en cada delegación, así una tool registrada después igual entra.
|
|
191
|
+
tool_allowlist_json: input.toolPatterns ? JSON.stringify(input.toolPatterns) : null,
|
|
192
|
+
mcp_server_ids_json: input.mcpServerIds ? JSON.stringify(input.mcpServerIds) : null,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
await c.put(id, doc, { expectedVersion: 0 });
|
|
196
|
+
log.info(`agente "${input.name}" creado (${id})`);
|
|
197
|
+
return toSummary(doc);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function getAgent(id: string): Promise<AgentSummary | null> {
|
|
201
|
+
const entry = await (await agentsCol()).get(id);
|
|
202
|
+
return entry ? toSummary(entry.doc) : null;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface ListAgentsOptions {
|
|
206
|
+
role?: AgentDoc["role"];
|
|
207
|
+
source?: "user" | "catalog";
|
|
208
|
+
includeDisabled?: boolean;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export async function listAgents(opts?: ListAgentsOptions): Promise<AgentSummary[]> {
|
|
212
|
+
const rows = await (await agentsCol()).scan({});
|
|
213
|
+
return rows
|
|
214
|
+
.map((e) => e.doc)
|
|
215
|
+
.filter((d) => (opts?.role ? d.role === opts.role : true))
|
|
216
|
+
.filter((d) => (opts?.source ? (d.source ?? "user") === opts.source : true))
|
|
217
|
+
.filter((d) => (opts?.includeDisabled ? true : d.enabled))
|
|
218
|
+
.map(toSummary)
|
|
219
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function updateAgent(id: string, changes: UpdateAgentInput): Promise<AgentSummary> {
|
|
223
|
+
await validateReferences(changes);
|
|
224
|
+
|
|
225
|
+
const c = await agentsCol();
|
|
226
|
+
const entry = await c.get(id);
|
|
227
|
+
if (!entry) throw new Error(`No existe el agente "${id}"`);
|
|
228
|
+
|
|
229
|
+
const doc: AgentDoc = { ...entry.doc, updated_at: Date.now() };
|
|
230
|
+
if (changes.name !== undefined) doc.name = changes.name;
|
|
231
|
+
if (changes.description !== undefined) doc.description = changes.description;
|
|
232
|
+
if (changes.systemPrompt !== undefined) doc.system_prompt = changes.systemPrompt;
|
|
233
|
+
if (changes.role !== undefined) doc.role = changes.role;
|
|
234
|
+
if (changes.enabled !== undefined) doc.enabled = changes.enabled;
|
|
235
|
+
if (changes.maxIterations !== undefined) doc.max_iterations = changes.maxIterations;
|
|
236
|
+
if (changes.providerId !== undefined) doc.provider_id = toIndexable(changes.providerId);
|
|
237
|
+
if (changes.modelId !== undefined) doc.model_id = toIndexable(changes.modelId);
|
|
238
|
+
if (changes.toolPatterns !== undefined) {
|
|
239
|
+
doc.tool_allowlist_json = JSON.stringify(changes.toolPatterns);
|
|
240
|
+
doc.tools_json = JSON.stringify(expandToolAllowlist(changes.toolPatterns));
|
|
241
|
+
}
|
|
242
|
+
if (changes.skills !== undefined) doc.skills_json = JSON.stringify(changes.skills);
|
|
243
|
+
if (changes.mcpServerIds !== undefined) doc.mcp_server_ids_json = JSON.stringify(changes.mcpServerIds);
|
|
244
|
+
|
|
245
|
+
await c.put(id, doc, { expectedVersion: entry.version });
|
|
246
|
+
return toSummary(doc);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Atajos legibles sobre `updateAgent`, que es lo que una UI llama al editar. */
|
|
250
|
+
export const assignTools = (id: string, toolPatterns: string[]) => updateAgent(id, { toolPatterns });
|
|
251
|
+
export const assignSkills = (id: string, skills: string[]) => updateAgent(id, { skills });
|
|
252
|
+
export const assignMcpServers = (id: string, mcpServerIds: string[]) => updateAgent(id, { mcpServerIds });
|
|
253
|
+
|
|
254
|
+
export const enableAgent = (id: string) => updateAgent(id, { enabled: true });
|
|
255
|
+
export const disableAgent = (id: string) => updateAgent(id, { enabled: false });
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Borra el agente y sus secretos. **No toca sus tools ni sus skills**: son
|
|
259
|
+
* filas globales que otros agentes comparten.
|
|
260
|
+
*/
|
|
261
|
+
export async function deleteAgent(id: string): Promise<boolean> {
|
|
262
|
+
const c = await agentsCol();
|
|
263
|
+
if (!(await c.get(id))) return false;
|
|
264
|
+
await c.delete(id);
|
|
265
|
+
await deleteAgentSecrets(id).catch(() => {});
|
|
266
|
+
log.info(`agente ${id} borrado (sus tools y skills quedan: son compartidas)`);
|
|
267
|
+
return true;
|
|
268
|
+
}
|