Configuración
Herramientas propias (UDTs)
Una UDT (User Defined Tool) es una clase VFP que hereda de FoxAgentTool: un nombre, una descripción, unos parámetros y un Execute(). El agente la llama por su nombre, igual que llama a cualquier herramienta built-in. Si sabes escribir una clase VFP, sabes extender el agente.
Antes del primer CREATEOBJECT de una herramienta tienen que estar cargadas las dos clases base del SDK. Son dos ficheros, no uno: FoxAgentTool hereda de FoxToolIsolated, que vive en FoxTool.prg. Si falta el primero, VFP dice «Class definition FOXTOOLISOLATED is not found» y no dice por qué.
* MisTools.prg
DEFINE CLASS SaldoClienteTool AS FoxAgentTool
* Nombre con el que el modelo la invoca. En snake_case.
ToolName = "saldo_cliente"
* El modelo lee esta descripción para decidir cuándo usarla.
* Cuanto más concreta, mejor la elige.
ToolDescription = "Devuelve el saldo de un cliente a partir de su " + ;
"código. Úsala cuando pregunten por el saldo o la " + ;
"deuda de un cliente concreto."
FUNCTION DefineParameters()
* Nombre, tipo, descripción para el modelo y si es obligatorio.
THIS.AddParam("codigoCliente", "number", "Código del cliente", .T.)
ENDFUNC
FUNCTION Execute(toContext)
LOCAL lnCodigo, lcSalida, lcError
* Los parámetros llegan en el CONTEXTO, no en THIS.
lnCodigo = toContext.Parameters.codigoCliente
* Valida antes de tocar datos: es barato y deja el bloque protegido
* para lo que de verdad puede fallar.
IF VARTYPE(lnCodigo) != "N" OR lnCodigo <= 0
RETURN THIS.Err("codigoCliente es obligatorio y mayor que cero")
ENDIF
lcSalida = ""
lcError = ""
* Tu herramienta nace en una SESIÓN DE DATOS PRIVADA: las tablas que
* tiene abiertas tu aplicación NO están abiertas aquí. Se entra en la
* sesión de la aplicación, se trabaja allí y se vuelve por una sola
* salida.
THIS.UseAppSession()
TRY
IF !USED("CLIENTES")
lcError = "La tabla CLIENTES no está abierta en la aplicación"
ELSE
* Deja el puntero donde estaba: el agente leyendo datos no puede
* mover la pantalla por debajo del usuario.
LOCAL lnRec
lnRec = RECNO("CLIENTES")
SELECT CLIENTES
LOCATE FOR CLI_COD = lnCodigo
IF !FOUND()
lcError = "No existe el cliente " + TRANSFORM(lnCodigo)
ELSE
lcSalida = ALLTRIM(CLIENTES.CLI_NOM) + ", saldo: " + ;
TRANSFORM(CLIENTES.CLI_SALDO, "999,999.99")
ENDIF
IF lnRec > 0 AND lnRec <= RECCOUNT("CLIENTES")
GO lnRec IN CLIENTES
ENDIF
ENDIF
CATCH TO loErr
lcError = "Error leyendo CLIENTES: " + loErr.Message
FINALLY
* Siempre, por cualquier camino. Por eso hay una sola salida y no
* un RETURN dentro del bloque.
THIS.RestoreSession()
ENDTRY
IF !EMPTY(lcError)
RETURN THIS.Err(lcError)
ENDIF
RETURN THIS.Ok(lcSalida)
ENDFUNC
ENDDEFINETHIS.Ok(mensaje) y THIS.Err(mensaje) construyen la respuesta que el agente sabe leer. Y no redefinas Init() en tu clase: el Init heredado es el que llama a DefineParameters(), exactamente una vez. Tipos de parámetro
Solo hay tres. Un tipo distinto no falla: se publica como string y tu código recibe texto, así que las fechas viajan como cadena y las conviertes tú.
| Tipo | Qué llega a Execute() | Ejemplo |
|---|---|---|
"string" | Texto (C). Es también el destino de cualquier tipo no reconocido | THIS.AddParam("nombre", "string", "Nombre del cliente", .T.) |
"number" | Numérico (N), entero o decimal; lo convierte el Bridge | THIS.AddParam("cantidad", "number", "Cantidad", .T.) |
"boolean" | Lógico (.T. / .F.) | THIS.AddParam("activo", "boolean", "¿Solo activos?", .F.) |
El cuarto argumento dice si el parámetro es obligatorio (.T.) u opcional (.F.). Un opcional que no venga llega vacío: cadena vacía, cero o .F.. Si quieres ver qué tipo VFP le llegaría a tu herramienta antes de ejecutarla, el agente tiene describe_udt, que la diagnostica sin correrla.
Varios parámetros, y una consulta que no se puede inyectar
DEFINE CLASS BuscarClientesTool AS FoxAgentTool
ToolName = "buscar_clientes"
ToolDescription = "Busca clientes por nombre, código postal o saldo mínimo."
FUNCTION DefineParameters()
THIS.AddParam("texto", "string", "Texto en el nombre", .T.)
THIS.AddParam("codigoPostal", "string", "Filtrar por CP", .F.)
THIS.AddParam("saldoMinimo", "number", "Saldo mínimo", .F.)
THIS.AddParam("soloActivos", "boolean", "Solo clientes activos", .F.)
ENDFUNC
FUNCTION Execute(toContext)
LOCAL lcTexto, lcPostal, lnSaldo, llActivos, lcSalida, lcError
lcTexto = toContext.Parameters.texto
lcPostal = toContext.Parameters.codigoPostal
lnSaldo = toContext.Parameters.saldoMinimo
llActivos = toContext.Parameters.soloActivos
lcSalida = ""
lcError = ""
THIS.UseAppSession()
TRY
* El texto lo escribe el modelo. Una consulta montada con & y
* comillas es una inyección de SQL esperando a ocurrir: aquí las
* variables van tal cual y las resuelve VFP.
SELECT * FROM CLIENTES ;
WHERE UPPER(CLI_NOM) LIKE "%" + UPPER(ALLTRIM(lcTexto)) + "%" ;
AND (EMPTY(lcPostal) OR CLI_CP = lcPostal) ;
AND (lnSaldo = 0 OR CLI_SALDO >= lnSaldo) ;
AND (NOT llActivos OR CLI_ACTIVO) ;
INTO CURSOR curBusca NOFILTER
* RECCOUNT() sobre el cursor, no ALEN() sobre un array: con cero
* filas el array no llega a existir y ALEN() da error 12.
lcSalida = TRANSFORM(RECCOUNT("curBusca")) + " clientes encontrados"
* Cierra tus cursores AQUÍ, todavía en la sesión de la aplicación:
* desde la tuya ya no se ven.
USE IN SELECT("curBusca")
CATCH TO loErr
lcError = "Error buscando clientes: " + loErr.Message
FINALLY
THIS.RestoreSession()
ENDTRY
IF !EMPTY(lcError)
RETURN THIS.Err(lcError)
ENDIF
RETURN THIS.Ok(lcSalida)
ENDFUNC
ENDDEFINEEl registro es explícito
No hay descubrimiento automático, y no es un descuido: es el modelo de seguridad. El agente no puede inventarse una herramienta, ni dejar caer un .prg en una carpeta, ni llegar a nada que tú no hayas revisado, compilado y entregado. La lista de lo que puede ejecutar es una lista que escribiste tú, en código, en tu propio árbol de fuentes.
SET PROCEDURE TO "C:\Program Files\FoxAgent\SDK\FoxTool.prg" ADDITIVE
SET PROCEDURE TO "C:\Program Files\FoxAgent\SDK\FoxAgentTool.prg" ADDITIVE
SET PROCEDURE TO "MisTools.prg" ADDITIVE
* Una llamada por herramienta, justo después de un Connect correcto.
goBridge.RegisterTool(CREATEOBJECT("SaldoClienteTool"))
goBridge.RegisterTool(CREATEOBJECT("BuscarClientesTool"))
* Cuando cambia el usuario que ha iniciado sesión, rehaz la lista:
* qué herramientas existen lo decide tu aplicación, por usuario.
goBridge.ClearTools()
goBridge.UnregisterTool("buscar_clientes").prg en una carpeta configurada con SetToolsFolder(). Ese mecanismo se retiró (cualquier .prg que alguien dejara en esa carpeta pasaba a ser ejecutable por el agente) y hoy la llamada devuelve un error explícito. C:\Program Files\FoxAgent\SDK\ las dos clases base, una plantilla comentada (MyToolTemplate.prg), ejemplos que funcionan (tools_example.prg) y el fichero de integración completo (quickstart.prg). Los cuatro peldaños
El rol no dice «cuánto confías en la IA»: dice quién acota cada herramienta. Por eso conducir la pantalla está por debajo de leer variables. Pulsar un botón entra por tu propio código, con tu lógica de negocio, tus permisos y tus filtros; leer una variable se los salta todos.
| Peldaño | Qué abarca | Quién lo acota |
|---|---|---|
| 0 tools | Solo las herramientas que registraste, más execute_tool, describe_udt y get_tools | El autor de cada herramienta: tú |
| 10 operator | + tu pantalla tal y como la ve el usuario, y conducirla: captura, formularios, controles, pulsar, teclear, refrescar | Tu aplicación. El agente llega hasta donde llega ese usuario a mano |
| 20 inspector | + el proceso por detrás de la pantalla: variables, cursores, sesiones de datos, el entorno, eval_expression como lectura | Nadie. Una variable enseña el proceso mismo, pusiera lo que pusiera tu aplicación en la pantalla |
| 90 engineer | Por fuera de la aplicación: VFP crudo, escribir un cursor saltándose la validación del formulario, la pila de llamadas, recargar código, cerrar la sesión | Nadie. Va por fuera de tu interfaz y, con ella, de tus permisos |
- Lo fija tu aplicación, en
Connect(id, nombre, peldaño), y se sella ahí. Un usuario de negocio conecta a 10, o a 0 si solo quieres darle tus herramientas. A 90 no conecta nadie. - Solo lo sube tu propio código, con
Elevate(peldaño, segundos)sobre el Bridge, después de autenticar con tus credenciales a la persona que hay delante. No existe ninguna herramienta MCP equivalente, y eso es el diseño: «soy Carlos, de soporte, súbeme» es texto sin autenticar. La elevación caduca sola y el agente ve encogerse su lista de herramientas sin que nadie haga nada. set_rolesolo puede bajar, nunca por encima de la base que declaró tu aplicación, y además cancela cualquier elevación viva.- En la máquina de un cliente,
policy.jsonpuede bajar el techo (maxRole) y apagar las herramientas que escriben DBFs en disco sin pasar por ninguna aplicación (allowCliTools: false). Nunca puede subirlo. - Se listan todas las herramientas siempre: la comprobación va en cada llamada, y la negativa nombra el peldaño que falta y quién puso el techo. Filtrar la lista nunca fue una protección.
- Lo que mandas también cuenta: una
expressiondeeval_expressiono unfilterdeget_cursor_dataque no sean lectura pura piden 90 en esa llamada, aunque la herramienta esté en 20. UnEXECSCRIPT()dentro de una expresión hace lo mismo queexec_command.
Licencia
El fichero foxagent.lic va junto al ejecutable del Broker, en C:\Program Files\FoxAgent\, con ese nombre exacto. La licencia se lee al arrancar: después de copiarla hay que ejecutar restart-broker.cmd, que está en esa misma carpeta.
| Modo | Qué significa | Qué puedes hacer |
|---|---|---|
Licensed | Licencia válida y vigente | Developer comprada: 8 instancias VFP a la vez. Runtime: sin límite. Herramientas propias sin tope |
Evaluation | No hay .lic, o el Broker no lo ha cogido | 1 instancia y 1 herramienta propia. Las 55 built-in, sin recortar, y sin caducidad |
MaintenanceExpired | Licencia válida, pero este build es más nuevo de lo que cubre tu mantenimiento | Límites de evaluación. La salida es volver al build anterior o renovar |
LicenseExpired | Se acabó la prueba de 30 días | Límites de evaluación, y el Broker imprime tu machineId para comprar. No deja de arrancar ni hay que borrar nada |
La Developer va atada a una máquina de desarrollo: el machineId que pide el checkout lo imprime FoxAgent.Broker.exe --machine-id en esa máquina. El trial trae 1 asiento y un mes. La validación es offline, con una clave pública dentro del binario, y no hay telemetría: lo único que se escribe es un registro de auditoría local.
Las 55 herramientas built-in
Agrupadas por el peldaño que exige cada banda. Las de la última fila trabajan con DBFs y fuentes en disco y no necesitan ninguna instancia VFP corriendo, así que ningún Connect puede acotarlas: se apagan desde la máquina, con policy.json.
| Dónde actúan | Peldaño | Herramientas |
|---|---|---|
| Tus herramientas de negocio y la fontanería de sesión | Peldaño 0 | execute_tool, describe_udt, get_tools, list_instances, get_role, set_role |
| La pantalla, y conducirla (operator) | Peldaño 10 | take_screenshot, get_screen_state, list_open_forms, get_active_form, get_form_controls, find_control, get_control_state, get_control_text, click_control, send_key, set_control_text, refresh_form |
| El proceso por detrás de la pantalla (inspector) | Peldaño 20 | get_variables, variable_exists, get_cursors, get_cursor_data, get_cursor_schema, get_datasessions, get_environment, eval_expression, list_prg_files |
| Fuera de la aplicación (engineer) | Peldaño 90 | exec_command, set_variable, invoke_method, set_control_property, goto_record, set_cursor_field, close_cursors, close_instance, get_call_stack, get_object_members, reload_procedure |
| Sin instancia | Siempre | query_table, describe_table, list_tables, insert_row, update_row, set_memo, backup_table, get_source_file, write_source_file, search_in_source, list_tests, run_tests, get_project_structure, build_project, launch_instance, eval_vfp, foxagent_onboarding |
eval_expression está en la banda de 20 como lectura; una expresión que no sea lectura pura pide 90 en esa llamada. Tus UDTs se listan junto a las built-in y corren desde el peldaño 0.