Class AI Agent for Harbour

160 views
Skip to first unread message

antonio....@gmail.com

unread,
Jun 14, 2026, 3:03:42 AM (10 days ago) Jun 14
to Harbour Users
An autonomous AI coding agent for Harbour


Join us on the webinar next friday 19 at 19 pm. See the Harbour AI Agents working

Source code:
#include "classes.ch"

#define AGENT_MAX_STEPS   14
#define AGENT_MODEL_DEF   "deepseek-v4-pro"

// ---------------------------------------------------------------------------
// Autonomous AI agent with tools, skills, and multi-agent dispatch
// ---------------------------------------------------------------------------

CLASS Agent

   // ---- conversation state ----
   DATA aMessages       INIT {}        // { {role, content}, ... }
   DATA cSystemPrompt                  // base system prompt + active skills
   DATA lRunning        INIT .F.

   // ---- tools ----
   DATA aBuiltinTools   INIT {}        // tools fijas (nombre → codeblock)
   DATA aUserTools      INIT {}        // agent-created tools {name → {desc, script, type}}
   DATA aSkills         INIT {}        // active skills {name → content}
   DATA aSkillsOn       INIT {}        // names of active skills

   // ---- planning ----
   DATA cGoal
   DATA aPlan           INIT {}

   // ---- configuration ----
   DATA cModel          INIT AGENT_MODEL_DEF
   DATA cLanguage       INIT "es"
   DATA nMaxSteps       INIT AGENT_MAX_STEPS
   DATA cApiKey
   DATA cApiUrl         INIT "https://api.deepseek.com/chat/completions"
   DATA lThinking       INIT .F.
   DATA lStreaming      INIT .T.

   // ---- metrics ----
   DATA nTokensIn       INIT 0
   DATA nTokensOut      INIT 0
   DATA nTokensCache    INIT 0
   DATA nCost           INIT 0

   // ---- control ----
   DATA lAbort          INIT .F.

   // ========================================================================
   // Initialization
   // ========================================================================

   METHOD New( cKey, cModel )
   METHOD InitTools()
   METHOD InitSkills()
   METHOD LoadState( cDir )
   METHOD SaveState( cDir )

   // ========================================================================
   // Main loop
   // ========================================================================

   METHOD Run( cPrompt )
   METHOD Step()
   METHOD SendToLLM( aMsgs )
   METHOD BuildSystemPrompt()
   METHOD BuildToolsArray()

   // ========================================================================
   // Tool dispatch
   // ========================================================================

   METHOD ExecTool( cName, hArgs )
   METHOD AskPermission( cAction, cPath )

   // ---- built-in tools ----
   METHOD Tool_ListFiles( cDir )
   METHOD Tool_ReadFile( cPath )
   METHOD Tool_WriteFile( cPath, cContent )
   METHOD Tool_DeleteFile( cPath )
   METHOD Tool_Shell( cCmd )
   METHOD Tool_Python( cCode )
   METHOD Tool_Sql( cDb, cQuery )
   METHOD Tool_WebSearch( cQuery )
   METHOD Tool_WebFetch( cUrl )

   // ---- dynamic tools ----
   METHOD RegisterTool( cName, cDesc, cScript, cType )
   METHOD UnregisterTool( cName )
   METHOD ListUserTools()
   METHOD ExecUserTool( cName, aArgs )

   // ---- skills ----
   METHOD ListSkills( cDir )
   METHOD CreateSkill( cName, cContent, cDir )
   METHOD ToggleSkill( cName, lOn )
   METHOD ActiveSkillsPrompt()

   // ========================================================================
   // Multi-agent
   // ========================================================================

   METHOD DispatchAgents( aTasks, cContract )
   METHOD SubAgentRun( cTask, cContract )

   // ========================================================================
   // Planning
   // ========================================================================

   METHOD GeneratePlan( cGoal )
   METHOD ExecutePlan()

   // ========================================================================
   // Utilities
   // ========================================================================

   METHOD AddMessage( cRole, cContent )
   METHOD UsageReport()
   METHOD Abort()

ENDCLASS

// ============================================================================
// Initialization
// ============================================================================

METHOD New( cKey, cModel ) CLASS Agent

   LOCAL cDir

   DEFAULT cModel TO AGENT_MODEL_DEF

   ::cApiKey := cKey
   IF !Empty( cModel )
      ::cModel := cModel
   ENDIF

   ::aMessages  := {}
   ::aPlan      := {}
   ::aUserTools := {}
   ::aSkillsOn  := {}
   ::lAbort     := .F.

   ::InitTools()
   ::InitSkills()

   // load tools/skills from disk
   cDir := hb_DirBase() + "agent_state" + hb_ps()
   IF hb_vfDirExists( cDir )
      ::LoadState( cDir )
   ENDIF

RETURN Self

// ---------------------------------------------------------------------------

METHOD InitTools() CLASS Agent

   ::aBuiltinTools := { ;
      "list_files"    => {|h| ::Tool_ListFiles( hb_HGetDef( h, "dir", "" ) )}, ;
      "read_file"     => {|h| ::Tool_ReadFile( hb_HGetDef( h, "path", "" ) )}, ;
      "write_file"    => {|h| iif( ::AskPermission( "escribir", h["path"] ), ;
                           ::Tool_WriteFile( h["path"], hb_HGetDef( h, "content", "" ) ), ;
                           "Rechazado por el usuario." )}, ;
      "delete_file"   => {|h| iif( ::AskPermission( "borrar", h["path"] ), ;
                           ::Tool_DeleteFile( h["path"] ), ;
                           "Rechazado por el usuario." )}, ;
      "shell"         => {|h| ::Tool_Shell( hb_HGetDef( h, "command", "" ) )}, ;
      "python"        => {|h| ::Tool_Python( hb_HGetDef( h, "code", "" ) )}, ;
      "sql"           => {|h| ::Tool_Sql( hb_HGetDef( h, "db", "" ), ;
                           hb_HGetDef( h, "query", "" ) )}, ;
      "web_search"    => {|h| ::Tool_WebSearch( hb_HGetDef( h, "query", "" ) )}, ;
      "web_fetch"     => {|h| ::Tool_WebFetch( hb_HGetDef( h, "url", "" ) )}, ;
      "register_tool" => {|h| ::RegisterTool( hb_HGetDef( h, "name", "" ), ;
                           hb_HGetDef( h, "description", "" ), ;
                           hb_HGetDef( h, "scriptPath", "" ) )}, ;
      "unregister_tool" => {|h| ::UnregisterTool( hb_HGetDef( h, "name", "" ) )}, ;
      "user_tools"    => {|h| ::ListUserTools() }, ;
      "create_skill"  => {|h| ::CreateSkill( hb_HGetDef( h, "name", "" ), ;
                           hb_HGetDef( h, "content", "" ) )}, ;
      "toggle_skill"  => {|h| ::ToggleSkill( hb_HGetDef( h, "name", "" ), ;
                           hb_HGetDef( h, "active", .T. ) )} }

   // User tools are added dynamically via RegisterTool()
   FOR EACH cName IN hb_HKeys( ::aUserTools )
      ::aBuiltinTools[ cName ] := {|h, a| ::ExecUserTool( a[1], a[2] )}
   NEXT

RETURN .T.

// ---------------------------------------------------------------------------

METHOD InitSkills() CLASS Agent

   ::aSkills := { ;
      "reviewer"   => "Actúa como revisor de código. Revisa los ficheros relevantes del disco y reporta conciso: bugs, riesgos y mejoras priorizadas.", ;
      "summarizer" => "Resume en bullets claros el contenido de los ficheros indicados.", ;
      "refactor"   => "Refactoriza el fichero indicado para legibilidad y simplicidad.", ;
      "documenter" => "Genera o actualiza README.md describiendo el propósito y los ficheros del disco.", ;
      "tester"     => "Propón y escribe tests para el código del disco." }

RETURN .T.

// ---------------------------------------------------------------------------

METHOD LoadState( cDir ) CLASS Agent

   LOCAL cFile, cJson, hState

   cFile := cDir + "user_tools.json"
   IF File( cFile )
      cJson := MemoRead( cFile )
      hb_JsonDecode( cJson, @hState )
      IF hb_HHasKey( hState, "tools" )
         ::aUserTools := hState["tools"]
      ENDIF
      IF hb_HHasKey( hState, "skills_on" )
         ::aSkillsOn := hState["skills_on"]
      ENDIF
   ENDIF

   // reload skills from disk
   ::ListSkills( cDir + ".." + hb_ps() + "skills" )

RETURN .T.

// ---------------------------------------------------------------------------

METHOD SaveState( cDir ) CLASS Agent

   LOCAL cFile, cJson, hState

   IF !hb_vfDirExists( cDir )
      hb_vfDirMake( cDir )
   ENDIF

   hState := { "tools" => ::aUserTools, "skills_on" => ::aSkillsOn }
   hb_JsonEncode( hState, @cJson )
   cFile := cDir + "user_tools.json"
   MemoWrit( cFile, cJson )

RETURN .T.

// ============================================================================
// Main loop
// ============================================================================

METHOD Run( cPrompt ) CLASS Agent

   LOCAL nStep := 0
   LOCAL cResponse, aToolCalls, cResult

   ::lRunning := .T.
   ::lAbort   := .F.

   ::AddMessage( "user", cPrompt )
   ::BuildSystemPrompt()

   DO WHILE ::lRunning .AND. !::lAbort .AND. nStep < ::nMaxSteps
      nStep++

      cResponse := ::Step()

      IF Empty( cResponse )
         LOOP
      ENDIF

      // tool calls present?
      aToolCalls := ::ParseToolCalls( cResponse )
      IF Len( aToolCalls ) > 0
         FOR EACH hTC IN aToolCalls
            cResult := ::ExecTool( hTC["name"], hTC["arguments"] )
            ::AddMessage( "tool", { "id" => hTC["id"], "result" => cResult } )
         NEXT
         LOOP
      ENDIF

      // final response (text, no tool calls)
      ::AddMessage( "assistant", cResponse )
      EXIT
   ENDDO

   ::SaveState( hb_DirBase() + "agent_state" + hb_ps() )
   ::lRunning := .F.

RETURN ::aMessages[ Len( ::aMessages ) ]

// ---------------------------------------------------------------------------

METHOD Step() CLASS Agent

   LOCAL oHttp, cBody, cResponse, hResponse, hChoice, hMessage
   LOCAL aMsgs

   aMsgs := { {"role" => "system", "content" => ::cSystemPrompt} }
   AEval( ::aMessages, {|m| AAdd( aMsgs, m ) } )

   cBody := hb_JsonEncode( { ;
      "model"    => ::cModel, ;
      "messages" => aMsgs, ;
      "tools"    => ::BuildToolsArray(), ;
      "stream"   => .F. } )

   oHttp := TIpClientHttp():New( ::cApiUrl, .T. )
   oHttp:cContentType := "application/json"
   oHttp:SetHeader( "Authorization", "Bearer " + ::cApiKey )

   IF !oHttp:Open()
      RETURN ""
   ENDIF

   cResponse := oHttp:Post( cBody )
   oHttp:Close()

   IF Empty( cResponse )
      RETURN ""
   ENDIF

   hb_JsonDecode( cResponse, @hResponse )
   IF !hb_HHasKey( hResponse, "choices" ) .OR. Empty( hResponse["choices"] )
      RETURN ""
   ENDIF

   // metrics
   IF hb_HHasKey( hResponse, "usage" )
      ::nTokensIn    += hb_HGetDef( hResponse["usage"], "prompt_tokens", 0 )
      ::nTokensOut   += hb_HGetDef( hResponse["usage"], "completion_tokens", 0 )
      ::nTokensCache += hb_HGetDef( hResponse["usage"], "prompt_cache_hit_tokens", 0 )
   ENDIF

   hChoice  := hResponse["choices"][1]
   hMessage := hChoice["message"]

RETURN hb_HGetDef( hMessage, "content", "" )

// ---------------------------------------------------------------------------

METHOD ParseToolCalls( cResponse ) CLASS Agent

   LOCAL hResponse, hChoice, hMessage, aToolCalls := {}

   hb_JsonDecode( cResponse, @hResponse )
   IF hb_HHasKey( hResponse, "choices" )
      hChoice  := hResponse["choices"][1]
      hMessage := hChoice["message"]
      IF hb_HHasKey( hMessage, "tool_calls" )
         FOR EACH hTC IN hMessage["tool_calls"]
            AAdd( aToolCalls, { ;
               "id"        => hTC["id"], ;
               "name"      => hTC["function"]["name"], ;
               "arguments" => hb_JsonDecode( hTC["function"]["arguments"] ) } )
         NEXT
      ENDIF
   ENDIF

RETURN aToolCalls

// ---------------------------------------------------------------------------

METHOD BuildSystemPrompt() CLASS Agent

   LOCAL cPrompt, cSkills

   cPrompt := "Eres Agents Web. Tienes un disco virtual; usa las tools. "
   cPrompt += "Responde SIEMPRE en " + ::cLanguage + ". "
   cPrompt += "Si debes elegir entre opciones concretas, usa ask_user. "
   cPrompt += "Sé conciso."

   // active skills
   cSkills := ::ActiveSkillsPrompt()
   IF !Empty( cSkills )
      cPrompt += CRLF + CRLF + "Skills activas (síguelas siempre):" + CRLF + cSkills
   ENDIF

   // available user tools
   IF !Empty( ::aUserTools )
      cPrompt += CRLF + CRLF + "Tus herramientas de usuario registradas:"
      FOR EACH cName IN hb_HKeys( ::aUserTools )
         cPrompt += CRLF + "- " + cName + ": " + ::aUserTools[ cName ]["desc"]
      NEXT
   ENDIF

   // active plan
   IF !Empty( ::cGoal )
      cPrompt += CRLF + CRLF + "Objetivo: " + ::cGoal
      IF !Empty( ::aPlan )
         cPrompt += CRLF + "Plan:"
         FOR EACH hStep IN ::aPlan
            cPrompt += CRLF + "  [" + hStep["state"] + "] " + hStep["title"]
         NEXT
      ENDIF
   ENDIF

   ::cSystemPrompt := cPrompt

RETURN cPrompt

// ---------------------------------------------------------------------------

METHOD BuildToolsArray() CLASS Agent

   LOCAL aTools := {}

   FOR EACH cName IN hb_HKeys( ::aBuiltinTools )
      AAdd( aTools, { ;
         "type" => "function", ;
         "function" => { "name" => cName } } )
   NEXT

   // add user tools as functions
   FOR EACH cName IN hb_HKeys( ::aUserTools )
      AAdd( aTools, { ;
         "type" => "function", ;
         "function" => { ;
            "name"        => cName, ;
            "description" => ::aUserTools[ cName ]["desc"], ;
            "parameters"  => { ;
               "type"       => "object", ;
               "properties" => { "args" => { "type" => "string" } } } } } )
   NEXT

RETURN aTools

// ============================================================================
// Tool dispatch
// ============================================================================

METHOD ExecTool( cName, hArgs ) CLASS Agent

   LOCAL cResult := ""

   IF hb_HHasKey( ::aBuiltinTools, cName )
      cResult := Eval( ::aBuiltinTools[ cName ], hArgs )
   ELSEIF hb_HHasKey( ::aUserTools, cName )
      cResult := ::ExecUserTool( cName, hArgs )
   ELSE
      cResult := "unknown tool: " + cName
   ENDIF

RETURN cResult

// ---------------------------------------------------------------------------

METHOD AskPermission( cAction, cPath ) CLASS Agent

   LOCAL nChoice

   // Console mode: ask via stdin
   ? "El agente desea " + cAction + " el archivo: " + cPath
   ? "[1] Permitir  [2] Rechazar"
   INPUT "Elección: " TO nChoice

RETURN ( nChoice == 1 )

// ============================================================================
// Built-in tools
// ============================================================================

METHOD Tool_ListFiles( cDir ) CLASS Agent

   LOCAL aFiles, cResult := ""

   DEFAULT cDir TO hb_DirBase()

   aFiles := Directory( cDir + hb_ps() + "*.*" )
   FOR EACH aFile IN aFiles
      cResult += aFile[1] + CRLF
   NEXT

RETURN iif( Empty( cResult ), "(vacío)", cResult )

// ---------------------------------------------------------------------------

METHOD Tool_ReadFile( cPath ) CLASS Agent

   IF !File( cPath )
      RETURN "archivo no encontrado: " + cPath
   ENDIF

RETURN MemoRead( cPath )

// ---------------------------------------------------------------------------

METHOD Tool_WriteFile( cPath, cContent ) CLASS Agent

   LOCAL cOld, cDir

   cDir := hb_DirName( cPath )
   IF !Empty( cDir ) .AND. !hb_vfDirExists( cDir )
      hb_vfDirMake( cDir )
   ENDIF

   MemoWrit( cPath, cContent )

RETURN "escrito: " + cPath

// ---------------------------------------------------------------------------

METHOD Tool_DeleteFile( cPath ) CLASS Agent

   IF File( cPath )
      hb_vfErase( cPath )
      RETURN "borrado: " + cPath
   ENDIF

RETURN "no encontrado: " + cPath

// ---------------------------------------------------------------------------

METHOD Tool_Shell( cCmd ) CLASS Agent

   LOCAL cOutput := ""

   // safe execution — output captured via hb_processOpen
   // (simplified for this skeleton)

RETURN "shell: " + cCmd + " (ejecutado)"

// ---------------------------------------------------------------------------

METHOD Tool_Python( cCode ) CLASS Agent

   LOCAL cTmp, cOutput

   cTmp := hb_DirTemp() + hb_ps() + "agent_py_" + hb_ntos( hb_Random() ) + ".py"
   MemoWrit( cTmp, cCode )
   cOutput := ::Tool_Shell( "python " + cTmp )
   hb_vfErase( cTmp )

RETURN cOutput

// ---------------------------------------------------------------------------

METHOD Tool_Sql( cDb, cQuery ) CLASS Agent
RETURN "sql ejecutado: " + cQuery + " en " + cDb

// ---------------------------------------------------------------------------

METHOD Tool_WebSearch( cQuery ) CLASS Agent
RETURN "búsqueda: " + cQuery

// ---------------------------------------------------------------------------

METHOD Tool_WebFetch( cUrl ) CLASS Agent
RETURN "fetch: " + cUrl

// ============================================================================
// Dynamic tools
// ============================================================================

METHOD RegisterTool( cName, cDesc, cScript, cType ) CLASS Agent

   LOCAL cExt

   DEFAULT cType TO iif( Lower( hb_FNameExt( cScript ) ) == ".py", "python", "shell" )

   IF Empty( cName ) .OR. !File( cScript )
      RETURN "Error: nombre o script inválido"
   ENDIF

   ::aUserTools[ cName ] := { "desc" => cDesc, "script" => cScript, "type" => cType }
   ::aBuiltinTools[ cName ] := {|h| ::ExecUserTool( cName, h ) }

   ::SaveState( hb_DirBase() + "agent_state" + hb_ps() )

RETURN "Tool registrada: " + cName

// ---------------------------------------------------------------------------

METHOD UnregisterTool( cName ) CLASS Agent

   IF !hb_HHasKey( ::aUserTools, cName )
      RETURN "Error: tool no registrada"
   ENDIF

   hb_HDel( ::aUserTools, cName )
   hb_HDel( ::aBuiltinTools, cName )
   ::SaveState( hb_DirBase() + "agent_state" + hb_ps() )

RETURN "Tool eliminada: " + cName

// ---------------------------------------------------------------------------

METHOD ListUserTools() CLASS Agent

   LOCAL cList := ""

   IF Empty( ::aUserTools )
      RETURN "(sin tools de usuario)"
   ENDIF

   FOR EACH cName IN hb_HKeys( ::aUserTools )
      cList += cName + " — " + ::aUserTools[ cName ]["desc"] + CRLF
   NEXT

RETURN cList

// ---------------------------------------------------------------------------

METHOD ExecUserTool( cName, hArgs ) CLASS Agent

   LOCAL hTool, cCmd, cArgs

   hTool := ::aUserTools[ cName ]

   cArgs := ""
   IF hb_HHasKey( hArgs, "args" )
      cArgs := hArgs["args"]
   ENDIF

   IF hTool["type"] == "python"
      RETURN ::Tool_Python( 'import sys;sys.argv=["' + hTool["script"] + '"]+' + ;
             cArgs + CRLF + MemoRead( hTool["script"] ) )
   ELSE
      RETURN ::Tool_Shell( hTool["script"] + " " + cArgs )
   ENDIF

RETURN ""

// ============================================================================
// Skills
// ============================================================================

METHOD ListSkills( cDir ) CLASS Agent

   LOCAL aFiles, cName, cContent, cDefaultDir

   cDefaultDir := hb_DirBase() + "skills" + hb_ps()
   DEFAULT cDir TO cDefaultDir

   IF hb_vfDirExists( cDir )
      aFiles := Directory( cDir + "*.md" )
      FOR EACH aFile IN aFiles
         cName    := hb_FNameName( aFile[1] )
         cContent := MemoRead( cDir + aFile[1] )
         IF !hb_HHasKey( ::aSkills, cName )
            ::aSkills[ cName ] := cContent
         ENDIF
      NEXT
   ENDIF

RETURN ::aSkills

// ---------------------------------------------------------------------------

METHOD CreateSkill( cName, cContent, cDir ) CLASS Agent

   LOCAL cDefaultDir, cPath

   cDefaultDir := hb_DirBase() + "skills" + hb_ps()
   DEFAULT cDir TO cDefaultDir

   IF !hb_vfDirExists( cDir )
      hb_vfDirMake( cDir )
   ENDIF

   cPath := cDir + cName + ".md"
   MemoWrit( cPath, cContent )
   ::aSkills[ cName ] := cContent

RETURN "Skill creado: " + cName

// ---------------------------------------------------------------------------

METHOD ToggleSkill( cName, lOn ) CLASS Agent

   DEFAULT lOn TO .T.

   IF !hb_HHasKey( ::aSkills, cName )
      RETURN "Skill no existe: " + cName
   ENDIF

   IF lOn
      IF AScan( ::aSkillsOn, cName ) == 0
         AAdd( ::aSkillsOn, cName )
      ENDIF
      RETURN "Skill activado: " + cName
   ELSE
      ::aSkillsOn := ASort( ::aSkillsOn, {|x| x != cName } )
      RETURN "Skill desactivado: " + cName
   ENDIF

RETURN ""

// ---------------------------------------------------------------------------

METHOD ActiveSkillsPrompt() CLASS Agent

   LOCAL cPrompt := ""

   FOR EACH cName IN ::aSkillsOn
      IF hb_HHasKey( ::aSkills, cName )
         cPrompt += "Skill " + cName + ": " + ::aSkills[ cName ] + CRLF
      ENDIF
   NEXT

RETURN cPrompt

// ============================================================================
// Multi-agent
// ============================================================================

METHOD DispatchAgents( aTasks, cContract ) CLASS Agent

   LOCAL aThreads := {}, aResults := {}, nTask, hResult

   FOR nTask := 1 TO Min( Len( aTasks ), 4 )
      AAdd( aThreads, hb_threadStart( ;
         hb_threadSelf():ClassH, ;
         "SubAgentRun", ;
         Self, aTasks[ nTask ], cContract ) )
   NEXT

   // join: wait for all
   FOR EACH hThread IN aThreads
      AAdd( aResults, hb_threadJoin( hThread ) )
   NEXT

RETURN aResults

// ---------------------------------------------------------------------------

METHOD SubAgentRun( cTask, cContract ) CLASS Agent

   LOCAL oAgent, cPrompt, cResult

   // each sub-agent has its own instance (no dispatch, no ask_user)
   oAgent := Agent():New( ::cApiKey, ::cModel )
   oAgent:aBuiltinTools := hb_HClone( ::aBuiltinTools )

   // remove dispatch and ask_user for sub-agents (avoid recursion)
   hb_HDel( oAgent:aBuiltinTools, "dispatch_agents" )
   hb_HDel( oAgent:aBuiltinTools, "ask_user" )

   oAgent:nMaxSteps := 5  // lower step limit for sub-agents

   cPrompt := iif( Empty( cContract ), "", ;
      "CONTRATO TÉCNICO (OBLIGATORIO): " + cContract + CRLF + CRLF ) + cTask

   oAgent:Run( cPrompt )
   cResult := oAgent:aMessages[ Len( oAgent:aMessages ) ]["content"]

RETURN cResult

// ============================================================================
// Planning
// ============================================================================

METHOD GeneratePlan( cGoal ) CLASS Agent

   LOCAL cPrompt, cResponse, aPlan := {}

   ::cGoal := cGoal

   cPrompt := "Eres un planificador. Divide esta tarea en 3-6 pasos concretos. "
   cPrompt += "Responde SOLO con JSON: {""steps"":[{""title"":""..."",""state"":""pending""}]}. "
   cPrompt += "El primer paso debe estar ""active"". Sin prosa."
   cPrompt += CRLF + CRLF + "Tarea: " + cGoal

   cResponse := ::Tool_Shell_Send( cPrompt )

   // parsear JSON
   IF !Empty( cResponse )
      ::aPlan := hb_JsonDecode( cResponse )["steps"]
   ENDIF

RETURN ::aPlan

// ---------------------------------------------------------------------------

METHOD ExecutePlan() CLASS Agent

   LOCAL nStep

   FOR nStep := 1 TO Len( ::aPlan )
      IF ::aPlan[ nStep ]["state"] == "active"
         ::Run( "Objetivo: " + ::cGoal + CRLF + ;
                "Plan en curso. Ejecuta SOLO el paso " + hb_ntos( nStep ) + ;
                ": " + ::aPlan[ nStep ]["title"] )
         ::aPlan[ nStep ]["state"] := "done"
         IF nStep < Len( ::aPlan )
            ::aPlan[ nStep + 1 ]["state"] := "active"
         ENDIF
      ENDIF
   NEXT

RETURN .T.

// ============================================================================
// Utilities
// ============================================================================

METHOD AddMessage( cRole, cContent ) CLASS Agent

   AAdd( ::aMessages, { "role" => cRole, "content" => cContent } )

   // keep context within ~128k tokens (window trimming)
   IF Len( ::aMessages ) > 200
      // compact: summarize the first 50 messages
      ::aMessages := ASize( ::aMessages, 150 )
   ENDIF

RETURN .T.

// ---------------------------------------------------------------------------

METHOD UsageReport() CLASS Agent

   LOCAL nInputCost, nOutputCost, nCacheCost, nTotal

   // DeepSeek approximate pricing ($/1M tokens)
   nInputCost  := ::nTokensIn    * 0.14 / 1000000
   nOutputCost := ::nTokensOut   * 0.28 / 1000000
   nCacheCost  := ::nTokensCache * 0.0028 / 1000000
   nTotal      := nInputCost + nOutputCost + nCacheCost

   ? "Tokens:  in=" + hb_ntos( ::nTokensIn )
   ?? " out=" + hb_ntos( ::nTokensOut )
   ?? " cache=" + hb_ntos( ::nTokensCache )
   ?? " cost=$" + Str( nTotal, 6, 4 )

RETURN nTotal

// ---------------------------------------------------------------------------

METHOD Abort() CLASS Agent

   ::lAbort   := .T.
   ::lRunning := .F.

RETURN .T.

// ============================================================================

best regards

Antonio

antonio....@gmail.com

unread,
Jun 14, 2026, 5:42:51 AM (10 days ago) Jun 14
to Harbour Users
Reply all
Reply to author
Forward
0 new messages