@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.
- 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/agent-loop.ts +2 -2
- package/packages/core/src/agent/compaction.ts +20 -1
- package/packages/core/src/agent/context-compiler.ts +7 -4
- 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 +37 -2
- 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 +68 -2
- 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/hive.ts +9 -3
- 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 +98 -14
- 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,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Modelos — la API, no la ruta.
|
|
3
|
+
*
|
|
4
|
+
* Dos operaciones tienen trampa y por eso están acá y no en una consulta suelta:
|
|
5
|
+
*
|
|
6
|
+
* - **Borrar** un modelo que algún agente usa lo dejaría apuntando a la nada,
|
|
7
|
+
* y el fallo aparecería en el próximo turno de ese agente. Se bloquea con el
|
|
8
|
+
* nombre de quiénes lo usan.
|
|
9
|
+
* - **Renombrar** significa cambiar el id, así que hay que mover la fila *y*
|
|
10
|
+
* re-apuntar a cada agente que la referenciaba, en un solo `batch()`. A
|
|
11
|
+
* medias dejaría agentes huérfanos.
|
|
12
|
+
*
|
|
13
|
+
* El id de catálogo no es el id del wire: los revendedores (openrouter, nvidia,
|
|
14
|
+
* groq…) sirven modelos de terceros y colisionan entre sí, así que `catalogModelKey`
|
|
15
|
+
* prefija y `wireModelId` deshace el prefijo antes de salir a la API.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { col, toIndexable, fromIndexable } from "../storage/hive.ts";
|
|
19
|
+
import { getHiveDb } from "../storage/hivedb.ts";
|
|
20
|
+
import type { BatchOp } from "@johpaz/hive-db";
|
|
21
|
+
import type { ModelDoc, AgentDoc } from "../storage/collections.ts";
|
|
22
|
+
import { catalogModelKey, wireModelId } from "../storage/model-id.ts";
|
|
23
|
+
import { logger } from "../utils/logger.ts";
|
|
24
|
+
|
|
25
|
+
const log = logger.child("services/models");
|
|
26
|
+
|
|
27
|
+
export interface ModelSummary {
|
|
28
|
+
id: string;
|
|
29
|
+
providerId: string;
|
|
30
|
+
name: string;
|
|
31
|
+
modelType: ModelDoc["model_type"];
|
|
32
|
+
contextWindow: number;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
active: boolean;
|
|
35
|
+
/** El id tal como lo espera la API del proveedor, sin el prefijo del revendedor. */
|
|
36
|
+
wireId: string;
|
|
37
|
+
source: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const toSummary = (d: ModelDoc): ModelSummary => ({
|
|
41
|
+
id: d.id,
|
|
42
|
+
providerId: d.provider_id,
|
|
43
|
+
name: d.name,
|
|
44
|
+
modelType: d.model_type,
|
|
45
|
+
contextWindow: d.context_window,
|
|
46
|
+
enabled: d.enabled,
|
|
47
|
+
active: d.active,
|
|
48
|
+
wireId: wireModelId(d.provider_id, d.id),
|
|
49
|
+
source: (d as { source?: string }).source ?? "catalog",
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
async function modelsCol() {
|
|
53
|
+
return col<ModelDoc>("models");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function listModels(opts?: {
|
|
57
|
+
providerId?: string;
|
|
58
|
+
modelType?: ModelDoc["model_type"];
|
|
59
|
+
includeInactive?: boolean;
|
|
60
|
+
}): Promise<ModelSummary[]> {
|
|
61
|
+
const rows = await (await modelsCol()).scan({});
|
|
62
|
+
return rows
|
|
63
|
+
.map((e) => e.doc)
|
|
64
|
+
.filter((d) => (opts?.providerId ? d.provider_id === opts.providerId : true))
|
|
65
|
+
.filter((d) => (opts?.modelType ? d.model_type === opts.modelType : true))
|
|
66
|
+
.filter((d) => (opts?.includeInactive ? true : d.active))
|
|
67
|
+
.map(toSummary)
|
|
68
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function getModel(id: string): Promise<ModelSummary | null> {
|
|
72
|
+
const entry = await (await modelsCol()).get(id);
|
|
73
|
+
return entry ? toSummary(entry.doc) : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Da de alta un modelo propio.
|
|
78
|
+
*
|
|
79
|
+
* Se marca `source: "discovered"` a propósito: el reseed del arranque borra y
|
|
80
|
+
* recrea sólo las filas `"catalog"`, así que un modelo agregado a mano
|
|
81
|
+
* sobrevive a los reinicios.
|
|
82
|
+
*/
|
|
83
|
+
export async function createModel(input: {
|
|
84
|
+
name: string;
|
|
85
|
+
providerId: string;
|
|
86
|
+
modelType?: ModelDoc["model_type"];
|
|
87
|
+
contextWindow?: number;
|
|
88
|
+
active?: boolean;
|
|
89
|
+
}): Promise<ModelSummary> {
|
|
90
|
+
if (!input.name?.trim()) throw new Error("El modelo necesita un nombre");
|
|
91
|
+
if (!input.providerId?.trim()) throw new Error("El modelo necesita un proveedor");
|
|
92
|
+
|
|
93
|
+
const c = await modelsCol();
|
|
94
|
+
const id = catalogModelKey(input.providerId, input.name);
|
|
95
|
+
if (await c.get(id)) throw new Error(`Ya existe el modelo "${id}"`);
|
|
96
|
+
|
|
97
|
+
const doc = {
|
|
98
|
+
id,
|
|
99
|
+
provider_id: input.providerId,
|
|
100
|
+
name: input.name,
|
|
101
|
+
model_type: input.modelType ?? "llm",
|
|
102
|
+
context_window: input.contextWindow ?? 0,
|
|
103
|
+
capabilities: null,
|
|
104
|
+
enabled: true,
|
|
105
|
+
active: input.active ?? false,
|
|
106
|
+
source: "discovered",
|
|
107
|
+
} as ModelDoc;
|
|
108
|
+
|
|
109
|
+
await c.put(id, doc, { expectedVersion: 0 });
|
|
110
|
+
return toSummary(doc);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export async function toggleModel(id: string, active: boolean): Promise<ModelSummary> {
|
|
114
|
+
const c = await modelsCol();
|
|
115
|
+
const entry = await c.get(id);
|
|
116
|
+
if (!entry) throw new Error(`No existe el modelo "${id}"`);
|
|
117
|
+
|
|
118
|
+
const doc: ModelDoc = { ...entry.doc, active };
|
|
119
|
+
await c.put(id, doc, { expectedVersion: entry.version });
|
|
120
|
+
return toSummary(doc);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Los agentes que apuntan a este modelo. Es lo que bloquea un borrado. */
|
|
124
|
+
export async function agentsUsingModel(id: string): Promise<string[]> {
|
|
125
|
+
const rows = await (await col<AgentDoc>("agents")).findBy("model_id", toIndexable(id));
|
|
126
|
+
return rows.map((e) => e.doc.name);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function deleteModel(id: string): Promise<boolean> {
|
|
130
|
+
const c = await modelsCol();
|
|
131
|
+
if (!(await c.get(id))) return false;
|
|
132
|
+
|
|
133
|
+
const enUso = await agentsUsingModel(id);
|
|
134
|
+
if (enUso.length > 0) {
|
|
135
|
+
throw new Error(`El modelo "${id}" lo usan: ${enUso.join(", ")}. Cámbialos antes de borrarlo`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
await c.delete(id);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Renombra un modelo re-apuntando a los agentes que lo usaban.
|
|
144
|
+
*
|
|
145
|
+
* Va en un solo `batch()` porque son dos escrituras que deben ocurrir juntas:
|
|
146
|
+
* si se moviera la fila sin actualizar los agentes, cada uno de ellos quedaría
|
|
147
|
+
* apuntando a un id inexistente hasta que alguien lo notara.
|
|
148
|
+
*/
|
|
149
|
+
export async function renameModel(id: string, nuevoNombre: string): Promise<ModelSummary> {
|
|
150
|
+
const c = await modelsCol();
|
|
151
|
+
const entry = await c.get(id);
|
|
152
|
+
if (!entry) throw new Error(`No existe el modelo "${id}"`);
|
|
153
|
+
|
|
154
|
+
const nuevoId = catalogModelKey(entry.doc.provider_id, nuevoNombre);
|
|
155
|
+
if (nuevoId !== id && (await c.get(nuevoId))) {
|
|
156
|
+
throw new Error(`Ya existe un modelo con id "${nuevoId}"`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const doc: ModelDoc = { ...entry.doc, id: nuevoId, name: nuevoNombre };
|
|
160
|
+
const agentesCol = await col<AgentDoc>("agents");
|
|
161
|
+
const afectados = await agentesCol.findBy("model_id", toIndexable(id));
|
|
162
|
+
|
|
163
|
+
const db = await getHiveDb();
|
|
164
|
+
const ops: BatchOp[] = [
|
|
165
|
+
{ op: "put", collection: "models", id: nuevoId, doc },
|
|
166
|
+
...(nuevoId !== id ? [{ op: "delete" as const, collection: "models", id }] : []),
|
|
167
|
+
...afectados.map((a) => ({
|
|
168
|
+
op: "put" as const,
|
|
169
|
+
collection: "agents",
|
|
170
|
+
id: a.id,
|
|
171
|
+
doc: { ...a.doc, model_id: toIndexable(nuevoId), updated_at: Date.now() },
|
|
172
|
+
expectedVersion: a.version,
|
|
173
|
+
})),
|
|
174
|
+
];
|
|
175
|
+
await db.batch(ops);
|
|
176
|
+
|
|
177
|
+
log.info(`modelo ${id} → ${nuevoId} (${afectados.length} agente(s) re-apuntados)`);
|
|
178
|
+
return toSummary(doc);
|
|
179
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Proveedores de modelos — la API, no la ruta.
|
|
3
|
+
*
|
|
4
|
+
* Dos cosas que una UI necesita y que en hive viven inline en
|
|
5
|
+
* `gateway/routes/providers.ts`:
|
|
6
|
+
*
|
|
7
|
+
* 1. **La cascada.** Activar o desactivar un proveedor arrastra a todos sus
|
|
8
|
+
* modelos. Sin eso quedan modelos "activos" de un proveedor apagado, que el
|
|
9
|
+
* selector ofrece y fallan al llamarse.
|
|
10
|
+
* 2. **La API key nunca se devuelve.** Se guarda cifrada
|
|
11
|
+
* (`storage/crypto.ts`) y hacia afuera sólo salen `hasApiKey` y una versión
|
|
12
|
+
* enmascarada. Una UI necesita mostrar "hay clave configurada" sin poder
|
|
13
|
+
* leerla.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { col, updateManyByIndex } from "../storage/hive.ts";
|
|
17
|
+
import type { ProviderDoc, ModelDoc } from "../storage/collections.ts";
|
|
18
|
+
import {
|
|
19
|
+
storeProviderApiKey, loadProviderApiKey, maskApiKey, deleteProviderSecrets, storeProviderHeaders,
|
|
20
|
+
} from "../storage/crypto.ts";
|
|
21
|
+
import { logger } from "../utils/logger.ts";
|
|
22
|
+
|
|
23
|
+
const log = logger.child("services/providers");
|
|
24
|
+
|
|
25
|
+
export interface ProviderSummary {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
baseUrl: string | null;
|
|
29
|
+
category: ProviderDoc["category"];
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
active: boolean;
|
|
32
|
+
/** Nunca la clave: sólo si existe y su versión enmascarada. */
|
|
33
|
+
hasApiKey: boolean;
|
|
34
|
+
maskedApiKey: string | null;
|
|
35
|
+
numCtx: number | null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function providersCol() {
|
|
39
|
+
return col<ProviderDoc>("providers");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function toSummary(doc: ProviderDoc): Promise<ProviderSummary> {
|
|
43
|
+
const key = await loadProviderApiKey(doc.id).catch(() => "");
|
|
44
|
+
return {
|
|
45
|
+
id: doc.id,
|
|
46
|
+
name: doc.name,
|
|
47
|
+
baseUrl: doc.base_url,
|
|
48
|
+
category: doc.category,
|
|
49
|
+
enabled: doc.enabled,
|
|
50
|
+
active: doc.active,
|
|
51
|
+
hasApiKey: !!key,
|
|
52
|
+
maskedApiKey: key ? maskApiKey(key) : null,
|
|
53
|
+
numCtx: doc.num_ctx,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function listProviders(opts?: { category?: ProviderDoc["category"]; includeInactive?: boolean }): Promise<ProviderSummary[]> {
|
|
58
|
+
const rows = await (await providersCol()).scan({});
|
|
59
|
+
const docs = rows
|
|
60
|
+
.map((e) => e.doc)
|
|
61
|
+
.filter((d) => (opts?.category ? d.category === opts.category : true))
|
|
62
|
+
.filter((d) => (opts?.includeInactive ? true : d.active));
|
|
63
|
+
return Promise.all(docs.map(toSummary));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function getProvider(id: string): Promise<ProviderSummary | null> {
|
|
67
|
+
const entry = await (await providersCol()).get(id);
|
|
68
|
+
return entry ? toSummary(entry.doc) : null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function createProvider(input: {
|
|
72
|
+
id: string;
|
|
73
|
+
name: string;
|
|
74
|
+
baseUrl?: string | null;
|
|
75
|
+
category?: ProviderDoc["category"];
|
|
76
|
+
apiKey?: string;
|
|
77
|
+
numCtx?: number | null;
|
|
78
|
+
}): Promise<ProviderSummary> {
|
|
79
|
+
if (!input.id?.trim()) throw new Error("El proveedor necesita un id");
|
|
80
|
+
|
|
81
|
+
const c = await providersCol();
|
|
82
|
+
if (await c.get(input.id)) throw new Error(`Ya existe el proveedor "${input.id}"`);
|
|
83
|
+
|
|
84
|
+
const doc: ProviderDoc = {
|
|
85
|
+
id: input.id,
|
|
86
|
+
name: input.name,
|
|
87
|
+
base_url: input.baseUrl ?? null,
|
|
88
|
+
category: input.category ?? "llm",
|
|
89
|
+
num_ctx: input.numCtx ?? null,
|
|
90
|
+
num_gpu: 0,
|
|
91
|
+
enabled: true,
|
|
92
|
+
active: false,
|
|
93
|
+
created_at: Date.now(),
|
|
94
|
+
};
|
|
95
|
+
await c.put(input.id, doc, { expectedVersion: 0 });
|
|
96
|
+
if (input.apiKey) await storeProviderApiKey(input.id, input.apiKey);
|
|
97
|
+
return toSummary(doc);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function updateProvider(
|
|
101
|
+
id: string,
|
|
102
|
+
changes: { name?: string; baseUrl?: string | null; apiKey?: string; headers?: Record<string, unknown>; numCtx?: number | null },
|
|
103
|
+
): Promise<ProviderSummary> {
|
|
104
|
+
const c = await providersCol();
|
|
105
|
+
const entry = await c.get(id);
|
|
106
|
+
if (!entry) throw new Error(`No existe el proveedor "${id}"`);
|
|
107
|
+
|
|
108
|
+
const doc: ProviderDoc = { ...entry.doc };
|
|
109
|
+
if (changes.name !== undefined) doc.name = changes.name;
|
|
110
|
+
if (changes.baseUrl !== undefined) doc.base_url = changes.baseUrl;
|
|
111
|
+
if (changes.numCtx !== undefined) doc.num_ctx = changes.numCtx;
|
|
112
|
+
await c.put(id, doc, { expectedVersion: entry.version });
|
|
113
|
+
|
|
114
|
+
if (changes.apiKey !== undefined) await storeProviderApiKey(id, changes.apiKey);
|
|
115
|
+
if (changes.headers !== undefined) await storeProviderHeaders(id, changes.headers);
|
|
116
|
+
return toSummary(doc);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Activa o desactiva el proveedor **y todos sus modelos**.
|
|
121
|
+
*
|
|
122
|
+
* La cascada no es una comodidad: un modelo activo de un proveedor apagado
|
|
123
|
+
* aparece en el selector y falla al llamarse, porque no hay credencial ni
|
|
124
|
+
* endpoint que lo atienda.
|
|
125
|
+
*/
|
|
126
|
+
export async function toggleProvider(id: string, active: boolean): Promise<ProviderSummary> {
|
|
127
|
+
const c = await providersCol();
|
|
128
|
+
const entry = await c.get(id);
|
|
129
|
+
if (!entry) throw new Error(`No existe el proveedor "${id}"`);
|
|
130
|
+
|
|
131
|
+
const doc: ProviderDoc = { ...entry.doc, active, enabled: active ? true : entry.doc.enabled };
|
|
132
|
+
await c.put(id, doc, { expectedVersion: entry.version });
|
|
133
|
+
|
|
134
|
+
const n = await updateManyByIndex<ModelDoc>("models", "provider_id", id, { active });
|
|
135
|
+
log.info(`proveedor ${id} ${active ? "activado" : "desactivado"} — ${n} modelo(s) en cascada`);
|
|
136
|
+
return toSummary(doc);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Borra el proveedor y sus secretos. Falla si algún modelo suyo sigue en uso. */
|
|
140
|
+
export async function deleteProvider(id: string): Promise<boolean> {
|
|
141
|
+
const c = await providersCol();
|
|
142
|
+
if (!(await c.get(id))) return false;
|
|
143
|
+
|
|
144
|
+
const modelos = await (await col<ModelDoc>("models")).findBy("provider_id", id);
|
|
145
|
+
if (modelos.length > 0) {
|
|
146
|
+
throw new Error(`El proveedor "${id}" todavía tiene ${modelos.length} modelo(s); bórralos primero`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
await c.delete(id);
|
|
150
|
+
await deleteProviderSecrets(id).catch(() => {});
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuración inicial de la colmena — elegir qué agentes se siembran.
|
|
3
|
+
*
|
|
4
|
+
* `seedAllData()` es todo o nada: siembra las 8 personas del catálogo en cada
|
|
5
|
+
* arranque, con sus tools y skills, quiera el usuario o no. Para un producto
|
|
6
|
+
* donde cada quien arma su enjambre eso es demasiado: alguien que sólo quiere un
|
|
7
|
+
* investigador web se lleva el ingeniero de software y el operador de navegador.
|
|
8
|
+
*
|
|
9
|
+
* Este módulo permite elegir. Y lo que hace no es "sembrar sólo lo de este
|
|
10
|
+
* agente", que es el error obvio: **las tools se comparten**. `web_fetch` lo
|
|
11
|
+
* declaran el investigador web y el operador de navegador; `fs_*`, el operador
|
|
12
|
+
* de archivos y el ingeniero. Se siembra la **unión** de lo que requieren los
|
|
13
|
+
* agentes elegidos, más las `MINIMAL_TOOLS` que el coordinador necesita siempre.
|
|
14
|
+
*
|
|
15
|
+
* Y al revés: **desactivar un agente no borra nada**. Las filas de `tools` y
|
|
16
|
+
* `skills` son globales y compartidas; borrar las de uno rompería a otro. Se
|
|
17
|
+
* marca el agente y se recalcula la unión.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { col } from "../storage/hive.ts";
|
|
21
|
+
import type { AgentDoc, ToolDoc, SkillDoc } from "../storage/collections.ts";
|
|
22
|
+
import {
|
|
23
|
+
CATALOG_AGENT_IDS, createSeedCatalogAgents, requiredCapabilitiesFor, listCatalogPersonas,
|
|
24
|
+
} from "../agent/agent-catalog.ts";
|
|
25
|
+
import { MINIMAL_TOOLS } from "../agent/minimal-loadout.ts";
|
|
26
|
+
import { expandToolAllowlist } from "../agent/delegation-runtime.ts";
|
|
27
|
+
import { syncToolCatalogToIndex } from "../agent/tool-selector.ts";
|
|
28
|
+
import { syncSkillsToIndex } from "../agent/skill-selector.ts";
|
|
29
|
+
import { logger } from "../utils/logger.ts";
|
|
30
|
+
|
|
31
|
+
const log = logger.child("services/setup");
|
|
32
|
+
|
|
33
|
+
export { listCatalogPersonas, CATALOG_AGENT_IDS };
|
|
34
|
+
|
|
35
|
+
export interface SeedPlan {
|
|
36
|
+
/** Agentes de catálogo que quedarán activos. */
|
|
37
|
+
agents: string[];
|
|
38
|
+
/** Tools que hacen falta: unión de las de los agentes + las mínimas. */
|
|
39
|
+
tools: string[];
|
|
40
|
+
/** Skills que hacen falta: unión de las de los agentes. */
|
|
41
|
+
skills: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Calcula qué haría falta sembrar para un conjunto de agentes, sin tocar nada.
|
|
46
|
+
*
|
|
47
|
+
* Existe separado de `applySeedPlan` para que una UI pueda mostrar "esto es lo
|
|
48
|
+
* que se va a instalar" antes de que el usuario confirme.
|
|
49
|
+
*/
|
|
50
|
+
export function planSeedFor(agentIds: string[]): SeedPlan {
|
|
51
|
+
const { toolPatterns, skills } = requiredCapabilitiesFor(agentIds);
|
|
52
|
+
|
|
53
|
+
// Los patrones se expanden contra el registro vivo: `fs_*` no es una tool,
|
|
54
|
+
// son varias, y hay que sembrarlas todas.
|
|
55
|
+
const delCatalogo = expandToolAllowlist(toolPatterns);
|
|
56
|
+
|
|
57
|
+
// Las mínimas van siempre: son la competencia del coordinador —delegar,
|
|
58
|
+
// buscar, avisar— y sin ellas no hay colmena, haya los agentes que haya.
|
|
59
|
+
const tools = [...new Set([...MINIMAL_TOOLS, ...delCatalogo])];
|
|
60
|
+
|
|
61
|
+
return { agents: [...new Set(agentIds)], tools, skills };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Deja activo exactamente el conjunto de agentes pedido.
|
|
66
|
+
*
|
|
67
|
+
* Las tools y skills que no hagan falta se **desactivan**, nunca se borran: otro
|
|
68
|
+
* agente —o uno que el usuario active mañana— puede necesitarlas, y una fila
|
|
69
|
+
* borrada habría que volver a sembrarla desde el código.
|
|
70
|
+
*/
|
|
71
|
+
export async function applySeedPlan(agentIds: string[]): Promise<SeedPlan> {
|
|
72
|
+
const plan = planSeedFor(agentIds);
|
|
73
|
+
const elegidos = new Set(plan.agents);
|
|
74
|
+
const necesarias = new Set(plan.tools);
|
|
75
|
+
const skillsNecesarias = new Set(plan.skills);
|
|
76
|
+
|
|
77
|
+
// ── Agentes ────────────────────────────────────────────────────────────────
|
|
78
|
+
const agentsCol = await col<AgentDoc>("agents");
|
|
79
|
+
const semillas = createSeedCatalogAgents();
|
|
80
|
+
for (const semilla of semillas) {
|
|
81
|
+
const quiere = elegidos.has(semilla.id);
|
|
82
|
+
const existente = await agentsCol.get(semilla.id);
|
|
83
|
+
|
|
84
|
+
if (!existente) {
|
|
85
|
+
// Los no elegidos no se crean: sembrarlos apagados llenaría la lista de
|
|
86
|
+
// agentes que el usuario nunca pidió.
|
|
87
|
+
if (!quiere) continue;
|
|
88
|
+
await agentsCol.put(semilla.id, semilla, { expectedVersion: 0 });
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (existente.doc.enabled !== quiere) {
|
|
92
|
+
await agentsCol.put(semilla.id, { ...existente.doc, enabled: quiere, updated_at: Date.now() },
|
|
93
|
+
{ expectedVersion: existente.version });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// ── Tools ──────────────────────────────────────────────────────────────────
|
|
98
|
+
const toolsCol = await col<ToolDoc>("tools");
|
|
99
|
+
let encendidas = 0, apagadas = 0;
|
|
100
|
+
for (const entry of await toolsCol.scan({})) {
|
|
101
|
+
const debeEstar = necesarias.has(entry.doc.name);
|
|
102
|
+
if (entry.doc.active === debeEstar) continue;
|
|
103
|
+
await toolsCol.put(entry.id, { ...entry.doc, active: debeEstar, updated_at: Date.now() },
|
|
104
|
+
{ expectedVersion: entry.version });
|
|
105
|
+
debeEstar ? encendidas++ : apagadas++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Skills ─────────────────────────────────────────────────────────────────
|
|
109
|
+
const skillsCol = await col<SkillDoc>("skills");
|
|
110
|
+
for (const entry of await skillsCol.scan({})) {
|
|
111
|
+
const debeEstar = skillsNecesarias.has(entry.doc.id) || skillsNecesarias.has(entry.doc.name);
|
|
112
|
+
if (entry.doc.active === debeEstar) continue;
|
|
113
|
+
await skillsCol.put(entry.id, { ...entry.doc, active: debeEstar, updated_at: Date.now() },
|
|
114
|
+
{ expectedVersion: entry.version });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
await syncToolCatalogToIndex().catch((e) => log.warn(`no pude reindexar tools: ${(e as Error).message}`));
|
|
118
|
+
await syncSkillsToIndex().catch((e) => log.warn(`no pude reindexar skills: ${(e as Error).message}`));
|
|
119
|
+
|
|
120
|
+
log.info(
|
|
121
|
+
`colmena configurada: ${plan.agents.length} agente(s), ` +
|
|
122
|
+
`${plan.tools.length} tool(s) activas (+${encendidas}/-${apagadas})`,
|
|
123
|
+
);
|
|
124
|
+
return plan;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Activa varios agentes del catálogo de una vez y siembra lo que les falte.
|
|
129
|
+
*
|
|
130
|
+
* Recalcula la unión con los que ya estaban: activar uno nunca puede apagar las
|
|
131
|
+
* tools de otro. Existe en plural porque activarlos de a uno recalcularía y
|
|
132
|
+
* reescribiría el catálogo entero una vez por agente — al armar un enjambre de
|
|
133
|
+
* cinco especialistas, cinco pasadas sobre todas las tools y skills.
|
|
134
|
+
*/
|
|
135
|
+
export async function enableCatalogAgents(agentIds: string[]): Promise<SeedPlan> {
|
|
136
|
+
const activos = await listEnabledCatalogAgents();
|
|
137
|
+
return applySeedPlan([...new Set([...activos, ...agentIds])]);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Activa un agente del catálogo y siembra lo que le falte.
|
|
142
|
+
*/
|
|
143
|
+
export async function enableCatalogAgent(agentId: string): Promise<SeedPlan> {
|
|
144
|
+
return enableCatalogAgents([agentId]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Lo que falta activar para que un conjunto de agentes pueda trabajar. */
|
|
148
|
+
export interface ActivationGap {
|
|
149
|
+
/** Agentes de catálogo pedidos que hoy están apagados. */
|
|
150
|
+
agents: string[];
|
|
151
|
+
/** Tools que necesitan y hoy están inactivas. */
|
|
152
|
+
tools: string[];
|
|
153
|
+
/** Skills que necesitan y hoy están inactivas. */
|
|
154
|
+
skills: string[];
|
|
155
|
+
/** Miembros que no son del catálogo: traen sus propias tools, no se siembran. */
|
|
156
|
+
nonCatalog: string[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Qué habría que encender para estos agentes, **sin encender nada**.
|
|
161
|
+
*
|
|
162
|
+
* Es el `planSeedFor` de un cambio concreto: devuelve el faltante contra lo que
|
|
163
|
+
* ya está activo, no el conjunto entero. Una UI lo usa para mostrar "esto se va
|
|
164
|
+
* a activar" antes de que el usuario confirme, que es la diferencia entre que
|
|
165
|
+
* el usuario decida y que el sistema decida por él.
|
|
166
|
+
*/
|
|
167
|
+
export async function planActivationFor(agentIds: string[]): Promise<ActivationGap> {
|
|
168
|
+
const catalogo = new Set<string>(CATALOG_AGENT_IDS);
|
|
169
|
+
const delCatalogo = [...new Set(agentIds.filter((id) => catalogo.has(id)))];
|
|
170
|
+
const nonCatalog = [...new Set(agentIds.filter((id) => !catalogo.has(id)))];
|
|
171
|
+
|
|
172
|
+
const activos = await listEnabledCatalogAgents();
|
|
173
|
+
const apagados = delCatalogo.filter((id) => !activos.includes(id));
|
|
174
|
+
|
|
175
|
+
if (apagados.length === 0) {
|
|
176
|
+
return { agents: [], tools: [], skills: [], nonCatalog };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// El faltante se calcula contra la unión final —los que ya estaban más los
|
|
180
|
+
// nuevos—, no sólo contra los nuevos: una tool que ya está activa porque otro
|
|
181
|
+
// agente la usa no es algo que "se vaya a activar", y listarla haría que la
|
|
182
|
+
// UI le prometa al usuario cambios que no van a ocurrir.
|
|
183
|
+
const plan = planSeedFor([...new Set([...activos, ...delCatalogo])]);
|
|
184
|
+
|
|
185
|
+
const toolsCol = await col<ToolDoc>("tools");
|
|
186
|
+
const tools: string[] = [];
|
|
187
|
+
for (const nombre of plan.tools) {
|
|
188
|
+
const entry = await toolsCol.get(nombre);
|
|
189
|
+
if (!entry?.doc.active) tools.push(nombre);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const skillsCol = await col<SkillDoc>("skills");
|
|
193
|
+
const activas = new Set<string>();
|
|
194
|
+
for (const entry of await skillsCol.scan({})) {
|
|
195
|
+
if (entry.doc.active) { activas.add(entry.doc.id); activas.add(entry.doc.name); }
|
|
196
|
+
}
|
|
197
|
+
const skills = plan.skills.filter((s) => !activas.has(s));
|
|
198
|
+
|
|
199
|
+
return { agents: apagados, tools, skills, nonCatalog };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Desactiva un agente del catálogo.
|
|
204
|
+
*
|
|
205
|
+
* Las tools que compartía con otros siguen activas — es la razón de recalcular
|
|
206
|
+
* la unión en vez de quitar las suyas.
|
|
207
|
+
*/
|
|
208
|
+
export async function disableCatalogAgent(agentId: string): Promise<SeedPlan> {
|
|
209
|
+
const activos = await listEnabledCatalogAgents();
|
|
210
|
+
return applySeedPlan(activos.filter((id) => id !== agentId));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Los agentes de catálogo actualmente habilitados. */
|
|
214
|
+
export async function listEnabledCatalogAgents(): Promise<string[]> {
|
|
215
|
+
const c = await col<AgentDoc>("agents");
|
|
216
|
+
const ids: string[] = [];
|
|
217
|
+
for (const id of CATALOG_AGENT_IDS) {
|
|
218
|
+
const entry = await c.get(id);
|
|
219
|
+
if (entry?.doc.enabled) ids.push(id);
|
|
220
|
+
}
|
|
221
|
+
return ids;
|
|
222
|
+
}
|