@johpaz/hive-sdk 0.2.0 → 0.3.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 (88) hide show
  1. package/CHANGELOG.md +306 -0
  2. package/README.md +11 -3
  3. package/package.json +10 -4
  4. package/packages/core/src/agent/agent-catalog.ts +81 -24
  5. package/packages/core/src/agent/agent-loop.ts +2 -2
  6. package/packages/core/src/agent/compaction.ts +20 -1
  7. package/packages/core/src/agent/context-compiler.ts +7 -4
  8. package/packages/core/src/agent/conversation-store.ts +136 -2
  9. package/packages/core/src/agent/curator.ts +12 -3
  10. package/packages/core/src/agent/llm-providers/nvidia.ts +39 -0
  11. package/packages/core/src/agent/llm-providers/openai-compat-base.ts +38 -2
  12. package/packages/core/src/agent/playbook-selector.ts +18 -3
  13. package/packages/core/src/agent/prompt-builder.ts +2 -2
  14. package/packages/core/src/agent/providers/index.ts +37 -2
  15. package/packages/core/src/agent/reflector.ts +32 -9
  16. package/packages/core/src/agent/skill-selector.ts +2 -2
  17. package/packages/core/src/agent/thread-store.ts +43 -0
  18. package/packages/core/src/agent/tool-selector.ts +2 -0
  19. package/packages/core/src/api/createAgent.ts +68 -2
  20. package/packages/core/src/artifacts/index.ts +15 -0
  21. package/packages/core/src/artifacts/store.ts +77 -2
  22. package/packages/core/src/canvas/index.ts +9 -0
  23. package/packages/core/src/ethics/EthicsGuard.ts +7 -1
  24. package/packages/core/src/events/index.ts +18 -0
  25. package/packages/core/src/events/tool-narration.ts +4 -0
  26. package/packages/core/src/gateway/channel-notify.ts +103 -6
  27. package/packages/core/src/gateway/durable-queue.ts +13 -1
  28. package/packages/core/src/gateway/index.ts +3 -0
  29. package/packages/core/src/gateway/job-store.ts +6 -0
  30. package/packages/core/src/harness/executors.ts +493 -0
  31. package/packages/core/src/harness/index.ts +12 -2
  32. package/packages/core/src/hooks/index.ts +203 -0
  33. package/packages/core/src/images/index.ts +161 -0
  34. package/packages/core/src/index.ts +1 -0
  35. package/packages/core/src/multimodal/vision-service.ts +45 -13
  36. package/packages/core/src/resilience/index.ts +13 -0
  37. package/packages/core/src/scheduler/CronScheduler.ts +48 -21
  38. package/packages/core/src/scheduler/cron/expression.ts +165 -0
  39. package/packages/core/src/scheduler/cron/index.ts +10 -0
  40. package/packages/core/src/scheduler/cron/job.ts +339 -0
  41. package/packages/core/src/scheduler/cron/next-run.ts +121 -0
  42. package/packages/core/src/scheduler/cron/zoned-time.ts +138 -0
  43. package/packages/core/src/scheduler/index.ts +21 -3
  44. package/packages/core/src/scheduler/integration.ts +16 -5
  45. package/packages/core/src/scheduler/types.ts +3 -18
  46. package/packages/core/src/services/agents.ts +268 -0
  47. package/packages/core/src/services/cron.ts +257 -0
  48. package/packages/core/src/services/endpoints.ts +289 -0
  49. package/packages/core/src/services/ethics.ts +107 -0
  50. package/packages/core/src/services/images.ts +212 -0
  51. package/packages/core/src/services/index.ts +112 -0
  52. package/packages/core/src/services/mcp.ts +201 -0
  53. package/packages/core/src/services/memory.ts +133 -0
  54. package/packages/core/src/services/models.ts +179 -0
  55. package/packages/core/src/services/providers.ts +152 -0
  56. package/packages/core/src/services/setup.ts +222 -0
  57. package/packages/core/src/services/skills.ts +241 -0
  58. package/packages/core/src/services/swarms.ts +307 -0
  59. package/packages/core/src/services/tools.ts +106 -0
  60. package/packages/core/src/sessions/index.ts +5 -3
  61. package/packages/core/src/sessions/resolve.ts +108 -0
  62. package/packages/core/src/skills/SkillLoader.ts +8 -1
  63. package/packages/core/src/skills/bundled/artifacts/artifact_reader/SKILL.md +105 -0
  64. package/packages/core/src/skills/bundled/cron_manager/SKILL.md +21 -11
  65. package/packages/core/src/skills/bundled/images/image_editor/SKILL.md +120 -0
  66. package/packages/core/src/skills/bundled/web/browser_automate/SKILL.md +12 -3
  67. package/packages/core/src/skills/bundled/web/browser_scrape/SKILL.md +22 -7
  68. package/packages/core/src/skills/bundled-data.generated.ts +110 -12
  69. package/packages/core/src/storage/bootstrap.ts +74 -5
  70. package/packages/core/src/storage/collections.ts +106 -1
  71. package/packages/core/src/storage/crypto.ts +24 -7
  72. package/packages/core/src/storage/hive.ts +9 -3
  73. package/packages/core/src/storage/index.ts +2 -1
  74. package/packages/core/src/storage/onboarding.ts +59 -43
  75. package/packages/core/src/storage/reconcile.ts +6 -1
  76. package/packages/core/src/storage/seed.ts +98 -14
  77. package/packages/core/src/swarm/types.ts +3 -18
  78. package/packages/core/src/tool-runtime/embedded-worker.generated.ts +21 -0
  79. package/packages/core/src/tool-runtime/index.ts +129 -14
  80. package/packages/core/src/tools/agents/index.ts +18 -60
  81. package/packages/core/src/tools/cli/index.ts +55 -0
  82. package/packages/core/src/tools/core/index.ts +50 -2
  83. package/packages/core/src/tools/cron/index.ts +4 -4
  84. package/packages/core/src/tools/images/index.ts +130 -0
  85. package/packages/core/src/tools/index.ts +14 -1
  86. package/packages/core/src/tools/office/office-escribir-xlsx.ts +2 -1
  87. package/packages/core/src/tools/office/office-leer-xlsx.ts +2 -1
  88. package/packages/core/src/tools/office/xlsx-loader.ts +19 -0
@@ -0,0 +1,241 @@
1
+ /**
2
+ * Skills — la API, no la tool.
3
+ *
4
+ * Una skill es instruccional: metadatos más un cuerpo markdown que se le inyecta
5
+ * al agente. No tiene `execute`, y por eso —a diferencia de una tool— **sí puede
6
+ * crearla un usuario desde una UI** sin abrir la puerta a ejecutar código
7
+ * arbitrario. hive ya lo permite por HTTP; acá esa lógica deja de estar inline
8
+ * en una ruta y pasa a ser una función que cualquier interfaz puede llamar.
9
+ *
10
+ * Conviven dos orígenes y no compiten:
11
+ *
12
+ * - **Disco** — `SkillLoader` lee carpetas con `SKILL.md` desde el bundle,
13
+ * `~/.hive/skills`, `extraDirs` y el workspace. Es la vía de `hives add-skill`,
14
+ * versionable con git.
15
+ * - **Base de datos** — la colección `skills`, que es lo que el runtime
16
+ * consulta y lo que una UI edita.
17
+ *
18
+ * `importSkillFromDisk()` es el puente: materializa una skill de disco como fila
19
+ * editable. La BD es la fuente de verdad en runtime; el disco es de dónde vino.
20
+ *
21
+ * Cada alta, edición o borrado re-sincroniza el índice BM25: una skill que no
22
+ * está indexada es una skill que el modelo no encuentra.
23
+ */
24
+
25
+ import { readFileSync, existsSync, statSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { col } from "../storage/hive.ts";
28
+ import type { SkillDoc } from "../storage/collections.ts";
29
+ import { parseFrontmatter } from "../skills/SkillLoader.ts";
30
+ import { syncSkillsToIndex } from "../agent/skill-selector.ts";
31
+ import { slugify } from "./agents.ts";
32
+ import { logger } from "../utils/logger.ts";
33
+
34
+ const log = logger.child("services/skills");
35
+
36
+ export interface SkillSummary {
37
+ id: string;
38
+ name: string;
39
+ description: string | null;
40
+ version: string;
41
+ author: string;
42
+ icon: string;
43
+ category: string;
44
+ /** Tools que la skill espera tener disponibles. */
45
+ tools: string[];
46
+ triggers: string[];
47
+ preferredAgents: string[];
48
+ /** El contenido markdown que se le inyecta al agente. */
49
+ body: string;
50
+ active: boolean;
51
+ createdAt: number;
52
+ updatedAt: number;
53
+ }
54
+
55
+ export interface CreateSkillInput {
56
+ id?: string;
57
+ name: string;
58
+ description?: string | null;
59
+ category?: string;
60
+ body: string;
61
+ tools?: string[];
62
+ triggers?: string[];
63
+ preferredAgents?: string[];
64
+ version?: string;
65
+ author?: string;
66
+ icon?: string;
67
+ active?: boolean;
68
+ }
69
+
70
+ export type UpdateSkillInput = Partial<Omit<CreateSkillInput, "id">>;
71
+
72
+ /** Los mismos defaults que usa hive al crear una skill desde su UI. */
73
+ const DEFAULTS = { version: "0.0.1", author: "Anonymous", icon: "🧩", category: "general" };
74
+
75
+ function parseList(raw: string | null | undefined): string[] {
76
+ if (!raw) return [];
77
+ try {
78
+ const v = JSON.parse(raw);
79
+ if (Array.isArray(v)) return v.filter((x): x is string => typeof x === "string");
80
+ } catch {
81
+ // Campo legacy en texto plano separado por comas.
82
+ }
83
+ return raw.split(",").map((s) => s.trim()).filter(Boolean);
84
+ }
85
+
86
+ function toSummary(doc: SkillDoc): SkillSummary {
87
+ return {
88
+ id: doc.id,
89
+ name: doc.name,
90
+ description: doc.description,
91
+ version: doc.version,
92
+ author: doc.author,
93
+ icon: doc.icon,
94
+ category: doc.category,
95
+ tools: parseList(doc.tools),
96
+ triggers: parseList(doc.triggers),
97
+ preferredAgents: parseList(doc.preferred_agents),
98
+ body: doc.body,
99
+ active: doc.active,
100
+ createdAt: doc.created_at,
101
+ updatedAt: doc.updated_at,
102
+ };
103
+ }
104
+
105
+ async function skillsCol() {
106
+ return col<SkillDoc>("skills");
107
+ }
108
+
109
+ export async function createSkill(input: CreateSkillInput): Promise<SkillSummary> {
110
+ if (!input.name?.trim()) throw new Error("La skill necesita un nombre");
111
+ if (!input.body?.trim()) throw new Error("La skill necesita un cuerpo: es lo que lee el agente");
112
+
113
+ const c = await skillsCol();
114
+ const id = input.id ?? crypto.randomUUID();
115
+ if (await c.get(id)) throw new Error(`Ya existe una skill con id "${id}"`);
116
+
117
+ const now = Date.now();
118
+ const doc: SkillDoc = {
119
+ id,
120
+ name: input.name,
121
+ description: input.description ?? null,
122
+ version: input.version ?? DEFAULTS.version,
123
+ author: input.author ?? DEFAULTS.author,
124
+ icon: input.icon ?? DEFAULTS.icon,
125
+ category: input.category ?? DEFAULTS.category,
126
+ permissions: "[]",
127
+ dependencies: "[]",
128
+ tools: JSON.stringify(input.tools ?? []),
129
+ triggers: JSON.stringify(input.triggers ?? []),
130
+ preferred_agents: JSON.stringify(input.preferredAgents ?? []),
131
+ body: input.body,
132
+ version_num: 1,
133
+ active: input.active ?? true,
134
+ created_at: now,
135
+ updated_at: now,
136
+ };
137
+
138
+ await c.put(id, doc, { expectedVersion: 0 });
139
+ await syncSkillsToIndex().catch((e) => log.warn(`no pude reindexar: ${(e as Error).message}`));
140
+ log.info(`skill "${input.name}" creada (${id})`);
141
+ return toSummary(doc);
142
+ }
143
+
144
+ export async function getSkill(id: string): Promise<SkillSummary | null> {
145
+ const entry = await (await skillsCol()).get(id);
146
+ return entry ? toSummary(entry.doc) : null;
147
+ }
148
+
149
+ export async function listSkills(opts?: { includeInactive?: boolean; category?: string }): Promise<SkillSummary[]> {
150
+ const rows = await (await skillsCol()).scan({});
151
+ return rows
152
+ .map((e) => e.doc)
153
+ .filter((d) => (opts?.includeInactive ? true : d.active))
154
+ .filter((d) => (opts?.category ? d.category === opts.category : true))
155
+ .map(toSummary)
156
+ .sort((a, b) => a.name.localeCompare(b.name));
157
+ }
158
+
159
+ export async function updateSkill(id: string, changes: UpdateSkillInput): Promise<SkillSummary> {
160
+ const c = await skillsCol();
161
+ const entry = await c.get(id);
162
+ if (!entry) throw new Error(`No existe la skill "${id}"`);
163
+
164
+ const doc: SkillDoc = { ...entry.doc, updated_at: Date.now() };
165
+ if (changes.name !== undefined) doc.name = changes.name;
166
+ if (changes.description !== undefined) doc.description = changes.description;
167
+ if (changes.category !== undefined) doc.category = changes.category;
168
+ if (changes.version !== undefined) doc.version = changes.version;
169
+ if (changes.author !== undefined) doc.author = changes.author;
170
+ if (changes.icon !== undefined) doc.icon = changes.icon;
171
+ if (changes.active !== undefined) doc.active = changes.active;
172
+ if (changes.tools !== undefined) doc.tools = JSON.stringify(changes.tools);
173
+ if (changes.triggers !== undefined) doc.triggers = JSON.stringify(changes.triggers);
174
+ if (changes.preferredAgents !== undefined) doc.preferred_agents = JSON.stringify(changes.preferredAgents);
175
+ if (changes.body !== undefined) {
176
+ doc.body = changes.body;
177
+ // Editar el contenido es una versión nueva: es lo que cambia el comportamiento.
178
+ doc.version_num = (entry.doc.version_num ?? 1) + 1;
179
+ }
180
+
181
+ await c.put(id, doc, { expectedVersion: entry.version });
182
+ await syncSkillsToIndex().catch((e) => log.warn(`no pude reindexar: ${(e as Error).message}`));
183
+ return toSummary(doc);
184
+ }
185
+
186
+ export const toggleSkill = (id: string, active: boolean) => updateSkill(id, { active });
187
+
188
+ export async function deleteSkill(id: string): Promise<boolean> {
189
+ const c = await skillsCol();
190
+ if (!(await c.get(id))) return false;
191
+ await c.delete(id);
192
+ await syncSkillsToIndex().catch((e) => log.warn(`no pude reindexar: ${(e as Error).message}`));
193
+ return true;
194
+ }
195
+
196
+ /**
197
+ * Importa una skill del disco a la base.
198
+ *
199
+ * Acepta la ruta de un `SKILL.md` o la de la carpeta que lo contiene — que es
200
+ * lo que produce `hives add-skill` y lo que el usuario escribe a mano. El
201
+ * frontmatter se parsea con el mismo `parseFrontmatter` que usa `SkillLoader`,
202
+ * para que disco y BD no puedan divergir en qué consideran válido.
203
+ *
204
+ * Es idempotente por id: reimportar actualiza en vez de duplicar, así se puede
205
+ * editar el archivo y volver a importarlo.
206
+ */
207
+ export async function importSkillFromDisk(path: string): Promise<SkillSummary> {
208
+ const file = existsSync(path) && statSync(path).isDirectory() ? join(path, "SKILL.md") : path;
209
+ if (!existsSync(file)) throw new Error(`No encuentro ${file}`);
210
+
211
+ const { frontmatter, body } = parseFrontmatter(readFileSync(file, "utf-8"));
212
+ const fm = frontmatter as Record<string, any>;
213
+
214
+ const name = String(fm.name ?? "").trim();
215
+ if (!name) throw new Error(`${file} no declara \`name\` en su frontmatter`);
216
+ if (!body.trim()) throw new Error(`${file} no tiene cuerpo: no hay nada que inyectarle al agente`);
217
+
218
+ const id = slugify(String(fm.id ?? name));
219
+ const asList = (v: unknown): string[] =>
220
+ Array.isArray(v) ? v.map(String) : typeof v === "string" ? v.split(",").map((x) => x.trim()).filter(Boolean) : [];
221
+
222
+ const campos = {
223
+ name,
224
+ description: fm.description ? String(fm.description) : null,
225
+ category: fm.category ? String(fm.category) : DEFAULTS.category,
226
+ body,
227
+ tools: asList(fm.tools),
228
+ triggers: asList(fm.triggers),
229
+ preferredAgents: asList(fm.preferred_agents),
230
+ version: fm.version ? String(fm.version) : DEFAULTS.version,
231
+ author: fm.author ? String(fm.author) : DEFAULTS.author,
232
+ icon: fm.icon ? String(fm.icon) : DEFAULTS.icon,
233
+ };
234
+
235
+ const existente = await getSkill(id);
236
+ if (existente) {
237
+ log.info(`skill "${name}" reimportada desde ${file}`);
238
+ return updateSkill(id, campos);
239
+ }
240
+ return createSkill({ id, ...campos });
241
+ }
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Enjambres — guardarlos, no sólo correrlos.
3
+ *
4
+ * `runRoleSwarm()` (swarm/RoleSwarm.ts) recibe los agentes en la llamada y no
5
+ * persiste nada: un enjambre existía sólo mientras se ejecutaba. Quien armara
6
+ * uno desde una interfaz lo perdía al cerrar la ventana. Ese era el bloqueador
7
+ * real para poner una UI encima del SDK — más que cualquier CRUD faltante.
8
+ *
9
+ * Este servicio guarda la definición y `runSwarm()` la carga y la ejecuta. La
10
+ * ejecución en sí no cambia: sigue siendo `runRoleSwarm`.
11
+ *
12
+ * Se valida al guardar, no al correr. Un enjambre jerárquico sin orquestador, o
13
+ * con un agente que ya no existe, es un error de configuración: descubrirlo
14
+ * cuando alguien lo ejecuta —posiblemente semanas después— es descubrirlo tarde.
15
+ */
16
+
17
+ import { col } from "../storage/hive.ts";
18
+ import type { SwarmDoc, SwarmMemberSpec, AgentDoc } from "../storage/collections.ts";
19
+ import { runRoleSwarm, type RoleSwarmResult, type SwarmMessage } from "../swarm/RoleSwarm.ts";
20
+ import type { ProviderCredentials } from "../agent/llm-client.ts";
21
+ import { slugify } from "./agents.ts";
22
+ import { logger } from "../utils/logger.ts";
23
+ import { enableCatalogAgents, planActivationFor, CATALOG_AGENT_IDS, type ActivationGap } from "./setup.ts";
24
+
25
+ const log = logger.child("services/swarms");
26
+
27
+ export interface SwarmMember {
28
+ agentId: string;
29
+ role: "orchestrator" | "worker";
30
+ orderIndex: number;
31
+ }
32
+
33
+ export interface SwarmSummary {
34
+ id: string;
35
+ name: string;
36
+ description: string | null;
37
+ strategy: SwarmDoc["strategy"];
38
+ orchestratorAgentId: string | null;
39
+ members: SwarmMember[];
40
+ enabled: boolean;
41
+ maxDelegations: number | null;
42
+ createdAt: number;
43
+ updatedAt: number;
44
+ /**
45
+ * Lo que hace falta encender para que este enjambre pueda trabajar.
46
+ *
47
+ * Un enjambre puede nombrar especialistas que el usuario no activó al
48
+ * instalar: la fila del agente existe igual (el seed las crea todas y sólo
49
+ * cambia `enabled`), así que sin esto el enjambre se guardaría sin una queja
50
+ * y correría con agentes apagados y sus tools inactivas. Vacío = listo para
51
+ * correr. Sólo lo traen `createSwarm` y `updateSwarm`.
52
+ */
53
+ pendingActivation?: ActivationGap;
54
+ }
55
+
56
+ export interface CreateSwarmInput {
57
+ id?: string;
58
+ name: string;
59
+ description?: string | null;
60
+ strategy: SwarmDoc["strategy"];
61
+ members: Array<{ agentId: string; role?: "orchestrator" | "worker"; orderIndex?: number }>;
62
+ orchestratorAgentId?: string | null;
63
+ maxDelegations?: number | null;
64
+ enabled?: boolean;
65
+ /**
66
+ * Encender los especialistas del enjambre y sembrar sus tools y skills.
67
+ *
68
+ * `false` por defecto **a propósito**: crear un enjambre no debería cambiar
69
+ * en silencio qué capacidades tiene la instalación entera. Con `false` el
70
+ * enjambre se crea igual y el faltante vuelve en `pendingActivation`, para
71
+ * que la UI lo muestre y el usuario decida. Con `true` se activa la unión con
72
+ * lo que ya estaba: encender un especialista nunca apaga los de otro enjambre.
73
+ */
74
+ activateMembers?: boolean;
75
+ }
76
+
77
+ export type UpdateSwarmInput = Partial<Omit<CreateSwarmInput, "id">>;
78
+
79
+ function parseMembers(json: string): SwarmMember[] {
80
+ try {
81
+ const v = JSON.parse(json);
82
+ return Array.isArray(v) ? (v as SwarmMemberSpec[]) : [];
83
+ } catch {
84
+ return [];
85
+ }
86
+ }
87
+
88
+ function toSummary(doc: SwarmDoc): SwarmSummary {
89
+ return {
90
+ id: doc.id,
91
+ name: doc.name,
92
+ description: doc.description,
93
+ strategy: doc.strategy,
94
+ orchestratorAgentId: doc.orchestrator_agent_id,
95
+ members: parseMembers(doc.agents_json),
96
+ enabled: doc.enabled,
97
+ maxDelegations: doc.max_delegations,
98
+ createdAt: doc.created_at,
99
+ updatedAt: doc.updated_at,
100
+ };
101
+ }
102
+
103
+ async function swarmsCol() {
104
+ return col<SwarmDoc>("swarms");
105
+ }
106
+
107
+ /**
108
+ * Normaliza y valida los integrantes.
109
+ *
110
+ * Un enjambre jerárquico necesita un orquestador y al menos un trabajador; los
111
+ * otros dos modos no admiten orquestador, porque no hay a quién delegar.
112
+ */
113
+ async function normalizeMembers(
114
+ input: CreateSwarmInput | UpdateSwarmInput,
115
+ strategy: SwarmDoc["strategy"],
116
+ ): Promise<{ members: SwarmMember[]; orchestratorId: string | null }> {
117
+ const raw = input.members ?? [];
118
+ if (raw.length === 0) throw new Error("Un enjambre necesita al menos un agente");
119
+
120
+ // Un id del catálogo es válido aunque todavía no tenga fila: con el seed en
121
+ // `"none"` los especialistas no se crean hasta que alguien los pide, y este
122
+ // enjambre es justamente el pedido. La fila la crea `withActivation`.
123
+ // Rechazarlos acá haría imposible armar un enjambre en una instalación limpia.
124
+ const delCatalogo = new Set<string>(CATALOG_AGENT_IDS);
125
+ const agentsCol = await col<AgentDoc>("agents");
126
+ const faltan: string[] = [];
127
+ for (const m of raw) {
128
+ if (delCatalogo.has(m.agentId)) continue;
129
+ if (!(await agentsCol.get(m.agentId))) faltan.push(m.agentId);
130
+ }
131
+ if (faltan.length) throw new Error(`agentes inexistentes: ${faltan.join(", ")}`);
132
+
133
+ const members: SwarmMember[] = raw.map((m, i) => ({
134
+ agentId: m.agentId,
135
+ role: m.role ?? "worker",
136
+ orderIndex: m.orderIndex ?? i,
137
+ }));
138
+
139
+ const declarado = input.orchestratorAgentId ?? null;
140
+ const porRol = members.find((m) => m.role === "orchestrator")?.agentId ?? null;
141
+ const orchestratorId = declarado ?? porRol;
142
+
143
+ if (strategy === "hierarchical") {
144
+ if (!orchestratorId) throw new Error("La estrategia jerárquica necesita un orquestador");
145
+ if (!members.some((m) => m.role === "worker")) {
146
+ throw new Error("La estrategia jerárquica necesita al menos un agente con rol worker");
147
+ }
148
+ if (!members.some((m) => m.agentId === orchestratorId)) {
149
+ members.push({ agentId: orchestratorId, role: "orchestrator", orderIndex: -1 });
150
+ }
151
+ }
152
+
153
+ return { members, orchestratorId: strategy === "hierarchical" ? orchestratorId : null };
154
+ }
155
+
156
+ export async function createSwarm(input: CreateSwarmInput): Promise<SwarmSummary> {
157
+ if (!input.name?.trim()) throw new Error("El enjambre necesita un nombre");
158
+
159
+ const c = await swarmsCol();
160
+ const id = input.id ?? slugify(input.name);
161
+ if (await c.get(id)) throw new Error(`Ya existe un enjambre con id "${id}"`);
162
+
163
+ const { members, orchestratorId } = await normalizeMembers(input, input.strategy);
164
+ const now = Date.now();
165
+ const doc: SwarmDoc = {
166
+ id,
167
+ name: input.name,
168
+ description: input.description ?? null,
169
+ strategy: input.strategy,
170
+ orchestrator_agent_id: orchestratorId,
171
+ agents_json: JSON.stringify(members),
172
+ enabled: input.enabled ?? true,
173
+ max_delegations: input.maxDelegations ?? null,
174
+ created_at: now,
175
+ updated_at: now,
176
+ };
177
+
178
+ await c.put(id, doc, { expectedVersion: 0 });
179
+ log.info(`enjambre "${input.name}" guardado (${id}, ${input.strategy}, ${members.length} agentes)`);
180
+ return withActivation(doc, members, input.activateMembers);
181
+ }
182
+
183
+ /**
184
+ * Resuelve la activación de los especialistas del enjambre y adjunta el
185
+ * faltante al resumen.
186
+ *
187
+ * Corre DESPUÉS de guardar la fila: si el sembrado fallara a mitad, el enjambre
188
+ * ya está persistido y el usuario puede reintentar la activación desde la UI.
189
+ * Al revés perdería la definición del enjambre por un problema del catálogo.
190
+ */
191
+ async function withActivation(
192
+ doc: SwarmDoc,
193
+ members: SwarmMember[],
194
+ activar: boolean | undefined,
195
+ ): Promise<SwarmSummary> {
196
+ const ids = members.map((m) => m.agentId);
197
+ if (activar) {
198
+ const gap = await planActivationFor(ids);
199
+ if (gap.agents.length > 0) {
200
+ await enableCatalogAgents(gap.agents);
201
+ log.info(`enjambre "${doc.id}": activados ${gap.agents.join(", ")}`);
202
+ }
203
+ }
204
+ // Se recalcula después de activar: así `pendingActivation` refleja lo que
205
+ // quedó pendiente de verdad, no lo que faltaba antes de encender nada.
206
+ return { ...toSummary(doc), pendingActivation: await planActivationFor(ids) };
207
+ }
208
+
209
+ export async function getSwarm(id: string): Promise<SwarmSummary | null> {
210
+ const entry = await (await swarmsCol()).get(id);
211
+ return entry ? toSummary(entry.doc) : null;
212
+ }
213
+
214
+ export async function listSwarms(opts?: { includeDisabled?: boolean }): Promise<SwarmSummary[]> {
215
+ const rows = await (await swarmsCol()).scan({});
216
+ return rows
217
+ .map((e) => e.doc)
218
+ .filter((d) => (opts?.includeDisabled ? true : d.enabled))
219
+ .map(toSummary)
220
+ .sort((a, b) => a.name.localeCompare(b.name));
221
+ }
222
+
223
+ export async function updateSwarm(id: string, changes: UpdateSwarmInput): Promise<SwarmSummary> {
224
+ const c = await swarmsCol();
225
+ const entry = await c.get(id);
226
+ if (!entry) throw new Error(`No existe el enjambre "${id}"`);
227
+
228
+ const doc: SwarmDoc = { ...entry.doc, updated_at: Date.now() };
229
+ if (changes.name !== undefined) doc.name = changes.name;
230
+ if (changes.description !== undefined) doc.description = changes.description;
231
+ if (changes.enabled !== undefined) doc.enabled = changes.enabled;
232
+ if (changes.maxDelegations !== undefined) doc.max_delegations = changes.maxDelegations;
233
+
234
+ const estrategia = changes.strategy ?? entry.doc.strategy;
235
+ if (changes.strategy !== undefined) doc.strategy = changes.strategy;
236
+
237
+ // Cambiar la estrategia revalida los integrantes: pasar a jerárquico sin
238
+ // orquestador tiene que fallar acá, no al ejecutarlo.
239
+ if (changes.members !== undefined || changes.strategy !== undefined || changes.orchestratorAgentId !== undefined) {
240
+ const { members, orchestratorId } = await normalizeMembers(
241
+ { ...changes, members: changes.members ?? parseMembers(entry.doc.agents_json) },
242
+ estrategia,
243
+ );
244
+ doc.agents_json = JSON.stringify(members);
245
+ doc.orchestrator_agent_id = orchestratorId;
246
+ }
247
+
248
+ await c.put(id, doc, { expectedVersion: entry.version });
249
+ // Agregar un especialista a un enjambre que ya existe pasa por el mismo
250
+ // camino que crearlo: si no, el enjambre editado quedaría con un miembro
251
+ // apagado y nadie se enteraría.
252
+ return withActivation(doc, parseMembers(doc.agents_json), changes.activateMembers);
253
+ }
254
+
255
+ export const toggleSwarm = (id: string, enabled: boolean) => updateSwarm(id, { enabled });
256
+
257
+ export async function deleteSwarm(id: string): Promise<boolean> {
258
+ const c = await swarmsCol();
259
+ if (!(await c.get(id))) return false;
260
+ await c.delete(id);
261
+ // Los agentes no se tocan: pertenecen a la colmena, no al enjambre.
262
+ return true;
263
+ }
264
+
265
+ export interface RunSwarmOptions {
266
+ /** Identifica la corrida; por defecto se genera uno. */
267
+ runId?: string;
268
+ channel?: string;
269
+ /** Credenciales del inquilino, propagadas a cada agente. */
270
+ credentials?: ProviderCredentials;
271
+ signal?: AbortSignal;
272
+ /** Se llama en cada paso; acá persiste el consumidor si quiere. */
273
+ onMessage?: (message: SwarmMessage) => void | Promise<void>;
274
+ }
275
+
276
+ /**
277
+ * Carga un enjambre guardado y lo ejecuta.
278
+ *
279
+ * Un enjambre deshabilitado no corre: apagarlo tiene que significar algo, o el
280
+ * interruptor de la UI es decorativo.
281
+ */
282
+ export async function runSwarm(
283
+ swarmId: string,
284
+ input: string,
285
+ opts?: RunSwarmOptions,
286
+ ): Promise<RoleSwarmResult> {
287
+ const swarm = await getSwarm(swarmId);
288
+ if (!swarm) throw new Error(`No existe el enjambre "${swarmId}"`);
289
+ if (!swarm.enabled) throw new Error(`El enjambre "${swarmId}" está deshabilitado`);
290
+
291
+ return runRoleSwarm({
292
+ agents: swarm.members.map((m) => ({
293
+ agentId: m.agentId,
294
+ role: m.role,
295
+ orderIndex: m.orderIndex,
296
+ })),
297
+ strategy: swarm.strategy,
298
+ input,
299
+ runId: opts?.runId ?? `swarm-${swarmId}-${Date.now()}`,
300
+ channel: opts?.channel,
301
+ orchestratorAgentId: swarm.orchestratorAgentId ?? undefined,
302
+ maxDelegations: swarm.maxDelegations ?? undefined,
303
+ credentials: opts?.credentials,
304
+ signal: opts?.signal,
305
+ onMessage: opts?.onMessage,
306
+ });
307
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Tools — la API, no la tool (valga la redundancia).
3
+ *
4
+ * Una tool es código con un `execute`, así que a diferencia de las skills **no
5
+ * se puede dar de alta una desde una UI**: no hay dónde poner el código. hive
6
+ * lo refleja exactamente así — su ruta expone listar, activar/desactivar y
7
+ * editar metadatos, pero no tiene `POST /api/tools`.
8
+ *
9
+ * Las tres vías reales para sumar capacidades son otras, y ninguna pasa por acá:
10
+ * - `registerAppTool()` — código propio, para quien construye sobre el SDK.
11
+ * - Un servidor MCP — proceso externo que expone sus tools (`services/mcp.ts`).
12
+ * - Un endpoint HTTP declarativo (`services/endpoints.ts`), donde el ejecutor
13
+ * es genérico y lo que el usuario aporta son datos, no código.
14
+ *
15
+ * Lo que este servicio sí resuelve es lo que una UI necesita a diario: ver el
16
+ * catálogo, encender y apagar, y corregir un nombre o una descripción.
17
+ */
18
+
19
+ import { col } from "../storage/hive.ts";
20
+ import type { ToolDoc } from "../storage/collections.ts";
21
+ import { syncToolCatalogToIndex } from "../agent/tool-selector.ts";
22
+ import { logger } from "../utils/logger.ts";
23
+
24
+ const log = logger.child("services/tools");
25
+
26
+ export interface ToolSummary {
27
+ id: string;
28
+ name: string;
29
+ description: string | null;
30
+ category: string | null;
31
+ /** `enabled` = disponible en la instalación; `active` = ofrecida ahora. */
32
+ enabled: boolean;
33
+ active: boolean;
34
+ }
35
+
36
+ function toSummary(doc: ToolDoc): ToolSummary {
37
+ return {
38
+ id: doc.id,
39
+ name: doc.name,
40
+ description: doc.description,
41
+ category: doc.category,
42
+ enabled: doc.enabled,
43
+ active: doc.active,
44
+ };
45
+ }
46
+
47
+ async function toolsCol() {
48
+ return col<ToolDoc>("tools");
49
+ }
50
+
51
+ export async function listTools(opts?: { category?: string; includeInactive?: boolean }): Promise<ToolSummary[]> {
52
+ const rows = await (await toolsCol()).scan({});
53
+ return rows
54
+ .map((e) => e.doc)
55
+ .filter((d) => (opts?.category ? d.category === opts.category : true))
56
+ .filter((d) => (opts?.includeInactive ? true : d.active))
57
+ .map(toSummary)
58
+ .sort((a, b) => a.name.localeCompare(b.name));
59
+ }
60
+
61
+ export async function getTool(id: string): Promise<ToolSummary | null> {
62
+ const entry = await (await toolsCol()).get(id);
63
+ return entry ? toSummary(entry.doc) : null;
64
+ }
65
+
66
+ /**
67
+ * Enciende o apaga una tool.
68
+ *
69
+ * Reindexa después: el índice BM25 es lo que consulta `search_knowledge`, y una
70
+ * tool apagada que siga indexada es una que el modelo encuentra y no puede usar.
71
+ */
72
+ export async function toggleTool(id: string, active: boolean): Promise<ToolSummary> {
73
+ const c = await toolsCol();
74
+ const entry = await c.get(id);
75
+ if (!entry) throw new Error(`No existe la tool "${id}"`);
76
+
77
+ const doc: ToolDoc = { ...entry.doc, active, updated_at: Date.now() };
78
+ await c.put(id, doc, { expectedVersion: entry.version });
79
+ await syncToolCatalogToIndex().catch((e) => log.warn(`no pude reindexar: ${(e as Error).message}`));
80
+ return toSummary(doc);
81
+ }
82
+
83
+ /**
84
+ * Corrige nombre, descripción o categoría.
85
+ *
86
+ * No toca `execute`: la implementación vive en el código, no en la fila. La
87
+ * descripción sí importa — es lo que el modelo lee para decidir si la tool le
88
+ * sirve, así que cambiarla cambia el comportamiento y por eso se reindexa.
89
+ */
90
+ export async function updateToolMetadata(
91
+ id: string,
92
+ changes: { name?: string; description?: string | null; category?: string | null },
93
+ ): Promise<ToolSummary> {
94
+ const c = await toolsCol();
95
+ const entry = await c.get(id);
96
+ if (!entry) throw new Error(`No existe la tool "${id}"`);
97
+
98
+ const doc: ToolDoc = { ...entry.doc, updated_at: Date.now() };
99
+ if (changes.name !== undefined) doc.name = changes.name;
100
+ if (changes.description !== undefined) doc.description = changes.description;
101
+ if (changes.category !== undefined) doc.category = changes.category;
102
+
103
+ await c.put(id, doc, { expectedVersion: entry.version });
104
+ await syncToolCatalogToIndex().catch((e) => log.warn(`no pude reindexar: ${(e as Error).message}`));
105
+ return toSummary(doc);
106
+ }
@@ -22,13 +22,13 @@
22
22
 
23
23
  import type { ContentPart } from "../multimodal/types.ts"
24
24
  import type { AgentRunDoc, ConversationThreadDoc } from "../storage/collections.ts"
25
- import { updateDoc } from "../storage/hive.ts"
26
25
  import {
27
26
  addMessage,
28
27
  getHistory,
29
28
  type StoredMessage,
30
29
  } from "../agent/conversation-store.ts"
31
30
  import {
31
+ archiveThread,
32
32
  createWebConversation,
33
33
  deleteThread,
34
34
  ensureThread,
@@ -37,6 +37,7 @@ import {
37
37
  mostRecentWebThread,
38
38
  renameThread,
39
39
  threadForChannel,
40
+ unarchiveThread,
40
41
  } from "../agent/thread-store.ts"
41
42
  import {
42
43
  deserializeCheckpoint,
@@ -45,6 +46,7 @@ import {
45
46
  } from "../agent/run-store.ts"
46
47
 
47
48
  export * from "../agent/thread-id.ts"
49
+ export * from "./resolve.ts"
48
50
 
49
51
  /** El estado de ejecución más reciente del hilo, si alguna vez corrió. */
50
52
  export interface SessionRun {
@@ -252,12 +254,12 @@ export async function renameSession(sessionId: string, title: string): Promise<v
252
254
  * Para borrarla de verdad, `deleteSession`.
253
255
  */
254
256
  export async function closeSession(sessionId: string): Promise<void> {
255
- await updateDoc<ConversationThreadDoc>("conversationThreads", sessionId, { archived: true })
257
+ return archiveThread(sessionId)
256
258
  }
257
259
 
258
260
  /** Reabre una sesión archivada. */
259
261
  export async function reopenSession(sessionId: string): Promise<void> {
260
- await updateDoc<ConversationThreadDoc>("conversationThreads", sessionId, { archived: false })
262
+ return unarchiveThread(sessionId)
261
263
  }
262
264
 
263
265
  /** Borra la sesión entera: mensajes, resumen, notas y su fila del catálogo. */