Class Agent

Source: source/classes/agent.prg

source\classes\agent.prg

An autonomous AI coding agent for Harbour/FiveWin. Implements the same agentic engine that powers Agents Web: streaming SSE chat, real tool-calling, dynamic tools, skills, and multi-agent dispatch via hb_threadStart().

Architecture

flowchart TB subgraph Core["Agent Core"] Run["Run() loop"] Step["Step() → LLM → tool_calls? → act"] Run --> Step --> Run end subgraph Tools["Tools"] BuiltIn["Built-in (18)"] User["User tools (dynamic)"] Skills["Skills (system prompt)"] end subgraph Advanced["Advanced"] Dispatch["DispatchAgents() → parallel"] Pipeline["DispatchAgentsPipeline() → sequential"] Roles["Roles: cRole + cRolePrompt"] Shared["SharedContext blackboard"] Plan["GeneratePlan() → ExecutePlan()"] Hooks["Hooks: pre/post tool callbacks"] Persist["SaveState() / LoadState()"] end Core --> Tools --> Advanced

Quick Start

oAgent := Agent():New( cApiKey )
oAgent:Run( "Crea una app TODO en PHP+SQLite con 3 sub-agentes" )
? oAgent:UsageReport()

DATA Members

DATATypeDescription
aMessagesArrayFull conversation {role, content}
aBuiltinToolsHashname → codeblock for all tools
aUserToolsHashname → {desc, script, type} for user-created tools
aSkillsHashname → content for all known skills
aSkillsOnArrayNames of currently active skills
cGoalStringCurrent objective
aPlanArraySteps {title, state}
cModelStringModel ID (default: deepseek-v4-pro)
nMaxStepsNumericMax iterations per Run() (default: 14)
nTokensIn/Out/CacheNumericToken counters
aHooksArrayRegistered hooks {name, event, block, priority, active}
cRoleStringAgent role: "planner", "analyst", "developer", "tester"
cRolePromptStringExtra system prompt injected by role
aSharedContextHashShared blackboard between agents for inter-agent communication
nSubAgentTimeoutNumericSeconds per sub-agent (default: 120)
bOnAgentProgressCodeblockCallback: {|cName, cStatus, cMsg| ...}

Core Methods

New( cKey, [cModel] ) → Agent

Constructor. Initializes tools, skills, loads saved state. Default model: deepseek-v4-pro.

Run( cPrompt ) → lastAssistantMessage

Main agent loop: observe → decide → act. Max nMaxSteps iterations (default 14). Returns the last assistant text response. Intermediate tool calls are executed and their results fed back into the conversation.

Step() → cResponse

Sends the current conversation to DeepSeek (POST api.deepseek.com/chat/completions), returns the raw response. Updates token metrics. This is the core LLM RPC.

BuildSystemPrompt()

Constructs the system prompt: base instructions + active skills + user tools list + goal/plan.

BuildToolsArray() → aTools

Returns the full OpenAI-format tools array (built-in + user-registered) for the LLM request.

Built-in Tools

ToolMethodDescription
list_filesTool_ListFiles([cDir])List files on disk
read_fileTool_ReadFile(cPath)Read file content
write_fileTool_WriteFile(cPath,cContent)Create/overwrite (asks permission)
delete_fileTool_DeleteFile(cPath)Delete file (asks permission)
shellTool_Shell(cCmd)Execute shell command
pythonTool_Python(cCode)Run Python (temp .py + execute)
sqlTool_Sql(cDb,cQuery)Execute SQL on a database
web_searchTool_WebSearch(cQuery)Search the web
web_fetchTool_WebFetch(cUrl)Fetch a URL

Dynamic Tools

MethodDescription
RegisterTool(cName,cDesc,cScript,[cType])Register script as callable tool. Auto-detects python/shell type.
UnregisterTool(cName)Remove tool from registry. Script stays on disk.
ListUserTools()Return names + descriptions of user tools.
ExecUserTool(cName,hArgs)Execute a user-registered tool.

Tools persist in agent_state/user_tools.json. Built-in tools are fixed; user tools are dynamic and survive between sessions.

Hooks

Hooks are callbacks that execute before/after tool calls, enabling logging, auditing, validation, and cancellation without modifying tool code.

MethodDescription
RegisterHook(cName,cEvent,bBlock,[nPriority])Register a hook. Events: pre_tool_use, post_tool_use, on_error, on_complete. Pre-hooks return .F. to cancel.
UnregisterHook(cName)Remove a hook by name.
ListHooks()List all registered hooks with event, priority, and status.
RunHooks(cEvent,hContext)Execute all hooks for an event. Called internally by ExecTool().
LLMRegisterHook(cName,cEvent,cAction,[nPriority])Register hook from LLM using preset actions: log, audit, block_shell, block_delete, block_write.

The hook context hash hContext contains: {name, args, result}. Pre-hooks receive the tool name and arguments; return .F. to cancel execution. Post-hooks receive the result and can modify hContext["result"].

Skills

MethodDescription
CreateSkill(cName,cContent,[cDir])Write skill file to skills/name.md
ToggleSkill(cName,lOn)Activate/deactivate without deleting the file
ActiveSkillsPrompt()Return prompt string with all active skills
ListSkills([cDir])Load skills from disk directory

5 built-in skills: reviewer, summarizer, refactor, documenter, tester. Skills inject reusable instructions into the system prompt. They shape HOW the agent thinks, not WHAT it can do (tools handle that).

Multi-Agent Orchestration

The Agent class provides a complete orchestration framework with 8 methods for coordinating multiple agents: parallel dispatch, sequential pipelines, role-based specialization, shared context, retry, timeout, aggregation, and progress monitoring.

Roles

Every Agent has a cRole and cRolePrompt property. Roles define what the agent specializes in. The role prompt is injected into the system prompt.

oDev := Agent():New( cKey )
oDev:cRole       := "developer"
oDev:cRolePrompt := "Eres desarrollador senior. Escribe código limpio y documentado."

DispatchAgents( aTasks, cContract ) → aResults

Parallel fan-out: launches up to 4 sub-agents simultaneously. Each sub-agent inherits the parent's role, shared context, and model. Results are collected synchronously.

DispatchAgentsPipeline( aTasks, cContract ) → aResults

Sequential pipeline: output of each agent feeds into the next. Use for dependent task chains where step N needs the result of step N-1.

aResults := oAgent:DispatchAgentsPipeline( { ;
   "Analiza los requisitos de la app", ;
   "Diseña el schema de la base de datos", ;
   "Genera el código backend", ;
   "Escribe los tests unitarios" }, cContract )

DispatchAgentsWithRoles( aAgentDefs, cContract ) → aResults

Fan-out with specialized roles. Each agent definition is a hash with: task, role, rolePrompt, model, nMaxSteps. Agents run in parallel with their own configuration.

aDefs := { ;
   { "task" => "Diseña el schema", "role" => "analyst", ;
     "rolePrompt" => "Eres analista de datos.", "nMaxSteps" => "8" }, ;
   { "task" => "Implementa la API", "role" => "developer", ;
     "model" => "gpt-4o", "nMaxSteps" => "10" }, ;
   { "task" => "Escribe tests", "role" => "tester", ;
     "rolePrompt" => "Eres tester QA.", "nMaxSteps" => "6" } }
aResults := oAgent:DispatchAgentsWithRoles( aDefs, cContract )

Shared Context (Blackboard Pattern)

Agents communicate via a shared hash. Changes sync back to the parent after each sub-agent completes. The shared context is injected into every agent's system prompt.

MethodDescription
GetSharedContext([cKey])Get entire context or a specific key.
SetSharedContext(cKey, xValue)Set a key in the shared context.
oAgent:SetSharedContext( "schema", "CREATE TABLE users..." )
// sub-agents will see this in their system prompt

AggregateResults( aResults, cMethod ) → cCombined

Combine results from multiple agents. Methods:

RetrySubAgent( cTask, cContract, nMaxRetries ) → cResult

Retry a failed sub-agent up to nMaxRetries times (default 3). Each attempt runs in a new agent instance with error recovery.

SubAgentRunWithTimeout( cTask, cContract, nTimeout ) → cResult

Run a sub-agent with a time limit in seconds (default 120). Uses the interrupt mechanism to abort if the agent exceeds the timeout.

NotifyProgress( cAgentName, cStatus, cMsg )

Send progress notifications. Set bOnAgentProgress codeblock to receive callbacks.

oAgent:bOnAgentProgress := {| cName, cStatus, cMsg | ;
   ? cName + ": " + cStatus + " — " + cMsg }

SubAgentRun( cTask, cContract ) → cResult

Individual sub-agent execution. Called by DispatchAgents() in its own thread.

Planning

GeneratePlan( cGoal ) → aPlan

Asks the LLM to break the goal into 3-6 JSON steps. Returns an array of {title, state} where the first step is "active" and the rest "pending".

ExecutePlan()

Runs the plan sequentially: for each "active" step, calls Run() with the step description, marks it "done", activates the next. Stops when all steps are done.

Persistence

MethodDescription
SaveState(cDir)Save user tools and active skills to user_tools.json
LoadState(cDir)Restore from disk on startup. Also loads skills from skills/ dir.

Utilities

MethodDescription
AddMessage(cRole,cContent)Append to conversation array. Auto-compacts at 200 msgs.
UsageReport()Print token counts and estimated cost.
Abort()Stop the agent loop immediately.

Complete Example

PROCEDURE Main()
   LOCAL oAgent, cResult

   oAgent := Agent():New( "sk-xxxx" )

   // Register a custom tool
   oAgent:RegisterTool( "contar", ;
      "Cuenta lineas, palabras y caracteres de un archivo", ;
      "contar.py" )

   // Create and activate a skill
   oAgent:CreateSkill( "prueba", ;
      "Responde siempre en español." + CRLF + ;
      "Verifica el disco antes de escribir código." + CRLF + ;
      "Escribe tests automáticamente." )
   oAgent:ToggleSkill( "prueba", .T. )

   // Register a hook to audit all tool calls
   oAgent:RegisterHook( "audit_log", "post_tool_use", ;
      {| h | ? "AUDIT:", h["name"], hb_ValToStr( h["args"] ) }, 50 )

   // Register a hook to block shell commands (pre-hook)
   oAgent:RegisterHook( "safe_mode", "pre_tool_use", ;
      {| h | iif( h["name"] == "shell", .F., .T. ) }, 10 )

   // Or use LLM-driven preset hooks
   oAgent:LLMRegisterHook( "llm_audit", "post_tool_use", "audit" )

   // Generate and execute a plan
   oAgent:GeneratePlan( "Crea una app TODO en PHP+SQLite" )
   oAgent:ExecutePlan()

   // Or run directly
   cResult := oAgent:Run( "Explica qué hay en el disco" )
   ? cResult

   // === Multi-agent orchestration ===
   oAgent:nSubAgentTimeout := 60

   // Parallel dispatch with roles
   aDefs := { ;
      { "task" => "Diseña el schema", "role" => "analyst", ;
        "rolePrompt" => "Eres analista de datos." }, ;
      { "task" => "Implementa la API", "role" => "developer", ;
        "model" => "gpt-4o" }, ;
      { "task" => "Escribe tests", "role" => "tester" } }
   aResults := oAgent:DispatchAgentsWithRoles( aDefs, cContract )

   // Aggregate results
   cSummary := oAgent:AggregateResults( aResults, "summary" )
   ? cSummary

   // Pipeline: sequential dependent tasks
   aResults := oAgent:DispatchAgentsPipeline( { ;
      "Analiza requisitos", "Diseña schema", "Implementa API" }, cContract )

   oAgent:UsageReport()
RETURN

Notes