Join us on the webinar next friday 19 at 19 pm. See the Harbour AI Agents working
#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.
// ============================================================================