@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,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
|
-
|
|
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
|
-
|
|
262
|
+
return unarchiveThread(sessionId)
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
/** Borra la sesión entera: mensajes, resumen, notas y su fila del catálogo. */
|