@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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver quién habla y en qué hilo, cuando el mensaje llega por un canal.
|
|
3
|
+
*
|
|
4
|
+
* Un mensaje de Telegram trae un id de Telegram, no un usuario de la colmena.
|
|
5
|
+
* Esto traduce: busca la identidad en `userIdentities` y devuelve el usuario, el
|
|
6
|
+
* hilo y el agente que deben atenderlo — creando el hilo si hace falta.
|
|
7
|
+
*
|
|
8
|
+
* Ojo con el comportamiento de auto-vinculación: si la identidad no existe, se
|
|
9
|
+
* asocia al **único usuario existente**. Es coherente con hive, que es
|
|
10
|
+
* mono-usuario, pero en un despliegue con varios significa que el primer
|
|
11
|
+
* desconocido que escriba por un canal quedaría vinculado a quien estuviera.
|
|
12
|
+
* Para eso está `security/pairing.ts`, que exige aprobación antes de crear la
|
|
13
|
+
* identidad: un host multi-usuario debe usarlo delante de esto.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { col } from "../storage/hive.ts"
|
|
17
|
+
import type { UserIdentityDoc, UserDoc, AgentDoc } from "../storage/collections.ts"
|
|
18
|
+
import { ensureThread, mostRecentWebThread, createWebConversation } from "../agent/thread-store.ts"
|
|
19
|
+
|
|
20
|
+
export interface ResolveContextResult {
|
|
21
|
+
userId: string
|
|
22
|
+
threadId: string
|
|
23
|
+
agentId: string
|
|
24
|
+
isNewUser: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ResolveContextOptions {
|
|
28
|
+
channel: string
|
|
29
|
+
channelUserId: string
|
|
30
|
+
/** Channel account the message arrived on — persisted so replies can be routed back to it. */
|
|
31
|
+
accountId?: string
|
|
32
|
+
/**
|
|
33
|
+
* Conversación concreta dentro del canal: el contacto o grupo en mensajería, el id
|
|
34
|
+
* de conversación en la web. Si falta, en los canales se usa `channelUserId` y en
|
|
35
|
+
* la web la conversación más reciente (o una nueva, si no hay ninguna).
|
|
36
|
+
*/
|
|
37
|
+
peerId?: string
|
|
38
|
+
peerKind?: "direct" | "group"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function resolveContext(options: ResolveContextOptions): Promise<ResolveContextResult> {
|
|
42
|
+
const { channel, channelUserId, accountId } = options
|
|
43
|
+
|
|
44
|
+
const identitiesCol = await col<UserIdentityDoc>("userIdentities")
|
|
45
|
+
const usersCol = await col<UserDoc>("users")
|
|
46
|
+
const agentsCol = await col<AgentDoc>("agents")
|
|
47
|
+
|
|
48
|
+
const allIdentities = await identitiesCol.scan({})
|
|
49
|
+
const identity = allIdentities.find(e => e.doc.channel === channel && e.doc.channel_user_id === channelUserId)
|
|
50
|
+
|
|
51
|
+
let userId: string
|
|
52
|
+
let isNewUser = false
|
|
53
|
+
|
|
54
|
+
if (identity) {
|
|
55
|
+
userId = identity.doc.user_id
|
|
56
|
+
// Backfill/refresh the owning account so replies survive a restart, when
|
|
57
|
+
// the in-memory session→account map in ChannelManager is empty.
|
|
58
|
+
if (accountId && identity.doc.account_id !== accountId) {
|
|
59
|
+
await identitiesCol.put(identity.id, { ...identity.doc, account_id: accountId })
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
// Sistema mono-usuario: reutilizar el usuario del onboarding
|
|
63
|
+
const allUsers = await usersCol.scan({})
|
|
64
|
+
const existingUser = [...allUsers].sort((a, b) => a.doc.created_at - b.doc.created_at)[0]
|
|
65
|
+
|
|
66
|
+
if (!existingUser) {
|
|
67
|
+
throw new Error("No user found in database. Please run the onboarding process first.")
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
userId = existingUser.id
|
|
71
|
+
|
|
72
|
+
// Vincular este canal al usuario existente (auto-link en el primer mensaje)
|
|
73
|
+
// put(): si ya existe una fila (user_id, channel), actualiza channel_user_id
|
|
74
|
+
// con el valor real del canal (e.g. chat ID numérico de Telegram).
|
|
75
|
+
await identitiesCol.put(`${userId}:${channel}`, {
|
|
76
|
+
user_id: userId, channel, channel_user_id: channelUserId,
|
|
77
|
+
account_id: accountId, linked_at: Date.now(),
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const coordinators = await agentsCol.findBy("role", "coordinator", { limit: 1 })
|
|
82
|
+
const agentId = coordinators[0]?.id || "bee"
|
|
83
|
+
|
|
84
|
+
// Un hilo por canal Y por contacto (`${user}/${canal}/${peer}` — ver
|
|
85
|
+
// agent/thread-id.ts). Antes todos los canales compartían un único hilo
|
|
86
|
+
// (`threadId = userId`), así que lo hablado por Telegram entraba en el mismo
|
|
87
|
+
// contexto que la web y un grupo de WhatsApp escribía en el chat privado del
|
|
88
|
+
// dueño. Los session IDs de transporte siguen enrutando las respuestas.
|
|
89
|
+
const threadId = options.peerId
|
|
90
|
+
? await ensureThread({
|
|
91
|
+
userId,
|
|
92
|
+
channel,
|
|
93
|
+
peerId: options.peerId,
|
|
94
|
+
peerKind: options.peerKind,
|
|
95
|
+
})
|
|
96
|
+
: channel === "webchat"
|
|
97
|
+
// Sin conversación indicada, la web sigue donde se quedó: la más reciente
|
|
98
|
+
// —que puede ser el hilo previo a la separación por canal— o una nueva.
|
|
99
|
+
? ((await mostRecentWebThread(userId))?.id ?? (await createWebConversation(userId)).id)
|
|
100
|
+
: await ensureThread({
|
|
101
|
+
userId,
|
|
102
|
+
channel,
|
|
103
|
+
peerId: channelUserId,
|
|
104
|
+
peerKind: options.peerKind,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return { userId, threadId, agentId, isNewUser }
|
|
108
|
+
}
|
|
@@ -134,7 +134,14 @@ export interface Skill {
|
|
|
134
134
|
examples?: SkillExample[];
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Separa el frontmatter YAML del cuerpo markdown de un `SKILL.md`.
|
|
139
|
+
*
|
|
140
|
+
* Exportada porque `services/skills.ts` importa skills del disco a la BD y
|
|
141
|
+
* necesita exactamente este parseo: duplicarlo garantizaría que un día
|
|
142
|
+
* acepten formatos distintos.
|
|
143
|
+
*/
|
|
144
|
+
export function parseFrontmatter(content: string): { frontmatter: Record<string, unknown>; body: string } {
|
|
138
145
|
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
139
146
|
|
|
140
147
|
if (!match) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: artifact_reader
|
|
3
|
+
description: "Leer archivos grandes que llegaron como artifact_ref: por tramos o buscando dentro, sin volcarlos enteros al contexto."
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: Hive Team
|
|
6
|
+
icon: "📎"
|
|
7
|
+
category: artifacts
|
|
8
|
+
permissions:
|
|
9
|
+
- artifact_read
|
|
10
|
+
dependencies: []
|
|
11
|
+
tools: [artifact_read, artifact_inspect]
|
|
12
|
+
|
|
13
|
+
# Structured skill fields
|
|
14
|
+
triggers:
|
|
15
|
+
- "leé el archivo adjunto"
|
|
16
|
+
- "read the attachment"
|
|
17
|
+
- "qué dice el documento"
|
|
18
|
+
- "what does the document say"
|
|
19
|
+
- "buscá en el archivo"
|
|
20
|
+
- "search in the file"
|
|
21
|
+
- "artifact_ref"
|
|
22
|
+
- "el resultado quedó truncado"
|
|
23
|
+
- "the result was truncated"
|
|
24
|
+
- "seguí leyendo"
|
|
25
|
+
- "keep reading"
|
|
26
|
+
|
|
27
|
+
preferred_agents: []
|
|
28
|
+
|
|
29
|
+
steps:
|
|
30
|
+
- step: 1
|
|
31
|
+
action: artifact_inspect
|
|
32
|
+
instruction: "Ver tamaño y tipo antes de leer. Un artefacto de 2 MB no se lee entero: se busca dentro"
|
|
33
|
+
params:
|
|
34
|
+
artifactId: "id del artifact_ref"
|
|
35
|
+
output: metadatos
|
|
36
|
+
|
|
37
|
+
- step: 2
|
|
38
|
+
action: artifact_read
|
|
39
|
+
instruction: "Si se busca algo puntual, usar `search` — devuelve extractos alrededor de cada coincidencia y cuesta una fracción de paginar. Si hace falta el texto seguido, usar offset/limit"
|
|
40
|
+
params:
|
|
41
|
+
artifactId: "id del artefacto"
|
|
42
|
+
search: "término a buscar (opcional)"
|
|
43
|
+
offset: "desde qué carácter (opcional)"
|
|
44
|
+
limit: "cuántos caracteres (opcional)"
|
|
45
|
+
output: contenido
|
|
46
|
+
|
|
47
|
+
- step: 3
|
|
48
|
+
action: synthesize
|
|
49
|
+
instruction: "Responder con lo encontrado, citando de dónde salió"
|
|
50
|
+
output: respuesta
|
|
51
|
+
|
|
52
|
+
rules:
|
|
53
|
+
- "**Buscar antes que paginar.** `search` devuelve extractos de todas las coincidencias en una sola llamada; paginar un archivo grande con offset/limit gasta varios turnos y llena el contexto con texto que no se necesitaba."
|
|
54
|
+
- "`artifact_inspect` no devuelve contenido: sirve para decidir cómo leer sin gastar contexto en averiguarlo."
|
|
55
|
+
- "Para continuar una lectura, usar el `next_offset` que devolvió la llamada anterior. No adivinar la posición."
|
|
56
|
+
- "Un `artifact_ref` aparece cuando un resultado fue demasiado grande para el contexto. No es un error: es el archivo esperando a que lo leas por partes."
|
|
57
|
+
- "Los artefactos de imagen no se leen con `artifact_read` — para eso están `image_metadata` e `image_transform`."
|
|
58
|
+
|
|
59
|
+
output_format:
|
|
60
|
+
structure: markdown
|
|
61
|
+
sections:
|
|
62
|
+
- "lo encontrado"
|
|
63
|
+
- "de qué parte del archivo salió"
|
|
64
|
+
max_length: "Sólo lo relevante, nunca el archivo entero"
|
|
65
|
+
|
|
66
|
+
examples:
|
|
67
|
+
- user_input: "buscá 'error de conexión' en el log adjunto"
|
|
68
|
+
expected_behavior: "artifact_read con search='error de conexión' — una llamada, no paginar"
|
|
69
|
+
|
|
70
|
+
- user_input: "qué dice el documento"
|
|
71
|
+
expected_behavior: "artifact_inspect para ver el tamaño → artifact_read del primer tramo → resumir"
|
|
72
|
+
|
|
73
|
+
- user_input: "seguí leyendo"
|
|
74
|
+
expected_behavior: "artifact_read con el next_offset de la llamada anterior"
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
# Artifact Reader Skill
|
|
78
|
+
|
|
79
|
+
## Cuándo se Activa
|
|
80
|
+
|
|
81
|
+
Cuando aparece un **`artifact_ref`**: un archivo, un adjunto o el resultado de
|
|
82
|
+
una tool que era demasiado grande para entrar en el contexto.
|
|
83
|
+
|
|
84
|
+
## Herramientas Disponibles
|
|
85
|
+
|
|
86
|
+
| Tool | Qué hace | Cuándo usarla |
|
|
87
|
+
|------|----------|---------------|
|
|
88
|
+
| `artifact_inspect` | Tamaño, tipo MIME, integridad | Antes de leer, para decidir cómo |
|
|
89
|
+
| `artifact_read` | Contenido, por tramos o buscando | Para leer de verdad |
|
|
90
|
+
|
|
91
|
+
## Lo Que Hay Que Entender
|
|
92
|
+
|
|
93
|
+
**Un `artifact_ref` no es un error.** Es el mecanismo por el que un archivo
|
|
94
|
+
grande queda fuera de la ventana de contexto y a la vez disponible. El archivo
|
|
95
|
+
está entero; lo que cambia es que se lee a pedido en vez de entrar completo en
|
|
96
|
+
cada turno de la conversación.
|
|
97
|
+
|
|
98
|
+
**Buscar cuesta mucho menos que paginar.** `artifact_read` con `search` recorre
|
|
99
|
+
el archivo del lado del servidor y devuelve extractos de cada coincidencia con
|
|
100
|
+
su contexto alrededor. Paginar el mismo archivo con `offset`/`limit` gasta un
|
|
101
|
+
turno por tramo y mete en el contexto un montón de texto que no hacía falta.
|
|
102
|
+
Cuando se sabe qué se busca, se busca.
|
|
103
|
+
|
|
104
|
+
**Continuar es explícito.** Cada lectura devuelve `next_offset`. Ese es el valor
|
|
105
|
+
que se pasa para seguir — no se calcula a mano.
|
|
@@ -54,8 +54,8 @@ steps:
|
|
|
54
54
|
cron_expression: "Cron expression for recurring (e.g., '0 9 * * *')"
|
|
55
55
|
fire_at: "ISO datetime for one_shot (e.g., '2026-04-20T09:00:00')"
|
|
56
56
|
channel: "Notification channel (telegram, discord, webchat)"
|
|
57
|
-
start_at: "Optional ISO datetime - start of execution window
|
|
58
|
-
stop_at: "Optional ISO datetime - end of execution window
|
|
57
|
+
start_at: "Optional ISO datetime - start of execution window"
|
|
58
|
+
stop_at: "Optional ISO datetime - end of execution window"
|
|
59
59
|
dom_and_dow: "0 = OR logic (default), 1 = AND logic for day-of-month + day-of-week"
|
|
60
60
|
max_runs: "Optional max executions"
|
|
61
61
|
output: cron_id
|
|
@@ -133,22 +133,32 @@ Para gestionar tareas programadas (cron jobs): crear, listar, actualizar, pausar
|
|
|
133
133
|
| `cron_expression` | string | Expresión cron (solo para recurring) |
|
|
134
134
|
| `fire_at` | string | Datetime ISO (solo para one_shot) |
|
|
135
135
|
| `channel` | string | Canal de notificación |
|
|
136
|
-
| `start_at` | string | Inicio de ventana opcional
|
|
137
|
-
| `stop_at` | string | Fin de ventana opcional
|
|
136
|
+
| `start_at` | string | Inicio de ventana opcional |
|
|
137
|
+
| `stop_at` | string | Fin de ventana opcional |
|
|
138
138
|
| `dom_and_dow` | number | 0=OR (default), 1=AND (día mes + día semana) |
|
|
139
|
+
| `max_runs` | number | Deja de correr después de N corridas ("recordámelo 3 veces") |
|
|
140
|
+
| `payload` | object | Datos que recibe el agente al ejecutarse |
|
|
141
|
+
| `agent_id` | string | Agente concreto que debe ejecutarla. Si se omite, decide el coordinador |
|
|
142
|
+
| `tool_name` | string | Ejecutar una tool directamente, sin pasar por un agente |
|
|
143
|
+
|
|
144
|
+
> **La zona horaria no se pasa acá**: sale del perfil del usuario. No la
|
|
145
|
+
> inventes ni la pidas — si el usuario dice "a las 9", son las 9 de su reloj.
|
|
139
146
|
|
|
140
147
|
## Cron Expression Format
|
|
141
148
|
|
|
142
149
|
```
|
|
143
|
-
|
|
144
|
-
│
|
|
145
|
-
│ │
|
|
146
|
-
│ │ │
|
|
147
|
-
│ │
|
|
148
|
-
│
|
|
149
|
-
|
|
150
|
+
┌───────── segundos (0-59) ← opcional, sólo si hacen falta
|
|
151
|
+
│ ┌─────── minuto (0-59)
|
|
152
|
+
│ │ ┌───── hora (0-23)
|
|
153
|
+
│ │ │ ┌─── día del mes (1-31)
|
|
154
|
+
│ │ │ │ ┌─ mes (1-12 o JAN-DEC)
|
|
155
|
+
│ │ │ │ │ ┌ día de semana (0-7 o SUN-SAT, 0 y 7 = domingo)
|
|
156
|
+
* * * * * *
|
|
150
157
|
```
|
|
151
158
|
|
|
159
|
+
Cinco campos, o seis poniendo los segundos adelante. Acepta `*`, listas `1,15`,
|
|
160
|
+
rangos `1-5`, pasos `*/2`, y nombres de mes y de día.
|
|
161
|
+
|
|
152
162
|
## Ejemplos Comunes
|
|
153
163
|
|
|
154
164
|
| Expresión | Significado |
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: image_editor
|
|
3
|
+
description: "Convertir, redimensionar y rotar imágenes con Bun.Image. Inspeccionar dimensiones y formato sin cargar la imagen al contexto."
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
author: Hive Team
|
|
6
|
+
icon: "🖼️"
|
|
7
|
+
category: images
|
|
8
|
+
permissions:
|
|
9
|
+
- image_processing
|
|
10
|
+
dependencies: []
|
|
11
|
+
tools: [image_metadata, image_transform, artifact_inspect]
|
|
12
|
+
|
|
13
|
+
# Structured skill fields
|
|
14
|
+
triggers:
|
|
15
|
+
- "convertí la imagen"
|
|
16
|
+
- "convert image"
|
|
17
|
+
- "redimensioná la imagen"
|
|
18
|
+
- "resize image"
|
|
19
|
+
- "achicá la foto"
|
|
20
|
+
- "make it smaller"
|
|
21
|
+
- "pasala a webp"
|
|
22
|
+
- "convert to webp"
|
|
23
|
+
- "rotá la imagen"
|
|
24
|
+
- "rotate image"
|
|
25
|
+
- "qué tamaño tiene"
|
|
26
|
+
- "image dimensions"
|
|
27
|
+
- "comprimí la imagen"
|
|
28
|
+
- "compress image"
|
|
29
|
+
- "hacé una miniatura"
|
|
30
|
+
- "make a thumbnail"
|
|
31
|
+
|
|
32
|
+
preferred_agents: []
|
|
33
|
+
|
|
34
|
+
steps:
|
|
35
|
+
- step: 1
|
|
36
|
+
action: image_metadata
|
|
37
|
+
instruction: "Leer ancho, alto y formato ANTES de transformar. Sin esto no se puede decidir un tamaño con criterio, y redimensionar a ciegas agranda imágenes chicas"
|
|
38
|
+
params:
|
|
39
|
+
artifact_id: "id del artefacto con la imagen"
|
|
40
|
+
output: dimensiones_originales
|
|
41
|
+
|
|
42
|
+
- step: 2
|
|
43
|
+
action: image_transform
|
|
44
|
+
instruction: "Transformar. Dando sólo ancho O sólo alto se mantiene la proporción; dando los dos, la imagen se deforma"
|
|
45
|
+
params:
|
|
46
|
+
artifact_id: "id del artefacto de origen"
|
|
47
|
+
width: "ancho en píxeles (opcional)"
|
|
48
|
+
format: "jpeg | png | webp | avif | heic (opcional)"
|
|
49
|
+
quality: "1-100, sólo para formatos con pérdida (opcional)"
|
|
50
|
+
output: artefacto_nuevo
|
|
51
|
+
|
|
52
|
+
- step: 3
|
|
53
|
+
action: notify
|
|
54
|
+
instruction: "Avisar con el id del artefacto resultante y su tamaño"
|
|
55
|
+
output: aviso
|
|
56
|
+
|
|
57
|
+
rules:
|
|
58
|
+
- "Las imágenes se manejan por `artifact_id`, nunca por base64 en el mensaje: una imagen incrustada llena la ventana de contexto y se reenvía en cada turno."
|
|
59
|
+
- "`image_transform` NO modifica el original: crea un artefacto nuevo y devuelve su id. El original queda intacto."
|
|
60
|
+
- "Para conservar la proporción, dar sólo `width` o sólo `height`. Dar los dos deforma la imagen; hacelo únicamente si el usuario lo pidió."
|
|
61
|
+
- "`quality` sólo aplica a formatos con pérdida (jpeg, webp, avif). En png se ignora."
|
|
62
|
+
- "`rotate` acepta 90, 180 o 270. Otros valores se rechazan."
|
|
63
|
+
- "Si no sabés qué formato quiere el usuario, webp es la mejor opción por defecto: pesa menos que jpeg y png con calidad equivalente."
|
|
64
|
+
|
|
65
|
+
output_format:
|
|
66
|
+
structure: markdown
|
|
67
|
+
sections:
|
|
68
|
+
- "artifact_id resultante"
|
|
69
|
+
- "formato y dimensiones"
|
|
70
|
+
- "tamaño en bytes"
|
|
71
|
+
max_length: "Breve — la imagen no se incrusta en la respuesta"
|
|
72
|
+
|
|
73
|
+
examples:
|
|
74
|
+
- user_input: "convertí esta imagen a webp"
|
|
75
|
+
expected_behavior: "image_metadata para ver el formato → image_transform con format=webp → devolver el id nuevo"
|
|
76
|
+
|
|
77
|
+
- user_input: "hacela de 800 de ancho"
|
|
78
|
+
expected_behavior: "image_transform con width=800 y SIN height, para no deformarla"
|
|
79
|
+
|
|
80
|
+
- user_input: "hacé una miniatura"
|
|
81
|
+
expected_behavior: "image_transform con width≈200, format=webp, quality≈80"
|
|
82
|
+
|
|
83
|
+
- user_input: "qué tamaño tiene esta imagen"
|
|
84
|
+
expected_behavior: "image_metadata solo — no hace falta transformar nada"
|
|
85
|
+
---
|
|
86
|
+
|
|
87
|
+
# Image Editor Skill
|
|
88
|
+
|
|
89
|
+
## Cuándo se Activa
|
|
90
|
+
|
|
91
|
+
Cuando el usuario quiere **cambiar** una imagen (formato, tamaño, rotación) o
|
|
92
|
+
**saber** sus características. Corre sobre `Bun.Image`, nativo del runtime.
|
|
93
|
+
|
|
94
|
+
## Herramientas Disponibles
|
|
95
|
+
|
|
96
|
+
| Tool | Qué hace | Cuándo usarla |
|
|
97
|
+
|------|----------|---------------|
|
|
98
|
+
| `image_metadata` | Ancho, alto y formato | Siempre antes de transformar |
|
|
99
|
+
| `image_transform` | Convierte, redimensiona, rota | El trabajo en sí |
|
|
100
|
+
| `artifact_inspect` | Tipo MIME, integridad, tamaño | Cuando no está claro si el artefacto es una imagen |
|
|
101
|
+
|
|
102
|
+
## Lo Que Hay Que Entender
|
|
103
|
+
|
|
104
|
+
**Todo pasa por artefactos.** Una imagen no viaja en el mensaje: vive como
|
|
105
|
+
artefacto y se la nombra por su `artifact_id`. Es lo que evita que una foto de 4
|
|
106
|
+
MB entre a la ventana de contexto y se reenvíe en cada turno de la conversación.
|
|
107
|
+
|
|
108
|
+
**Transformar no destruye.** `image_transform` devuelve un artefacto **nuevo**.
|
|
109
|
+
El original sigue disponible, así que se puede probar un tamaño, ver que no
|
|
110
|
+
gustó y probar otro sin haber perdido nada.
|
|
111
|
+
|
|
112
|
+
**La proporción se pierde en silencio.** Si se pasan `width` y `height` juntos,
|
|
113
|
+
la imagen se estira sin avisar. Pasando uno solo, el otro se calcula.
|
|
114
|
+
|
|
115
|
+
## Formatos
|
|
116
|
+
|
|
117
|
+
`jpeg` · `png` · `webp` · `avif` · `heic`
|
|
118
|
+
|
|
119
|
+
Ante la duda, **webp**: pesa bastante menos que jpeg y png a calidad comparable,
|
|
120
|
+
y lo entienden todos los navegadores actuales.
|
|
@@ -8,7 +8,7 @@ category: web
|
|
|
8
8
|
permissions:
|
|
9
9
|
- browser_control
|
|
10
10
|
dependencies: []
|
|
11
|
-
tools: [browser_navigate, browser_click, browser_type, browser_screenshot]
|
|
11
|
+
tools: [browser_navigate, browser_wait, browser_click, browser_type, browser_screenshot, browser_script, computer_use_task]
|
|
12
12
|
|
|
13
13
|
# Structured skill fields
|
|
14
14
|
triggers:
|
|
@@ -38,13 +38,20 @@ steps:
|
|
|
38
38
|
output: page_loaded
|
|
39
39
|
|
|
40
40
|
- step: 2
|
|
41
|
+
action: browser_wait
|
|
42
|
+
instruction: "Wait for the element before acting on it. Clicking or typing before the element exists is the most common failure in web automation, and it fails silently"
|
|
43
|
+
params:
|
|
44
|
+
selector: "CSS selector of the element about to be used"
|
|
45
|
+
output: element_ready
|
|
46
|
+
|
|
47
|
+
- step: 3
|
|
41
48
|
action: browser_click
|
|
42
49
|
instruction: "Click on elements (buttons, links) to navigate or trigger actions"
|
|
43
50
|
params:
|
|
44
51
|
selector: "CSS selector for element"
|
|
45
52
|
output: click_result
|
|
46
53
|
|
|
47
|
-
- step:
|
|
54
|
+
- step: 4
|
|
48
55
|
action: browser_type
|
|
49
56
|
instruction: "Type text into form fields (inputs, textareas)"
|
|
50
57
|
params:
|
|
@@ -52,12 +59,14 @@ steps:
|
|
|
52
59
|
text: "text to type"
|
|
53
60
|
output: type_result
|
|
54
61
|
|
|
55
|
-
- step:
|
|
62
|
+
- step: 5
|
|
56
63
|
action: browser_screenshot
|
|
57
64
|
instruction: "Take screenshot to verify state after interactions"
|
|
58
65
|
output: verification_screenshot
|
|
59
66
|
|
|
60
67
|
rules:
|
|
68
|
+
- "Escalar sólo cuando haga falta, en este orden: `browser_click`/`browser_type` con un selector → `browser_script` si el elemento no se puede alcanzar con un selector estable → `computer_use_task` si la página no tiene selectores usables. Empezar por el último es lento y caro: mira la pantalla y razona en cada paso."
|
|
69
|
+
- "Esperar con `browser_wait` antes de cada clic o tipeo, no confiar en que la página ya cargó: es la falla más común de la automatización web, y falla en silencio."
|
|
61
70
|
- "Wait for page to fully load after each navigation or significant interaction"
|
|
62
71
|
- "Use specific, stable CSS selectors (IDs preferred over classes)"
|
|
63
72
|
- "Take screenshots after critical steps for verification"
|
|
@@ -8,7 +8,7 @@ category: web
|
|
|
8
8
|
permissions:
|
|
9
9
|
- browser_control
|
|
10
10
|
dependencies: []
|
|
11
|
-
tools: [browser_navigate, browser_screenshot,
|
|
11
|
+
tools: [browser_navigate, browser_screenshot, browser_extract, browser_wait]
|
|
12
12
|
|
|
13
13
|
# Structured skill fields
|
|
14
14
|
triggers:
|
|
@@ -39,16 +39,26 @@ steps:
|
|
|
39
39
|
output: screenshot
|
|
40
40
|
|
|
41
41
|
- step: 3
|
|
42
|
-
action:
|
|
43
|
-
instruction: "
|
|
44
|
-
|
|
42
|
+
action: browser_wait
|
|
43
|
+
instruction: "Wait for the content selector before extracting — an SPA renders after load fires"
|
|
44
|
+
params:
|
|
45
|
+
selector: "CSS selector of the content that must be present"
|
|
46
|
+
output: content_ready
|
|
45
47
|
|
|
46
48
|
- step: 4
|
|
49
|
+
action: browser_extract
|
|
50
|
+
instruction: "Extract the rendered DOM. Omit the selector for a compact accessibility snapshot of the whole page"
|
|
51
|
+
params:
|
|
52
|
+
selector: "CSS selector, or 'body' for the whole page"
|
|
53
|
+
output: extracted_content
|
|
54
|
+
|
|
55
|
+
- step: 5
|
|
47
56
|
action: synthesize
|
|
48
57
|
instruction: "Combine screenshot and text content for comprehensive capture"
|
|
49
58
|
output: scraped_data
|
|
50
59
|
|
|
51
60
|
rules:
|
|
61
|
+
- "Extraer con `browser_extract`, NUNCA con `web_fetch`: web_fetch vuelve a pedir la URL al servidor y recibe el HTML sin renderizar, que es justamente lo que esta skill existe para evitar. En un SPA devuelve una cáscara vacía."
|
|
52
62
|
- "Wait for full page load including JavaScript-rendered content"
|
|
53
63
|
- "Take screenshot before extracting text to capture initial state"
|
|
54
64
|
- "For infinite scroll pages, scroll down and capture multiple screenshots"
|
|
@@ -66,10 +76,10 @@ output_format:
|
|
|
66
76
|
|
|
67
77
|
examples:
|
|
68
78
|
- user_input: "capturá el contenido de https://example.com/dashboard"
|
|
69
|
-
expected_behavior: "browser_navigate →
|
|
79
|
+
expected_behavior: "browser_navigate → browser_wait → browser_screenshot → browser_extract → return both"
|
|
70
80
|
|
|
71
81
|
- user_input: "obtené la página renderizada de la app"
|
|
72
|
-
expected_behavior: "Navigate →
|
|
82
|
+
expected_behavior: "Navigate → browser_wait for the SPA root → screenshot + browser_extract"
|
|
73
83
|
|
|
74
84
|
- user_input: "scrapeá este sitio con javascript"
|
|
75
85
|
expected_behavior: "Full browser render → capture visual and text content"
|
|
@@ -87,7 +97,12 @@ Esta skill se activa para sitios web dinámicos que requieren JavaScript renderi
|
|
|
87
97
|
|------|----------|---------------|
|
|
88
98
|
| `browser_navigate` | Navega y renderiza página completa | Sitios con JavaScript/SPA |
|
|
89
99
|
| `browser_screenshot` | Captura estado visual | Evidencia de contenido renderizado |
|
|
90
|
-
| `
|
|
100
|
+
| `browser_extract` | Extrae del DOM ya renderizado | Contenido textual o estructurado del SPA |
|
|
101
|
+
| `browser_wait` | Espera a que aparezca un selector | Antes de extraer, en páginas que cargan por partes |
|
|
102
|
+
|
|
103
|
+
> **No usar `web_fetch` acá.** Vuelve a pedir la URL al servidor y recibe el HTML
|
|
104
|
+
> sin JavaScript ejecutado — en un SPA, una cáscara vacía. Para eso está
|
|
105
|
+
> `browser_extract`, que lee el DOM que el navegador ya renderizó.
|
|
91
106
|
|
|
92
107
|
## Workflow
|
|
93
108
|
|