Technical Documentation: vxcontrol/pentagi
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (README.md)
PentAGI
Table of Contents
- Overview
- Features
- Architecture
- Quick Start
- How to Use PentAGI After Login
- API Access
- Advanced Setup
- Development
- Testing LLM Agents
- Embedding Configuration and Testing
- Function Testing with ftester
- Building
- Credits
- License
Overview
PentAGI is an open-source, self-hosted multi-agent system for autonomous penetration testing. You describe a target and a goal in plain language; a team of specialized AI agents plans the work, runs 20+ professional tools such as nmap, Metasploit and sqlmap inside an isolated Docker sandbox, remembers what it learned and hands you a report.
- Who it is for. Security engineers, penetration testers, researchers and red teams who want an autonomous assistant on their own infrastructure, for authorized testing only.
- What sets it apart. It runs on your own servers with the model of your choice: 10+ cloud LLM providers, your own OpenAI-compatible gateway, or local models through Ollama or vLLM. Results and successful approaches are kept in PostgreSQL with pgvector for later flows, an optional Neo4j knowledge graph tracks hosts, services and findings, execution monitoring and task planning help smaller open models, and REST and GraphQL APIs automate it. PentAGI Enterprise adds agent profiles, plugins and MCP servers, customer-ready reports and a real-time attack-chain view.
- How to install. An interactive installer for Linux, macOS and Windows sets PentAGI up with Docker Compose — see Quick Start.
PentAGI is developed by VXControl, a security engineering company based in Dubai.
You can watch the video PentAGI overview:
Features
- Secure & Isolated. All operations are performed in a sandboxed Docker environment with complete isolation.
- Fully Autonomous. AI-powered agent that automatically determines and executes penetration testing steps with optional execution monitoring and intelligent task planning for enhanced reliability.
- Professional Pentesting Tools. Built-in suite of 20+ professional security tools including nmap, metasploit, sqlmap, and more.
- Smart Memory System. Long-term storage of research results and successful approaches for future use.
- Optional Knowledge Graph Integration. Graphiti-powered knowledge graph using Neo4j for semantic relationship tracking and advanced context understanding.
- Web Intelligence. Built-in browser via scraper for gathering latest information from web sources.
- External Search Systems. Integration with advanced search APIs including Tavily, Firecrawl, Traversaal, Perplexity, DuckDuckGo, Google Custom Search, Sploitus Search and SearXNG for comprehensive information gathering.
- Team of Specialists. Delegation system with specialized AI agents for research, development, and infrastructure tasks, enhanced with optional execution monitoring and intelligent task planning for optimal performance with smaller models.
- Comprehensive Monitoring. Detailed logging and integration with Grafana/Prometheus for real-time system observation.
- Detailed Reporting. Generation of thorough vulnerability reports with exploitation guides.
- Smart Container Management. Automatic Docker image selection based on specific task requirements.
- Modern Interface. Clean and intuitive web UI for system management and monitoring.
- Comprehensive APIs. Full-featured REST and GraphQL APIs with Bearer token authentication for automation and integration.
- Persistent Storage. All commands and outputs are stored in PostgreSQL with pgvector extension.
- Scalable Architecture. Microservices-based design supporting horizontal scaling.
- Self-Hosted Solution. Complete control over your deployment and data.
- Flexible Authentication. Support for 10+ LLM providers (OpenAI, Anthropic, Google AI/Gemini, AWS Bedrock, Ollama, DeepSeek, GLM, Kimi, Qwen, MiniMax, Mistral, xAI, Custom for any OpenAI-compatible endpoint including Azure OpenAI) plus aggregators (OpenRouter, DeepInfra, Atlas Cloud, OpenCode Go plan). For production local deployments, see our vLLM + Qwen3.5-27B-FP8 guide.
- API Token Authentication. Secure Bearer token system for programmatic access to REST and GraphQL APIs.
- Quick Deployment. Easy setup through Docker Compose with comprehensive environment configuration.
Current Capability Boundaries
- PentAGI today is an autonomous and assistant-guided penetration testing platform, not a CALDERA-style Breach and Attack Simulation (BAS) or adversary emulation product with predefined campaigns or attack plans.
- BAS-like agent-authored attack scripts should be treated as conceptual or future work, not as a feature that is implemented today.
- The current flow report UI supports web view, copy to clipboard, Markdown download, and PDF download. JSON flow-report export is not documented as a supported output format today.
- Provider flexibility is available today through built-in providers and custom/OpenAI-compatible endpoints. See Custom LLM Provider Configuration and the vLLM + Qwen3.5-27B-FP8 guide.
Architecture
System Context
flowchart TB
classDef person fill:#08427B,stroke:#073B6F,color:#fff
classDef system fill:#1168BD,stroke:#0B4884,color:#fff
classDef external fill:#666666,stroke:#0B4884,color:#fff
pentester["👤 Security Engineer
(User of the system)"]
pentagi["✨ PentAGI
(Autonomous penetration testing system)"]
target["🎯 target-system
(System under test)"]
llm["🧠 llm-provider
(OpenAI/Anthropic/Ollama/Bedrock/Gemini/Custom)"]
search["🔍 search-systems
(Google/DuckDuckGo/Tavily/Firecrawl/Traversaal/Perplexity/Sploitus/SearXNG)"]
langfuse["📊 langfuse-ui
(LLM Observability Dashboard)"]
grafana["📈 grafana
(System Monitoring Dashboard)"]
pentester --> |Uses HTTPS| pentagi
pentester --> |Monitors AI HTTPS| langfuse
pentester --> |Monitors System HTTPS| grafana
pentagi --> |Tests Various protocols| target
pentagi --> |Queries HTTPS| llm
pentagi --> |Searches HTTPS| search
pentagi --> |Reports HTTPS| langfuse
pentagi --> |Reports HTTPS| grafana
class pentester person
class pentagi system
class target,llm,search,langfuse,grafana external
linkStyle default stroke:#ffffff,color:#ffffffContainer Architecture (click to expand)
/* Detailed source-code truncated for AI context efficiency. */Entity Relationship (click to expand)
/* Detailed source-code truncated for AI context efficiency. */Agent Interaction (click to expand)
sequenceDiagram
participant O as Orchestrator
participant R as Researcher
participant D as Developer
participant E as Executor
participant VS as Vector Store
participant KB as Knowledge Base
Note over O,KB: Flow Initialization
O->>VS: Query similar tasks
VS-->>O: Return experiences
O->>KB: Load relevant knowledge
KB-->>O: Return context
Note over O,R: Research Phase
O->>R: Analyze target
R->>VS: Search similar cases
VS-->>R: Return patterns
R->>KB: Query vulnerabilities
KB-->>R: Return known issues
R->>VS: Store findings
R-->>O: Research results
Note over O,D: Planning Phase
O->>D: Plan attack
D->>VS: Query exploits
VS-->>D: Return techniques
D->>KB: Load tools info
KB-->>D: Return capabilities
D-->>O: Attack plan
Note over O,E: Execution Phase
O->>E: Execute plan
E->>KB: Load tool guides
KB-->>E: Return procedures
E->>VS: Store results
E-->>O: Execution statusMemory System (click to expand)
graph TB
subgraph "Long-term Memory"
VS[(Vector Store<br/>Embeddings DB)]
KB[Knowledge Base<br/>Domain Expertise]
Tools[Tools Knowledge<br/>Usage Patterns]
end
subgraph "Working Memory"
Context[Current Context<br/>Task State]
Goals[Active Goals<br/>Objectives]
State[System State<br/>Resources]
end
subgraph "Episodic Memory"
Actions[Past Actions<br/>Commands History]
Results[Action Results<br/>Outcomes]
Patterns[Success Patterns<br/>Best Practices]
end
Context --> |Query| VS
VS --> |Retrieve| Context
Goals --> |Consult| KB
KB --> |Guide| Goals
State --> |Record| Actions
Actions --> |Learn| Patterns
Patterns --> |Store| VS
Tools --> |Inform| State
Results --> |Update| Tools
VS --> |Enhance| KB
KB --> |Index| VS
classDef ltm fill:#f9f,stroke:#333,stroke-width:2px,color:#000
classDef wm fill:#bbf,stroke:#333,stroke-width:2px,color:#000
classDef em fill:#bfb,stroke:#333,stroke-width:2px,color:#000
class VS,KB,Tools ltm
class Context,Goals,State wm
class Actions,Results,Patterns emChain Summarization (click to expand)
The chain summarization system manages conversation context growth by selectively summarizing older messages. This is critical for preventing token limits from being exceeded while maintaining conversation coherence.
flowchart TD
A[Input Chain] --> B{Needs Summarization?}
B -->|No| C[Return Original Chain]
B -->|Yes| D[Convert to ChainAST]
D --> E[Apply Section Summarization]
E --> F[Process Oversized Pairs]
F --> G[Manage Last Section Size]
G --> H[Apply QA Summarization]
H --> I[Rebuild Chain with Summaries]
I --> J{Is New Chain Smaller?}
J -->|Yes| K[Return Optimized Chain]
J -->|No| C
classDef process fill:#bbf,stroke:#333,stroke-width:2px,color:#000
classDef decision fill:#bfb,stroke:#333,stroke-width:2px,color:#000
classDef output fill:#fbb,stroke:#333,stroke-width:2px,color:#000
class A,D,E,F,G,H,I process
class B,J decision
class C,K outputThe algorithm operates on a structured representation of conversation chains (ChainAST) that preserves message types including tool calls and their responses. All summarization operations maintain critical conversation flow while reducing context size.
Global Summarizer Configuration Options
| Parameter | Environment Variable | Default | Description |
|---|---|---|---|
| Preserve Last | SUMMARIZER_PRESERVE_LAST |
true |
Whether to keep all messages in the last section intact |
| Use QA Pairs | SUMMARIZER_USE_QA |
true |
Whether to use QA pair summarization strategy |
| Summarize Human in QA | SUMMARIZER_SUM_MSG_HUMAN_IN_QA |
false |
Whether to summarize human messages in QA pairs |
| Last Section Size | SUMMARIZER_LAST_SEC_BYTES |
51200 |
Maximum byte size for last section (50KB) |
| Max Body Pair Size | SUMMARIZER_MAX_BP_BYTES |
16384 |
Maximum byte size for a single body pair (16KB) |
| Max QA Sections | SUMMARIZER_MAX_QA_SECTIONS |
10 |
Maximum QA pair sections to preserve |
| Max QA Size | SUMMARIZER_MAX_QA_BYTES |
65536 |
Maximum byte size for QA pair sections (64KB) |
| Keep QA Sections | SUMMARIZER_KEEP_QA_SECTIONS |
1 |
Number of recent QA sections to keep without summarization |
Assistant Summarizer Configuration Options
Assistant instances can use customized summarization settings to fine-tune context management behavior:
| Parameter | Environment Variable | Default | Description |
|---|---|---|---|
| Preserve Last | ASSISTANT_SUMMARIZER_PRESERVE_LAST |
true |
Whether to preserve all messages in the assistant's last section |
| Last Section Size | ASSISTANT_SUMMARIZER_LAST_SEC_BYTES |
76800 |
Maximum byte size for assistant's last section (75KB) |
| Max Body Pair Size | ASSISTANT_SUMMARIZER_MAX_BP_BYTES |
16384 |
Maximum byte size for a single body pair in assistant context (16KB) |
| Max QA Sections | ASSISTANT_SUMMARIZER_MAX_QA_SECTIONS |
7 |
Maximum QA sections to preserve in assistant context |
| Max QA Size | ASSISTANT_SUMMARIZER_MAX_QA_BYTES |
76800 |
Maximum byte size for assistant's QA sections (75KB) |
| Keep QA Sections | ASSISTANT_SUMMARIZER_KEEP_QA_SECTIONS |
3 |
Number of recent QA sections to preserve without summarization |
The assistant summarizer configuration provides more memory for context retention compared to the global settings, preserving more recent conversation history while still ensuring efficient token usage.
Summarizer Environment Configuration
# Default values for global summarizer logic
SUMMARIZER_PRESERVE_LAST=true
SUMMARIZER_USE_QA=true
SUMMARIZER_SUM_MSG_HUMAN_IN_QA=false
SUMMARIZER_LAST_SEC_BYTES=51200
SUMMARIZER_MAX_BP_BYTES=16384
SUMMARIZER_MAX_QA_SECTIONS=10
SUMMARIZER_MAX_QA_BYTES=65536
SUMMARIZER_KEEP_QA_SECTIONS=1
# Default values for assistant summarizer logic
ASSISTANT_SUMMARIZER_PRESERVE_LAST=true
ASSISTANT_SUMMARIZER_LAST_SEC_BYTES=76800
ASSISTANT_SUMMARIZER_MAX_BP_BYTES=16384
ASSISTANT_SUMMARIZER_MAX_QA_SECTIONS=7
ASSISTANT_SUMMARIZER_MAX_QA_BYTES=76800
ASSISTANT_SUMMARIZER_KEEP_QA_SECTIONS=3Advanced Agent Supervision (click to expand)
PentAGI includes sophisticated multi-layered agent supervision mechanisms to ensure efficient task execution, prevent infinite loops, and provide intelligent recovery from stuck states:
Execution Monitoring (Beta)
- Automatic Mentor Intervention: Adviser agent (mentor) is automatically invoked when execution patterns indicate potential issues
- Pattern Detection: Monitors identical tool calls (threshold: 5, configurable) and total tool calls (threshold: 10, configurable)
- Progress Analysis: Evaluates whether agent advances toward subtask objective, detects loops and inefficiencies
- Alternative Strategies: Recommends different approaches when current strategy fails
- Information Retrieval Guidance: Suggests searching for established solutions instead of reinventing
- Enhanced Response Format: Tool responses include both
<original_result>and<mentor_analysis>sections - Configurable: Enable via
EXECUTION_MONITOR_ENABLED(default: false), customize thresholds withEXECUTION_MONITOR_SAME_TOOL_LIMITandEXECUTION_MONITOR_TOTAL_TOOL_LIMIT
Best for: Smaller models (< 32B parameters), complex attack scenarios requiring continuous guidance, preventing agents from getting stuck on single approach
Performance Impact: 2-3x increase in execution time and token usage, but delivers 2x improvement in result quality based on testing with Qwen3.5-27B-FP8
Intelligent Task Planning (Beta)
- Automated Decomposition: Planner (adviser in planning mode) generates 3-7 specific, actionable steps before specialist agents begin work
- Context-Aware Plans: Analyzes full execution context via enricher agent to create informed plans
- Structured Assignment: Original request wrapped in
<task_assignment>structure with execution plan and instructions - Scope Management: Prevents scope creep by keeping agents focused on current subtask only
- Enriched Instructions: Plans highlight critical actions, potential pitfalls, and verification points
- Configurable: Enable via
AGENT_PLANNING_STEP_ENABLED(default: false)
Best for: Models < 32B parameters, complex penetration testing workflows, improving success rates on sophisticated tasks
Enhanced Adviser Configuration: Works exceptionally well when adviser agent uses stronger model or enhanced settings. Example: using same base model with maximum reasoning mode for adviser (see vllm-qwen3.5-27b-fp8.provider.yml) enables comprehensive task analysis and strategic planning from identical model architecture.
Performance Impact: Adds planning overhead but significantly improves completion rates and reduces redundant work
Tool Call Limits (Always Active)
- Hard Limits: Prevent runaway executions regardless of supervision mode status
- Differentiated by Agent Type:
- General agents (Assistant, Primary Agent, Pentester, Coder, Installer):
MAX_GENERAL_AGENT_TOOL_CALLS(default: 100) - Limited agents (Searcher, Enricher, Memorist, Generator, Reporter, Adviser, Reflector, Planner):
MAX_LIMITED_AGENT_TOOL_CALLS(default: 20)
- General agents (Assistant, Primary Agent, Pentester, Coder, Installer):
- Graceful Termination: Reflector guides agents to proper completion when approaching limits
- Resource Protection: Ensures system stability and prevents resource exhaustion
Reflector Integration (Always Active)
- Automatic Correction: Invoked when LLM fails to generate tool calls after 3 attempts
- Strategic Guidance: Analyzes failures and guides agents toward proper tool usage or barrier tools (
done,ask) - Recovery Mechanism: Provides contextual guidance based on specific failure patterns
- Limit Enforcement: Coordinates graceful termination when tool call limits are reached
Recommendations for Open Source Models
Must-Have for Models < 32B Parameters:
Testing with Qwen3.5-27B-FP8 demonstrates that enabling both Execution Monitoring and Task Planning is essential for smaller open source models:
- Quality Improvement: 2x better results compared to baseline execution without supervision
- Loop Prevention: Significantly reduces infinite loops and redundant work
- Attack Diversity: Encourages exploration of multiple attack vectors instead of fixating on single approach
- Air-Gapped Deployments: Enables production-grade autonomous pentesting in closed network environments with local LLM inference
Trade-offs:
- Token consumption: 2-3x increase due to mentor/planner invocations
- Execution time: 2-3x longer due to analysis and planning steps
- Result quality: 2x improvement in completeness, accuracy, and attack coverage
- Model requirements: Works best when adviser uses enhanced configuration (higher reasoning parameters, stronger model variant, or different model)
Configuration Strategy:
For optimal performance with smaller models, configure adviser agent with enhanced settings:
- Use same model with maximum reasoning mode (example:
vllm-qwen3.5-27b-fp8.provider.yml) - Or use stronger model for adviser while keeping base model for other agents
- Adjust monitoring thresholds based on task complexity and model capabilities
The architecture of PentAGI is designed to be modular, scalable, and secure. Here are the key components:
Core Services
- Frontend UI: React-based web interface with TypeScript for type safety
- Backend API: Go-based REST and GraphQL APIs with Bearer token authentication for programmatic access
- Vector Store: PostgreSQL with pgvector for semantic search and memory storage
- Task Queue: Async task processing system for reliable operation
- AI Agent: Multi-agent system with specialized roles for efficient testing
Optional Knowledge Graph
- Graphiti: Knowledge graph API for semantic relationship tracking and contextual understanding
- Neo4j: Graph database for storing and querying relationships between entities, actions, and outcomes
- When enabled, automatically captures agent responses and tool executions for a flow-scoped knowledge base
Monitoring Stack
- OpenTelemetry: Unified observability data collection and correlation
- Grafana: Real-time visualization and alerting dashboards
- VictoriaMetrics: High-performance time-series metrics storage
- Jaeger: End-to-end distributed tracing for debugging
- Loki: Scalable log aggregation and analysis
Analytics Platform
- Langfuse: Advanced LLM observability and performance analytics
- ClickHouse: Column-oriented analytics data warehouse
- Redis: High-speed caching and rate limiting
- MinIO: S3-compatible object storage for artifacts
Security Tools
- Web Scraper: Isolated browser environment for safe web interaction
- Pentesting Tools: Comprehensive suite of 20+ professional security tools
- Sandboxed Execution: All operations run in isolated containers
Memory Systems
- Long-term Memory: Persistent storage of knowledge and experiences
- Working Memory: Active context and goals for current operations
- Episodic Memory: Historical actions and success patterns
- Knowledge Base: Structured domain expertise and tool capabilities
- Context Management: Intelligently manages growing LLM context windows using chain summarization
The system uses Docker containers for isolation and easy deployment, with separate networks for core services, monitoring, and analytics to ensure proper security boundaries. Each component is designed to scale horizontally and can be configured for high availability in production environments.
Quick Start
For a step-by-step walkthrough that connects installation, configuration, LLM and embedding provider testing, and your first login, see the Installing and Configuring PentAGI guide. The sections below remain the detailed reference for each step.
System Requirements
- Docker and Docker Compose (or Podman - see Podman configuration)
- Minimum 2 vCPU
- Minimum 4GB RAM
- 20GB free disk space
- Internet access for downloading images and updates
Using Installer (Recommended)
PentAGI provides an interactive installer with a terminal-based UI for streamlined configuration and deployment. The installer guides you through system checks, LLM provider setup, search engine configuration, and security hardening.
Supported Platforms:
- Linux: amd64 download | arm64 download
- Windows: amd64 download
- macOS: amd64 (Intel) download | arm64 (M-series) download
macOS security warning: If macOS flags a downloaded installer, use only the official PentAGI links above, choose the archive that matches your CPU architecture, verify the source before continuing, and follow the installer troubleshooting guide before allowing the app to run.
Quick Installation (Linux amd64):
# Create installation directory
mkdir -p pentagi && cd pentagi
# Download installer
wget -O installer.zip https://pentagi.com/downloads/linux/amd64/installer-latest.zip
# Extract
unzip installer.zip
# Run interactive installer
./installerPrerequisites & Permissions:
The installer requires appropriate privileges to interact with the Docker API for proper operation. By default, it uses the Docker socket (/var/run/docker.sock) which requires either:
Option 1 (Recommended for production): Run the installer as root:
bashsudo ./installerOption 2 (Development environments): Grant your user access to the Docker socket by adding them to the
dockergroup:bash# Add your user to the docker group sudo usermod -aG docker $USER # Log out and log back in, or activate the group immediately newgrp docker # Verify Docker access (should run without sudo) docker ps⚠️ Security Note: Adding a user to the
dockergroup grants root-equivalent privileges. Only do this for trusted users in controlled environments. For production deployments, consider using rootless Docker mode or running the installer with sudo.
The installer will:
- System Checks: Verify Docker, network connectivity, and system requirements
- Environment Setup: Create and configure
.envfile with optimal defaults - Provider Configuration: Set up LLM providers (OpenAI, Anthropic, Gemini, Bedrock, Ollama, DeepSeek, GLM, Kimi, Qwen, MiniMax, Mistral, xAI, Custom)
- Search Engines: Configure DuckDuckGo, Google, Tavily, Firecrawl, Traversaal, Perplexity, Sploitus, SearXNG, and the optional internal browser-analytics fallback engine
- Security Hardening: Generate secure credentials and configure SSL certificates
- Deployment: Start PentAGI with docker-compose
Current Web Settings Coverage
The PentAGI web console already manages several settings areas after the server is up and running:
- Settings -> Providers: Create, edit, delete, and test user-defined provider profiles for supported provider types. These profiles control per-agent model selection, runtime parameters, reasoning options, and pricing metadata.
- Settings -> Prompts: Manage system, human, and tool prompt templates.
- Settings -> PentAGI API: Create and manage PentAGI Bearer tokens for REST and GraphQL access.
- Other UI-managed preferences: Favorite flows are stored as user preferences, and theme selection is handled from the main sidebar/profile controls rather than the Settings pages.
Still Server-Managed
The following configuration areas still need to be set on the server through environment variables, compose files, or mounted config files:
- LLM credentials and connection details: API keys, endpoints, auth modes, and provider-specific connection settings for OpenAI, Anthropic, Bedrock, Ollama, custom providers, and similar backends; config-path settings apply only where supported, such as
OLLAMA_SERVER_CONFIG_PATH,LLM_SERVER_CONFIG_PATH, andBEDROCK_CONFIG_PATH. - Search provider credentials and options: Settings such as
DUCKDUCKGO_*,GOOGLE_*,TAVILY_API_KEY,FIRECRAWL_API_*,TRAVERSAAL_API_KEY,PERPLEXITY_*,SEARXNG_*,SPLOITUS_ENABLED, and the optionalWEB_SEARCH_INTERNAL_*browser-analytics fallback settings. - Third-party integrations: Langfuse, Graphiti, and similar external services remain server-side configuration.
For Production & Enhanced Security:
For production deployments or security-sensitive environments, we strongly recommend using a distributed two-node architecture where worker operations are isolated on a separate server. This prevents untrusted code execution and network access issues on your main system.
See detailed guide: Worker Node Setup
The two-node setup provides:
- Isolated Execution: Worker containers run on dedicated hardware
- Network Isolation: Separate network boundaries for penetration testing
- Security Boundaries: Docker-in-Docker with TLS authentication
- OOB Attack Support: Dedicated port ranges for out-of-band techniques
Giving Agents Docker Without Giving Away the Host
Many pentest workflows need docker inside the agent's sandbox. There are two ways to provide it, and they differ sharply in risk.
Recommended — point sandboxes at a hardened dind daemon over TLS. Set DOCKER_INSIDE=true, leave DOCKER_SOCKET empty, and configure the daemon the sandbox may talk to:
DOCKER_INSIDE=true
DOCKER_SOCKET= # mount no socket
DOCKER_INSIDE_HOST=tcp://10.0.0.5:3376 # hardened dind endpoint
DOCKER_INSIDE_TLS_VERIFY=1
DOCKER_INSIDE_CERT_PATH=/etc/docker/dind/certs/client # path on the worker node
DOCKER_INSIDE_POLICY_TESTS=true # prove it on every worker; off by defaultPentAGI injects these into every worker container as DOCKER_HOST, DOCKER_TLS_VERIFY and DOCKER_CERT_PATH (the _INSIDE_ segment is dropped) and bind-mounts the certificate directory read-only at the same path, so docker works inside the sandbox with no further setup.
Not recommended — bind-mounting a Docker socket (DOCKER_SOCKET). This has two failure modes:
- Boot-order race: a bind-mount source that does not exist yet is created by Docker as a directory. After a worker-node reboot, a worker container can start before dind has recreated its socket — Docker then puts a directory where the socket belongs, and dind cannot start until it is removed by hand.
- Blast radius: the race is only reliably avoided when the mounted socket is the host daemon's, since that one always exists first. But that grants an autonomous agent the host Docker API: it can start a privileged container, mount
/, and compromise the entire node — PentAGI included.
Use DOCKER_SOCKET only on single-node development setups where the host daemon is already trusted.
See: Worker Node Setup for the full dind hardening and TLS configuration, and Worker Docker Access for the exact resolution algorithm.
Running Several Instances (`TENANT_ID`)
A single PentAGI installation needs none of this — leave TENANT_ID empty (the default) and nothing changes.
Set it when several PentAGI installations share external resources: one PostgreSQL server, one worker node, one Neo4j/Graphiti, one Langfuse. The typical case is a management backend per server with a common worker node and database. Because every instance numbers its flows from 1, they would otherwise collide on container names, database rows, knowledge-graph namespaces and session cookies. TENANT_ID namespaces all of it:
| Area | Effect when TENANT_ID=acme |
|---|---|
| PostgreSQL | The instance creates and works inside schema acme instead of public; extensions stay shared in DATABASE_EXTENSIONS_SCHEMA (default public, extensions on Supabase) |
| Worker containers | acme-pentagi-terminal-<flow> instead of pentagi-terminal-<flow>; volumes and hostnames follow, and both carry a pentagi.tenant label |
| Knowledge graph | Graphiti/Neo4j group ids become acme-flow- |
| Auth | Cookie and API token keys are derived from COOKIE_SIGNING_SALT plus the tenant, and the session cookie is renamed |
| Telemetry | Langfuse traces carry the tenant as their environment and a tenant:acme tag; OTel resources gain tenant_id |
The value must match ^[a-z][a-z0-9_]{0,31}$ — an invalid one aborts startup rather than being silently normalised.
Some things stay yours to set per instance, because they are host resources rather than names: DATA_DIR (two instances sharing it will overwrite each other's flow data), DOCKER_PORTS_BASE, the published ports, and INSTALLATION_ID. The effective values are printed at startup under Instance identity.
The installer provisions one instance per server. Running several on one server is possible — for example behind a shared nginx — but the stock docker-compose.yml uses fixed container and network names, so it has to be adapted to your own network layout first.
See: Multi-Instance Deployment for validation rules, upgrade notes and the full list of operator responsibilities.
Manual Installation
- Create a working directory or clone the repository:
mkdir pentagi && cd pentagi- Copy
.env.exampleto.envor download it:
curl -o .env https://raw.githubusercontent.com/vxcontrol/pentagi/master/.env.example- Touch examples files (
example.custom.provider.yml,example.ollama.provider.yml,example.bedrock.provider.yml) or download it.docker-compose.ymlmounts each of them into the container, and Docker creates a directory in place of a mount source that does not exist:
curl -o example.custom.provider.yml https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/custom-openai.provider.yml
curl -o example.ollama.provider.yml https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/ollama-llama318b.provider.yml
curl -o example.bedrock.provider.yml https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/configs/bedrock.provider.yml- Fill in the required API keys in
.envfile.
/* Detailed source-code truncated for AI context efficiency. */Change all security related environment variables in
.envfile to improve security.Security related environment variables
Main Security Settings
COOKIE_SIGNING_SALT- Salt for cookie signing, change to random valuePUBLIC_URL- Public URL of your server (eg.https://pentagi.example.com)SERVER_SSL_CRTandSERVER_SSL_KEY- Custom paths to your existing SSL certificate and key for HTTPS (these paths should be used in the docker-compose.yml file to mount as volumes)TENANT_ID- Leave empty unless this instance shares external resources with another PentAGI installation. When set, it is mixed into the cookie and API token signing keys and renames the session cookie, so a session minted by one instance is rejected by the others even though they share the sameCOOKIE_SIGNING_SALT. See Running Several Instances
Scraper Access
SCRAPER_PUBLIC_URL- Public URL for scraper if you want to use different scraper server for public URLsSCRAPER_PRIVATE_URL- Private URL for scraper (local scraper server in docker-compose.yml file to access it to local URLs)
Access Credentials
PENTAGI_POSTGRES_USERandPENTAGI_POSTGRES_PASSWORD- PostgreSQL credentialsNEO4J_USERandNEO4J_PASSWORD- Neo4j credentials (for Graphiti knowledge graph)
- Remove all inline comments from
.envfile if you want to use it in VSCode or other IDEs as a envFile option:
perl -i -pe 's/\s+#.*$//' .env- Run the PentAGI stack:
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose.yml
docker compose up -dVisit localhost:8443 to access PentAGI Web UI (default is [email protected] / admin)
Web UI Accounts
PentAGI does not expose public self-service sign-up from the login page. A fresh installation creates the default local administrator account:
- Email:
[email protected] - Password:
admin
On first login, change the default password before using the instance for real work. If the administrator password is lost later, use the installer maintenance menu to reset the default [email protected] account password.
For multi-user setups, an authenticated administrator can manage local users through the Users REST API (/api/v1/users/). The OpenAPI UI is available at https://localhost:8443/api/v1/swagger/index.html after the instance is running.
[!NOTE]
If you caught an error aboutpentagi-networkorobservability-networkorlangfuse-networkyou need to rundocker-compose.ymlfirstly to create these networks and after that rundocker-compose-langfuse.yml,docker-compose-graphiti.yml, anddocker-compose-observability.ymlto use Langfuse, Graphiti, and Observability services.You have to set at least one Language Model provider (OpenAI, Anthropic, Gemini, AWS Bedrock, or Ollama) to use PentAGI. AWS Bedrock provides enterprise-grade access to multiple foundation models from leading AI companies, while Ollama provides zero-cost local inference if you have sufficient computational resources. Additional API keys for search engines are optional but recommended for better results.
For fully local deployment with advanced models: See our comprehensive guide on Running PentAGI with vLLM and Qwen3.5-27B-FP8 for a production-grade local LLM setup. This configuration achieves ~13,000 TPS for prompt processing and ~650 TPS for completion on 4× RTX 5090 GPUs, supporting 12+ concurrent flows with complete independence from cloud providers.
LLM_SERVER_*environment variables are experimental feature and will be changed in the future. Right now you can use them to specify custom LLM server URL and one model for all agent types.
PROXY_URLroutes the backend's own HTTP calls through a proxy: every request to LLM and embedding providers and to search engines — self-hosted ones such as Ollama or SearXNG included, as there is noNO_PROXYexemption — and the update check. Sandbox containers (where agents run their commands), the scraper, the Graphiti service, OAuth sign-in, and Langfuse/OpenTelemetry export do not use it, so if you need isolation from external networks, restrict their outbound traffic at the network level.The
docker-compose.ymlfile runs the PentAGI service as root user because it needs access to docker.sock for container management. If you're using TCP/IP network connection to Docker instead of socket file, you can remove root privileges and use the defaultpentagiuser for better security.
Accessing PentAGI from External Networks
By default, the PentAGI web interface and the other compose services bind to 127.0.0.1 (localhost only) for security. To access PentAGI from other machines on your network, you need to configure external access.
[!IMPORTANT]
Flow sandboxes are not bound to localhost. Each flow publishes two TCP ports for out-of-band callbacks such as reverse shells, taken from the 2000-port window that starts atDOCKER_PORTS_BASE(28000–29999by default), onDOCKER_PUBLIC_IP—0.0.0.0by default, that is every interface of the Docker host. Docker routes published ports aroundufwrules and lets them throughfirewalldwith its owndockerzone, so restrict this range with a firewall in front of the host or, with Docker's default iptables backend, with rules in theDOCKER-USERchain. WithDOCKER_NETWORK=host, sandboxes share the host's network stack, so their listeners are exposed without any port mapping.
Configuration Steps
- Update
.envfile with your server's IP address:
# Network binding - allow external connections
PENTAGI_LISTEN_IP=0.0.0.0
PENTAGI_LISTEN_PORT=8443
# Public URL - use your actual server IP or hostname
# Replace 192.168.1.100 with your server's IP address
PUBLIC_URL=https://192.168.1.100:8443
# CORS origins - list all URLs that will access PentAGI
# Include localhost for local access AND your server IP for external access
CORS_ORIGINS=https://localhost:8443,https://192.168.1.100:8443[!IMPORTANT]
- Replace
192.168.1.100with your actual server's IP address- Do NOT use
0.0.0.0inPUBLIC_URLorCORS_ORIGINS- use the actual IP address- Include both localhost and your server IP in
CORS_ORIGINSfor flexibility
- Recreate containers to apply the changes:
docker compose down
docker compose up -d --force-recreate- Verify port binding:
docker ps | grep pentagiYou should see 0.0.0.0:8443->8443/tcp or :::8443->8443/tcp.
If you see 127.0.0.1:8443->8443/tcp, the environment variable wasn't picked up. In this case, directly edit docker-compose.yml line 31:
ports:
- "0.0.0.0:8443:8443"Then recreate containers again.
- Configure firewall to allow incoming connections on port 8443:
# Ubuntu/Debian with UFW
sudo ufw allow 8443/tcp
sudo ufw reload
# CentOS/RHEL with firewalld
sudo firewall-cmd --permanent --add-port=8443/tcp
sudo firewall-cmd --reload- Access PentAGI:
- Local access:
https://localhost:8443 - Network access:
https://your-server-ip:8443
[!NOTE]
You'll need to accept the self-signed SSL certificate warning in your browser when accessing via IP address.
Running PentAGI with Podman
PentAGI fully supports Podman as a Docker alternative. However, when using Podman in rootless mode, the scraper service requires special configuration because rootless containers cannot bind privileged ports (ports below 1024).
Podman Rootless Configuration
The default scraper configuration uses port 443 (HTTPS), which is a privileged port. For Podman rootless, reconfigure the scraper to use a non-privileged port:
1. Edit docker-compose.yml - modify the scraper service (around line 199):
scraper:
image: vxcontrol/scraper:latest
restart: unless-stopped
container_name: scraper
hostname: scraper
expose:
- 3000/tcp # Changed from 443 to 3000
ports:
- "${SCRAPER_LISTEN_IP:-127.0.0.1}:${SCRAPER_LISTEN_PORT:-9443}:3000" # Map to port 3000
environment:
- MAX_CONCURRENT_SESSIONS=${LOCAL_SCRAPER_MAX_CONCURRENT_SESSIONS:-10}
- USERNAME=${LOCAL_SCRAPER_USERNAME:-someuser}
- PASSWORD=${LOCAL_SCRAPER_PASSWORD:-somepass}
logging:
options:
max-size: 50m
max-file: "7"
volumes:
- scraper-ssl:/usr/src/app/ssl
networks:
- pentagi-network
shm_size: 2g2. Update .env file - change the scraper URL to use HTTP and port 3000:
# Scraper configuration for Podman rootless
SCRAPER_PRIVATE_URL=http://someuser:somepass@scraper:3000/
LOCAL_SCRAPER_USERNAME=someuser
LOCAL_SCRAPER_PASSWORD=somepass[!IMPORTANT]
Key changes for Podman:
- Use HTTP instead of HTTPS for
SCRAPER_PRIVATE_URL- Use port 3000 instead of 443
- Change internal
exposeto3000/tcp- Update port mapping to target
3000instead of443
3. Recreate containers:
podman-compose down
podman-compose up -d --force-recreate4. Test scraper connectivity:
# Test from within the pentagi container
podman exec -it pentagi wget -O- "http://someuser:somepass@scraper:3000/html?url=http://example.com"If you see HTML output, the scraper is working correctly.
Podman Rootful Mode
If you're running Podman in rootful mode (with sudo), you can use the default configuration without modifications. The scraper will work on port 443 as intended.
Docker Compatibility
All Podman configurations remain fully compatible with Docker. The non-privileged port approach works identically on both container runtimes.
Assistant Configuration
PentAGI allows you to configure default behavior for assistants:
| Variable | Default | Description |
|---|---|---|
ASSISTANT_USE_AGENTS |
false |
Controls the default value for agent usage when creating new assistants |
The ASSISTANT_USE_AGENTS setting affects the initial state of the "Use Agents" toggle when creating a new assistant in the UI:
false(default): New assistants are created with agent delegation disabled by defaulttrue: New assistants are created with agent delegation enabled by default
Note that users can always override this setting by toggling the "Use Agents" button in the UI when creating or editing an assistant. This environment variable only controls the initial default state.
How to Use PentAGI After Login
Once the stack is running and you can sign in to the web UI, the fastest way to start is through the Flows workflow.
1. Create your first flow
- Open Flows in the sidebar.
- Click New Flow.
- Choose the mode that fits your goal:
- Automation: fully autonomous execution for a testing goal you want PentAGI to carry out end-to-end
- Assistant: interactive back-and-forth help when you want to steer the investigation step by step. In this mode you can also enable the Use Agents toggle to let PentAGI delegate subtasks to specialized sub-agents for more complex investigations.
- Select the LLM provider you want to use for this flow.
- Describe the target and the objective in natural language in the message box.
Good first prompts usually include:
- the target system or URL
- the type of assessment you want
- any scope limitations or rules of engagement
- the result you expect, such as a vulnerability report or validation of a hypothesis
Example:
Assess https://target.example for common web application vulnerabilities. Focus on authentication, file handling, and injection issues. Stay within the provided target only and summarize confirmed findings with reproduction steps.Only test systems you own or are explicitly authorized to assess. See EULA.md for the acceptable use requirements.
2. Use templates for repeatable workflows
The new flow form includes a template picker, which can prefill the message box with a saved flow template. This is useful when you run similar assessments repeatedly.
- Use an existing template if you already have one saved in Templates
- Start from the example prompt in
examples/prompts/base_web_pentest.mdif you need a practical baseline for web testing - Adjust the target, scope, and constraints before starting the flow
Templates are starting points. You do not need special syntax to use PentAGI: plain natural-language instructions work well as long as the target and goal are clear.
3. Monitor execution and review output
After submitting the flow, PentAGI opens the flow page automatically.
- Use the main flow view to follow messages, agent activity, and task progress
- Inspect tool activity and terminal output as the flow runs
- Review generated tasks and subtasks to understand what PentAGI is doing
Once the flow has enough results, use the Report menu on the flow page to:
- open the report in a web view
- copy the generated report to the clipboard
- download the report as Markdown
- download the report as PDF
4. Use the Assistant view to steer an active flow
Each flow also includes an Assistant view for interactive guidance. This is useful when the autonomous run uncovers something that needs human direction instead of a hard restart.
- Open the Assistant view for the same flow when you want to inspect the current state before changing anything.
- Use the assistant to check flow status, stop the current task, submit follow-up instructions, or patch the remaining planned subtasks before the next step runs.
- Treat this as an explicit control path for the current flow, not as an invisible background queue. If you want to change direction, say so clearly and keep the new instruction tied to the current engagement scope.
- This works best for clarifying scope, redirecting priorities after intermediate findings, or answering an automation checkpoint without losing the rest of the flow context.
5. Manage flow-scoped files
Each flow has its own Files tab in the flow page. Files are scoped to the parent flow: they live in {dataDir}/flow-{id}-data/ on the host and never leak into other flows.
The tab exposes three sources of files:
- Uploads (
uploads/): files you provide from the web UI. Use the Upload files action, or drag and drop directly onto the Files tab. While the agent container is running, uploaded files are also pushed into it at/work/uploads/so the agent can read them with normal shell tools. - Resources (
resources/): files attached from your saved user resources library via Attach resources from library. Attached resources are copied into the flow and pushed into the running container at/work/resources/. - Container (
container/): snapshots pulled from the running agent container via Pull file or directory from container. These are read-only on the flow side and are never sent back to the container.
Per-file actions in the Files tab include Download, Copy path, Save as resource (promote a flow file into your reusable resources library), and Delete. The Pull action is disabled when the container is not running, with the tooltip "Container is not running".
Uploaded files and attached resources are listed automatically in the agent's system prompts via the {{.UserFiles}} template variable, which renders a compact <task_files> XML block (with nested `` and <resources> sections), so the assistant and automation agents can reference them by path without you pasting the contents into chat. Container snapshots are visible in the UI only and are not auto-injected back into the prompt.
Current limits and limitations to be aware of:
- Maximum upload file size is 300 MB; per upload request up to 1000 files and 2 GB total. File names are capped at 255 bytes (roughly 255 ASCII characters; non-ASCII names use multiple bytes per character).
- Uploads and resources are mirrored into the running container at the fixed paths
/work/uploads/and/work/resources/; files written to other container paths are not auto-mirrored back into the flow file model. Container snapshots can originate from any container path you pull (for example/etc/...) and are cached on the flow side undercontainer/; they are not pushed back into the container. - Container snapshots are point-in-time pulls. Editing a snapshot in the UI does not write back into the running container.
- Deleting a flow today removes the flow record and its long-term memory entries, but does not yet archive or remove the flow's
flow-{id}-data/directory on disk. Operators are still expected to clean up the data directory manually if they want to reclaim the space.
For early testing, start with a narrow target and a single clear objective. This makes the output easier to review and helps you refine your prompts before running larger assessments.
API Access
PentAGI provides comprehensive programmatic access through both REST and GraphQL APIs, allowing you to integrate penetration testing workflows into your automation pipelines, CI/CD processes, and custom applications.
Generating API Tokens
API tokens are managed through the PentAGI web interface:
- Navigate to Settings → API Tokens in the web UI
- Click Create Token to generate a new API token
- Configure token properties:
- Name (optional): A descriptive name for the token
- Expiration Date: When the token will expire (minimum 1 minute, maximum 3 years)
- Click Create and copy the token immediately - it will only be shown once for security reasons
- Use the token as a Bearer token in your API requests
Each token is associated with your user account and inherits your role's permissions.
Using API Tokens
Include the API token in the Authorization header of your HTTP requests:
# GraphQL API example
curl -X POST https://your-pentagi-instance:8443/api/v1/graphql \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"query": "{ flows { id title status } }"}'
# REST API example
curl https://your-pentagi-instance:8443/api/v1/flows \
-H "Authorization: Bearer YOUR_API_TOKEN"API Exploration and Testing
PentAGI provides interactive documentation for exploring and testing API endpoints:
GraphQL Playground
Access the GraphQL Playground at https://your-pentagi-instance:8443/api/v1/graphql/playground
- Click the HTTP Headers tab at the bottom
- Add your authorization header:json
{ "Authorization": "Bearer YOUR_API_TOKEN" } - Explore the schema, run queries, and test mutations interactively
Swagger UI
Access the REST API documentation at https://your-pentagi-instance:8443/api/v1/swagger/index.html
- Click the Authorize button
- Enter your token in the format:
Bearer YOUR_API_TOKEN - Click Authorize to apply
- Test endpoints directly from the Swagger UI
Generating API Clients
You can generate type-safe API clients for your preferred programming language using the schema files included with PentAGI:
GraphQL Clients
The GraphQL schema is available at:
- Web UI: Navigate to Settings to download
schema.graphqls - Direct file:
backend/pkg/graph/schema.graphqlsin the repository
Generate clients using tools like:
- GraphQL Code Generator (JavaScript/TypeScript): https://the-guild.dev/graphql/codegen
- genqlient (Go): https://github.com/Khan/genqlient
- Apollo iOS (Swift): https://www.apollographql.com/docs/ios
REST API Clients
The OpenAPI specification is available at:
- Swagger JSON:
https://your-pentagi-instance:8443/api/v1/swagger/doc.json - Swagger YAML: Available in
backend/pkg/server/docs/swagger.yaml
Generate clients using:
OpenAPI Generator: https://openapi-generator.tech
bashopenapi-generator-cli generate \ -i https://your-pentagi-instance:8443/api/v1/swagger/doc.json \ -g python \ -o ./pentagi-clientSwagger Codegen: https://github.com/swagger-api/swagger-codegen
bashswagger-codegen generate \ -i https://your-pentagi-instance:8443/api/v1/swagger/doc.json \ -l typescript-axios \ -o ./pentagi-clientswagger-typescript-api (TypeScript): https://github.com/acacode/swagger-typescript-api
bashnpx swagger-typescript-api \ -p https://your-pentagi-instance:8443/api/v1/swagger/doc.json \ -o ./src/api \ -n pentagi-api.ts
API Usage Examples
Creating a New Flow (GraphQL)
mutation CreateFlow {
createFlow(
modelProvider: "openai"
input: "Test the security of https://example.com"
) {
id
title
status
createdAt
}
}Listing Flows (REST API)
curl https://your-pentagi-instance:8443/api/v1/flows \
-H "Authorization: Bearer YOUR_API_TOKEN" \
| jq '.flows[] | {id, title, status}'Python Client Example
import requests
class PentAGIClient:
def __init__(self, base_url, api_token):
self.base_url = base_url
self.headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json"
}
def create_flow(self, provider, target):
query = """
mutation CreateFlow($provider: String!, $input: String!) {
createFlow(modelProvider: $provider, input: $input) {
id
title
status
}
}
"""
response = requests.post(
f"{self.base_url}/api/v1/graphql",
json={
"query": query,
"variables": {
"provider": provider,
"input": target
}
},
headers=self.headers
)
return response.json()
def get_flows(self):
response = requests.get(
f"{self.base_url}/api/v1/flows",
headers=self.headers
)
return response.json()
# Usage
client = PentAGIClient(
"https://your-pentagi-instance:8443",
"your_api_token_here"
)
# Create a new flow
flow = client.create_flow("openai", "Scan https://example.com for vulnerabilities")
print(f"Created flow: {flow}")
# List all flows
flows = client.get_flows()
print(f"Total flows: {len(flows['flows'])}")TypeScript Client Example
/* Detailed source-code truncated for AI context efficiency. */Security Best Practices
When working with API tokens:
- Never commit tokens to version control - use environment variables or secrets management
- Rotate tokens regularly - set appropriate expiration dates and create new tokens periodically
- Use separate tokens for different applications - makes it easier to revoke access if needed
- Monitor token usage - review API token activity in the Settings page
- Revoke unused tokens - disable or delete tokens that are no longer needed
- Use HTTPS only - never send API tokens over unencrypted connections
Token Management
- View tokens: See all your active tokens in Settings → API Tokens
- Edit tokens: Update token names or revoke tokens
- Delete tokens: Permanently remove tokens (this action cannot be undone)
- Token ID: Each token has a unique ID that can be copied for reference
The token list shows:
- Token name (if provided)
- Token ID (unique identifier)
- Status (active/revoked/expired)
- Creation date
- Expiration date
Custom LLM Provider Configuration
When using custom LLM providers with the LLM_SERVER_* variables, you can fine-tune the reasoning format used in requests.
[!TIP]
For production-grade local deployments, consider using vLLM with Qwen3.5-27B-FP8 for optimal performance. See our comprehensive deployment guide which includes hardware requirements, configuration templates (thinking mode and non-thinking mode), and performance benchmarks showing 13K TPS prompt processing on 4× RTX 5090 GPUs.
| Variable | Default | Description |
|---|---|---|
LLM_SERVER_URL |
Base URL for the custom LLM API endpoint | |
LLM_SERVER_KEY |
API key for the custom LLM provider | |
LLM_SERVER_MODEL |
Default model to use (can be overridden in provider config) | |
LLM_SERVER_CONFIG_PATH |
Path to the YAML configuration file for agent-specific models | |
LLM_SERVER_PROVIDER |
Provider name prefix for model names (e.g., openrouter, deepseek for LiteLLM proxy) |
|
LLM_SERVER_PRESERVE_REASONING |
false |
Preserve reasoning content in multi-turn conversations (required by some providers) |
LLM_SERVER_API_TYPE |
azure or azure_ad for an Azure OpenAI deployment, empty for a plain endpoint; see Using Azure OpenAI |
|
LLM_SERVER_API_VERSION |
2024-10-21 |
The api-version Azure requires; ignored by a plain endpoint |
The LLM_SERVER_PROVIDER setting is particularly useful when using LiteLLM proxy, which adds a provider prefix to model names. For example, when connecting to Moonshot API through LiteLLM, models like kimi-2.5 become moonshot/kimi-2.5. By setting LLM_SERVER_PROVIDER=moonshot, you can use the same provider configuration file for both direct API access and LiteLLM proxy access without modifications.
The LLM_SERVER_PRESERVE_REASONING setting controls whether reasoning content is preserved in multi-turn conversations:
false(default): Reasoning content is not preserved in conversation historytrue: Reasoning content is preserved and sent in subsequent API calls
This setting is required by some LLM providers (e.g., Moonshot) that return errors like "thinking is enabled but reasoning_content is missing in assistant tool call message" when reasoning content is not included in multi-turn conversations. Enable this setting if your provider requires reasoning content to be preserved.
Using Azure OpenAI
The custom provider calls Azure OpenAI deployments directly:
LLM_SERVER_URL=https://<resource>.openai.azure.com # the Endpoint from the resource's Keys and Endpoint page, without an /openai path
LLM_SERVER_KEY=your_azure_openai_api_key # KEY 1 or KEY 2 from the same page
LLM_SERVER_API_TYPE=azure # the api-key header and deployment URLs
LLM_SERVER_API_VERSION=2024-10-21 # the default; a newer version works as well
LLM_SERVER_MODEL= # Leave empty, models are specified in the config
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/azure-openai.provider.ymlAzure addresses a deployment, not a model: PentAGI calls <LLM_SERVER_URL>/openai/deployments/<model>/chat/completions?api-version=<version>, so every model: in the provider config must be the name of a deployment in your resource, and LLM_SERVER_PROVIDER must stay empty, because its prefix would become part of the deployment name. The bundled azure-openai.provider.yml expects deployments named after their models: gpt-4.1, gpt-4.1-mini and o4-mini. If yours are named differently, copy the file, change the model: values, and mount your copy:
PENTAGI_LLM_SERVER_CONFIG_PATH=/path/on/host/my-azure.provider.yml # mounted at /opt/pentagi/conf/custom.provider.yml
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/custom.provider.ymlAfter docker compose up -d, check the deployments before running a flow. Without -config, ctester tests the file LLM_SERVER_CONFIG_PATH names, the bundled one or your copy; the bundled configuration's own run is in examples/tests/azure-openai-report.md:
docker exec -it pentagi /opt/pentagi/bin/ctester- PentAGI does not read a model catalogue from Azure, so it knows no context window for a deployment and does not compact agent chains to fit one.
LLM_SERVER_API_TYPE=azure_adsendsLLM_SERVER_KEYas a Microsoft Entra ID bearer token instead of an API key. PentAGI does not refresh it, and Entra tokens expire after about an hour, so use an API key for anything longer.- Azure's OpenAI-compatible v1 API works as a plain endpoint too: set
LLM_SERVER_URL=https://<resource>.openai.azure.com/openai/v1and leaveLLM_SERVER_API_TYPEempty;model:is still the deployment name. - The installer's custom provider form has API Type and API Version fields for the same two settings.
- Embeddings are configured separately through
EMBEDDING_*and have no Azure mode;LLM_SERVER_API_TYPEdoes not apply to them.
Troubleshooting: tool-call (function-call) parser errors
PentAGI drives its agents with tool calls (also called function calls), so any custom OpenAI-compatible backend configured through LLM_SERVER_* must return valid tool-call JSON in the format the OpenAI Chat Completions API defines. When the backend emits malformed, truncated, or non-conforming tool-call arguments, the agent chain cannot continue.
Self-hosted engines such as llama.cpp, SGLang, and vLLM usually require a specific tool-call parser and a matching chat template to produce correct tool-call output. If the parser is missing or mismatched for the model you are serving, tool-call arguments can come back corrupted. Compatibility therefore depends on the backend's tool-call/function-call behavior and configuration, not on PentAGI alone; not every llama.cpp or SGLang setup produces valid tool calls out of the box.
Typical symptoms:
- Backend or proxy errors such as
Failed to parse tool call arguments as JSON(often surfaced through a LiteLLM proxy as an HTTP 500), or other unexpected 5xx/4xx responses from the LLM endpoint. - A flow that runs for a few steps and then stops responding to new input in the UI.
- Repeated or looping tool calls that never converge.
- A flow that fails right at the start with
failed to select primary docker image via llm call, because the first action in a flow is an LLM tool call to choose the container image; a backend that cannot return a valid tool call fails at this step too.
How to investigate:
- Check both sides of the connection: the PentAGI logs (
docker compose logs -f pentagi) and the inference backend or proxy logs (llama.cpp, SGLang, vLLM, or LiteLLM). The backend log usually shows the same parse error when it produced the malformed tool call. - Validate the provider before running a full flow with the
ctesterutility, which exercises tool-calling agent types directly. See Testing LLM Agents. - Confirm the backend's tool-call parser and chat template are the ones recommended for the model you are serving, and that the model itself supports tool calling.
- Update PentAGI to the latest build. Recent versions sanitize malformed function-call arguments returned by the model so a single bad response no longer stalls the whole flow; older builds forwarded the corrupted arguments and could get stuck.
Ollama Provider Configuration
PentAGI supports Ollama for both local LLM inference (zero-cost, enhanced privacy) and Ollama Cloud (managed service with free tier).
Configuration Variables
| Variable | Default | Description |
|---|---|---|
OLLAMA_SERVER_URL |
URL of your Ollama server or Ollama Cloud | |
OLLAMA_SERVER_API_KEY |
API key for Ollama Cloud authentication | |
OLLAMA_SERVER_MODEL |
Default model for inference | |
OLLAMA_SERVER_CONFIG_PATH |
Path to custom agent configuration file | |
OLLAMA_SERVER_PULL_MODELS_TIMEOUT |
600 |
Timeout for model downloads (seconds) |
OLLAMA_SERVER_PULL_MODELS_ENABLED |
false |
Auto-download models on startup |
OLLAMA_SERVER_LOAD_MODELS_ENABLED |
false |
Query server for available models |
Ollama Cloud Configuration
Ollama Cloud provides managed inference with a free tier and paid plans. Paid usage is billed from included or purchased credits at each model's published per-token rate; off-peak discounts apply to selected DeepSeek models.
Free Tier Setup (Single Model)
# Free tier allows one model at a time
OLLAMA_SERVER_URL=https://ollama.com
OLLAMA_SERVER_API_KEY=your_ollama_cloud_api_key
OLLAMA_SERVER_MODEL=gpt-oss:120b # Example: OpenAI OSS 120B modelPaid Tier Setup (Multi-Model with Pre-built Configuration)
For paid tiers supporting concurrent requests and pay-as-you-go credits, use the pre-built Ollama Cloud configuration:
# Using pre-built Ollama Cloud configuration (included in Docker image)
OLLAMA_SERVER_URL=https://ollama.com
OLLAMA_SERVER_API_KEY=your_ollama_cloud_api_key
OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama-cloud.provider.ymlThe pre-built ollama-cloud.provider.yml configuration includes optimized model assignments for all agent types:
- Simple/Reflector/Enricher:
minimax-m2.7:cloud- Reliable low-latency utility model - Simple JSON/Primary Agent/Assistant/Pentester/Searcher:
deepseek-v4.1-flash:cloud- Efficient long-context model at the same input/output rate as MiniMax M2.7 - Generator/Adviser:
kimi-k3:cloud- Flagship long-horizon planning and knowledge-work model - Refiner:
glm-5.3:cloud- Flagship coding and agentic model - Coder/Installer:
kimi-k2.7-code:cloud- Coding-specialized long-context model
Custom Configuration (Advanced)
To create your own agent configuration, mount a custom file from your host filesystem:
# Using custom provider configuration
OLLAMA_SERVER_URL=https://ollama.com
OLLAMA_SERVER_API_KEY=your_ollama_cloud_api_key
OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama.provider.yml
# Mount custom configuration from host filesystem (in .env or docker-compose override)
PENTAGI_OLLAMA_SERVER_CONFIG_PATH=/path/on/host/my-ollama-config.ymlThe PENTAGI_OLLAMA_SERVER_CONFIG_PATH environment variable maps your host configuration file to /opt/pentagi/conf/ollama.provider.yml inside the container.
Example custom configuration (my-ollama-config.yml):
primary_agent:
model: "deepseek-v4-flash:cloud"
temperature: 1.0
top_p: 0.95
max_tokens: 32768
coder:
model: "kimi-k2.7-code:cloud"
temperature: 1.0
max_tokens: 20480Local Ollama Configuration
For self-hosted Ollama instances:
# Basic local Ollama setup
OLLAMA_SERVER_URL=http://localhost:11434
OLLAMA_SERVER_MODEL=llama3.1:8b-instruct-q8_0
# Production setup with auto-pull and model discovery
OLLAMA_SERVER_URL=http://ollama-server:11434
OLLAMA_SERVER_PULL_MODELS_ENABLED=true
OLLAMA_SERVER_PULL_MODELS_TIMEOUT=900
OLLAMA_SERVER_LOAD_MODELS_ENABLED=true
# Using pre-built configurations from Docker image
OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama-llama318b.provider.yml
# or
OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama-qwen332b-fp16-tc.provider.yml
# or
OLLAMA_SERVER_CONFIG_PATH=/opt/pentagi/conf/ollama-qwq32b-fp16-tc.provider.ymlPerformance Considerations:
- Model Discovery (
OLLAMA_SERVER_LOAD_MODELS_ENABLED=true): Adds 1-2s startup latency querying Ollama API - Auto-pull (
OLLAMA_SERVER_PULL_MODELS_ENABLED=true): First startup may take several minutes downloading models - Pull timeout (
OLLAMA_SERVER_PULL_MODELS_TIMEOUT=900): 15 minutes in seconds - Static Config: Disable both flags and specify models in config file for fastest startup
Creating Custom Ollama Models with Extended Context
PentAGI requires models with larger context windows than the default Ollama configurations. You need to create custom models with increased num_ctx parameter through Modelfiles. While typical agent workflows consume around 64K tokens, PentAGI uses 110K context size for safety margin and handling complex penetration testing scenarios.
Important: The num_ctx parameter can only be set during model creation via Modelfile - it cannot be changed after model creation or overridden at runtime.
Example: Qwen3 32B FP16 with Extended Context
Create a Modelfile named Modelfile_qwen3_32b_fp16_tc:
FROM qwen3:32b-fp16
PARAMETER num_ctx 110000
PARAMETER temperature 0.3
PARAMETER top_p 0.8
PARAMETER min_p 0.0
PARAMETER top_k 20
PARAMETER repeat_penalty 1.1Build the custom model:
ollama create qwen3:32b-fp16-tc -f Modelfile_qwen3_32b_fp16_tcExample: QwQ 32B FP16 with Extended Context
Create a Modelfile named Modelfile_qwq_32b_fp16_tc:
FROM qwq:32b-fp16
PARAMETER num_ctx 110000
PARAMETER temperature 0.2
PARAMETER top_p 0.7
PARAMETER min_p 0.0
PARAMETER top_k 40
PARAMETER repeat_penalty 1.2Build the custom model:
ollama create qwq:32b-fp16-tc -f Modelfile_qwq_32b_fp16_tcNote: The QwQ 32B FP16 model requires approximately 71.3 GB VRAM for inference. Ensure your system has sufficient GPU memory before attempting to use this model.
These custom models are referenced in the pre-built provider configuration files (ollama-qwen332b-fp16-tc.provider.yml and ollama-qwq32b-fp16-tc.provider.yml) that are included in the Docker image at /opt/pentagi/conf/.
OpenAI Provider Configuration
PentAGI integrates with OpenAI's comprehensive model lineup, featuring advanced reasoning capabilities with extended chain-of-thought, agentic models with enhanced tool integration, and specialized code models for security engineering.
Configuration Variables
| Variable | Default | Description |
|---|---|---|
OPEN_AI_KEY |
API key for OpenAI services | |
OPEN_AI_SERVER_URL |
https://api.openai.com/v1 |
OpenAI API endpoint |
Configuration Examples
# Basic OpenAI setup
OPEN_AI_KEY=your_openai_api_key
OPEN_AI_SERVER_URL=https://api.openai.com/v1
# Using with proxy for enhanced security
OPEN_AI_KEY=your_openai_api_key
PROXY_URL=http://your-proxy:8080Supported Models
PentAGI supports 32 OpenAI models with tool calling, streaming, reasoning modes, and prompt caching. Models marked with * are used in default configuration. Models marked ⚠️ are deprecated by OpenAI and kept only for backward compatibility with agent configs already pinned to those names — avoid them for new assignments.
GPT-5.6 Series - Latest Frontier (Feb 2026 knowledge cutoff, 1.05M context, 128K max output)
| Model ID | Thinking | Reasoning Effort | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|
gpt-5.6-sol |
✅ | low/medium/high/xhigh | $5.00/$30.00/$0.50 | Frontier model for complex professional work, most demanding autonomous pentesting, sophisticated exploit chain development, deep multi-stage attack simulation |
gpt-5.6-terra* |
✅ | low/medium/high/xhigh | $2.50/$15.00/$0.25 | Balances intelligence and cost; multi-phase security assessments, coordinated multi-tool pentesting (generator/refiner/adviser/coder default) |
gpt-5.6-luna |
✅ | low/medium/high/xhigh | $1.00/$6.00/$0.10 | Optimized for cost-sensitive, high-volume workloads; rapid reconnaissance, bulk vulnerability scanning, real-time monitoring |
GPT-5.5 Series - Frontier (Dec 2025 knowledge cutoff, 1.05M context, 128K max output)
| Model ID | Thinking | Reasoning Effort | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|
gpt-5.5 |
✅ | none/low/medium/high/xhigh | $5.00/$30.00/$0.50 | New class of intelligence for coding and professional work; complex security research, advanced autonomous pentesting |
gpt-5.5-pro |
✅ | medium/high/xhigh | $30.00/$180.00/$0.00 | Uses more compute for smarter, more precise responses; no cached-input discount; mission-critical security research, zero-day discovery |
GPT-5.4 Series - Advanced Reasoning at Scale (1M context)
| Model ID | Thinking | Reasoning Effort | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|
gpt-5.4 |
✅ | low/medium/high/xhigh | $2.50/$15.00/$0.25 | Best intelligence at scale for agentic, coding, and professional workflows; maximum cognitive depth for pentesting |
gpt-5.4-mini* |
✅ | low/medium/high/xhigh | $0.75/$4.50/$0.075 | Strongest mini model for coding, computer use, subagents (primary_agent/assistant/reflector/installer/pentester default) |
gpt-5.4-nano* |
✅ | low/medium/high/xhigh | $0.20/$1.25/$0.02 | Cheapest GPT-5.4-class model for simple, high-volume tasks (simple/simple_json/searcher/enricher default) |
GPT-5.2 Series - Previous Flagship Agentic
| Model ID | Thinking | Reasoning Effort | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|
gpt-5.2 |
✅ | low/medium/high/xhigh | $1.75/$14.00/$0.175 | Superseded by 5.4/5.6; autonomous security research, complex exploit chain development |
gpt-5.2-pro |
✅ | medium/high/xhigh | $21.00/$168.00/$0.00 | Superior agentic coding and long-context performance, mission-critical security research, zero-day discovery |
GPT-5/5.1 Series - Advanced Agentic Models
| Model ID | Thinking | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|
gpt-5 |
✅ | $1.25/$10.00/$0.125 | Autonomous security research, exploit chain development, coordinating multi-tool pentesting workflows |
gpt-5.1 |
✅ | $1.25/$10.00/$0.125 | Bridges GPT-5 and GPT-5.2 with faster responses; balanced penetration testing with strong tool coordination |
gpt-5-pro |
✅ (high) | $15.00/$120.00/$0.00 | Reduced hallucinations, exceptional accuracy, critical security operations |
gpt-5-mini |
✅ | $0.25/$2.00/$0.025 | Automated vulnerability analysis, exploit generation with strong function calling |
gpt-5-nano |
✅ | $0.05/$0.40/$0.005 | High-throughput security scanning, reconnaissance, real-time monitoring |
GPT-4.1 Series - Enhanced Intelligence (Non-Reasoning)
| Model ID | Thinking | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|
gpt-4.1 |
❌ | $2.00/$8.00/$0.50 | Superior function calling, complex threat analysis, sophisticated exploit development |
gpt-4.1-mini |
❌ | $0.40/$1.60/$0.10 | Routine security assessments, automated code analysis (no longer used in default configuration) |
GPT-4o Series - Multimodal (Non-Reasoning)
| Model ID | Thinking | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|
gpt-4o-mini |
❌ | $0.15/$0.60/$0.075 | Compact multimodal with strong function calling, high-frequency scanning, cost-effective bulk operations |
o-Series - Advanced Reasoning Models (Current)
| Model ID | Thinking | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|
o3 |
✅ | $2.00/$8.00/$0.50 | Succeeded by GPT-5; multi-stage attack chains, deep vulnerability analysis |
o3-pro |
✅ | $20.00/$80.00/$0.00 | More compute for better responses; zero-day research, critical security investigations |
Deprecated Models - Kept for Backward Compatibility ⚠️
These models were marked deprecated by OpenAI. PentAGI keeps them defined only so that pre-existing agent configs pinned to these names keep working; do not assign them to new agents.
| Model ID | Thinking | Price (Input/Output/Cache) | Notes |
|---|---|---|---|
gpt-5.2-codex |
✅ | $1.75/$14.00/$0.175 | Superseded code-specialized model; use gpt-5.6-terra/gpt-5.4-mini instead |
gpt-5.1-codex-max |
✅ | $1.25/$10.00/$0.125 | Superseded; enhanced reasoning for coding workflows |
gpt-5.1-codex |
✅ | $1.25/$10.00/$0.125 | Superseded standard code-optimized model |
gpt-5-codex |
✅ | $1.25/$10.00/$0.125 | Superseded foundational code-specialized model |
gpt-5.1-codex-mini |
✅ | $0.25/$2.00/$0.025 | Superseded compact code model |
codex-mini-latest |
✅ | $1.50/$6.00/$0.375 | Superseded compact code model |
gpt-4o |
❌ | $2.50/$10.00/$1.25 | Superseded by GPT-5.x/5.6 series multimodal flagship |
gpt-4.1-nano |
❌ | $0.10/$0.40/$0.025 | Superseded ultra-fast lightweight model |
o3-mini |
✅ | $1.10/$4.40/$0.55 | Superseded compact reasoning model |
o4-mini |
✅ | $1.10/$4.40/$0.275 | Succeeded by gpt-5-mini |
o1 |
✅ | $15.00/$60.00/$7.50 | Superseded premier reasoning model |
o1-pro |
✅ | $150.00/$600.00/$0.00 | Superseded, highest cost point of the o-series |
Prices: Per 1M tokens. Reasoning models include thinking tokens in output pricing.
[!WARNING]
GPT-5/5.1/5.2 Models - Trusted Access RequiredThe original GPT-5, GPT-5.1, and GPT-5.2 models (
gpt-5,gpt-5.1,gpt-5.2,gpt-5-pro,gpt-5.2-pro, and all deprecated Codex variants) work unstably with PentAGI and may trigger OpenAI's cybersecurity safety mechanisms without verified access. This does not affect the newer GPT-5.4/5.5/5.6 series used in PentAGI's default configuration below.To use these models reliably:
- Individual users: Verify your identity at chatgpt.com/cyber
- Enterprise teams: Request trusted access through your OpenAI representative
- Security researchers: Apply for the Cybersecurity Grant Program (includes $10M in API credits)
Recommended alternatives without verification:
- Use PentAGI's defaults —
gpt-5.4-mini/gpt-5.4-nano/gpt-5.6-terra— which work out of the box- Use
o3/o3-profor reasoning tasks- Use
gpt-4.1series for general intelligence and function calling without reasoning
Reasoning Configuration:
- Reasoning forced off by default: every default agent assigned
gpt-5.4-miniorgpt-5.6-terra(primary_agent, assistant, generator, refiner, adviser, reflector, coder, installer, pentester) setsreasoning: {mode: off}— this genuinely disables reasoning, it is not simply "low effort". PentAGI calls OpenAI exclusively through/v1/chat/completions(never/v1/responses), and this endpoint rejects requests that combine function tools with these models' default-on thinking; forcing thinking off is required for tool calls to work reliably (see the investigation notes inbackend/pkg/providers/openai/config.yml). - No override needed for
gpt-5.4-nano: used for simple, simple_json, searcher, and enricher, this tier does not default to thinking on, so tools attach without conflict and noreasoningoverride is required. - Manual tuning available: outside the default assignments, GPT-5.6/5.5/5.4/5.2 series models expose explicit reasoning effort levels (
low/medium/high/xhigh, plusnoneon GPT-5.5) for custom agent configs that need variable reasoning depth with tool calling disabled or via/v1/responses.
Key Features:
- Extended Reasoning: GPT-5.4/5.5/5.6 and o-series models with chain-of-thought for complex security analysis
- Agentic Intelligence: GPT-5.4/5.5/5.6 series with enhanced tool integration, million-token context windows, and autonomous capabilities
- Prompt Caching: Cost reduction on repeated context (10-50% of input price)
- Code Specialization: Legacy Codex models remain available (deprecated) for vulnerability discovery and exploit development in pinned configs
- Multimodal Support:
gpt-4o-minifor vision-based security assessments - Tool Calling: Robust function calling across all models for pentesting tool orchestration
- Streaming: Real-time response streaming for interactive workflows
- Proven Track Record: Industry-leading models with CVE discoveries and real-world security applications
Anthropic Provider Configuration
PentAGI integrates with Anthropic's Claude models, featuring advanced extended thinking capabilities, exceptional safety mechanisms, and sophisticated understanding of complex security contexts with prompt caching.
Configuration Variables
| Variable | Default | Description |
|---|---|---|
ANTHROPIC_API_KEY |
API key for Anthropic services | |
ANTHROPIC_SERVER_URL |
https://api.anthropic.com/v1 |
Anthropic API endpoint |
Configuration Examples
# Basic Anthropic setup
ANTHROPIC_API_KEY=your_anthropic_api_key
ANTHROPIC_SERVER_URL=https://api.anthropic.com/v1
# Using with proxy for secure environments
ANTHROPIC_API_KEY=your_anthropic_api_key
PROXY_URL=http://your-proxy:8080
# Workload Identity Federation instead of a long-lived key (leave ANTHROPIC_API_KEY empty)
ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
ANTHROPIC_SERVICE_ACCOUNT_ID=svac_...
ANTHROPIC_WORKSPACE_ID=wrkspc_...
ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic.com/tokenWith Workload Identity Federation, PentAGI exchanges the identity token your platform issues (Kubernetes, GitHub Actions, cloud IAM, any OIDC issuer) for a short-lived Claude API token and refreshes it on its own. The token file is re-read on every exchange and must be mounted into the pentagi container. A token carrying a jti claim, as Kubernetes and GitHub Actions tokens do, can be exchanged only once, so the file must hold a new token before each refresh: rotate it well within the lifetime of the minted token. ANTHROPIC_IDENTITY_TOKEN takes the token itself where the platform injects it as a variable, but it is read once and cannot be rotated, so it suits only runs shorter than the identity token's lifetime. A set ANTHROPIC_API_KEY wins over federation. See backend/docs/config.md for every variable.
[!NOTE]
Google Vertex AI for Claude modelsPentAGI does not currently expose a dedicated Google Vertex AI configuration path for Anthropic Claude in
.env. There is no separate Vertex AI API key field at this time, and the existing Anthropic variables (ANTHROPIC_API_KEY,ANTHROPIC_SERVER_URL) target the direct Anthropic API. Supported routes for Claude are:
- Direct Anthropic API:
ANTHROPIC_API_KEYandANTHROPIC_SERVER_URL(see above).- AWS Bedrock:
BEDROCK_*variables (see AWS Bedrock Provider Configuration).If you need to use Vertex AI today, the safest supported workaround is to expose Vertex AI through an OpenAI-compatible proxy or gateway that translates Vertex AI calls into the Chat Completions format while preserving the chat and tool-call behavior PentAGI relies on, then point the Custom LLM provider at that gateway via
LLM_SERVER_URL,LLM_SERVER_KEY, andLLM_SERVER_MODEL. This path is only as reliable as the gateway you choose.
Supported Models
PentAGI supports 9 Claude models with tool calling, streaming, extended thinking, adaptive thinking, and prompt caching. Models marked with * are used in default configuration.
Claude 5 Series - Newest Models (2026)
| Model ID | Thinking | Release Date | Price (Input/Output/Cache R/W) | Use Case |
|---|---|---|---|---|
claude-sonnet-5* |
✅ | Jul 2026 | $3.00/$15.00/$0.30/$3.75 | Best combination of speed and intelligence for coding, agents, and professional work at scale. Adaptive thinking only (manual budget thinking rejected); sampling parameters not supported. Default model for primary agent, assistant, coder, adviser, installer, pentester |
claude-fable-5 |
✅ | Jun 2026 | $10.00/$50.00/$1.00/$12.50 | Anthropic's most capable widely released model for long-running agents and the most demanding reasoning workloads. Adaptive thinking always on (budget thinking and an explicit disable are rejected); sampling parameters not supported |
Claude 4 Series
| Model ID | Thinking | Release Date | Price (Input/Output/Cache R/W) | Use Case |
|---|---|---|---|---|
claude-opus-4-8* |
✅ | May 2026 | $5.00/$25.00/$0.50/$6.25 | Flagship for coding, agents, and deep reasoning in enterprise security workflows. Adaptive thinking only — budget thinking and sampling params (temperature/top_p/top_k) are rejected. Default model for generator and refiner; most demanding exploit development and multi-stage attack simulation |
claude-opus-4-7 |
✅ | Apr 2026 | $5.00/$25.00/$0.50/$6.25 | Advanced software engineering and long-running agentic security analysis. Adaptive thinking only (manual budget thinking rejected) |
claude-sonnet-4-6 |
✅ | Feb 2026 | $3.00/$15.00/$0.30/$3.75 | Best speed/intelligence balance with adaptive thinking. Multi-phase security assessments, intelligent vulnerability analysis, real-time threat hunting |
claude-opus-4-6 |
✅ | Feb 2026 | $5.00/$25.00/$0.50/$6.25 | Most intelligent model for autonomous agents and coding. Extended + adaptive thinking for complex exploit development, multi-stage attack simulation |
claude-haiku-4-5* |
❌ | Oct 2025 | $1.00/$5.00/$0.10/$1.25 | Fast and efficient model with exceptional function calling and low latency, no thinking support. Default model for simple, simple_json, reflector, searcher, enricher; high-frequency scanning, real-time monitoring, bulk automated testing |
Legacy Models - Still Supported
| Model ID | Thinking | Release Date | Price (Input/Output/Cache R/W) | Use Case |
|---|---|---|---|---|
claude-sonnet-4-5 |
✅ | Sep 2025 | $3.00/$15.00/$0.30/$3.75 | State-of-the-art reasoning (superseded by sonnet-4-6/sonnet-5). Sophisticated penetration testing, advanced threat analysis |
claude-opus-4-5 |
✅ | Nov 2025 | $5.00/$25.00/$0.50/$6.25 | Ultimate reasoning (superseded by opus-4-6/4-7/4-8). Critical security research, zero-day discovery, red team operations |
Prices: Per 1M tokens. Cache pricing includes both Read and Write costs.
Extended Thinking Configuration (default agent config, see backend/pkg/providers/anthropic/config.yml):
- Generator / Refiner (
claude-opus-4-8): adaptive reasoning atxhigh/higheffort for maximum reasoning depth on complex exploit development - Primary agent, assistant, coder, adviser, installer, pentester (
claude-sonnet-5): adaptive reasoning (adviser atxhigheffort) for balanced code analysis and vulnerability research - Reflector, searcher (
claude-haiku-4-5): fixed reasoning budget of 1024 tokens for focused reasoning on specific tasks - Simple, simple_json, enricher (
claude-haiku-4-5): no thinking, optimized for speed
Key Features:
- Extended Thinking: All Claude 4.5+ models support configurable chain-of-thought reasoning depths for complex security analysis
- Adaptive Thinking: Claude 4.6 series (Opus/Sonnet) dynamically adjusts reasoning depth based on task complexity; Claude Opus 4.7/4.8 and the Claude 5 series (Sonnet/Fable) are adaptive-thinking-only (manual budget thinking and sampling parameters are rejected with HTTP 400)
- Prompt Caching: Significant cost reduction with separate read/write pricing (10% read, 125% write of input)
- Extended Context Window: 200K tokens standard, up to 1M tokens (beta) for Claude Opus/Sonnet 4.6 for comprehensive codebase analysis
- Tool Calling: Robust function calling with exceptional accuracy for security tool orchestration
- Streaming: Real-time response streaming for interactive penetration testing workflows
- Safety-First Design: Built-in safety mechanisms ensuring responsible security testing practices
- Multimodal Support: Vision capabilities in latest models for screenshot analysis and UI security assessment
- Constitutional AI: Advanced safety training providing reliable and ethical security guidance
Google AI (Gemini) Provider Configuration
PentAGI integrates with Google's Gemini models through the Google AI API, offering state-of-the-art multimodal reasoning capabilities with extended thinking and context caching.
Configuration Variables
| Variable | Default | Description |
|---|---|---|
GEMINI_API_KEY |
API key for Google AI services | |
GEMINI_SERVER_URL |
https://generativelanguage.googleapis.com |
Google AI API endpoint |
Configuration Examples
# Basic Gemini setup
GEMINI_API_KEY=your_gemini_api_key
GEMINI_SERVER_URL=https://generativelanguage.googleapis.com
# Using with proxy
GEMINI_API_KEY=your_gemini_api_key
PROXY_URL=http://your-proxy:8080Supported Models
PentAGI lists 14 Gemini model IDs that responded through the tested API endpoint. Availability in the catalogue does not mean a model is suitable for every agent role: the saved-chain results below determine the bundled defaults. Models marked with * are used in config.yml; the full replay matrix describes role and outcome limits.
| Model ID | Thinking | Context | Price (Input/Output/Cache) | Saved-chain result |
|---|---|---|---|---|
gemini-3.8-flash |
✅ | 1M | $0.75/$3.75/$0.075 | API responds; 10/10 saved generator calls were content-filtered |
gemini-3.7-flash |
✅ | 1M | $0.75/$3.75/$0.075 | API responds; 10/10 saved generator calls were content-filtered; former default |
gemini-3.6-flash |
✅ | 1M | $0.75/$3.75/$0.075 | API responds; 10/10 saved generator calls returned text without a tool |
gemini-3.5-flash |
✅ | 1M | $1.5/$9/$0.15 | API responds; 10/10 saved generator calls returned text without a tool |
gemini-3.5-flash-lite* |
✅ | 1M | $0.3/$2.5/$0.03 | Complex-role default; eight direct plans and two after a safe memorist continuation |
gemini-3.1-pro-preview |
✅ | 1M | $2/$12/$0.2 | API responds; 10/10 saved generator calls returned text without a tool |
gemini-3.1-pro-preview-customtools |
✅ | 1M | $2/$12/$0.2 | API responds; 10/10 saved generator calls returned text without a tool |
gemini-3.1-flash-lite* |
✅ | 1M | $0.25/$1.5/$0.025 | Simple-role default; direct plan on all ten saved generator chains |
gemini-3-flash-preview |
✅ | 1M | $0.5/$3/$0.05 | API responds; three direct plans and five generator timeouts |
gemini-2.5-pro |
✅ | 1M | $1.25/$10/$0.125 | API responds; ten direct plans, but four role-smoke provider errors |
gemini-2.5-flash |
✅ | 1M | $0.3/$2.5/$0.03 | Ten direct plans; access is limited for new API projects |
gemini-2.5-flash-lite |
✅ | 1M | $0.1/$0.4/$0.01 | API responds; five of ten generator answers were empty |
gemma-4-31b-it |
✅ | 256K | Free/Free/Free | API responds; seven direct plans and three generator timeouts |
gemma-4-26b-a4b-it |
✅ | 256K | Free/Free/Free | Direct plan on all ten saved generator chains |
Prices: Per 1M tokens. Google limits Gemini 2.5 access for new projects, so PentAGI uses the tested 3.1 Flash-Lite as its fallback and simple-role default. Existing custom 2.5 configurations remain usable where the project has access.
Default Model Assignments (config.yml):
gemini-3.1-flash-lite-simple,simple_json,reflector,searcher,enrichergemini-3.5-flash-lite-primary_agent,assistant,generator,refiner,adviser,coder,installer,pentester
Both default models passed a basic API/configuration smoke for every role. Google reports stronger agentic and coding performance for 3.5 Flash-Lite than 3.1 Flash-Lite; PentAGI's saved production chains cover generator and reflector only, so the smoke does not establish semantic quality or multi-turn reliability for other roles. On two saved generator chains, 3.5 Flash-Lite first chose the allowed memorist tool and called subtask_list after a synthetic tool response; no real tool was executed in that continuation.
Reasoning Effort Levels:
- High: Deeper reasoning for planning and review (
primary_agent,generator,refiner,adviser) - Medium: Balanced reasoning for execution roles (
assistant,coder,installer,pentester) - Not set: No thinking setting is sent and the model's own default applies (
simple,simple_json,reflector,searcher,enricher)
AWS Bedrock Provider Configuration
PentAGI integrates with Amazon Bedrock, offering access to 20+ foundation models from leading AI companies including Anthropic, Amazon, Cohere, DeepSeek, OpenAI, Qwen, Mistral, and Moonshot.
Configuration Variables
| Variable | Default | Description |
|---|---|---|
BEDROCK_REGION |
us-east-1 |
AWS region for Bedrock service |
BEDROCK_DEFAULT_AUTH |
false |
Use AWS SDK default credential chain (environment, EC2 role, ~/.aws/credentials) - highest priority |
BEDROCK_BEARER_TOKEN |
Bearer token authentication - priority over static credentials | |
BEDROCK_ACCESS_KEY_ID |
AWS access key ID for static credentials | |
BEDROCK_SECRET_ACCESS_KEY |
AWS secret access key for static credentials | |
BEDROCK_SESSION_TOKEN |
AWS session token for temporary credentials (optional, used with static credentials) | |
BEDROCK_SERVER_URL |
Custom Bedrock endpoint (VPC endpoints, local testing) | |
BEDROCK_CONFIG_PATH |
Path to a custom YAML provider config file (overrides the built-in default config for model/pricing definitions) |
Authentication Priority: BEDROCK_DEFAULT_AUTH → BEDROCK_BEARER_TOKEN → BEDROCK_ACCESS_KEY_ID+BEDROCK_SECRET_ACCESS_KEY
Configuration Examples
# Recommended: Default AWS SDK authentication (EC2/ECS/Lambda roles)
BEDROCK_REGION=us-east-1
BEDROCK_DEFAULT_AUTH=true
# Bearer token authentication (AWS STS, custom auth)
BEDROCK_REGION=us-east-1
BEDROCK_BEARER_TOKEN=your_bearer_token
# Static credentials (development, testing)
BEDROCK_REGION=us-east-1
BEDROCK_ACCESS_KEY_ID=your_aws_access_key
BEDROCK_SECRET_ACCESS_KEY=your_aws_secret_key
# With proxy and custom endpoint
BEDROCK_REGION=us-east-1
BEDROCK_DEFAULT_AUTH=true
BEDROCK_SERVER_URL=https://bedrock-runtime.us-east-1.vpce-xxx.amazonaws.com
PROXY_URL=http://your-proxy:8080Custom Provider Config and Models (advanced)
By default the Bedrock provider uses a per-agent config and model catalog compiled into the binary. One optional path overrides them without rebuilding:
BEDROCK_CONFIG_PATH— a YAML file (same shape as the other provider configs) that replaces the built-in per-agent model/price assignments. Seeexamples/configs/bedrock-glm-flash.provider.yml.
This is useful to expose a Bedrock model newer than the compiled-in catalog — for example Z.AI's zai.glm-4.7-flash. Use the exact Model ID from the model's AWS Bedrock detail page; add a us./eu./apac. inference-profile prefix only when that page marks the model as requiring cross-region inference (zai.glm-4.7-flash is In-Region, so it is used as-is, with no prefix).
With Docker Compose the file is mounted from the host at /opt/pentagi/conf/bedrock.provider.yml. The default source is ./example.bedrock.provider.yml beside docker-compose.yml, a copy of the built-in config (examples/configs/bedrock.provider.yml) that the installer extracts, so editing it and pointing the backend at the mounted path is enough:
# tell the backend to read the mounted file
BEDROCK_CONFIG_PATH=/opt/pentagi/conf/bedrock.provider.yml
# optional: mount another file from the host instead of ./example.bedrock.provider.yml
PENTAGI_BEDROCK_CONFIG_PATH=/path/on/host/my-bedrock.provider.ymlSupported Models
PentAGI supports 24 AWS Bedrock models with tool calling, streaming, and multimodal capabilities. Models marked with * are used in default configuration.
| Model ID | Provider | Thinking | Multimodal | Price (Input/Output) | Use Case |
|---|---|---|---|---|---|
us.amazon.nova-2-lite-v1:0 |
Amazon Nova | ❌ | ✅ | $0.33/$2.75 | Adaptive reasoning, efficient thinking |
us.amazon.nova-premier-v1:0 |
Amazon Nova | ❌ | ✅ | $2.50/$12.50 | Complex reasoning, advanced analysis |
us.amazon.nova-pro-v1:0 |
Amazon Nova | ❌ | ✅ | $0.80/$3.20 | Balanced accuracy, speed, cost |
us.amazon.nova-lite-v1:0 |
Amazon Nova | ❌ | ✅ | $0.06/$0.24 | Fast processing, high-volume operations |
us.amazon.nova-micro-v1:0 |
Amazon Nova | ❌ | ❌ | $0.035/$0.14 | Ultra-low latency, real-time monitoring |
us.anthropic.claude-opus-4-8 |
Anthropic | ✅ | ✅ | $5.00/$25.00 | Flagship coding/agents/deep reasoning; adaptive thinking only (sampling params rejected) |
us.anthropic.claude-opus-4-7 |
Anthropic | ✅ | ✅ | $5.00/$25.00 | Advanced engineering, long-running agents; adaptive thinking only |
us.anthropic.claude-opus-4-6-v1* |
Anthropic | ✅ | ✅ | $5.00/$25.00 | World-class coding, enterprise agents |
us.anthropic.claude-sonnet-4-6 |
Anthropic | ✅ | ✅ | $3.00/$15.00 | Frontier intelligence, enterprise scale |
us.anthropic.claude-opus-4-5-20251101-v1:0 |
Anthropic | ✅ | ✅ | $5.00/$25.00 | Multi-day software development |
us.anthropic.claude-haiku-4-5-20251001-v1:0* |
Anthropic | ✅ | ✅ | $1.00/$5.00 | Near-frontier performance, high speed |
us.anthropic.claude-sonnet-4-5-20250929-v1:0* |
Anthropic | ✅ | ✅ | $3.00/$15.00 | Real-world agents, coding excellence |
us.anthropic.claude-sonnet-4-20250514-v1:0 |
Anthropic | ✅ | ✅ | $3.00/$15.00 | Balanced performance, production-ready |
us.anthropic.claude-3-5-haiku-20241022-v1:0 |
Anthropic | ❌ | ❌ | $0.80/$4.00 | Fastest model, cost-effective scanning |
cohere.command-r-plus-v1:0 |
Cohere | ❌ | ❌ | $3.00/$15.00 | Large-scale operations, superior RAG |
deepseek.v3.2 |
DeepSeek | ❌ | ❌ | $0.58/$1.68 | Long-context reasoning, efficiency |
openai.gpt-oss-120b-1:0* |
OpenAI (OSS) | ✅ | ❌ | $0.15/$0.60 | Strong reasoning, scientific analysis |
openai.gpt-oss-20b-1:0 |
OpenAI (OSS) | ✅ | ❌ | $0.07/$0.30 | Efficient coding, software development |
qwen.qwen3-next-80b-a3b |
Qwen | ❌ | ❌ | $0.15/$1.20 | Ultra-long context, flagship reasoning |
qwen.qwen3-32b-v1:0 |
Qwen | ❌ | ❌ | $0.15/$0.60 | Balanced reasoning, research use cases |
qwen.qwen3-coder-30b-a3b-v1:0 |
Qwen | ❌ | ❌ | $0.15/$0.60 | Vibe coding, natural-language first |
qwen.qwen3-coder-next |
Qwen | ❌ | ❌ | $0.45/$1.80 | Tool use, function calling optimized |
mistral.mistral-large-3-675b-instruct |
Mistral | ❌ | ✅ | $4.00/$12.00 | Advanced multimodal, long-context |
moonshotai.kimi-k2.5 |
Moonshot | ❌ | ✅ | $0.60/$3.00 | Vision, language, code in one model |
Prices: Per 1M tokens. Models with thinking/reasoning support additional compute costs during reasoning phase.
Tested but Incompatible Models
Some AWS Bedrock models were tested but are not supported due to technical limitations:
| Model Family | Reason for Incompatibility |
|---|---|
| GLM (Z.AI) | Tool calling format incompatible with Converse API (expects string instead of JSON) |
| AI21 Jamba | Severe rate limits (1-2 req/min) prevent reliable testing and production use |
| Meta Llama 3.3/3.1 | Unstable tool call result processing, causes unexpected failures in multi-turn workflows |
| Mistral Magistral | Tool calling not supported by the model |
| Moonshot K2-Thinking | Unstable streaming behavior with tool calls, unreliable in production |
| Qwen3-VL | Unstable streaming with tool calling, multimodal + tools combination fails intermittently |
[!IMPORTANT]
Rate Limits & Quota ManagementDefault AWS Bedrock quotas for Claude models are extremely restrictive (2-20 requests/minute for new accounts). For production penetration testing:
- Request quota increases through AWS Service Quotas console for models you plan to use
- Use Amazon Nova models - higher default quotas and excellent performance
- Enable provisioned throughput for consistent high-volume testing
- Monitor usage - AWS throttles aggressively at quota limits
Without quota increases, expect frequent delays and workflow interruptions.
[!WARNING]
Converse API RequirementsPentAGI uses Amazon Bedrock Converse API for unified model access. All supported models require:
- ✅ Converse/ConverseStream API support
- ✅ Tool use (function calling) for penetration testing workflows
- ✅ Streaming tool use for real-time feedback
Verify model capabilities at: AWS Bedrock Model Features
Key Features:
- Automatic Prompt Caching: 40-70% cost reduction on repeated context (Claude 4.x models)
- Extended Thinking: Step-by-step reasoning for complex security analysis (Claude, DeepSeek R1, OpenAI GPT)
- Multimodal Analysis: Process screenshots, diagrams, video for comprehensive testing (Nova, Claude, Mistral, Kimi)
- Tool Calling: Seamless integration with 20+ pentesting tools via function calling
- Streaming: Real-time response streaming for interactive security assessment workflows
DeepSeek Provider Configuration
PentAGI integrates with DeepSeek, providing access to advanced AI models with strong reasoning, coding capabilities, and context caching at competitive prices.
Configuration Variables
| Variable | Default Value | Description |
|---|---|---|
DEEPSEEK_API_KEY |
DeepSeek API key for authentication | |
DEEPSEEK_SERVER_URL |
https://api.deepseek.com |
DeepSeek API endpoint URL |
DEEPSEEK_PROVIDER |
Provider prefix for LiteLLM integration (optional) |
Configuration Examples
# Direct API usage
DEEPSEEK_API_KEY=your_deepseek_api_key
DEEPSEEK_SERVER_URL=https://api.deepseek.com
# With LiteLLM proxy
DEEPSEEK_API_KEY=your_litellm_key
DEEPSEEK_SERVER_URL=http://litellm-proxy:4000
DEEPSEEK_PROVIDER=deepseek # Adds prefix to model names (deepseek/deepseek-v4-flash) for LiteLLMSupported Models
PentAGI supports 2 DeepSeek V4 models with tool calling, streaming, hybrid thinking/non-thinking modes, and context caching. Both models think by default and switch to non-thinking mode with reasoning.mode: off. Models marked with * are used in default configuration.
| Model ID | Thinking | Max Output | Context | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|
deepseek-v4-flash* |
✅ hybrid | 384K | 1M | $0.14/$0.28/$0.0028 | Utility agents, general dialogue, fast tool calling |
deepseek-v4-pro* |
✅ hybrid | 384K | 1M | $1.74/$3.48/$0.0145 | Advanced reasoning, complex logic, security analysis |
Prices: Per 1M tokens. Cache pricing applies to prompt tokens served from cache (input cache hit, reduced to 1/10 of launch price since 2026-04-26). Both models support hybrid thinking — thinking mode is enabled by default; set reasoning.mode: off (Reasoning Mode Off in the UI) to switch to non-thinking mode for faster/cheaper responses.
Pricing Note (deepseek-v4-pro): The 75% promotional discount on
deepseek-v4-proofficially ended on 2026-05-31 15:59 UTC. The prices above reflect the standard post-promotional pricing. If you have legacy configurations using the discounted prices ($0.435/$0.87/$0.003625), update them to the current rates for accurate cost tracking.
The legacy model names
deepseek-chatanddeepseek-reasonerare scheduled
for deprecation by DeepSeek on 2026-07-24. Existing user configurations
referencing the legacy names continue to work until then; the defaults above
use the current V4 names.deepseek-chatmaps todeepseek-v4-flash
non-thinking mode;deepseek-reasonermaps todeepseek-v4-flashthinking mode.
Default Agent Configuration:
Strategy: prefer deepseek-v4-flash (12x cheaper input, 12x cheaper output) as the workhorse for utility/lightweight agents; reserve deepseek-v4-pro for complex multi-step reasoning. The installer agent runs on Flash with thinking enabled because environment setup tasks (shell commands, config edits) rarely require pro-level reasoning. Run A/B tests on your own workloads before promoting more agents to Pro.
| Agent Role | Default Model | Thinking | Reasoning Effort | Max Output | Temperature | Top P |
|---|---|---|---|---|---|---|
| Generator / Refiner | deepseek-v4-pro |
Enabled | High | 32768 | (auto) | (auto) |
| Coder | deepseek-v4-pro |
Enabled | High | 20480 | (auto) | (auto) |
| Primary Agent / Assistant / Pentester | deepseek-v4-pro |
Enabled | High | 16384 | (auto) | (auto) |
| Adviser (mentor/planner) | deepseek-v4-pro |
Enabled | High | 8192 | (auto) | (auto) |
| Installer | deepseek-v4-flash |
Enabled | High | 12288 | (auto) | (auto) |
| Reflector / Searcher / Enricher | deepseek-v4-flash |
Disabled | — | 4096 | 0.5 | 0.9 |
| Simple / Simple JSON | deepseek-v4-flash |
Disabled | — | 2048 | 0.3 | 0.9 |
Note: DeepSeek ignores
presence_penaltyandfrequency_penaltyon both models. While thinking runs,temperaturehas no effect and langchaingo drops it, buttop_pis sent and applied (values below 0.95 are raised to 0.95); in non-thinking modetop_pis fixed at 1.0, so langchaingo drops it and sendstemperatureinstead. The thinking agents leave both unset, shown as "(auto)" in the table above.
Key Features:
- Hybrid Thinking Modes: Switch between thinking (deep reasoning) and non-thinking (fast) modes via
reasoning.mode - Automatic Prompt Caching: Significant cost reduction on repeated context via cache-hit pricing (1/10 of launch price)
- Extended Thinking: Reinforcement learning CoT for complex security analysis (both V4 models)
- Strong Coding: Optimized for code generation and exploit development
- Long Context: 1M token context window with up to 384K output tokens
- Tool Calling: Seamless integration with 20+ pentesting tools via function calling
- Streaming: Real-time response streaming for interactive workflows
- Multilingual: Strong Chinese and English support
- Additional Features: JSON Output, Chat Prefix Completion (beta), FIM/Fill-in-the-Middle Completion (non-thinking mode only)
Concurrency Limits: deepseek-v4-flash: 2500 concurrent requests; deepseek-v4-pro: 500 concurrent requests.
LiteLLM Integration: Set DEEPSEEK_PROVIDER=deepseek to enable model name prefixing when using default PentAGI configurations with LiteLLM proxy. Leave empty for direct API usage.
GLM Provider Configuration
PentAGI integrates with GLM from Zhipu AI (Z.AI), providing advanced language models with MoE architecture, strong reasoning, and agentic capabilities developed by Tsinghua University.
Configuration Variables
| Variable | Default Value | Description |
|---|---|---|
GLM_API_KEY |
GLM API key for authentication | |
GLM_SERVER_URL |
https://api.z.ai/api/paas/v4 |
GLM API endpoint URL (international) |
GLM_PROVIDER |
Provider prefix for LiteLLM integration (optional) |
Configuration Examples
# Direct API usage (international endpoint)
GLM_API_KEY=your_glm_api_key
GLM_SERVER_URL=https://api.z.ai/api/paas/v4
# Alternative endpoints
GLM_SERVER_URL=https://open.bigmodel.cn/api/paas/v4 # China
GLM_SERVER_URL=https://api.z.ai/api/coding/paas/v4 # Coding-specific
# With LiteLLM proxy
GLM_API_KEY=your_litellm_key
GLM_SERVER_URL=http://litellm-proxy:4000
GLM_PROVIDER=zai # Adds prefix to model names (zai/glm-4) for LiteLLMSupported Models
PentAGI supports 16 GLM models with tool calling, streaming, reasoning controls, and prompt caching. Models marked with * are used in the default configuration. GLM-5.3, GLM-5.3-Flash, and GLM-5.3-FlashX always reason and accept low, high, or max effort.
GLM-5.x Series - Latest Generation
| Model ID | Thinking | Context | Max Output | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|
glm-5.3* |
✅ Always | 1M | 128K | $1.40/$4.40/$0.26 | Current flagship for complex software engineering, agent tasks, and vulnerability research |
glm-5.3-flash* |
✅ Always | 1M | 128K | $0.15/$0.50/$0.03 | Cost-efficient native multimodal model for utility and latency-sensitive agents |
glm-5.3-flashx |
✅ Always | 1M | 128K | $0.37/$1.25/$0.075 | Accelerated GLM-5.3-Flash endpoint delivering up to 200 output tokens per second |
glm-5.2 |
✅ Hybrid | 1M | 128K | $1.40/$4.40/$0.26 | Previous flagship for long-horizon engineering tasks |
glm-5.1 |
✅ Hybrid | 200K | 128K | $1.40/$4.40/$0.26 | Long-horizon tasks: 8h sustained autonomous execution, Claude Opus 4.6-aligned coding |
glm-5 |
✅ Hybrid | 200K | 128K | $1.00/$3.20/$0.20 | Foundation for Agentic Engineering, MoE 744B/40B active, Claude Opus 4.5-level coding |
GLM-4.7 Series - Premium with Interleaved Thinking
| Model ID | Thinking | Context | Max Output | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|
glm-4.7 |
✅ Hybrid | 200K | 128K | $0.60/$2.20/$0.11 | Enhanced programming, stable multi-step reasoning |
glm-4.7-flashx |
✅ Hybrid | 200K | 128K | $0.07/$0.40/$0.01 | Ultra-cheap with priority GPU, but lower RPM limits (avoid for high-frequency use) |
glm-4.7-flash |
✅ Hybrid | 200K | 128K | Free/Free/Free | Free ~30B SOTA model, 1 concurrent request |
GLM-4.6 Series - Balanced with Auto-Thinking
| Model ID | Thinking | Context | Max Output | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|
glm-4.6 |
✅ Auto | 200K | 128K | $0.60/$2.20/$0.11 | Balanced, streaming tool calls, token-efficient |
GLM-4.5 Series - Unified Reasoning/Coding/Agents
| Model ID | Thinking | Context | Max Output | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|
glm-4.5 |
✅ Auto | 128K | 96K | $0.60/$2.20/$0.11 | Unified, MoE 355B/32B active |
glm-4.5-x |
✅ Auto | 128K | 96K | $2.20/$8.90/$0.45 | Ultra-fast premium, lowest latency |
glm-4.5-air |
✅ Auto | 128K | 96K | $0.20/$1.10/$0.03 | Cost-effective MoE 106B/12B |
glm-4.5-airx |
✅ Auto | 128K | 96K | $1.10/$4.50/$0.22 | Accelerated Air with priority GPU |
glm-4.5-flash |
✅ Auto | 128K | 96K | Free/Free/Free | Free with reasoning/coding/agents support |
GLM-4 Legacy - Dense Architecture
| Model ID | Thinking | Context | Max Output | Price (Input/Output) | Use Case |
|---|---|---|---|---|---|
glm-4-32b-0414-128k |
❌ | 128K | 16K | $0.10/$0.10 | Ultra-budget dense 32B, parsing without reasoning |
Prices: Per 1M tokens. Cache pricing is for prompt cache hit; cache storage is currently free per Z.AI promotion. GLM-4-32B has no cache support.
Default Agent Configuration:
Strategy: glm-5.3 for critical reasoning and glm-5.3-flash for inexpensive utility and latency-sensitive work. glm-5.3-flashx remains selectable but is not a default because not every compatible gateway deploys it.
| Agent Role | Default Model | Thinking | Temperature | Top P | Max Output |
|---|---|---|---|---|---|
| Generator / Refiner | glm-5.3 |
Max | 1.0 | 0.95 | 32768 |
| Coder | glm-5.3 |
Max | 1.0 | 0.95 | 20480 |
| Adviser / Pentester | glm-5.3 |
Max | 1.0 | 0.95 | 16384 |
| Primary Agent | glm-5.3 |
High | 1.0 | 0.95 | 16384 |
| Assistant / Installer | glm-5.3-flash |
High | 1.0 | 0.95 | 16384 |
| Simple / Reflector | glm-5.3-flash |
Low | 1.0 | 0.95 | 8192 |
| Searcher / Enricher / Simple JSON | glm-5.3-flash |
Low | 1.0 | 0.95 | 4096 |
All default roles use Z.AI's recommended temperature: 1.0 and top_p: 0.95. The full provider replay passed 288 of 289 cases; the sole failure was a transient gateway timeout and its focused retry passed.
Thinking Modes:
- Always on (GLM-5.3, GLM-5.3-Flash, GLM-5.3-FlashX): Cannot be disabled;
reasoning.effortacceptslow,high, ormax - Hybrid (GLM-5.2 and earlier GLM-5.x, GLM-4.7): Explicit toggle via
reasoning.mode - Auto (GLM-4.6, GLM-4.5 series): Model automatically determines when reasoning is needed
- Preserved Thinking (Z.AI Coding capability): all thinking-enabled agents in PentAGI also pass
extra_body.thinking.clear_thinking: falseso thatreasoning_contentfrom previous assistant turns is retained across the conversation. This is required on the standard API endpoint (/api/paas/v4) — on the Coding Plan endpoint it would be enabled by default. Improves reasoning continuity and cache hit rates in multi-turn tool call chains. - All thinking-enabled agents also pass
extra_body.tool_choice: autodefensively
Key Features:
- Long-Horizon Tasks: GLM-5.1 supports 8-hour sustained autonomous execution, ideal for complex multi-stage agentic workflows
- OpenClaw-Native Orchestration: GLM-5-Turbo is specifically optimized for tool invocation, instruction following, and long-chain execution
- Prompt Caching: Significant cost reduction on repeated context (cached input pricing shown)
- Ultra-Long Context: 200K tokens for GLM-5.x/4.7/4.6 series
- MoE Architecture: Efficient 744B/40B active (GLM-5/5.1), 355B/32B (GLM-4.5), 106B/12B (GLM-4.5-Air)
- Tool Calling: Seamless integration with 20+ pentesting tools via function calling
- Streaming: Real-time streaming with streaming tool calls support (GLM-4.6+)
- Multilingual: Exceptional Chinese and English NLP capabilities
- Free Options: GLM-4.7-Flash and GLM-4.5-Flash for prototyping and experimentation
LiteLLM Integration: Set GLM_PROVIDER=zai to enable model name prefixing when using default PentAGI configurations with LiteLLM proxy. Leave empty for direct API usage.
Kimi Provider Configuration
PentAGI integrates with Kimi from Moonshot AI, providing ultra-long context models with multimodal capabilities perfect for analyzing extensive codebases and documentation.
Configuration Variables
| Variable | Default Value | Description |
|---|---|---|
KIMI_API_KEY |
Kimi API key for authentication | |
KIMI_SERVER_URL |
https://api.moonshot.ai/v1 |
Kimi API endpoint URL (international) |
KIMI_PROVIDER |
Provider prefix for LiteLLM integration (optional) |
Configuration Examples
# Direct API usage (international endpoint)
KIMI_API_KEY=your_kimi_api_key
KIMI_SERVER_URL=https://api.moonshot.ai/v1
# Alternative endpoint
KIMI_SERVER_URL=https://api.moonshot.cn/v1 # China
# With LiteLLM proxy
KIMI_API_KEY=your_litellm_key
KIMI_SERVER_URL=http://litellm-proxy:4000
KIMI_PROVIDER=moonshot # Adds prefix to model names (moonshot/kimi-k3) for LiteLLMSupported Models
PentAGI supports the four current Kimi models with tool calling, streaming, context caching, and multimodal input. Kimi K2.5, the older K2 series, kimi-latest, kimi-thinking-preview, and all Moonshot V1 models are retired and not included. Models marked with * are used in the default configuration.
Kimi K3 - Flagship (Always Reasoning)
| Model ID | Thinking | Multimodal | Context | Price (Input Miss / Output / Cache Hit) | Use Case |
|---|---|---|---|---|---|
kimi-k3* |
✅ always | ✅ | 1M | $3.00 / $15.00 / $0.30 | Flagship for long-horizon coding and knowledge work; used by adviser with reasoning_effort: max |
Kimi K2.7 Code Series - Coding-Focused
| Model ID | Thinking | Multimodal | Context | Price (Input Miss / Output / Cache Hit) | Use Case |
|---|---|---|---|---|---|
kimi-k2.7-code* |
✅ always | ✅ | 256K | $0.95 / $4.00 / $0.19 | Coding-focused model used by generator, refiner, coder, and pentester |
kimi-k2.7-code-highspeed* |
✅ always | ✅ | 256K | $1.90 / $8.00 / $0.38 | Same model as kimi-k2.7-code with higher output throughput (~180-260 tokens/s) (primary_agent/assistant default) |
Kimi K2.6 - Multimodal Model
| Model ID | Thinking | Multimodal | Context | Price (Input Miss / Output / Cache Hit) | Use Case |
|---|---|---|---|---|---|
kimi-k2.6* |
✅ hybrid | ✅ | 256K | $0.95 / $4.00 / $0.16 | Utility and installer model with optional thinking |
Prices: Per 1M tokens. K3 cache writes cost $3.00 with the default five-minute TTL or $6.00 with a one-hour TTL; cache hits cost $0.30. K2.7 and K2.6 table prices distinguish cache misses from cache hits.
CRITICAL — Kimi parameter constraints per model family: API returns
invalid_request_errorfor any deviation:
kimi-k3: always reasons, nothinkingparam at all; reasoning depth is set via the top-levelreasoning_effortfield (low/high/max, defaultmax) — PentAGI pins it tomaxfor all agents using this model.temperatureMUST be1.0,top_pMUST be0.95,nMUST be1,presence_penalty/frequency_penaltyMUST be0. Do not switch effort per call — it invalidates the prefix cache.kimi-k2.7-code/kimi-k2.7-code-highspeed:thinkingmay be omitted; if set explicitly, only{"type":"enabled","keep":"all"}is accepted (type: disabledis rejected).reasoning_effortis not supported.temperatureMUST be1.0,top_pMUST be0.95,nMUST be1;tool_choice: requiredis not supported (useauto).kimi-k2.6: thinking mode needstemperature=1.0,top_p=0.95,n=1,thinking.keep="all"; non-thinking mode needstemperature=0.6,top_p=0.95,n=1.- All Kimi models:
presence_penalty=0,frequency_penalty=0,tool_choicein{auto, none}.
Default Agent Configuration:
Strategy: kimi-k2.6 handles utility and installer work, kimi-k2.7-code-highspeed serves the primary/assistant loop, kimi-k2.7-code serves tool-heavy generator/refiner/coder/pentester roles, and kimi-k3 handles adviser reasoning. This configuration passed all 294 conformance cases.
| Agent Role | Default Model | Thinking | Temperature | Top P | Max Output |
|---|---|---|---|---|---|
| Adviser (mentor/planner) | kimi-k3 |
Always (effort=max) | 1.0 | 0.95 | 8192 |
| Generator / Refiner | kimi-k2.7-code |
Always (keep=all) | 1.0 | 0.95 | 32768 |
| Coder | kimi-k2.7-code |
Enabled (keep=all) | 1.0 | 0.95 | 20480 |
| Pentester | kimi-k2.7-code |
Enabled (keep=all) | 1.0 | 0.95 | 16384 |
| Primary Agent / Assistant | kimi-k2.7-code-highspeed |
Enabled (keep=all) | 1.0 | 0.95 | 16384 |
| Installer | kimi-k2.6 |
Enabled | 1.0 | 0.95 | 16384 |
| Reflector / Searcher / Enricher | kimi-k2.6 |
Disabled | 0.6 | 0.95 | 4096 |
| Simple | kimi-k2.6 |
Disabled | 0.6 | 0.95 | 8192 |
| Simple JSON | kimi-k2.6 |
Disabled | 0.6 | 0.95 | 4096 |
Key Features:
- Always-On Reasoning Flagship:
kimi-k3never disables thinking and offers a 1M token context for the most demanding long-horizon coding and knowledge work - Ultra-Long Context: Up to 256K tokens (K2.7/K2.x) or 1M tokens (K3) for comprehensive codebase/documentation analysis
- Native Multimodal: K3, K2.7, and K2.6 support text, image, and video input
- Hybrid Thinking: K2.6 can toggle between thinking and non-thinking via
reasoning.mode; K3 and K2.7 always reason - Preserved Thinking (K2.7, K2.6):
thinking.keep: "all"preserves historicalreasoning_contentacross turns — required for multi-turn tool call chains - Automatic Context Caching: K3/K2.7/K2.x models cache repeated prefixes
- Tool Calling: Full function-calling support for K3, K2.7, K2.x, and Moonshot V1
- Coding-Optimized Variants:
kimi-k2.7-code/kimi-k2.7-code-highspeedtarget higher success rates on long-context programming tasks, with the highspeed variant tuned for throughput - Multilingual: Strong Chinese, English, and multi-language support
Multi-turn with thinking + tool calls: PentAGI's universal reasoning preservation pattern (TextPartWithReasoning + WithPreserveReasoningContent) automatically ensures reasoning_content is sent back in the required TextContent → ToolCall order, satisfying Moonshot's "thinking is enabled but reasoning_content is missing in assistant tool call message" requirement.
LiteLLM Integration: Set KIMI_PROVIDER=moonshot to enable model name prefixing when using default PentAGI configurations with LiteLLM proxy. Leave empty for direct API usage.
Qwen Provider Configuration
PentAGI uses one OpenAI-compatible door for both Qwen Cloud and Alibaba Cloud Model Studio (DashScope). The bundled catalogue is the union of their agent-capable Chat Completions models, while the default role configuration uses only model IDs available on both platforms.
Configuration Variables
| Variable | Default Value | Description |
|---|---|---|
QWEN_API_KEY |
Qwen Cloud, DashScope, or gateway API key | |
QWEN_SERVER_URL |
https://dashscope-us.aliyuncs.com/compatible-mode/v1 |
Direct API or OpenAI-compatible gateway base URL |
QWEN_PROVIDER |
Gateway route prefix: qwen_cloud or dashscope |
Configuration Examples
# Direct Alibaba Cloud Model Studio usage (Global/US endpoint)
QWEN_API_KEY=your_qwen_api_key
QWEN_SERVER_URL=https://dashscope-us.aliyuncs.com/compatible-mode/v1
QWEN_PROVIDER=
# Alternative endpoints
QWEN_SERVER_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1 # International (Singapore)
QWEN_SERVER_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 # Chinese Mainland (Beijing)
# Qwen Cloud through LiteLLM
QWEN_API_KEY=your_litellm_key
QWEN_SERVER_URL=http://litellm-proxy:4000
QWEN_PROVIDER=qwen_cloud
# Alibaba Cloud through LiteLLM
QWEN_PROVIDER=dashscopeSupported Models
PentAGI ships 50 text, reasoning, coding, and vision-language catalogue entries available from at least one of the two platforms. Image generation, video, speech, realtime, translation-only, embedding, reranking, and decision APIs use different protocols or billing units and are not exposed as PentAGI chat models. Prices below are Alibaba Cloud international list rates per 1M tokens where Alibaba offers the model; Qwen Cloud can bill differently.
Cross-platform model IDs used for portable configuration
| Availability | Models |
|---|---|
| Qwen Cloud and DashScope | qwen3.8-max, qwen3.8-flash, qwen3.7-plus, qwen3.7-max, qwen3.6-flash, deepseek-v4.1-flash, deepseek-v4-pro |
| Qwen Cloud only | deepseek-v4-pro-0813 |
| DashScope only | All other entries in the bundled catalogue |
Flagship Models (Top-tier Reasoning)
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3.8-max |
✅ | — | — | — | $2.00/$6.00/$0.25 | Newest flagship succeeding Qwen3.7-Max |
qwen3.8-omni-flash |
✅ | ✅ | ✅ | ✅ | $0.15/$0.47/— | Current multimodal Omni tier |
qwen3.7-max |
✅ | ✅ | ✅ | ✅ | $2.50/$7.50/$0.50 | Next-gen flagship for agent-centric era |
qwen3.6-max-preview |
✅ | ✅ | ✅ | ✅ | $1.30/$7.80/$0.13 | Preview Max with enhanced vibe coding & front-end skills |
qwen3-max |
✅ | ✅ | ✅ | ✅ | $1.20/$6.00/$0.24 | Previous-gen flagship with agent programming upgrades |
qwen-max |
❌ | ✅ | ✅ | ✅ | $1.60/$6.40/— | Legacy 32K text flagship |
qwen-plus |
✅ | ✅ | ✅ | ✅ | $0.40/$4.00/$0.08 | Qwen3-backbone Plus with switchable thinking modes |
Balanced Plus Models (Mid-tier)
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3.7-plus |
✅ | — | — | — | $0.40/$1.60/$0.08 | Cost-efficient tier of the Qwen3.7 generation |
qwen3.6-plus |
✅ | ✅ | ✅ | ✅ | $0.50/$3.00/$0.05 | Native VL Plus with agentic coding |
qwen3.5-plus |
✅ | ✅ | ✅ | ✅ | $0.40/$2.40/$0.04 | Previous-gen native VL with strong multimodal capabilities |
Fast Flash Models (Cost-optimized)
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3.8-flash |
✅ | — | — | — | $0.15/$0.47/$0.016 | Cost-efficient tier of the Qwen3.8 generation |
qwen3.7-flash |
✅ | — | — | — | $0.03/$0.13/$0.02 | Low-latency tier of the Qwen3.7 generation |
qwen3.6-flash* |
✅ | ✅ | ✅ | ✅ | $0.25/$1.50/$0.05 | Shared utility model |
qwen3.5-flash |
✅ | ✅ | ✅ | ✅ | $0.10/$0.40/$0.01 | Ultra-fast lightweight |
qwen-flash |
✅ | ✅ | ✅ | ✅ | $0.05/$0.40/$0.01 | Qwen3-series Flash with 1M context, tiered pricing |
Code-Specialized Models
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3-coder-plus |
❌ | ✅ | ✅ | ✅ | $1.00/$5.00/$0.20 | Strong coding agent with autonomous programming |
qwen3-coder-flash |
❌ | ✅ | ✅ | ✅ | $0.30/$1.50/$0.06 | Fast code-gen with multi-turn tool stability |
qwen3-coder-next |
❌ | ✅ | ✅ | ✅ | $0.30/$1.50/— | Open-source code generation, SOTA at same scale |
Vision-Language Models (Browser & Screenshot Analysis)
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3-vl-plus |
✅ | ✅ | ✅ | ✅ | $0.20/$1.60/$0.04 | VL with visual agent capabilities, ultra-long video understanding |
qwen3-vl-flash |
✅ | ✅ | ✅ | ✅ | $0.05/$0.40/$0.01 | Small VL with 2D/3D localization for browser triage |
qvq-max |
✅ | ✅ | ✅ | ✅ | $1.20/$4.80/— | Visual reasoning with chain-of-thought |
qwen3-vl-235b-a22b-thinking |
✅ | — | — | — | $0.40/$4.00/— | Open-source 235B MoE VL (~22B active) with reasoning |
qwen3-vl-235b-a22b-instruct |
❌ | — | — | — | $0.40/$1.60/— | Open-source 235B MoE VL (~22B active), instruction-following |
qwen3-vl-32b-instruct |
❌ | — | — | — | $0.16/$0.64/— | Open-source 32B dense VL, instruction-following |
qwen3-vl-30b-a3b-thinking |
✅ | — | — | — | $0.20/$2.40/— | Open-source 30B MoE VL (~3B active) with reasoning |
qwen3-vl-30b-a3b-instruct |
❌ | — | — | — | $0.20/$0.80/— | Open-source 30B MoE VL (~3B active), instruction-following |
Open-Source Qwen3.8 Series
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3.8-2.4t-a95b |
✅ | — | — | — | $2.00/$6.00/— | Open-source 3.8 flagship, always reasons |
qwen3.8-27b |
✅ | — | — | — | $0.50/$3.00/$0.10 | Dense open-weight 3.8 tier, hybrid thinking, 1M context |
Open-Source Qwen3.6 Series
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3.6-27b |
✅ | ✅ | ✅ | ✅ | $0.60/$3.60/— | Native VL on hybrid architecture, on-premises ready |
qwen3.6-35b-a3b |
✅ | ✅ | ✅ | ✅ | $0.25/$1.49/— | Efficient 35B MoE (~3B active) for continuous monitoring |
Open-Source Qwen3.5 Series
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3.5-397b-a17b |
✅ | ✅ | ✅ | ✅ | $0.60/$3.60/— | Largest 397B params (~17B active), exceptional reasoning |
qwen3.5-122b-a10b |
✅ | ✅ | ✅ | ✅ | $0.40/$3.20/— | Large 122B params (~10B active), strong balance |
qwen3.5-35b-a3b |
✅ | ✅ | ✅ | ✅ | $0.25/$2.00/— | Efficient 35B MoE (~3B active), cost-effective |
qwen3.5-27b |
✅ | ✅ | ✅ | ✅ | $0.30/$2.40/— | Medium 27B with hybrid linear attention + sparse MoE |
Open-Source Qwen3 Coder Series
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3-coder-480b-a35b-instruct |
❌ | ✅ | ✅ | ✅ | $1.50/$7.50/— | Largest open coder MoE (480B/~35B active) |
qwen3-coder-30b-a3b-instruct |
❌ | ✅ | ✅ | ✅ | $0.45/$2.25/— | Efficient 30B MoE (~3B active), repository-scale |
Open-Source Qwen3 Dense & MoE Series
| Model ID | Thinking | Intl | Global/US | China | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|---|---|
qwen3-next-80b-a3b-thinking |
✅ | ✅ | ✅ | ✅ | $0.15/$1.20/— | Next-gen 80B MoE (~3B active) thinking-only |
qwen3-next-80b-a3b-instruct |
❌ | ✅ | ✅ | ✅ | $0.15/$1.20/— | Next-gen 80B MoE instruction-following |
qwen3-235b-a22b-thinking-2507 |
✅ | — | — | — | $0.23/$2.30/— | 235B MoE (~22B active) thinking variant |
qwen3-30b-a3b-thinking-2507 |
✅ | — | — | — | $0.20/$2.40/— | 30B MoE (~3B active) thinking variant |
qwen3-30b-a3b-instruct-2507 |
❌ | — | — | — | $0.20/$0.80/— | 30B MoE (~3B active) non-thinking variant |
Third-Party Models
These models share the OpenAI-compatible Qwen door. Availability differs by platform; the cross-platform table above is authoritative for portable role configuration.
| Model ID | Thinking | Context | Price (Input/Output/Cache) | Use Case |
|---|---|---|---|---|
glm-5.3-prime |
✅ | 1M | $2.80/$8.80/— | DashScope priority GLM route |
glm-5.2-fast-preview |
✅ | 1M | $2.80/$8.80/— | DashScope high-throughput GLM 5.2 route |
glm-5.2 |
✅ | 1M | $1.40/$4.40/$0.28 | Zhipu AI GLM flagship route |
deepseek-v4.1-flash* |
✅ | 1M | $0.15/$0.60/— | Current shared low-latency DeepSeek model |
deepseek-v4-pro* |
✅ | 1M | $2.40/$4.80/$0.20 | Shared model for security and tool-heavy roles |
deepseek-v4-pro-0813 |
✅ | 1M | $2.40/$4.80/$0.20 | Qwen Cloud-only V4 Pro snapshot |
deepseek-v4-flash-0731 |
✅ | 1M | $0.44/$1.32/$0.088 | Legacy DashScope Flash snapshot |
kimi-k3 |
✅ | 1M | $3.00/$15.00/$0.30 | Current Kimi flagship on DashScope |
kimi-k2.7-code |
✅ | 262K | $0.95/$4.00/$0.19 | Moonshot coding model, thinking always on, 16K output ceiling |
Prices: Per 1M tokens. Cache pricing reflects implicit cache hit (when available); MoE/dense open-source models do not expose cache pricing. Tiered models (Max/Plus) show lowest-tier pricing (typically ≤32k or ≤256k input); larger contexts incur higher rates per Alibaba Cloud pricing. deepseek-v4-flash-0731 shows the busy-hours rate; idle hours bill half.
Region Availability:
- Intl (International): Singapore region (
dashscope-intl.aliyuncs.com) - Global/US: US Virginia region (
dashscope-us.aliyuncs.com) - China: Chinese Mainland Beijing region (
dashscope.aliyuncs.com) - —: availability in that region is not recorded here
Default Agent Configuration:
| Agent Role | Default Model | Tier |
|---|---|---|
simple, simple_json, reflector, searcher, enricher |
qwen3.6-flash |
Utility |
primary_agent, assistant, coder, installer, pentester |
deepseek-v4.1-flash |
Workhorse |
generator, refiner, adviser |
deepseek-v4-pro |
Planning |
The role configuration must run unchanged behind qwen_cloud/ and dashscope/. Qwen 3.6 Flash handles utility calls, DeepSeek V4.1 Flash provides the lower-cost workhorse tier, and DeepSeek V4 Pro remains on planning and advice. Thinking controls are left at provider defaults because Qwen Cloud does not accept the same disable operation that DashScope supports.
Key Features:
- Agent-Centric Design: Qwen3.7-Max is purpose-built for long-horizon autonomous execution and tool invocation
- Automatic Context Caching: 30-50% cost reduction on repeated context with implicit cache
- Extended Thinking: Chain-of-thought reasoning for complex security analysis (Qwen3.7/3.6/3.5/3-Max, QVQ-Max)
- Code Specialization: Qwen3-Coder series with multi-turn tool interaction and repository-level understanding
- Vision-Language: Qwen3-VL series for browser screenshot triage, 2D/3D localization, OCR-level analysis
- Tool Calling: Seamless integration with 20+ pentesting tools via function calling
- Streaming: Real-time response streaming for interactive workflows
- Multilingual: Strong Chinese, English, and multi-language support
- Open-Source Variants: Dense and MoE models from 27B to 2.4T for on-premises/air-gapped deployments
LiteLLM Integration: Set QWEN_PROVIDER=qwen_cloud for Qwen Cloud routes or QWEN_PROVIDER=dashscope for Alibaba Cloud routes. Leave it empty for direct DashScope API usage.
Alternative Integrations
DashScope is fully OpenAI-compatible, so Qwen can also power two other PentAGI subsystems through the standard OpenAI client.
As embedding provider (text-embedding-v4, see Alibaba Cloud Model Studio pricing):
EMBEDDING_PROVIDER=openai
EMBEDDING_URL=https://dashscope-intl.aliyuncs.com/compatible-mode/v1 # International (Singapore)
# EMBEDDING_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 # Chinese Mainland
EMBEDDING_KEY=sk-*******
EMBEDDING_MODEL=text-embedding-v4
EMBEDDING_BATCH_SIZE= # optional, default applies
EMBEDDING_STRIP_NEW_LINES= # optional, default appliesNote: the Global/US DashScope endpoint (
dashscope-us.aliyuncs.com) does not expose embedding APIs — use the International or China endpoints fortext-embedding-v4.
As OpenAI-typed custom LLM provider: instead of the dedicated QWEN_* variables, you can wire any Qwen chat model through PentAGI's custom OpenAI-compatible provider by pointing OPENAI_SERVER_URL (or a custom provider entry) to the DashScope /compatible-mode/v1 endpoint and selecting the desired Qwen model name. Useful when you already manage all model traffic through a single OpenAI-shaped client (e.g. shared with LiteLLM/OneAPI proxies).
MiniMax Provider Configuration
PentAGI integrates with MiniMax's M-series through the OpenAI-compatible https://api.minimax.io/v1 endpoint: large-context agentic models with tool calling, JSON output, and streaming.
Configuration Variables
| Variable | Default Value | Description |
|---|---|---|
MINIMAX_API_KEY |
MiniMax API key for authentication | |
MINIMAX_SERVER_URL |
https://api.minimax.io/v1 |
MiniMax API endpoint URL |
MINIMAX_PROVIDER |
Provider prefix for LiteLLM integration (optional) |
Configuration Examples
# Direct API usage
MINIMAX_API_KEY=your_minimax_api_key
MINIMAX_SERVER_URL=https://api.minimax.io/v1
# With LiteLLM proxy
MINIMAX_API_KEY=your_litellm_key
MINIMAX_SERVER_URL=http://litellm-proxy:4000
MINIMAX_PROVIDER=minimax # Adds prefix to model names (minimax/MiniMax-M3) for LiteLLMSupported Models
PentAGI ships 3 MiniMax models with tool calling, JSON output, and streaming. MiniMax-M3 is the default catalogue model; the bundled agent configuration uses MiniMax-M2.7 for roles where conformance testing showed more reliable context retention and unified-diff generation.
| Model ID | Context | Price (Input/Output, ≤512K context) | Use Case |
|---|---|---|---|
MiniMax-M3* |
~1M | $0.30/$1.20 (2x above 512K tokens) | Latest flagship for agentic reasoning, tool use, code generation, and long-context tasks |
MiniMax-M2.7 |
204K | $0.30/$1.20 | Reliable reasoning and coding model used by stateful and diff-producing agents |
MiniMax-M2.7-highspeed |
204K | $0.60/$2.40 | Low-latency variant of M2.7 for fast-response scenarios |
LiteLLM Integration: Set MINIMAX_PROVIDER=minimax to enable model name prefixing when using default PentAGI configurations with LiteLLM proxy. Leave empty for direct API usage.
Mistral Provider Configuration
PentAGI talks to Mistral through the OpenAI-compatible https://api.mistral.ai/v1 endpoint: the
Medium/Large/Small line, the Ministral small models, Codestral for code, and one Z.ai model Mistral
serves itself.
Configuration Variables
| Variable | Default Value | Description |
|---|---|---|
MISTRAL_API_KEY |
Mistral API key for authentication | |
MISTRAL_SERVER_URL |
https://api.mistral.ai/v1 |
Mistral API endpoint URL |
MISTRAL_PROVIDER |
Provider prefix for LiteLLM integration (optional) |
Configuration Examples
# Direct API usage
MISTRAL_API_KEY=your_mistral_api_key
MISTRAL_SERVER_URL=https://api.mistral.ai/v1
# With LiteLLM proxy
MISTRAL_API_KEY=your_litellm_key
MISTRAL_SERVER_URL=http://litellm-proxy:4000
MISTRAL_PROVIDER=mistral # Adds prefix to model names (mistral/mistral-small-latest) for LiteLLMSupported Models
PentAGI ships 6 Mistral models. The default configuration uses mistral-small-latest for utility and read-heavy agents, mistral-large-latest for primary and execution agents, and mistral-medium-latest for planning and advice. Small, Medium, and Large each have a 256K-token context window.
| Model ID | Price (Input/Output) | Reasons | Use Case |
|---|---|---|---|
mistral-small-latest* |
$0.15/$0.60 | yes | Hybrid instruct/reasoning/coding model for utility and read-heavy agents |
mistral-medium-latest |
$1.50/$7.50 | yes | Frontier-class multimodal model for planning and advice |
mistral-large-latest |
$0.50/$1.50 | no | General-purpose model for primary, assistant, coding, and pentesting |
codestral-latest |
$0.30/$0.90 | no | Code completion and fill-in-the-middle |
zai-glm-5-2 |
$1.40/$4.40 | yes | Z.ai GLM 5.2 served by Mistral, public preview |
zai-glm-5-3 |
$1.40/$4.40 | yes | Z.ai GLM 5.3 served by Mistral, public preview; thinking cannot be turned off |
LiteLLM Integration: Set MISTRAL_PROVIDER=mistral to enable model name prefixing when using default PentAGI configurations with LiteLLM proxy. Leave empty for direct API usage.
xAI (Grok) Provider Configuration
PentAGI talks to xAI through the OpenAI-compatible https://api.x.ai/v1 endpoint: the Grok 4.x line with large context windows, plus a coding model.
Configuration Variables
| Variable | Default Value | Description |
|---|---|---|
XAI_API_KEY |
xAI API key for authentication | |
XAI_SERVER_URL |
https://api.x.ai/v1 |
xAI API endpoint URL |
XAI_PROVIDER |
Provider prefix for LiteLLM integration (optional) |
Configuration Examples
# Direct API usage
XAI_API_KEY=your_xai_api_key
XAI_SERVER_URL=https://api.x.ai/v1
# With LiteLLM proxy
XAI_API_KEY=your_litellm_key
XAI_SERVER_URL=http://litellm-proxy:4000
XAI_PROVIDER=xai # Adds prefix to model names (xai/grok-4.3) for LiteLLMSupported Models
PentAGI lists seven xAI Chat Completions model IDs that responded through the tested API. Catalogue availability does not imply suitability as an agent default. The bundled configuration uses grok-4.3 for tool-heavy roles, including searcher, and grok-4.20-0309-non-reasoning for simple, simple_json, reflector, and enricher; the exact bindings are in backend/pkg/providers/xai/config.yml. In saved generator chains, grok-4.3 called subtask_list directly on all nine, whereas grok-4.6 returned text without a tool call on all nine. The full replay matrix records outcomes and limitations. The Responses-only multi-agent model is not listed because PentAGI agents supply their own client-side tools.
| Model ID | Context | Price (Input/Output) | Reasons | Saved-chain result |
|---|---|---|---|---|
grok-4.7 |
500K | $2.00/$6.00 | yes | API responds; generator/reflector refusals make it unsuitable as a default |
grok-4.6 |
500K | $2.00/$6.00 | yes | API responds; all nine saved generator requests returned text without a tool |
grok-4.5 |
500K | $2.00/$6.00 | yes | API responds; eight of nine generator requests explicitly refused |
grok-4.3* |
1M | $1.25/$2.50 | yes | Direct plan on all nine saved generator chains; tool-role default and fallback |
grok-4.20-0309-reasoning |
1M | $1.25/$2.50 | yes | Four direct plans, three more after safe tool continuation, two pending |
grok-4.20-0309-non-reasoning* |
1M | $1.25/$2.50 | no | Utility-role default; six plans after safe tool continuation, three pending |
grok-build-0.1 |
256K | $1.00/$2.00 | yes | Coding model; five plans after safe tool continuation, four pending |
Note: prices are for prompts under 200K tokens; at or above that threshold xAI applies the higher long-context rate to every token in the request.
LiteLLM Integration: Set XAI_PROVIDER=xai to enable model name prefixing when using default PentAGI configurations with LiteLLM proxy. Leave empty for direct API usage.
Advanced Setup
Langfuse Integration
Langfuse provides advanced capabilities for monitoring and analyzing AI agent operations.
Configure Langfuse environment variables in existing
.envfile.Langfuse valuable environment variables
Database Credentials
LANGFUSE_POSTGRES_USERandLANGFUSE_POSTGRES_PASSWORD- Langfuse PostgreSQL credentialsLANGFUSE_CLICKHOUSE_USERandLANGFUSE_CLICKHOUSE_PASSWORD- ClickHouse credentialsLANGFUSE_REDIS_AUTH- Redis password
Encryption and Security Keys
LANGFUSE_SALT- Salt for hashing in Langfuse Web UILANGFUSE_ENCRYPTION_KEY- Encryption key (32 bytes in hex)LANGFUSE_NEXTAUTH_SECRET- Secret key for NextAuth
Admin Credentials
LANGFUSE_INIT_USER_EMAIL- Admin emailLANGFUSE_INIT_USER_PASSWORD- Admin passwordLANGFUSE_INIT_USER_NAME- Admin username
API Keys and Tokens
LANGFUSE_INIT_PROJECT_PUBLIC_KEY- Project public key (used from PentAGI side too)LANGFUSE_INIT_PROJECT_SECRET_KEY- Project secret key (used from PentAGI side too)
S3 Storage
LANGFUSE_S3_ACCESS_KEY_ID- S3 access key IDLANGFUSE_S3_SECRET_ACCESS_KEY- S3 secret access key
- Enable integration with Langfuse for PentAGI service in
.envfile.
LANGFUSE_BASE_URL=http://langfuse-web:3000
LANGFUSE_PROJECT_ID= # default: value from ${LANGFUSE_INIT_PROJECT_ID}
LANGFUSE_PUBLIC_KEY= # default: value from ${LANGFUSE_INIT_PROJECT_PUBLIC_KEY}
LANGFUSE_SECRET_KEY= # default: value from ${LANGFUSE_INIT_PROJECT_SECRET_KEY}- Run the Langfuse stack:
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose-langfuse.yml
docker compose -f docker-compose.yml -f docker-compose-langfuse.yml up -dVisit localhost:4000 to access Langfuse Web UI with credentials from .env file:
LANGFUSE_INIT_USER_EMAIL- Admin emailLANGFUSE_INIT_USER_PASSWORD- Admin password
Monitoring and Observability
For detailed system operation tracking, integration with monitoring tools is available.
- Enable integration with OpenTelemetry and all observability services for PentAGI in
.envfile.
OTEL_HOST=otelcol:8148- Run the observability stack:
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose-observability.yml
docker compose -f docker-compose.yml -f docker-compose-observability.yml up -dVisit localhost:3000 to access Grafana Web UI.
[!NOTE]
If you want to use Observability stack with Langfuse, you need to enable integration in.envfile to setLANGFUSE_OTEL_EXPORTER_OTLP_ENDPOINTtohttp://otelcol:4318.To run all available stacks together (Langfuse, Graphiti, and Observability):
bashdocker compose -f docker-compose.yml -f docker-compose-langfuse.yml -f docker-compose-graphiti.yml -f docker-compose-observability.yml up -dYou can also register aliases for these commands in your shell to run it faster:
bashalias pentagi="docker compose -f docker-compose.yml -f docker-compose-langfuse.yml -f docker-compose-graphiti.yml -f docker-compose-observability.yml" alias pentagi-up="docker compose -f docker-compose.yml -f docker-compose-langfuse.yml -f docker-compose-graphiti.yml -f docker-compose-observability.yml up -d" alias pentagi-down="docker compose -f docker-compose.yml -f docker-compose-langfuse.yml -f docker-compose-graphiti.yml -f docker-compose-observability.yml down"
Knowledge Graph Integration (Graphiti)
[!IMPORTANT]
Graphiti is an optional beta integration and is disabled by default. Review Limitations and Security before enabling it in production.
PentAGI integrates with Graphiti, a temporal knowledge graph system powered by Neo4j, to provide advanced semantic understanding and relationship tracking for AI agent operations. The vxcontrol fork provides custom entity and edge types that are specific to pentesting purposes.
What is Graphiti?
Graphiti asynchronously extracts structured knowledge from agent interactions and builds a graph of entities, relationships, evidence, and temporal context. PentAGI sends agent responses and tool executions to Graphiti and exposes the graphiti_search tool to enabled agents. Graphiti complements the primary pgvector memory; it does not replace it.
- Semantic Memory: Store and recall relationships between tools, targets, vulnerabilities, and techniques
- Contextual Understanding: Track how different pentesting actions relate to each other over time
- Flow-Scoped Recall: Reuse knowledge within the active flow without exposing data from other engagements by default
- Advanced Querying: Search temporal context, relationships, successful tools, recent episodes, and entities by type
When enabled, PentAGI captures agent responses, tool execution details, and flow/task/subtask context. Ingestion is asynchronous, so newly submitted events can take time to become searchable.
Deployment Modes and Enabling
Graphiti can run as the bundled Neo4j + Graphiti stack, as an external service, or remain disabled.
For the bundled stack, configure .env:
GRAPHITI_ENABLED=true
GRAPHITI_TIMEOUT=30
GRAPHITI_URL=http://graphiti:8000
GRAPHITI_LLM_CLIENT_TYPE=openai
# Reused by the Graphiti OpenAI preset
OPEN_AI_KEY=your_openai_api_key
OPEN_AI_SERVER_URL=https://api.openai.com/v1
# Bundled Neo4j
NEO4J_USER=neo4j
NEO4J_DATABASE=neo4j
NEO4J_PASSWORD=replace_with_a_strong_password
NEO4J_URI=bolt://neo4j:7687Download the optional compose file and the Neo4j settings with the APOC plugin Graphiti needs when installing manually, then start both stacks:
curl -O https://raw.githubusercontent.com/vxcontrol/pentagi/master/docker-compose-graphiti.yml
mkdir -p neo4j/conf neo4j/plugins
for f in conf/neo4j.conf conf/apoc.conf plugins/apoc-5.26.19-core.jar; do
curl -fsSL -o "neo4j/$f" "https://raw.githubusercontent.com/vxcontrol/pentagi/master/examples/neo4j/$f"
done
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml up -dThe base stack must create the external pentagi-network before the Graphiti stack can start. The installer handles stack ordering automatically.
For an external Graphiti deployment, set GRAPHITI_ENABLED=true and point GRAPHITI_URL to its API. Do not start docker-compose-graphiti.yml; configure providers, embeddings, the graph database, and ingest tuning on the external service itself.
PentAGI enables its client only when both GRAPHITI_ENABLED=true and GRAPHITI_URL is non-empty. At startup it performs three health-check attempts with a two-second backoff. If they all fail, PentAGI logs a warning and continues with Graphiti disabled.
LLM Provider and Model Presets
GRAPHITI_LLM_CLIENT_TYPE selects one deployment-wide preset. Model names and call parameters are not environment variables; they live in graphiti/.yaml.
| Preset | Credentials and endpoint | Shipped main model |
|---|---|---|
openai |
OPEN_AI_KEY, OPEN_AI_SERVER_URL |
openai/gpt-5-mini |
gemini |
GEMINI_API_KEY, GEMINI_SERVER_URL |
gemini/gemini-3.5-flash-lite |
custom |
LLM_SERVER_KEY, LLM_SERVER_URL |
Qwen/Qwen3.6-27B-FP8 |
litellm |
GRAPHITI_LITELLM_API_KEY, GRAPHITI_LITELLM_BASE_URL |
openrouter/openai/gpt-oss-20b |
The Gemini preset uses Graphiti's LiteLLM/OpenAI-compatible client path. Point GEMINI_SERVER_URL at a compatible gateway if the native Gemini endpoint does not provide the required OpenAI-compatible API.
Each preset file must contain a matching provider plus MODEL_NAME and SMALL_MODEL_NAME mappings. The small model is used for reranking and lighter calls. Supported call settings include temperature, token limits, sampling and penalty parameters, JSON mode, reasoning effort, verbosity, pricing metadata, and provider-specific extra_body.
The installer copies examples/graphiti beside the installation as ./graphiti. The compose mount is controlled by:
GRAPHITI_CONFIG_PATH=./graphiti
GRAPHITI_CONFIG_DIR=llm_configsGRAPHITI_CONFIG_PATH may point directly to ./examples/graphiti for development. GRAPHITI_CONFIG_DIR=llm_configs activates the mounted presets. If an older .env omits that variable, a newer compose file mounts an empty host directory at the unused configs path instead of hiding the presets built into the image.
[!NOTE]
GRAPHITI_MODEL_NAMEis obsolete and ignored. Edit the active YAML preset instead, then restart the Graphiti container.
Graphiti Embedding Configuration
By default, Graphiti uses the active LLM preset's credentials and its default OpenAI embedding model. To use PentAGI's shared embedding endpoint explicitly:
GRAPHITI_SEPARATE_EMBEDDING=true
EMBEDDING_URL=https://embedding.example.com/v1
EMBEDDING_KEY=your_embedding_api_key
EMBEDDING_MODEL=openai/text-embedding-3-largeGraphiti's embedder is OpenAI-compatible. EMBEDDING_PROVIDER is used by PentAGI but is not passed to Graphiti, so a non-OpenAI-compatible embedding provider cannot be shared directly.
Ingestion and Extraction Tuning
The supplied defaults prioritize flow isolation and limit expensive extraction to useful events.
Ingest policy actions:
REJECT: do not store the episode.SKIP_LLM: store the episode for retrieval but do not extract nodes or edges.PROCESS: store the episode and run full LLM extraction.
| Variable | Default | When to change it |
|---|---|---|
GRAPHITI_INGEST_POLICY_RULES |
{"graphiti_search":"REJECT","tool_execution_terminal":"PROCESS","tool_execution_file":"PROCESS"} |
Add narrow, case-insensitive name/source patterns when specific events need different handling |
GRAPHITI_INGEST_POLICY_FIELD |
both |
Restrict matching to name or source_description only when event naming is controlled |
GRAPHITI_INGEST_POLICY_DEFAULT_ACTION |
SKIP_LLM |
Use PROCESS only when every unmatched event justifies extraction cost |
GRAPHITI_INGEST_USE_GROUP_ACTORS |
true |
Keep enabled to preserve FIFO ordering independently for each flow |
GRAPHITI_INGEST_WORKER_COUNT |
16 |
Raise for more concurrent flows when the LLM and database have capacity; lower to control load |
GRAPHITI_INGEST_LOCK_BY_GROUP_ID |
true |
Used only in shared-pool mode; ignored when group actors are enabled |
GRAPHITI_INGEST_TASK_MAX_RETRIES |
1 (0-5) |
Increase for transient LLM/network failures |
GRAPHITI_INGEST_TASK_RETRY_DELAY_SEC |
2.0 (0.5-60) |
Increase when an upstream service needs more recovery time |
GRAPHITI_INGEST_TASK_TIMEOUT_SEC |
0 (0-3600) |
Set a finite value to prevent one stalled request from blocking a flow; 0 disables the timeout |
GRAPHITI_INGEST_QUEUE_MAX_SIZE |
0 |
Set a bound to return HTTP 429 instead of allowing an unlimited backlog |
GRAPHITI_INGEST_DEAD_LETTER_ENABLED |
false |
Enable when failed episodes must be retained for operational review |
Extraction uses the following fallback order: full combined extraction (nodes, attributes, summaries, and edges in one call), regular combined extraction (nodes and edges), then separate node/edge extraction. Empty or failed combined results automatically fall back; these log messages are expected during normal operation.
| Variable | Default | Effect |
|---|---|---|
GRAPHITI_TAXONOMY_LAYER_PROFILE |
STRUCTURAL,EVIDENCE,PROGRESS,ATTEMPT |
Controls which edge classes appear in prompts and pass validation; full/all enables every class and minimal selects the core attack graph |
GRAPHITI_USE_COMBINED_FULL_EXTRACTION |
true |
Enables the most compact single-call extraction path |
GRAPHITI_USE_COMBINED_EXTRACTION |
true |
Enables the regular combined fallback |
GRAPHITI_COMBINED_FULL_GATING_ENABLED |
true |
Skips expensive full extraction for low-signal administrative/search events |
GRAPHITI_COMBINED_DIAGNOSTIC_SAMPLES |
false |
Includes content samples in diagnostics; keep disabled because pentest output can contain credentials |
GRAPHITI_ANCHOR_NODE_MODE |
smart |
smart loads all key entities plus limited high-volume types; limit applies one total cap |
GRAPHITI_ANCHOR_NODE_LIMIT |
25 (1-500) |
Total anchor cap in limit mode |
GRAPHITI_ANCHOR_MASS_TYPE_LIMIT |
10 (1-100) |
Per-type cap in smart mode; 0 is invalid |
GRAPHITI_ANCHOR_QUERY_TIMEOUT |
10 (1-60) |
Bounds anchor lookup; timeout degrades gracefully to no anchors |
Anchors connect entities across episodes and are used by the separate extraction path. Combined extraction has already produced its edges and does not perform this anchor lookup.
These flags are passed as process environment variables by the bundled compose file. This is important for combined extraction because Graphiti reads those flags when Python modules are imported.
Runtime, Logging, and Neo4j
The values below are PentAGI's recommended .env.example/compose defaults, not the raw Graphiti image fallbacks. Running a freshly pulled image behind an old compose file can instead enable telemetry and global search, use one shared-pool worker with PROCESS as the unmatched ingest action, enable the full taxonomy, and disable combined extraction. Keep the image, compose file, .env, and presets in sync.
| Variable | Default | Guidance |
|---|---|---|
GRAPHITI_CPUS, GRAPHITI_MEMORY |
2.0, 2G |
Container limits; raise together with concurrency only after observing CPU and memory pressure |
GRAPHITI_SEMAPHORE_LIMIT |
20 |
Limits parallel Graphiti coroutines; it is separate from ingest worker concurrency |
GRAPHITI_TELEMETRY_ENABLED |
false |
Enables anonymous Graphiti telemetry when set to true |
GRAPHITI_LOG_LEVEL |
INFO |
Use DEBUG temporarily; it can produce sensitive and high-volume output |
GRAPHITI_LOG_STDOUT |
events |
off, events, or full; events is recommended for containers |
GRAPHITI_FLOW_LOGGER_WARN_COUNT |
256 |
Warns about growth of cached per-flow loggers; 0 disables the warning |
GRAPHITI_DEBUG_RUNTIME_RESOURCES |
false |
Enables /debug/runtime-resources; expose only to trusted operators |
GRAPHITI_SEARCH_SCOPE |
flowid |
Keep for flow/tenant isolation; all enables global searches and can expose other engagements |
GRAPHITI_LOG_FORMAT |
json |
Reserved by the current deployment contract; the Graphiti logger does not yet apply it |
NEO4J_CPUS, NEO4J_MEMORY |
4.0, 4G |
Neo4j container limits; use neo4j-admin server memory-recommendation --docker for production sizing |
NEO4J_SHM_SIZE |
4g |
/dev/shm limit; actual use counts toward the container memory limit |
NEO4J_NOFILE |
65536 |
Open-file soft/hard limit, suitable for many indexes and concurrent connections |
NEO4J_HEAP_INITIAL_SIZE, NEO4J_HEAP_MAX_SIZE, NEO4J_PAGECACHE_SIZE |
2G, 2G, 1G |
JVM heap/page cache sizing; NEO4J_CPUS/NEO4J_MEMORY only cap the container, the JVM does not reliably size itself to fit inside that cap on its own. Defaults favor heap over page cache — suited to a low write-throughput deployment with occasional wide reads, since query execution/result materialization lives in heap while a small dataset is already comfortably held by 1G of page cache; re-run neo4j-admin server memory-recommendation --docker once real data volume is known |
NEO4J_TRANSACTION_MAX |
1G |
Caps a single transaction's memory (db.memory.transaction.max) so one runaway/unbounded query (e.g. a Cartesian product or an unbounded variable-length path before a LIMIT) fails cleanly with an out-of-memory Cypher error instead of exhausting the whole heap and taking down every other query on the server |
NEO4J_BOLT_ADVERTISED_ADDRESS |
empty | Set only when Neo4j Browser and the Bolt connector are reverse-proxied on different public domains (e.g. behind Guarder with CORS/cookie-group support for cross-origin bolt access); format host:port. Left empty, Neo4j's discovery endpoint advertises whatever Host header the request arrived with, which is correct only when both share one domain |
NEO4J_HTTP_ADVERTISED_ADDRESS |
empty | Set only when a reverse proxy in front of Neo4j Browser strips or rewrites the Host header, making the dynamic Host-header-based advertised address incorrect; format host:port. Leave empty in the common case (proxy forwards Host unchanged) |
NEO4J_USER, NEO4J_PASSWORD, NEO4J_URI, and NEO4J_DATABASE configure the bundled connection. Neo4j Community Edition supports only its default database; do not configure a separate database name that requires Enterprise multi-database support.
The installer copies examples/neo4j beside the installation as ./neo4j. It contains static, non-.env-tunable settings that don't have a NEO4J_* variable: conf/neo4j.conf and conf/apoc.conf, plus a version-pinned plugins/apoc-*-core.jar. The compose mount is controlled by:
NEO4J_DIR=./neo4jNEO4J_DIR may point directly to ./examples/neo4j for development. Both conf/ and plugins/ are mounted read-only; the stack still starts on Neo4j's built-in defaults (without APOC) if the directory is absent, since Docker creates an empty one automatically — but Graphiti writes relationships through APOC, so its graph then stays empty while PentAGI logs no error. Do not duplicate any NEO4J_* variable from the table above inside conf/neo4j.conf — the Neo4j Docker entrypoint always strips a matching line from the mounted file and re-appends the environment variable's value, so a duplicated setting in the file would be silently ignored.
The bundled stack currently wires Neo4j only. The Graphiti image contains FalkorDB support, but using it requires a separately configured deployment because the stock compose file does not expose GRAPHITI_GRAPH_BACKEND or FALKORDB_*.
Verification and Troubleshooting
Check service health, queue state, and logs:
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml ps graphiti neo4j
docker compose -f docker-compose.yml -f docker-compose-graphiti.yml logs -f graphiti
curl -fsS http://localhost:8000/healthcheck
curl -fsS http://localhost:8000/queue-sizeNeo4j Browser is available at http://localhost:7474; the Graphiti OpenAPI UI is at http://localhost:8000/docs. Both are bound to localhost by the stock compose file.
Common failures:
- A missing API key or base URL for the selected preset, a missing YAML file, or a YAML
providermismatch causes the Graphiti container to fail startup validation. LLM_CLIENT_TYPE=openairejects local/custom model prefixes; use thecustompreset for an OpenAI-compatible local server.- In
flowidsearch mode, requests without a group ID are rejected. PentAGI supplies the flow-derived group ID automatically. - A bounded full queue returns HTTP 429.
/queue-sizereports waiting, processing, active-group, and dropped counters. - Invalid retry, timeout, or anchor ranges fail startup rather than being silently normalized.
Update .env, docker-compose-graphiti.yml, the Graphiti image, and the graphiti preset directory together. Pulling only a new image can retain older compose defaults and silently change extraction behavior.
Limitations and Security
- Graphiti is beta and has no in-app graph explorer.
- One provider preset is active for the entire Graphiti deployment; it is not selected per PentAGI agent or flow.
- Graphiti extraction, reranking, and embeddings incur billing independently of the model used by the main PentAGI flow.
- Search is flow-scoped by default. Cross-flow reuse requires an explicit global-search design and must not be enabled on shared or multi-tenant deployments without additional isolation.
- The Graphiti HTTP API has no authentication layer in the bundled service. The stock compose binds it and Neo4j to
127.0.0.1; secure external deployments with network controls and authentication at a trusted reverse proxy. - Agent and tool output may contain credentials and exploitation evidence. Protect Neo4j data, logs, dead letters, diagnostics, and backups accordingly.
- If Graphiti is unavailable, PentAGI continues with its primary memory and vector store after logging the failed startup health check. Set
GRAPHITI_ENABLED=falseto disable the integration explicitly.
GitHub and Google OAuth Integration
OAuth integration with GitHub and Google allows users to authenticate using their existing accounts on these platforms. This provides several benefits:
- Simplified login process without need to create separate credentials
- Enhanced security through trusted identity providers
- Access to user profile information from GitHub/Google accounts
- Seamless integration with existing development workflows
PentAGI uses PUBLIC_URL as the public origin/base URL for OAuth redirects. In the default deployment, both GitHub and Google callbacks are handled by:
${PUBLIC_URL}/api/v1/auth/login-callbackFor GitHub OAuth:
- Create a new OAuth App in your GitHub account.
- Set Homepage URL to your
PUBLIC_URL. - Set Authorization callback URL to
${PUBLIC_URL}/api/v1/auth/login-callback. - Add the client credentials to your
.envfile:
PUBLIC_URL=https://pentagi.example.com
OAUTH_GITHUB_CLIENT_ID=your_github_client_id
OAUTH_GITHUB_CLIENT_SECRET=your_github_client_secretFor Google OAuth:
- Create OAuth credentials in your Google Cloud project.
- Use the same callback endpoint:
${PUBLIC_URL}/api/v1/auth/login-callback. - Add the client credentials to your
.envfile:
PUBLIC_URL=https://pentagi.example.com
OAUTH_GOOGLE_CLIENT_ID=your_google_client_id
OAUTH_GOOGLE_CLIENT_SECRET=your_google_client_secretMake sure PUBLIC_URL matches the externally accessible HTTPS address of your PentAGI instance and does not include the callback path itself. If the URL configured in the OAuth provider does not exactly match the callback generated by PentAGI, the provider will reject the login attempt with a redirect URI mismatch error.
Docker Image Configuration
PentAGI allows you to configure Docker image selection for executing various tasks. The system automatically chooses the most appropriate image based on the task type, but you can constrain this selection by specifying your preferred images:
| Variable | Default | Description |
|---|---|---|
PENTAGI_IMAGE |
vxcontrol/pentagi:latest |
Docker image used for the main PentAGI application service |
DOCKER_DEFAULT_IMAGE |
debian:latest |
Default Docker image for general tasks and ambiguous cases |
DOCKER_DEFAULT_IMAGE_FOR_PENTEST |
vxcontrol/kali-linux |
Default Docker image for security/penetration testing tasks |
DOCKER_ALLOWED_IMAGES |
empty | Comma-separated allow-list the selected image must belong to |
DOCKER_IMAGE_SELECTION_MODE |
llm |
llm lets the model pick the image, fixed always uses the pentest image |
PENTAGI_IMAGE changes the image used by the main pentagi service in docker-compose.yml. The DOCKER_DEFAULT_IMAGE and DOCKER_DEFAULT_IMAGE_FOR_PENTEST variables only affect automatic worker image selection for task execution inside PentAGI. They do not rewrite the rest of the Compose stack, so services such as pgvector, scraper, and the optional graphiti stack still use the image references defined in the compose files.
DOCKER_DEFAULT_IMAGE and DOCKER_DEFAULT_IMAGE_FOR_PENTEST are suggestions the model receives in the image selection prompt, not limits: on their own they do not stop a model from answering with some other image. Two settings turn them into limits:
DOCKER_ALLOWED_IMAGESrestricts the choice. An answer outside the list is discarded and the pentest image is used instead. An entry without a tag admits any tag of that repository, sovxcontrol/kali-linuxalso admitsvxcontrol/kali-linux:2026.1. While the list is empty the model may pick any image, which is the default behaviour.DOCKER_IMAGE_SELECTION_MODE=fixedremoves the choice altogether: the flow always runs inDOCKER_DEFAULT_IMAGE_FOR_PENTESTand no model call is made. Use it when weaker or local models answer with the wrong environment.
Regardless of these settings, an answer that is not a usable image reference never reaches Docker: the flow falls back to the pentest image and the rejected answer is logged.
Restricting the choice is useful for:
- Security Enforcement: Restricting usage to only verified and trusted images
- Environment Standardization: Using corporate or customized images across all operations
- Performance Optimization: Utilizing pre-built images with necessary tools already installed
Configuration examples:
# Using a custom PentAGI application image
PENTAGI_IMAGE=registry.example.com/security/pentagi:latest
# Using a custom image for general tasks
DOCKER_DEFAULT_IMAGE=mycompany/custom-debian:latest
# Using a specialized image for penetration testing
DOCKER_DEFAULT_IMAGE_FOR_PENTEST=mycompany/pentest-tools:v2.0[!NOTE]
If a user explicitly specifies a particular Docker image in their task, the system will try to use that exact image, ignoring these settings. These variables only affect the system's automatic image selection process.
For an advanced OpenVAS/GVM experiment that uses a custom pentest image, see OpenVAS via a Custom Pentest Image.
Restricted Networks, Docker Mirrors, and Proxies
If your environment cannot reach Docker Hub (docker.io) directly, changing PentAGI environment variables is usually not enough to fix image download failures. PentAGI still relies on Docker's own registry access for Compose-managed services, and the installer network checks also validate Docker Hub reachability.
For restricted networks:
- Confirm that the host can resolve and reach
docker.io. - If your environment requires an outbound proxy for PentAGI or installer HTTP traffic, set the
PROXY_URLenvironment variable. To route Docker image pulls through a proxy, configure the Docker daemon or Docker Desktop proxy separately — Docker does not use PentAGI'sPROXY_URLfor registry access. - If Docker Hub is blocked or heavily rate-limited, configure an organization-approved registry mirror or registry proxy before running the installer or
docker compose up. - Restart Docker after changing the daemon configuration, then rerun the installer checks or Compose startup.
Example Docker daemon mirror configuration:
{
"registry-mirrors": ["https://mirror.example.com"]
}On Linux, this is typically configured in /etc/docker/daemon.json. On Docker Desktop, use the equivalent Docker Engine or proxy settings. A Docker Hub mirror covers Docker Hub-hosted images such as vxcontrol/*, but the main Compose stack already includes quay.io/prometheuscommunity/postgres-exporter, the optional observability stack includes gcr.io/cadvisor/cadvisor, and the optional Langfuse stack includes cgr.dev/chainguard/minio. Those registries still need direct access or individually approved proxy/mirror paths.
See the official Docker documentation for registry mirrors and daemon proxy configuration.
Troubleshooting: "failed to select primary docker image via llm call"
A flow that fails immediately with failed to select primary docker image via llm call usually indicates a problem with the configured LLM backend, not with Docker or the image registry. Older PentAGI versions reported the same failure as failed to get primary docker image, which led users to debug Docker even though the registry was healthy.
When a flow starts, PentAGI makes its first LLM call to choose the primary Docker image for the task. This image-selection call runs through the simple agent type, so a failure here points at the model assigned to that agent type rather than at Docker. A message such as API returned unexpected status code: 502 or 404 in this context is returned by the LLM backend, not by Docker Hub.
This is distinct from the registry reachability problems described above: if Docker pulls succeed and the Compose stack starts, but flow creation still fails at image selection, investigate the LLM backend rather than Docker.
To diagnose:
- Check PentAGI logs first:
docker logs pentagi. - Check the logs of your configured LLM backend (the server behind your provider or
LLM_SERVER_URL). - Verify that the base URL, API key, and model name in Custom LLM Provider Configuration are correct and reachable from the container. If you assign different models per agent type, check the model used by the
simpleagent type, since image selection runs through it. - For custom, OpenAI-compatible, vLLM, or SGLang backends, confirm that the model supports tool calling (function calling) and that the matching tool-call parser is enabled. A missing or mismatched tool-call parser is a known cause of this failure.
Development
Development Requirements
- golang
- nodejs
- docker
- postgres
Environment Setup
Backend Setup
Run once cd backend && go mod download to install needed packages.
For generating swagger files have to run
swag init -g ../../pkg/server/router.go -o pkg/server/docs/ --parseDependency --parseInternal --parseDepth 2 -d cmd/pentagibefore installing swag package via
go install github.com/swaggo/swag/cmd/[email protected]For generating graphql resolver files have to run
go run github.com/99designs/gqlgen --config ./gqlgen/gqlgen.ymlafter that you can see the generated files in pkg/graph folder.
For generating ORM methods (database package) from sqlc configuration
docker run --rm -v $(pwd):/src -w /src --network pentagi-network -e DATABASE_URL="{URL}" sqlc/sqlc:1.27.0 generate -f sqlc/sqlc.ymlFor generating Langfuse SDK from OpenAPI specification
fern generate --localand to install fern-cli
pnpm add fern-apiTesting
For running tests cd backend && go test -v ./...
Frontend Setup
Run once cd frontend && pnpm install to install needed packages.
For generating graphql files have to run pnpm run graphql:generate which using graphql-codegen.ts file.
Be sure that you have graphql-codegen installed globally:
pnpm add graphql-codegenAfter that you can run:
pnpm run prettierto check if your code is formatted correctlypnpm run prettier:fixto fix itpnpm run lintto check if your code is linted correctlypnpm run lint:fixto fix it
For generating SSL certificates you need to run pnpm run ssl:generate which using generate-ssl.ts file or it will be generated automatically when you run pnpm run dev.
Backend Configuration
Edit the configuration for backend in .vscode/launch.json file:
DATABASE_URL- PostgreSQL database URL (eg.postgres://postgres:postgres@localhost:5432/pentagidb?sslmode=disable)DOCKER_HOST- Docker SDK API (eg. for macOSDOCKER_HOST=unix:///Users/<my-user>/Library/Containers/com.docker.docker/Data/docker.raw.sock) more info
Optional:
SERVER_PORT- Port to run the server (default:8443)SERVER_USE_SSL- Enable SSL for the server (default:false)
PostgreSQL / pgvector connection pool sizing
PentAGI opens two independent connection pools to the same Postgres instance:
| Pool | Env var | Default | Used by |
|---|---|---|---|
Shared sql.DB |
DATABASE_MAX_OPEN_CONNS |
25 |
All sqlc queries and GORM handlers share a single *sql.DB |
Shared pgxpool |
DATABASE_VECTOR_MAX_CONNS |
10 |
All pgvector stores (agent memory + knowledge API) share a single pool |
Additional tuning knob:
DATABASE_MAX_IDLE_CONNS— maximum idle connections kept open in thesql.DBpool between requests (default:5).
Budget for the stock vxcontrol/pgvector image (max_connections = 100, superuser_reserved_connections = 3):
Available for client connections = 97
pentagi sql.DB (DATABASE_MAX_OPEN_CONNS) = 25
pentagi pgxpool (DATABASE_VECTOR_MAX_CONNS) = 10
pgexporter = 3
autovacuum workers = 3
─────────────────────────────────────────
Total consumed = 41
Free buffer = 56 (≈ 58 %)The defaults are sized for 10 parallel flows with concurrent API requests. If you run more flows or deploy multiple PentAGI instances against the same Postgres, raise max_connections via the command override in docker-compose.yml and increase the pool sizes proportionally:
pgvector:
image: vxcontrol/pgvector:latest
command: postgres -c max_connections=200To inspect the live connection budget on a running deployment:
# Postgres limits
docker exec pgvector sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c \
"SELECT name, setting FROM pg_settings
WHERE name IN ('"'"'max_connections'"'"', '"'"'superuser_reserved_connections'"'"');"'
# Current usage vs. available
docker exec pgvector sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c \
"SELECT max_conn, used, max_conn - used AS available
FROM (SELECT current_setting('"'"'max_connections'"'"')::int AS max_conn,
count(*) AS used FROM pg_stat_activity) t;"'
# Breakdown by client
docker exec pgvector sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c \
"SELECT application_name, client_addr, state, count(*)
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
GROUP BY 1, 2, 3 ORDER BY count DESC;"'External PostgreSQL and schema handling
DATABASE_URL may point at any PostgreSQL instance, not only the bundled pgvector container. Two extra knobs apply when — and only when — TENANT_ID is set, because that is when PentAGI creates its own schema and rewrites the connection's search_path:
| Env var | Default | Purpose |
|---|---|---|
DATABASE_EXTENSIONS_SCHEMA |
public |
Schema holding the shared vector and pg_trgm extensions that every tenant's search_path must reach |
DATABASE_SEARCH_PATH_VIA_OPTIONS |
false |
Send the tenant search_path inside the options startup parameter instead of as a bare connection parameter |
Supabase (cloud or self-hosted) needs both of them considered, and is the reason they exist:
- Supabase installs its bundled extensions into an
extensionsschema instead ofpublic, so setDATABASE_EXTENSIONS_SCHEMA=extensions. Without it, startup aborts with an error naming the schema wherevectorwas actually found — no need to move a provider-managed extension withALTER EXTENSION. - Supabase's pooler (Supavisor) does not reliably forward a bare
search_pathconnection parameter. Prefer a direct PostgreSQL connection: self-hosted, expose thedbservice port and bypass thesupavisorservice; cloud, use the "Direct connection" string (or the IPv4 add-on on IPv4-only networks). If the pooler cannot be bypassed, use its session mode and tryDATABASE_SEARCH_PATH_VIA_OPTIONS=true— PentAGI verifies the effective schema on boot and refuses to start if it did not take effect, so a silent cross-tenant data mix-up is not possible.
Both settings are managed by the installer under Server Settings, next to TENANT_ID — see Running Several Instances for that scenario, and Multi-Instance Deployment for the full matrix, including the PgBouncer recipe (pool_mode = session, ignore_startup_parameters, per-tenant connect_query).
Frontend Configuration
Edit the configuration for frontend in .vscode/launch.json file:
VITE_API_URL- Backend API URL. Omit the URL scheme (e.g.,localhost:8080NOThttp://localhost:8080)VITE_USE_HTTPS- Enable SSL for the server (default:false)VITE_PORT- Port to run the server (default:8000)VITE_HOST- Host to run the server (default:0.0.0.0)
Running the Application
Backend
Run the command(s) in backend folder:
- Use
.envfile to set environment variables like asource .env - Run
go run cmd/pentagi/main.goto start the server
[!NOTE]
The first run can take a while as dependencies and docker images need to be downloaded to setup the backend environment.
Frontend
Run the command(s) in frontend folder:
- Run
pnpm installto install the dependencies - Run
pnpm run devto run the web app - Run
pnpm run buildto build the web app
Open your browser and visit the web app URL.
Testing LLM Agents
PentAGI includes a powerful utility called ctester for testing and validating LLM agent capabilities. This tool helps ensure your LLM provider configurations work correctly with different agent types, allowing you to optimize model selection for each specific agent role.
The utility features parallel testing of multiple agents, detailed reporting, and flexible configuration options.
Key Features
- Parallel Testing: Tests multiple agents simultaneously for faster results
- Comprehensive Test Suite: Evaluates basic completion, JSON responses, function calling, and penetration testing knowledge
- Detailed Reporting: Generates markdown reports with success rates and performance metrics
- Flexible Configuration: Test specific agents or test groups as needed
- Specialized Test Groups: Includes domain-specific tests for cybersecurity and penetration testing scenarios
Usage Scenarios
For Developers (with local Go environment)
If you've cloned the repository and have Go installed:
# Default configuration with .env file
cd backend
go run cmd/ctester/*.go -verbose
# Custom provider configuration
go run cmd/ctester/*.go -config ../examples/configs/openrouter.provider.yml -verbose
# Generate a report file
go run cmd/ctester/*.go -config ../examples/configs/deepinfra.provider.yml -report ../test-report.md
# Test specific agent types only
go run cmd/ctester/*.go -agents simple,simple_json,primary_agent -verbose
# Test specific test groups only
go run cmd/ctester/*.go -groups basic,advanced -verboseFor Users (using Docker image)
If you prefer to use the pre-built Docker image without setting up a development environment:
# Using Docker to test with default environment
docker run --rm -v $(pwd)/.env:/opt/pentagi/.env vxcontrol/pentagi /opt/pentagi/bin/ctester -verbose
# Test with your custom provider configuration
docker run --rm \
-v $(pwd)/.env:/opt/pentagi/.env \
-v $(pwd)/my-config.yml:/opt/pentagi/config.yml \
vxcontrol/pentagi /opt/pentagi/bin/ctester -config /opt/pentagi/config.yml -agents simple,primary_agent,coder -verbose
# Generate a detailed report
docker run --rm \
-v $(pwd)/.env:/opt/pentagi/.env \
-v $(pwd):/opt/pentagi/output \
vxcontrol/pentagi /opt/pentagi/bin/ctester -report /opt/pentagi/output/report.mdUsing Pre-configured Providers
The Docker image comes with built-in support for major providers (OpenAI, Anthropic, Gemini, Ollama) and pre-configured provider files for additional services (OpenRouter, OpenCode, Atlas, OrcaRouter, DeepInfra, DeepSeek, Moonshot, Novita, xAI):
/* Detailed source-code truncated for AI context efficiency. */To use these configurations, your .env file only needs to contain:
/* Detailed source-code truncated for AI context efficiency. */Using OpenAI with Unverified Organizations
For OpenAI accounts with unverified organizations that don't have access to the latest reasoning models (o1, o3, o4-mini), you need to use a custom configuration.
To use OpenAI with unverified organization accounts, configure your .env file as follows:
LLM_SERVER_URL=https://api.openai.com/v1
LLM_SERVER_KEY=your_openai_api_key
LLM_SERVER_MODEL= # Leave empty, models are specified in config
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/custom-openai.provider.ymlThis configuration uses the pre-built custom-openai.provider.yml file that maps all agent types to models available for unverified organizations, using o3-mini instead of models like o1, o3, and o4-mini.
You can test this configuration using:
# Test with custom OpenAI configuration for unverified accounts
docker exec -it pentagi /opt/pentagi/bin/ctester -config /opt/pentagi/conf/custom-openai.provider.ymlUsing LiteLLM Proxy
When using LiteLLM proxy to access various LLM providers, model names are prefixed with the provider name (e.g., moonshot/kimi-2.5 instead of kimi-2.5). To use the same provider configuration files with both direct API access and LiteLLM proxy, set the LLM_SERVER_PROVIDER variable:
# Direct access to Moonshot API
LLM_SERVER_URL=https://api.moonshot.ai/v1
LLM_SERVER_KEY=your_moonshot_api_key
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/moonshot.provider.yml
LLM_SERVER_PROVIDER= # Empty for direct access
# Access via LiteLLM proxy
LLM_SERVER_URL=http://litellm-proxy:4000
LLM_SERVER_KEY=your_litellm_api_key
LLM_SERVER_CONFIG_PATH=/opt/pentagi/conf/moonshot.provider.yml
LLM_SERVER_PROVIDER=moonshot # Provider prefix for LiteLLMWith LLM_SERVER_PROVIDER=moonshot, the system automatically prefixes all model names from the configuration file with moonshot/, making them compatible with LiteLLM's model naming convention.
LiteLLM Provider Name Mapping:
When using LiteLLM proxy, set the corresponding *_PROVIDER variable to enable model prefixing:
deepseek- for DeepSeek models (DEEPSEEK_PROVIDER=deepseek→deepseek/deepseek-v4-flash)zai- for GLM models (GLM_PROVIDER=zai→zai/glm-4)moonshot- for Kimi models (KIMI_PROVIDER=moonshot→moonshot/kimi-k3)dashscope- for Qwen models (QWEN_PROVIDER=dashscope→dashscope/qwen-plus)openai,anthropic,gemini- for major cloud providersopencode- for OpenCode Go planopenrouter- for OpenRouter aggregatororcarouter- for OrcaRouter aggregatordeepinfra- for DeepInfra hostingnovita- for Novita AIxai- for xAI (grok-*models)- Any other provider name configured in your LiteLLM instance
Example with LiteLLM:
# Use DeepSeek models via LiteLLM proxy with model prefixing
DEEPSEEK_API_KEY=your_litellm_proxy_key
DEEPSEEK_SERVER_URL=http://litellm-proxy:4000
DEEPSEEK_PROVIDER=deepseek # Models become deepseek/deepseek-v4-flash, deepseek/deepseek-v4-pro for LiteLLM
# Direct DeepSeek API usage (no prefix needed)
DEEPSEEK_API_KEY=your_deepseek_api_key
DEEPSEEK_SERVER_URL=https://api.deepseek.com
# Leave DEEPSEEK_PROVIDER emptyThis approach allows you to:
- Use the same configuration files for both direct and proxied access
- Switch between providers without modifying configuration files
- Easily test different routing strategies with LiteLLM
Running Tests in a Production Environment
If you already have a running PentAGI container and want to test the current configuration:
# Run ctester in an existing container using current environment variables
docker exec -it pentagi /opt/pentagi/bin/ctester -verbose
# Test specific agent types with deterministic ordering
docker exec -it pentagi /opt/pentagi/bin/ctester -agents simple,primary_agent,pentester -groups basic,knowledge -verbose
# Generate a report file inside the container
docker exec -it pentagi /opt/pentagi/bin/ctester -report /opt/pentagi/data/agent-test-report.md
# Access the report from the host
docker cp pentagi:/opt/pentagi/data/agent-test-report.md ./Command-line Options
The utility accepts several options:
-env- Path to environment file (default:.env)-type- Provider type:custom,openai,anthropic,ollama,bedrock,gemini(default:custom)-config- Path to custom provider config (default: fromLLM_SERVER_CONFIG_PATHenv variable)-tests- Path to custom tests YAML file (optional)-report- Path to write the report file (optional)-agents <list>- Comma-separated list of agent types to test (default:all)-groups <list>- Comma-separated list of test groups to run (default:all)-verbose- Enable verbose output with detailed test results for each agent
Available Agent Types
Agents are tested in the following deterministic order:
- simple - Basic completion tasks
- simple_json - JSON-structured responses
- primary_agent - Main reasoning agent
- assistant - Interactive assistant mode
- generator - Content generation
- refiner - Content refinement and improvement
- adviser - Expert advice and consultation
- reflector - Self-reflection and analysis
- searcher - Information gathering and search
- enricher - Data enrichment and expansion
- coder - Code generation and analysis
- installer - Installation and setup tasks
- pentester - Penetration testing and security assessment
Available Test Groups
- basic - Fundamental completion and prompt response tests
- advanced - Complex reasoning and function calling tests
- json - JSON format validation and structure tests (specifically designed for
simple_jsonagent) - knowledge - Domain-specific cybersecurity and penetration testing knowledge tests
Note: The
jsontest group is specifically designed for thesimple_jsonagent type, while all other agents are tested withbasic,advanced, andknowledgegroups. This specialization ensures optimal testing coverage for each agent's intended purpose.
Example Provider Configuration
Provider configuration defines which models to use for different agent types:
simple:
model: "provider/model-name"
temperature: 0.7
top_p: 0.95
n: 1
max_tokens: 4000
simple_json:
model: "provider/model-name"
temperature: 0.7
top_p: 1.0
n: 1
max_tokens: 4000
json: true
# ... other agent types ...Optimization Workflow
- Create a baseline: Run tests with default configuration to establish benchmark performance
- Analyze agent-specific performance: Review the deterministic agent ordering to identify underperforming agents
- Test specialized configurations: Experiment with different models for each agent type using provider-specific configs
- Focus on domain knowledge: Pay special attention to knowledge group tests for cybersecurity expertise
- Validate function calling: Ensure tool-based tests pass consistently for critical agent types
- Compare results: Look for the best success rate and performance across all test groups
- Deploy optimal configuration: Use in production with your optimized setup
This tool helps ensure your AI agents are using the most effective models for their specific tasks, improving reliability while optimizing costs.
Embedding Configuration and Testing
PentAGI uses vector embeddings for semantic search, knowledge storage, and memory management. The system supports multiple embedding providers that can be configured according to your needs and preferences.
Supported Embedding Providers
PentAGI supports the following embedding providers:
- OpenAI (default): Uses OpenAI's text embedding models
- Ollama: Local embedding model through Ollama
- Mistral: Mistral AI's embedding models
- Jina: Jina AI's embedding service
- HuggingFace: Models from HuggingFace
- GoogleAI: Google's embedding models
- VoyageAI: VoyageAI's embedding models
OpenAI-compatible third parties: any provider exposing OpenAI's
/embeddingsAPI can be plugged in viaEMBEDDING_PROVIDER=openaiwith a customEMBEDDING_URL. For example, Qwen DashScope offerstext-embedding-v4through the/compatible-mode/v1endpoint (International and Chinese Mainland regions only — the US region does not expose embeddings). See the Qwen Alternative Integrations subsection for the full configuration snippet.
Embedding Provider Configuration (click to expand)
Environment Variables
To configure the embedding provider, set the following environment variables in your .env file:
# Primary embedding configuration
EMBEDDING_PROVIDER=openai # Provider type (openai, ollama, mistral, jina, huggingface, googleai, voyageai)
EMBEDDING_MODEL=text-embedding-3-small # Model name to use
EMBEDDING_URL= # Optional custom API endpoint
EMBEDDING_KEY= # API key for the provider (if required)
EMBEDDING_BATCH_SIZE=100 # Number of documents to process in a batch
EMBEDDING_STRIP_NEW_LINES=true # Whether to remove new lines from text before embedding
EMBEDDING_MAX_TEXT_BYTES=8192 # Max bytes of text sent to embedding model per document (byte proxy for token limit)
# Advanced settings
PROXY_URL= # Optional proxy for all backend LLM, embedding, search and update-check calls
HTTP_CLIENT_TIMEOUT=600 # Timeout in seconds for external API calls (default: 600, 0 = no timeout)
TERMINAL_TOOL_TIMEOUT=1200 # Default timeout in seconds for terminal tool commands when timeout=0 or negative (range: 1–10800; values <= 0 or above 10800 are clamped to 10800 = 3 hours)
# SSL/TLS Certificate Configuration (for external communication with LLM backends and tool servers)
EXTERNAL_SSL_CA_PATH= # Path to custom CA certificate file (PEM format) inside the container
# Must point to /opt/pentagi/ssl/ directory (e.g., /opt/pentagi/ssl/ca-bundle.pem)
EXTERNAL_SSL_INSECURE=false # Skip certificate verification (use only for testing)How to Add Custom CA Certificates (click to expand)
If you see this error: tls: failed to verify certificate: x509: certificate signed by unknown authority
Step 1: Get your CA certificate bundle in PEM format (can contain multiple certificates)
Step 2: Place the file in the SSL directory on your host machine:
# Default location (if PENTAGI_SSL_DIR is not set)
cp ca-bundle.pem ./pentagi-ssl/
# Or custom location (if using PENTAGI_SSL_DIR in docker-compose.yml)
cp ca-bundle.pem /path/to/your/ssl/dir/Step 3: Set the path in .env file (path must be inside the container):
# The volume pentagi-ssl is mounted to /opt/pentagi/ssl inside the container
EXTERNAL_SSL_CA_PATH=/opt/pentagi/ssl/ca-bundle.pem
EXTERNAL_SSL_INSECURE=falseStep 4: Restart PentAGI:
docker compose restart pentagiNotes:
- The
pentagi-sslvolume is mounted to/opt/pentagi/sslinside the container - You can change host directory using
PENTAGI_SSL_DIRvariable in docker-compose.yml - File supports multiple certificates and intermediate CAs in one PEM file
- Use
EXTERNAL_SSL_INSECURE=trueonly for testing (not recommended for production)
Provider-Specific Limitations
Each provider has specific limitations and supported features:
- OpenAI: Supports all configuration options
- Ollama: Does not support
EMBEDDING_KEYas it uses local models - Mistral: Supports all configuration options
- Jina: Supports all configuration options
- HuggingFace: Requires
EMBEDDING_KEYand supports all other options - GoogleAI: Does not support
EMBEDDING_URL, requiresEMBEDDING_KEY - VoyageAI: Supports all configuration options
If EMBEDDING_URL and EMBEDDING_KEY are both left empty, the system uses the corresponding LLM provider's server and key together (e.g., OPEN_AI_SERVER_URL and OPEN_AI_KEY when EMBEDDING_PROVIDER=openai). A key set in EMBEDDING_KEY without EMBEDDING_URL goes to the provider's public endpoint, and an EMBEDDING_URL set without a key receives the LLM provider's key only when it is that provider's own server. So an OpenAI-compatible endpoint of another vendor needs its key in EMBEDDING_KEY, and a keyless local server (LocalAI, vLLM, TEI) takes any non-empty value there. Whenever the Mistral embedder uses MISTRAL_SERVER_URL, it also names the model the way that server expects: MISTRAL_PROVIDER=mistral turns mistral-embed into mistral/mistral-embed, and a model that already starts with mistral/ is sent as is.
Why Consistent Embedding Providers Matter
It's crucial to use the same embedding provider consistently because:
- Vector Compatibility: Different providers produce vectors with different dimensions and mathematical properties
- Semantic Consistency: Changing providers can break semantic similarity between previously embedded documents
- Memory Corruption: Mixed embeddings can lead to poor search results and broken knowledge base functionality
If you change your embedding provider, you should flush and reindex your entire knowledge base (see etester utility below).
Embedding Tester Utility (etester)
PentAGI includes a specialized etester utility for testing, managing, and debugging embedding functionality. This tool is essential for diagnosing and resolving issues related to vector embeddings and knowledge storage.
Etester Commands (click to expand)
# Test embedding provider and database connection
cd backend
go run cmd/etester/main.go test -verbose
# Show statistics about the embedding database
go run cmd/etester/main.go info
# Delete all documents from the embedding database (use with caution!)
go run cmd/etester/main.go flush
# Recalculate embeddings for all documents (after changing provider)
go run cmd/etester/main.go reindex
# Search for documents in the embedding database
go run cmd/etester/main.go search -query "How to install PostgreSQL" -limit 5Using Docker
If you're running PentAGI in Docker, you can use etester from within the container:
# Test embedding provider
docker exec -it pentagi /opt/pentagi/bin/etester test
# Show detailed database information
docker exec -it pentagi /opt/pentagi/bin/etester info -verboseAdvanced Search Options
The search command supports various filters to narrow down results:
# Filter by document type
docker exec -it pentagi /opt/pentagi/bin/etester search -query "Security vulnerability" -doc_type guide -threshold 0.8
# Filter by flow ID
docker exec -it pentagi /opt/pentagi/bin/etester search -query "Code examples" -doc_type code -flow_id 42
# All available search options
docker exec -it pentagi /opt/pentagi/bin/etester search -helpAvailable search parameters:
-query STRING: Search query text (required)-doc_type STRING: Filter by document type (answer, memory, guide, code)-flow_id NUMBER: Filter by flow ID (positive number)-answer_type STRING: Filter by answer type (guide, vulnerability, code, tool, other)-guide_type STRING: Filter by guide type (install, configure, use, pentest, development, other)-limit NUMBER: Maximum number of results (default: 3)-threshold NUMBER: Similarity threshold (0.0-1.0, default: 0.7)
Memory Lifecycle Across Flows
PentAGI stores several kinds of vector documents, and they serve different purposes:
memorycaptures flow-specific execution history such as tool results and agent observationsguide,answer, andcodeare intended for reusable knowledge that can help future runs
If you want to inspect what happened in one engagement, search the vector store with the related flow_id. If you want knowledge to survive beyond a single run, store the durable result explicitly as a guide, answer, or code document instead of relying on execution memory alone.
For example, if a target has recurring setup notes, authentication quirks, or target-specific testing methodology, instruct the agent to save that information as a guide and search for it at the beginning of the next engagement. This is the safest current workflow when you want a new flow to start with reusable context.
Flow deletion removes the flow from normal queries through PentAGI's soft-delete mechanism, so reusable knowledge should be treated as a separate concern from per-flow execution history. If you enable the optional Graphiti knowledge graph described earlier in this README, treat its current search context as scoped to the active flow or engagement unless you explicitly build a separate cross-flow reuse workflow.
Common Troubleshooting Scenarios
- After changing embedding provider: Always run
flushorreindexto ensure consistency - Poor search results: Try adjusting the similarity threshold or check if embeddings are correctly generated
- Database connection issues: Verify PostgreSQL is running with pgvector extension installed
- Missing API keys: Check environment variables for your chosen embedding provider
Troubleshooting: Flow Stalls or Hangs Without Progress
If a flow starts but then appears to wait indefinitely with no subtasks progressing, a common cause is an embedding provider that is misconfigured or unreachable. PentAGI uses the embedding provider to store and search vector memory while a flow runs, so embedding calls that fail or hang can leave a flow waiting instead of advancing.
1. Check the container logs first. Embedding errors surface in the PentAGI logs:
docker logs pentagiLook for embedding-related failures such as authentication errors (401/403), wrong-model or not-found errors (404), connection timeouts, or TLS certificate errors. These point at the embedding provider configuration rather than at the flow itself.
2. Validate the provider with etester. The Embedding Tester Utility (etester) checks both the embedding provider and the database connection without starting a flow:
docker exec -it pentagi /opt/pentagi/bin/etester test -verboseA failing test confirms the problem is in the embedding configuration rather than in the flow.
3. Verify the configuration. Check the following in your .env file against the Supported Embedding Providers list and each provider's documented limitations:
EMBEDDING_PROVIDERis one of the supported providers (defaultopenai).EMBEDDING_MODELis a valid model name for that provider.EMBEDDING_URLandEMBEDDING_KEYare correct for the provider. If both are left empty, PentAGI falls back to the matching LLM provider settings (for exampleOPEN_AI_KEYandOPEN_AI_SERVER_URLwhenEMBEDDING_PROVIDER=openai), so a missing or wrong key there can break embeddings too.- The endpoint is reachable from inside the container. If outbound calls go through a proxy, confirm
PROXY_URLis set; if calls hang rather than fail quickly,HTTP_CLIENT_TIMEOUTcontrols how long PentAGI waits on the provider before giving up.
Changing provider? If you switch embedding providers after data has already been indexed, run
flushorreindexwith etester so old and new vectors are not mixed. See Why Consistent Embedding Providers Matter above.
Function Testing with ftester
PentAGI includes a versatile utility called ftester for debugging, testing, and developing specific functions and AI agent behaviors. While ctester focuses on testing LLM model capabilities, ftester allows you to directly invoke individual system functions and AI agent components with precise control over execution context.
Key Features
- Direct Function Access: Test individual functions without running the entire system
- Mock Mode: Test functions without a live PentAGI deployment using built-in mocks
- Interactive Input: Fill function arguments interactively for exploratory testing
- Detailed Output: Color-coded terminal output with formatted responses and errors
- Context-Aware Testing: Debug AI agents within the context of specific flows, tasks, and subtasks
- Observability Integration: All function calls are logged to Langfuse and Observability stack
Usage Modes
Command Line Arguments
Run ftester with specific function and arguments directly from the command line:
# Basic usage with mock mode
cd backend
go run cmd/ftester/main.go [function_name] -[arg1] [value1] -[arg2] [value2]
# Example: Test terminal command in mock mode
go run cmd/ftester/main.go terminal -command "ls -la" -message "List files"
# Using a real flow context
go run cmd/ftester/main.go -flow 123 terminal -command "whoami" -message "Check user"
# Testing AI agent in specific task/subtask context
go run cmd/ftester/main.go -flow 123 -task 456 -subtask 789 pentester -message "Find vulnerabilities"Interactive Mode
Run ftester without arguments for a guided interactive experience:
# Start interactive mode
go run cmd/ftester/main.go [function_name]
# For example, to interactively fill browser tool arguments
go run cmd/ftester/main.go browserAvailable Functions (click to expand)
Environment Functions
- terminal: Execute commands in a container and return the output
- file: Perform file operations (read, write, list) in a container
Search Functions
- browser: Access websites and capture screenshots
- web_search: Unified search orchestrator that agents actually call — pass a
queryand amode(links,answer,research,exploit) and it auto-selects, retries, and falls back across the engines below, so you never name an engine explicitly - google: Search the web using Google Custom Search
- duckduckgo: Search the web using DuckDuckGo
- tavily: Search using Tavily AI search engine
- firecrawl: Search using Firecrawl with main-content markdown scraping
- traversaal: Search using Traversaal AI search engine
- perplexity: Search using Perplexity AI
- sploitus: Search for security exploits, vulnerabilities (CVEs), and pentesting tools
- searxng: Search using SearXNG meta search engine (aggregates results from multiple engines)
- internal (ftester-only debug function, not an agent tool): Opt-in browser-analytics fallback engine that discovers links, scrapes each page, and summarizes the result; requires
WEB_SEARCH_INTERNAL_ENABLED=true, a configured scraper, and at least one available link engine
Vector Database Functions
- search_in_memory: Search for information in vector database
- search_guide: Find guidance documents in vector database
- search_answer: Find answers to questions in vector database
- search_code: Find code examples in vector database
AI Agent Functions
- advice: Get expert advice from an AI agent
- coder: Request code generation or modification
- maintenance: Run system maintenance tasks
- memorist: Store and organize information in vector database
- pentester: Perform security tests and vulnerability analysis
- search: Complex search across multiple sources
Utility Functions
- describe: Show information about flows, tasks, and subtasks
Debugging Flow Context (click to expand)
The describe function provides detailed information about tasks and subtasks within a flow. This is particularly useful for diagnosing issues when PentAGI encounters problems or gets stuck.
# List all flows in the system
go run cmd/ftester/main.go describe
# Show all tasks and subtasks for a specific flow
go run cmd/ftester/main.go -flow 123 describe
# Show detailed information for a specific task
go run cmd/ftester/main.go -flow 123 -task 456 describe
# Show detailed information for a specific subtask
go run cmd/ftester/main.go -flow 123 -task 456 -subtask 789 describe
# Show verbose output with full descriptions and results
go run cmd/ftester/main.go -flow 123 describe -verboseThis function allows you to identify the exact point where a flow might be stuck and resume processing by directly invoking the appropriate agent function.
Function Help and Discovery (click to expand)
Each function has a help mode that shows available parameters:
# Get help for a specific function
go run cmd/ftester/main.go [function_name] -help
# Examples:
go run cmd/ftester/main.go terminal -help
go run cmd/ftester/main.go browser -help
go run cmd/ftester/main.go describe -helpYou can also run ftester without arguments to see a list of all available functions:
go run cmd/ftester/main.goOutput Format (click to expand)
The ftester utility uses color-coded output to make interpretation easier:
- Blue headers: Section titles and key names
- Cyan [INFO]: General information messages
- Green [SUCCESS]: Successful operations
- Red [ERROR]: Error messages
- Yellow [WARNING]: Warning messages
- Yellow [MOCK]: Indicates mock mode operation
- Magenta values: Function arguments and results
JSON and Markdown responses are automatically formatted for readability.
Advanced Usage Scenarios (click to expand)
Debugging Stuck AI Flows
When PentAGI gets stuck in a flow:
- Pause the flow through the UI
- Use
describeto identify the current task and subtask - Directly invoke the agent function with the same task/subtask IDs
- Examine the detailed output to identify the issue
- Resume the flow or manually intervene as needed
Testing Environment Variables
Verify that API keys and external services are configured correctly:
# Test Google search API configuration
go run cmd/ftester/main.go google -query "pentesting tools"
# Test browser access to external websites
go run cmd/ftester/main.go browser -url "https://example.com"Developing New AI Agent Behaviors
When developing new prompt templates or agent behaviors:
- Create a test flow in the UI
- Use ftester to directly invoke the agent with different prompts
- Observe responses and adjust prompts accordingly
- Check Langfuse for detailed traces of all function calls
Verifying Docker Container Setup
Ensure containers are properly configured:
go run cmd/ftester/main.go -flow 123 terminal -command "env | grep -i proxy" -message "Check proxy settings"Docker Container Usage (click to expand)
If you have PentAGI running in Docker, you can use ftester from within the container:
# Run ftester inside the running PentAGI container
docker exec -it pentagi /opt/pentagi/bin/ftester [arguments]
# Examples:
docker exec -it pentagi /opt/pentagi/bin/ftester -flow 123 describe
docker exec -it pentagi /opt/pentagi/bin/ftester -flow 123 terminal -command "ps aux" -message "List processes"This is particularly useful for production deployments where you don't have a local development environment.
Integration with Observability Tools (click to expand)
All function calls made through ftester are logged to:
- Langfuse: Captures the entire AI agent interaction chain, including prompts, responses, and function calls
- OpenTelemetry: Records metrics, traces, and logs for system performance analysis
- Terminal Output: Provides immediate feedback on function execution
To access detailed logs:
- Check Langfuse UI for AI agent traces (typically at
http://localhost:4000) - Use Grafana dashboards for system metrics (typically at
http://localhost:3000) - Examine terminal output for immediate function results and errors
Command-line Options
The main utility accepts several options:
-env- Path to environment file (optional, default:.env)-provider <type>- Provider type to use (default:custom, options:openai,anthropic,gemini,bedrock,ollama,deepseek,glm,kimi,qwen,minimax,custom)-flow- Flow ID for testing functions that require it (0 means using mocks, default:0)-user- User ID for testing functions that require it (default:0;1is the default admin user)-task- Task ID for agent context (optional)-subtask- Subtask ID for agent context (optional)
Function-specific arguments are passed after the function name using -name value format.
Pentesting Prompt Methodology
When refining prompts for offensive security work, give the agent a clear methodology instead of a flat list of payloads:
- Start with explicit scope, authorization, and success criteria
- Map the application first: roles, routes, parameters, uploads, integrations, and trust boundaries
- Prioritize attack surfaces systematically instead of testing everything at once
- Validate findings with reproducible evidence before escalating to deeper exploitation
- Finish with report-ready notes that capture impact, prerequisites, and next steps
For PentAGI-specific prompt guidance, see backend/docs/prompt_engineering_pentagi.md. For a practical starting point, reuse and adapt examples/prompts/base_web_pentest.md to match the target application, technology stack, and engagement scope.
Building
Building Docker Image
The Docker build process automatically embeds version information from git tags. To properly version your build, use the provided scripts:
Linux/macOS
# Load version variables
source ./scripts/version.sh
# Standard build
docker build \
--build-arg PACKAGE_VER=$PACKAGE_VER \
--build-arg PACKAGE_REV=$PACKAGE_REV \
-t pentagi:$PACKAGE_VERSION_FULL .
# Multi-platform build
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg PACKAGE_VER=$PACKAGE_VER \
--build-arg PACKAGE_REV=$PACKAGE_REV \
-t pentagi:$PACKAGE_VERSION_FULL .
# Build and push
docker buildx build \
--platform linux/amd64,linux/arm64 \
--build-arg PACKAGE_VER=$PACKAGE_VER \
--build-arg PACKAGE_REV=$PACKAGE_REV \
-t myregistry/pentagi:$PACKAGE_VERSION_FULL \
--push .Windows (PowerShell)
# Load version variables
. .\scripts\version.ps1
# Standard build
docker build `
--build-arg PACKAGE_VER=$env:PACKAGE_VER `
--build-arg PACKAGE_REV=$env:PACKAGE_REV `
-t pentagi:$env:PACKAGE_VERSION_FULL .
# Multi-platform build
docker buildx build `
--platform linux/amd64,linux/arm64 `
--build-arg PACKAGE_VER=$env:PACKAGE_VER `
--build-arg PACKAGE_REV=$env:PACKAGE_REV `
-t pentagi:$env:PACKAGE_VERSION_FULL .Quick build without version
For development builds without version tracking:
docker build -t pentagi:dev .[!NOTE]
- The build scripts automatically determine version from git tags
- Release builds (on tag commit) have no revision suffix
- Development builds (after tag) include commit hash as revision (e.g.,
1.1.0-ce.hbc6e800); the edition iscefor Community andeefor Enterprise, and a release build is1.1.0-ce- To use the built image locally, update the image name in
docker-compose.ymlor use thebuildoption
Credits
This project is made possible thanks to the following research and developments:
- Emerging Architectures for LLM Applications
- A Survey of Autonomous LLM Agents
- Codel by Andriy Semenets - initial architectural inspiration for agent-based automation
License
PentAGI is licensed under the MIT License.
Copyright (c) 2025 PentAGI Development Team
Third-Party Dependencies
All third-party dependencies use MIT-compatible licenses. See licenses/ directory for detailed license reports.
VXControl Cloud Services
⚠️ Note: While the VXControl Cloud SDK code is MIT licensed, accessing VXControl Cloud Services (threat intelligence, AI support, premium features) requires a separate License Key and compliance with Terms of Service.
The SDK code itself is free to use - service access requires registration.
For questions contact: [email protected] or [email protected]
Backend Docs Installer Installer Overview (`backend/docs/installer/installer-overview.md`)
PentAGI Installer Overview
Comprehensive guide to the PentAGI installer - a robust Terminal User Interface (TUI) for configuring and deploying PentAGI services.
🎯 **Project Overview**
The PentAGI installer provides a modern, interactive Terminal User Interface for configuring and deploying the PentAGI autonomous penetration testing platform. Built using the Charm tech stack, it implements responsive design patterns optimized for terminal environments.
**Core Purpose**
- Configuration Management: Interactive setup of LLM providers, monitoring, and security settings
- Environment Setup: Automated configuration of Docker services and environment variables
- User Experience: Professional TUI with intuitive navigation and real-time validation
- Production Ready: Robust error handling, state persistence, and graceful degradation
**Build Command**
# From backend/ directory
go build -o ../build/installer ./cmd/installer/main.go
# Monitor debug output
tail -f log.json | jq '.'🏗️ **Technology Stack**
**Core Technologies**
- TUI Framework: BubbleTea (Model-View-Update pattern)
- Styling: Lipgloss (CSS-like styling for terminals)
- Components: Bubbles (viewport, textinput, etc.)
- Markdown: Glamour (markdown rendering)
- Language: Go 1.21+
**Architecture Components**
- Navigation: Type-safe screen routing with parameter passing
- State Management: Persistent configuration with environment variable integration
- Layout System: Responsive design with breakpoint-based layouts
- Form System: Dynamic forms with validation and auto-completion
- Controller Layer: Business logic abstraction from UI components
🎯 **Key Features**
**Responsive Design**
- Adaptive Layout: Automatically adjusts to terminal size
- Breakpoint System: Horizontal/vertical layouts based on terminal width
- Content Hiding: Graceful degradation when space is insufficient
- Dynamic Sizing: Form fields and panels resize automatically
**Interactive Configuration**
- LLM Providers: Support for OpenAI, Anthropic, Gemini, Bedrock, DeepSeek, GLM, Kimi, Qwen, Ollama, Custom endpoints
- Monitoring Setup: Langfuse integration for LLM observability
- Observability: Complete monitoring stack with Grafana, VictoriaMetrics, Jaeger
- Summarization: Advanced context management for LLM interactions
**Professional UX**
- Auto-Scrolling Forms: Fields automatically scroll into view when focused
- Tab Completion: Boolean fields offer
true/falsesuggestions - Real-time Validation: Immediate feedback with human-readable error messages
- Resource Estimation: Live calculation of token usage and memory requirements
- State Persistence: Navigation and form state preserved across sessions
🏗️ **Architecture Overview**
**Directory Structure**
backend/cmd/installer/
├── main.go # Application entry point
├── wizard/
│ ├── app.go # Main application controller
│ ├── controller/ # Business logic layer
│ │ └── controller.go
│ ├── locale/ # Localization constants
│ │ └── locale.go
│ ├── logger/ # TUI-safe logging
│ │ └── logger.go
│ ├── models/ # Screen implementations
│ │ ├── welcome.go # Welcome screen
│ │ ├── eula.go # EULA acceptance
│ │ ├── main_menu.go # Main navigation
│ │ ├── llm_providers.go
│ │ ├── llm_provider_form.go
│ │ ├── summarizer.go
│ │ ├── summarizer_form.go
│ │ ├── update_overview.go # What an update changes (informational)
│ │ ├── installer_update.go # Installer build download
│ │ └── types.go # Shared types
│ ├── styles/ # Styling and layout
│ │ └── styles.go
│ └── window/ # Terminal size management
│ └── window.go**Component Responsibilities**
**App Layer** (`app.go`)
- Global navigation management
- Screen lifecycle (creation, initialization, cleanup)
- Unified header and footer rendering
- Window size distribution to models
- Global event handling (ESC, Ctrl+C, resize)
**Models Layer** (`models/`)
- Screen-specific logic and state
- User interaction handling
- Content rendering (content area only)
- Local state management
**Controller Layer** (`controller/`)
- Business logic abstraction
- Environment variable management
- Configuration persistence
- State validation
**Styles Layer** (`styles/`)
- Centralized styling and theming
- Dimension management (singleton pattern)
- Memoised glamour renderers: one per wrap width, plus the default 80-column one
- Responsive style calculations
**Window Layer** (`window/`)
- Terminal size management
- Content area size calculations
- Dimension change coordination
🎯 **Navigation System**
**Composite ScreenID Architecture**
The installer implements a sophisticated navigation system using composite screen IDs:
// Format: "screen§arg1§arg2§..."
type ScreenID string
// Examples:
"welcome" // Simple screen
"main_menu§llm_providers" // Menu with selection
"llm_provider_form§openai" // Form with provider type
"summarizer_form§general" // Form with configuration type**Navigation Features**
- Parameter Preservation: Arguments maintained across navigation
- Stack Management: Proper back navigation without loops
- State Persistence: Complete navigation state restoration
- Universal ESC: Always returns to welcome screen
- Type Safety: Compile-time validation of screen IDs
**Navigation Flow Example**
1. Start: ["welcome"]
2. Continue: ["welcome", "main_menu"]
3. LLM Providers: ["welcome", "main_menu§llm_providers", "llm_providers"]
4. OpenAI Form: [..., "llm_provider_form§openai"]
5. GoBack: [..., "llm_providers§openai"]
6. ESC: ["welcome"]🎯 **Form System Architecture**
**Advanced Form Patterns**
- Boolean Fields: Tab completion with
true/falsesuggestions - Integer Fields: Range validation with human-readable formatting
- Environment Integration: Direct EnvVar integration with presence detection
- Smart Cleanup: Automatic removal of cleared environment variables
- Resource Estimation: Real-time calculation of token/memory usage
**Dynamic Field Generation**
Forms adapt based on configuration type:
// Type-specific field generation
switch m.configType {
case "general":
m.addBooleanField("use_qa", "Use QA Pairs", envVar)
m.addIntegerField("max_sections", "Max Sections", envVar, 1, 50)
case "assistant":
m.addIntegerField("keep_sections", "Keep Sections", envVar, 1, 10)
}**Viewport-Based Scrolling**
Forms automatically scroll to keep focused fields visible:
- Auto-scroll: Focused field automatically stays visible
- Smart positioning: Calculates field heights for precise scroll positioning
- No extra hotkeys: Uses existing navigation keys
🎯 **Configuration Management**
**Supported Configurations**
**LLM Providers**
- OpenAI: GPT-4, GPT-3.5-turbo with API key configuration
- Anthropic: Claude-3, Claude-2 with API key configuration
- Google Gemini: Gemini Pro, Ultra with API key configuration
- AWS Bedrock: Multi-model support with AWS credentials
- DeepSeek/GLM/Kimi/Qwen: Base URL + API Key + Provider Name (optional, for LiteLLM)
- Ollama: Local model server integration
- Custom: OpenAI-compatible endpoint configuration
**Monitoring & Observability**
- Langfuse: LLM observability (embedded or external)
- Observability Stack: Grafana, VictoriaMetrics, Jaeger, Loki
- Performance Monitoring: System metrics and health checks
**Summarization Settings**
- General: Global conversation context management
- Assistant: Specialized settings for AI assistant contexts
- Token Estimation: Real-time calculation of context size
🎯 **Update Screens**
Two screens under Maintenance stand outside the generic operation form: neither edits configuration, and neither is an operation the form framework describes for itself. They sit at different distances from the form system — the overview is a plain viewport over a rendered document, while the installer update is still a BaseScreen, built with an empty field list, which is what gives it the help panel and the vertical/horizontal layout.
The two entries appear under different conditions. Update Installer shows only when a newer build is on offer and the update server answered. Update PentAGI shows whenever an installed stack is not known to be up to date — which includes a check that failed, because a failed check resets every per-stack verdict instead of leaving a stale one in place. So the overview is reachable with nothing yet known to need doing, and that is exactly why it has to be able to say the check did not complete.
**Update Overview** (`models/update_overview.go`)
Sits between the "Update PentAGI" menu entry and the apply form, which the entry used to point at directly — so the first thing the user saw was "are you sure?" about something the interface had never described. The overview describes it, then continues to the same form, confirmation and all.
- Informational only: the document is built from the update check already in memory, so the screen makes no network call and cannot fail
- Per stack: the version move (
current → target), a note when components come from different releases and the version shown is therefore the oldest of them, and the per-component plan - Three component verdicts, spelled out in words:
will be updated,unchanged, andcannot verify — nothing to compare against. The third is not a synonym for the second — a component with nothing to compare against is not a component that is current, and collapsing the two is how an installation gets told it is up to date on no evidence at all - The release notes of every release the update crosses, oldest first, each with its date and a
(preview)marker when it is not stable. An answer that carries no release list falls back to the target release's changelog and release notes - Stacks with nothing to do are gathered in one list at the bottom rather than omitted: silence about a stack reads as "not installed"
- A check that did not complete says so instead of saying "up to date" — the screen is reachable from a menu drawn by an earlier check, and "up to date" there would be the opposite claim
- Keys:
entercontinues to the apply form;↑/↓,pgup/pgdown,home/endscroll. Deliberately not the EULA'senter-pages-down: this screen asks for no consent, so the one unambiguous key belongs to the one action it offers - Width-aware markdown: rendered through
styles.GetRendererForWidth()(clamped to a readable band, memoised) instead of the fixed 80-column renderer, and re-rendered only when the wrap width actually changes — a drag-resize delivers aWindowSizeMsgper column and glamour is not cheap
**Installer Update** (`models/installer_update.go`)
Its own screen rather than an operation form, because the operation is a single verified download and the generic form described it as replacing the running binary and exiting.
- Says what it will do before it does it: running version, offered version, platform (
os/arch), download size and the full path the file will be written to. The package description is fetched on entry so the size is on screen before the user agrees to the download; the answer is reused by the download itself rather than asked for twice - Download and verification are the whole operation. Length,
sha256and signature are checked in a single pass as the bytes arrive; anything that fails is deleted rather than left behind under a release-looking name - The installer never replaces itself, and that is deliberate rather than unfinished: a process cannot reliably overwrite the binary it is executing — Linux refuses the write, Windows holds the file locked — and failing halfway leaves the user with no installer at all. Instead the screen prints the exact command:
mv "<downloaded file>" "<running installer>", ormove /Yon Windows plus a note to close the installer first. Both paths are quoted, so an installation under a path with a space does not become two arguments; when the running executable's path cannot be determined, no command is printed at all, because a command with a guessed destination is worse than none - One confirmation, for one thing: a file already sitting under that exact name — a previous download, or one interrupted partway. Writing over empty space is not a question, so it is not asked. Declining still prints the move command, since the file the user chose to keep is the build they came for
- Where the file lands: beside the environment file, named
installer_<version>(.exeon Windows), written directly under its final name — staging an executable through the system temp directory and moving it is a pattern antivirus and EDR products score heavily. A download that would land on the running binary is refused outright - Progress is reported against the size from the package description rather than a
Content-Length, which the encrypted response body does not carry, and printed once per 10% rather than once per read — a line every few kilobytes would bury everything around it in a panel that scrolls, and the path and the verification result are printed after the last of them
🎯 **Localization Architecture**
**Centralized Constants**
All user-visible text stored in locale/locale.go:
// Screen-specific constants
const (
WelcomeTitle = "PentAGI Installer"
WelcomeGreeting = "Welcome to PentAGI!"
// Form help text with practical guidance
LLMFormOpenAIHelp = `OpenAI provides access to GPT models...
Get your API key from:
https://platform.openai.com/api-keys`
)**Multi-line Help Text**
Detailed guidance integrated into forms:
- Provider-specific setup instructions
- Configuration recommendations
- Troubleshooting tips
- Best practices
🎯 **Error Handling & Recovery**
**Graceful Degradation**
- Dimension Fallbacks: Handles invalid terminal sizes
- Content Fallbacks: Shows loading states and error messages
- Network Resilience: Offline operation support
- State Recovery: Automatic restoration from corrupted state
**User-Friendly Error Messages**
- Validation Errors: Real-time feedback with clear guidance
- System Errors: Plain English explanations with suggested fixes
- Network Errors: Offline alternatives and retry mechanisms
🎯 **Performance Considerations**
**Optimizations**
- Lazy Loading: Content loaded on-demand when screens accessed
- Memoised Renderers: one cached glamour instance per wrap width, built once instead of per render
- Efficient Scrolling: Viewport-based rendering for large content
- Memory Management: Proper cleanup and resource sharing
**Responsive Performance**
- Breakpoint-Based: Layout decisions based on terminal capabilities
- Content Adaptation: Hide non-essential content on small screens
- Progressive Enhancement: Full features on capable terminals
🎯 **Development Workflow**
**File Organization**
- One Model Per File: Clear separation of screen logic
- Shared Constants: Type definitions in
types.go - Centralized Locale: All text in
locale.go - Clean Dependencies: Business logic isolated in controllers
**Code Style**
- Compact Syntax: Where appropriate for readability
- Expanded Logic: For complex business rules
- Comments: Explain "why" and "how", not "what"
- Error Handling: Graceful degradation with user guidance
**Testing Strategy**
- Build Testing: Successful compilation verification
- Manual Testing: Interactive validation on various terminal sizes
- Dimension Testing: Minimum (80x24) to large terminal support
- Navigation Testing: Complete flow validation
This overview provides the foundation for understanding the PentAGI installer's architecture, features, and development approach. The system prioritizes user experience, maintainability, and production reliability.
Backend Docs Analytics Api (`backend/docs/analytics_api.md`)
Analytics API
GraphQL API for system analytics and statistics monitoring. This document covers three main use cases for developers building dashboards and analytics tools.
Important Data Notes
Duration Storage and Calculation
Database columns (as of migration 20260129_120000):
toolcalls.duration_seconds: Pre-calculated duration for completed toolcalls- Automatically set during migration:
EXTRACT(EPOCH FROM (updated_at - created_at)) - Set to 0.0 for incomplete toolcalls (
received,running) - Column is
NOT NULL DEFAULT 0.0 - Incremental updates: Backend calculates delta time and passes it to SQL
- Formula:
duration_seconds = duration_seconds + duration_delta(parameter from Go code) - Updated by SQL queries:
UpdateToolcallStatus,UpdateToolcallFinishedResult,UpdateToolcallFailedResult
- Automatically set during migration:
msgchains.duration_seconds: Pre-calculated duration for message chains- Automatically set during migration:
EXTRACT(EPOCH FROM (updated_at - created_at)) - Set to 0.0 for incomplete msgchains
- Column is
NOT NULL DEFAULT 0.0 - Incremental updates: Backend calculates delta time and passes it to SQL
- Formula:
duration_seconds = duration_seconds + duration_delta(parameter from Go code) - Updated by SQL queries:
UpdateMsgChain,UpdateMsgChainUsage
- Automatically set during migration:
Benefits of pre-calculated durations:
- Faster query performance (no real-time
EXTRACT(EPOCH FROM ...)calculations) - Consistent duration values (accumulated over multiple updates)
- Simpler SQL queries for analytics (just
tc.duration_secondsinstead of complex expressions) - Reduced database load for dashboard queries
- Incremental tracking: accurately captures time even when records are updated multiple times
- All analytics SQL queries updated to use
duration_secondscolumn directly
Incremental update logic:
- Backend code tracks execution time and calculates delta in seconds
- Delta is passed as parameter to SQL update queries
- SQL adds delta to existing
duration_secondsvalue - Prevents overwrites and ensures accurate cumulative duration
- Works correctly for long-running operations with multiple status updates
- Backend has full control over time measurement (can use high-precision timers)
Execution Time Metrics (`totalDurationSeconds`)
What the data shows:
Subtask duration: Linear wall-clock time from subtask start to finish (created_at → updated_at)
- Excludes subtasks in
createdorwaitingstatus (not yet started) - Includes only
running,finished,failedsubtasks - NOT the sum of toolcalls (avoids double-counting nested agent calls)
- Excludes subtasks in
Task duration: Total execution time including:
- Generator Agent execution (runs before subtasks)
- All subtasks execution (with overlap compensation for batch-created subtasks)
- Refiner Agent execution(s) (runs between subtasks)
Flow duration: Complete flow execution time including:
- All tasks duration (generator + subtasks + refiner)
- Flow-level toolcalls (e.g., Assistant toolcalls without task/subtask binding)
- EXCLUDES Assistant msgchain lifetime (only active toolcall time counted)
Why not sum of toolcalls:
- Primary Agent calls Coder Agent → Coder calls Terminal
- Summing toolcalls would count Terminal execution twice (in Coder time AND separately)
- Linear time gives accurate wall-clock duration
Overlap compensation:
- Generator creates multiple subtasks simultaneously (same created_at)
- Subtasks execute sequentially, not in parallel
- System compensates for this overlap to show real execution time
Toolcalls Count Metrics (`totalCount`, `totalToolcallsCount`)
What the data shows:
- Only completed toolcalls (status =
finishedorfailed) - Excludes
received(not started) andrunning(in progress) toolcalls - Represents actual completed operations, not attempted or pending
Use this for:
- Counting successful/failed operations
- Calculating success rate:
finished_count / (finished_count + failed_count) - Understanding actual work performed
Agent Type Breakdown
Important distinction:
primary_agent: Main orchestrator for subtasksgenerator: Creates subtask plans (runs once per task, before subtasks)refiner: Updates subtask plans (runs between subtasks, can run multiple times)reporter: Generates final task reportsassistant: Interactive mode (can have long idle periods)- Specialist agents:
coder,pentester,installer,searcher,memorist,adviser
Usage interpretation:
- High
generatorusage = complex task decomposition - High
refinerusage = adaptive planning (many plan adjustments) - High specialist usage = delegated work (Primary Agent using team members)
Agent vs Non-Agent Tools (`isAgent` field)
The isAgent field in FunctionToolcallsStats categorizes tools into two types:
Agent Tools (isAgent: true):
- Delegation to AI agents (e.g.,
coder,pentester,searcher) - Store results from agent execution (e.g.,
coder_result,hack_result) - Execution time represents delegation overhead + agent decision time
- Does NOT include nested tool calls made by the agent
- Examples:
coder,pentester,installer,searcher,memorist,adviser
Non-Agent Tools (isAgent: false):
- Direct tool execution (e.g.,
terminal,browser,file) - Search engines (e.g.,
google,duckduckgo,tavily,firecrawl,sploitus,searxng) - Vector database operations (e.g.,
search_in_memory,store_guide) - Environment operations (e.g.,
terminal,file) - Execution time represents actual operation duration
- Examples:
terminal,browser,file,google,search_in_memory
Use this distinction for:
- Analyzing delegation overhead vs direct execution time
- Identifying which agents are most frequently used
- Optimizing agent selection strategies
- Understanding the balance between AI-driven and direct operations
- Dashboard visualizations: color-code or separate agent vs non-agent tools
Common Fragments
Define these fragments once and reuse across queries:
fragment UsageStats on UsageStats {
totalUsageIn
totalUsageOut
totalUsageCacheIn
totalUsageCacheOut
totalUsageCostIn
totalUsageCostOut
}
fragment ToolcallsStats on ToolcallsStats {
totalCount
totalDurationSeconds
}
fragment FlowsStats on FlowsStats {
totalFlowsCount
totalTasksCount
totalSubtasksCount
totalAssistantsCount
}
fragment FlowStats on FlowStats {
totalTasksCount
totalSubtasksCount
totalAssistantsCount
}
fragment FunctionToolcallsStats on FunctionToolcallsStats {
functionName
isAgent
totalCount
totalDurationSeconds
avgDurationSeconds
}
fragment SubtaskExecutionStats on SubtaskExecutionStats {
subtaskId
subtaskTitle
totalDurationSeconds
totalToolcallsCount
}
fragment TaskExecutionStats on TaskExecutionStats {
taskId
taskTitle
totalDurationSeconds
totalToolcallsCount
subtasks {
...SubtaskExecutionStats
}
}
fragment FlowExecutionStats on FlowExecutionStats {
flowId
flowTitle
totalDurationSeconds
totalToolcallsCount
totalAssistantsCount
tasks {
...TaskExecutionStats
}
}Use Case 1: Time-Filtered Dashboard
Purpose: Display system activity over time periods (week/month/quarter) with day-by-day breakdowns and execution metrics.
Query
query TimeDashboard($period: UsageStatsPeriod!) {
# LLM token usage over time
usageStatsByPeriod(period: $period) {
date
stats { ...UsageStats }
}
# Toolcalls activity over time
toolcallsStatsByPeriod(period: $period) {
date
stats { ...ToolcallsStats }
}
# Flows/tasks/subtasks created over time
flowsStatsByPeriod(period: $period) {
date
stats { ...FlowsStats }
}
# Flow execution times with full hierarchy
flowsExecutionStatsByPeriod(period: $period) {
...FlowExecutionStats
}
}Variables:
{ "period": "week" } // or "month", "quarter"Data Interpretation
Daily Usage Trends:
usageStatsByPeriod: Track token consumption patterns (input/output, cache hits, costs)- Chart: Line graph showing daily token usage and costs
- Insights: Identify peak usage days, cost optimization opportunities
Toolcalls Performance:
toolcallsStatsByPeriod: Monitor tool execution frequency and duration- Count: Only completed operations (finished/failed), excludes pending/running
- Duration: Sum of individual toolcall execution times (created_at → updated_at)
- Chart: Dual-axis chart (count bars + duration line)
- Insights: Detect performance degradation, identify bottlenecks
- Note: Duration here IS sum of toolcalls (unlike execution stats which use wall-clock)
Flow Activity:
flowsStatsByPeriod: Track system load (flows/tasks/subtasks/assistants created per day)- Chart: Stacked bar chart showing hierarchy depth
- Insights: Understand workload distribution, capacity planning, assistant usage patterns
Execution Breakdown:
flowsExecutionStatsByPeriod: Hierarchical view of execution times- Chart: Treemap or sunburst showing time distribution across flows → tasks → subtasks
- Important: Duration is wall-clock time, not sum of toolcalls
- Subtask: Linear time from start to finish (excludes created/waiting subtasks)
- Task: Subtasks + Generator + Refiner agents
- Flow: Tasks + Flow-level toolcalls (Assistant active time, NOT lifetime)
- Count: Only completed toolcalls (finished/failed status)
- Insights: Identify slow flows/tasks, optimize critical paths
- Note: Batch-created subtasks have overlap compensation applied
Cross-Correlations:
- Compare token usage vs execution time (efficiency metric)
- Correlate toolcall count with flow complexity
- Cost per flow:
totalUsageCost / flowsCount - Average toolcalls per task:
totalToolcallsCount / totalTasksCount
Use Case 2: Overall System Statistics
Purpose: Get comprehensive system-wide metrics without time filters.
Query
query SystemOverview {
# Total LLM usage
usageStatsTotal { ...UsageStats }
# Usage breakdown by provider
usageStatsByProvider {
provider
stats { ...UsageStats }
}
# Usage breakdown by model
usageStatsByModel {
model
provider
stats { ...UsageStats }
}
# Usage breakdown by agent type
usageStatsByAgentType {
agentType
stats { ...UsageStats }
}
# Total toolcalls stats
toolcallsStatsTotal { ...ToolcallsStats }
# Toolcalls breakdown by function
toolcallsStatsByFunction {
...FunctionToolcallsStats
}
# Total flows/tasks/subtasks
flowsStatsTotal { ...FlowsStats }
}Data Interpretation
Token Economics:
usageStatsTotal: Overall system cost and token consumption- Metrics: Total spend, cache efficiency (
cacheIn / (cacheIn + usageIn)) - Dashboard KPI: Display as headline metrics
Provider Distribution:
usageStatsByProvider: Cost and usage per LLM provider- Chart: Pie chart showing provider share by cost
- Insights: Identify most/least cost-effective providers
Model Efficiency:
usageStatsByModel: Granular per-model breakdown- Chart: Table sorted by cost with usage metrics
- Metrics:
- Cost per token:
(costIn + costOut) / (usageIn + usageOut) - Cache hit rate per model
- Cost per token:
- Insights: Choose optimal models for different tasks
Agent Performance:
usageStatsByAgentType: Resource consumption by agent role- Chart: Horizontal bar chart showing usage by agent
- Insights: Understand which agents consume most resources
Tool Usage Patterns:
toolcallsStatsByFunction: Top tool functions by usage and duration- Agent Classification: The
isAgentfield indicates if the function is an agent delegation tool- Agent tools (e.g.,
coder,pentester) show their own execution time - Does NOT include time of nested calls they make
- Example:
codertoolcall = time for Coder Agent to decide + delegate, not terminal commands - Non-agent tools (e.g.,
terminal,browser) show direct execution time
- Agent tools (e.g.,
- Chart: Table with sortable columns (count, duration, average, agent type)
- Metrics:
- Slowest tools: Sort by
avgDurationSeconds - Most used tools: Sort by
totalCount(only completed) - Time sinks: Sort by
totalDurationSeconds - Agent vs non-agent breakdown: Filter by
isAgent
- Slowest tools: Sort by
- Insights: Optimize frequently-used slow tools, identify unused tools, distinguish delegation overhead from direct execution
System Scale:
flowsStatsTotal: Total entities in system- Metrics:
- Tasks per flow:
totalTasksCount / totalFlowsCount - Subtasks per task:
totalSubtasksCount / totalTasksCount - Assistants per flow:
totalAssistantsCount / totalFlowsCount
- Tasks per flow:
- Insights: Understand average flow complexity and assistant usage
Use Case 3: Flow-Specific Dashboard
Purpose: Deep dive into a specific flow's metrics.
Query
query FlowAnalytics($flowId: ID!) {
# Basic flow info
flow(flowId: $flowId) {
id
title
status
createdAt
updatedAt
}
# LLM usage for this flow
usageStatsByFlow(flowId: $flowId) { ...UsageStats }
# Agent usage breakdown
usageStatsByAgentTypeForFlow(flowId: $flowId) {
agentType
stats { ...UsageStats }
}
# Toolcalls stats
toolcallsStatsByFlow(flowId: $flowId) { ...ToolcallsStats }
# Tool function breakdown
toolcallsStatsByFunctionForFlow(flowId: $flowId) {
...FunctionToolcallsStats
}
# Example: Separate agent and non-agent tools
# Filter client-side: stats.filter(s => s.isAgent) for agent tools
# Filter client-side: stats.filter(s => !s.isAgent) for direct execution tools
# Flow structure stats
flowStatsByFlow(flowId: $flowId) { ...FlowStats }
}Variables:
{ "flowId": "123" }Data Interpretation
Flow Performance Summary:
usageStatsByFlow: Total LLM costs for this flow (all msgchains)toolcallsStatsByFlow: Execution metrics (duration, toolcall count)- Count: Only completed toolcalls (finished/failed)
- Duration: Sum of individual toolcall times
- KPIs:
- Cost per toolcall:
totalUsageCost / totalToolcallsCount - Average toolcall duration:
totalDurationSeconds / totalCount - Cost efficiency: tokens per second
- Cost per toolcall:
- Note: For actual flow wall-clock time, use
flowsExecutionStatsByPeriod
Agent Activity:
usageStatsByAgentTypeForFlow: Which agents were most active- Chart: Donut chart showing token distribution by agent
- Insights: Understand which agents drive flow execution
Tool Usage Analysis:
toolcallsStatsByFunctionForFlow: Detailed breakdown per tool- Chart: Bubble chart (x=count, y=avgDuration, size=totalDuration, color=isAgent)
- Metrics:
- Identify bottleneck tools (high avgDuration)
- Find frequently-used tools (high totalCount)
- Calculate tool efficiency scores
- Separate agent delegation overhead from direct execution time using
isAgentfield
Flow Complexity:
flowStatsByFlow: Structural metrics- Metrics:
- Subtasks per task:
totalSubtasksCount / totalTasksCount - Toolcalls per task:
toolcallsCount / tasksCount - Assistants count:
totalAssistantsCount
- Subtasks per task:
- Insights: Compare against average complexity to identify outliers, track assistant usage per flow
Cross-Flow Comparisons:
Fetch multiple flows and compare:
- Cost efficiency (cost per task)
- Execution speed (duration per task)
- Tool utilization patterns
- Agent composition differences
Understanding Metric Differences
Execution Stats vs Toolcalls Stats
These two metric types measure different aspects of system performance:
Execution Stats (flowsExecutionStatsByPeriod):
- Purpose: Measure real wall-clock time for flows/tasks/subtasks
- Duration calculation: Linear time (start → end timestamp)
- What it shows: How long a flow/task/subtask actually ran
- Use for: Performance analysis, SLA monitoring, user-facing progress
- Example: Subtask ran for 100 seconds (even if it made 10 toolcalls)
Toolcalls Stats (toolcallsStatsByPeriod, toolcallsStatsByFunction):
- Purpose: Measure individual tool execution metrics
- Duration calculation: Sum of toolcall durations (each toolcall's created_at → updated_at)
- What it shows: Aggregate time spent in specific tools
- Use for: Tool optimization, identifying slow functions, resource attribution
- Example: 50 terminal toolcalls totaling 300 seconds
Key difference:
Flow execution time = 100 seconds (wall-clock)
Toolcalls total time = 150 seconds (sum of all toolcalls)
Why different?
- Flow time is LINEAR (real time elapsed)
- Toolcalls time INCLUDES OVERLAPS (nested agent calls counted in parent time)When to use which:
- User wants to know "how long did my pentest take?" → Use Execution Stats
- Developer wants to optimize slow tools → Use Toolcalls Stats
- Manager wants to see system utilization → Use Toolcalls Stats
- SLA monitoring → Use Execution Stats
Subtask Status and Inclusion
Included in metrics (counted):
running: Currently executing (duration = created_at → now)finished: Completed successfully (duration = created_at → updated_at)failed: Terminated with error (duration = created_at → updated_at)
Excluded from metrics (NOT counted):
created: Generated but not started yet (duration = 0)waiting: Paused for user input (duration = 0)
Why this matters:
- Generator creates 10 subtasks at once, only 1 starts executing
- You'll see 1 subtask in stats, not 10
- As more subtasks execute, they appear in metrics
- Final stats include only executed subtasks
Assistant Time Accounting
Assistant msgchains:
- Can exist for days/weeks (created once, used intermittently)
- Their lifetime is NOT counted in flow duration
- Only their active toolcalls are counted
Example:
Assistant created: Monday 9 AM
User asks question: Monday 10 AM (toolcall 1: 5 seconds)
User asks question: Tuesday 3 PM (toolcall 2: 3 seconds)
Flow duration contribution: 8 seconds (5 + 3)
NOT: 30+ hours (Monday to Tuesday)Generator and Refiner Inclusion
Task execution includes:
Generator Agent (runs once at task start):
- Creates initial subtask plan
- Has msgchain with task_id, NO subtask_id
- Time is added to task duration
Subtasks (execute sequentially):
- Each has Primary Agent msgchain
- Individual durations with overlap compensation
Refiner Agent (runs between subtasks):
- Updates subtask plan based on results
- Can run multiple times per task
- Each run has msgchain with task_id, NO subtask_id
- Total refiner time added to task duration
Example task timeline:
Generator (5s) → Subtask 1 (10s) → Refiner (3s) → Subtask 2 (15s) → Refiner (2s) → Subtask 3 (8s)
Task duration = 5 + 10 + 3 + 15 + 2 + 8 = 43 secondsAdvanced Analytics Patterns
1. Cost Optimization Dashboard
Combine queries to identify cost reduction opportunities:
query CostOptimization {
usageStatsByModel {
model
provider
stats { ...UsageStats }
}
toolcallsStatsByFunction {
functionName
isAgent
totalCount
avgDurationSeconds
}
}Analysis:
- Expensive models with high usage → candidates for cheaper alternatives
- Slow tools called frequently → optimization targets
- Calculate ROI per model:
performance_gain / cost_increase
2. Performance Monitoring
Track system responsiveness:
query PerformanceMetrics($period: UsageStatsPeriod!) {
toolcallsStatsByPeriod(period: $period) {
date
stats { ...ToolcallsStats }
}
flowsExecutionStatsByPeriod(period: $period) {
flowTitle
totalDurationSeconds
totalToolcallsCount
}
}Metrics:
- Average execution time per flow:
totalDurationSeconds / flowsCount - Toolcalls per day trend (detect performance degradation)
- P95/P99 flow durations (for SLA monitoring)
Important for performance analysis:
- Use
flowsExecutionStatsByPeriodfor wall-clock time (what users experience) - Compare with
toolcallsStatsByPeriodto detect overhead (high ratio = optimization needed) - Ratio > 2.0 suggests significant nested agent call overhead
3. Resource Attribution
Understand resource consumption patterns:
query ResourceAttribution {
usageStatsByAgentType {
agentType
stats { ...UsageStats }
}
toolcallsStatsByFunction {
functionName
isAgent
totalDurationSeconds
}
}Analysis:
- Which agents consume most resources
- Tool time distribution (execution time budget)
- Cost attribution by capability (pentesting vs coding vs searching)
Implementation Tips
Caching Strategy:
- Cache
*StatsTotalqueries (update every 5-10 minutes) - Cache
*StatsByPeriodper period (update hourly) - Real-time for flow-specific queries
Visualization Libraries:
- Time series: Recharts, Chart.js (daily trends)
- Hierarchical: D3 treemap/sunburst (execution breakdown)
- Tables: TanStack Table with sorting/filtering (function stats)
Performance Optimization:
- Use query batching for multiple flows
- Implement pagination for large datasets
- Add loading states for slow queries (execution stats can be heavy)
Data Refresh:
- Overall stats: Manual refresh or 10min polling
- Time-filtered: Auto-refresh on period change
- Flow-specific: Subscribe to flow updates for real-time metrics
Practical Examples
Example 1: Understanding a Slow Flow
Scenario: Flow took 300 seconds but only has 50 toolcalls
Investigation:
query InvestigateSlowFlow($flowId: ID!) {
flowsExecutionStatsByPeriod(period: week) {
flowId
flowTitle
totalDurationSeconds
totalToolcallsCount
tasks {
taskTitle
totalDurationSeconds
totalToolcallsCount
subtasks {
subtaskTitle
totalDurationSeconds
totalToolcallsCount
}
}
}
}Analysis:
- Check task breakdown: Which task took longest?
- Check subtask breakdown: Which subtasks in slow task took longest?
- Check toolcall count: High duration + low count = slow individual operations
- Check toolcalls by function: Which tools are slow?
Common causes:
- Long-running terminal commands (compilation, scanning)
- Slow search engine responses (tavily, perplexity)
- Large file operations
- Network latency for browser tool
Example 2: Cost Attribution
Scenario: Need to understand cost per capability
Query:
query CostAttribution {
usageStatsByAgentType {
agentType
stats {
totalUsageCostIn
totalUsageCostOut
}
}
toolcallsStatsByFunction {
functionName
totalCount
avgDurationSeconds
}
}Interpretation:
primary_agent: Orchestration overheadgenerator: Planning cost (usually low, runs once per task)refiner: Replanning cost (high value = many adjustments)pentester: Security testing operationscoder: Development worksearcher: Research and information gathering
Cost optimization:
- High generator cost → simplify task descriptions
- High refiner cost → improve initial planning (fewer adjustments needed)
- High searcher cost → use memory tools more (cheaper than web search)
Example 3: Detecting Inefficient Flows
Red flags:
Flow A: 100 toolcalls, 500 seconds → 5s per toolcall (GOOD)
Flow B: 100 toolcalls, 5000 seconds → 50s per toolcall (INVESTIGATE)Check:
query DetectInefficiency {
toolcallsStatsByFunction {
functionName
isAgent
totalCount
avgDurationSeconds
}
}Common issues:
- High
terminalavg → long commands, consider timeout tuning - High
browseravg → slow websites, consider caching - High
tavily/perplexityavg → deep research, optimize queries - High agent tools (
coder,pentester) avg → complex delegated work
Example 4: Understanding Task Complexity
Scenario: Why do some tasks take so long?
Metrics to check:
Task complexity indicators:
- High subtask count → decomposition into many steps
- High refiner calls → adaptive planning (many plan changes)
- High generator time → complex initial planning
- Low subtask count + high duration → individual subtasks are slowQuery:
query TaskComplexity($flowId: ID!) {
flowsExecutionStatsByPeriod(period: week) {
flowId
tasks {
taskTitle
totalDurationSeconds
totalToolcallsCount
subtasks {
subtaskTitle
totalDurationSeconds
}
}
}
usageStatsByAgentTypeForFlow(flowId: $flowId) {
agentType
stats { totalUsageIn totalUsageOut }
}
}Analysis patterns:
- Many subtasks + low generator usage → simple decomposition
- Many subtasks + high generator usage → complex planning
- High refiner usage → dynamic adaptation (plan changed during execution)
- Few subtasks + high duration → intensive work per subtask
Data Quality Guarantees
Accuracy
Time measurements:
- ✅ No double-counting of nested agent calls
- ✅ Overlap compensation for batch-created subtasks
- ✅ Excludes non-started subtasks (created/waiting)
- ✅ Includes all execution phases (generator, subtasks, refiner)
Count measurements:
- ✅ Only completed operations (finished/failed)
- ✅ Excludes pending (received) and in-progress (running)
- ✅ Consistent across all queries
Cost measurements:
- ✅ Aggregated from msgchains (source of truth for LLM calls)
- ✅ Includes cache hits and misses
- ✅ Separate input/output costs
Known Limitations
Current limitations:
Historical data: Subtasks created before this update may have:
- Missing primary_agent subtask_id (known bug, now fixed)
- Use linear time fallback (still accurate)
Running entities: Duration calculated as
created_at → now- Updates as entity continues execution
- Final duration set when status changes to finished/failed
Assistant lifetime: Long-lived assistants
- Only active toolcall time counted (correct behavior)
- Msgchain lifetime NOT included in flow duration
Edge cases handled:
- ✅ Batch-created subtasks (overlap compensation)
- ✅ Missing primary_agent msgchain (fallback to linear time)
- ✅ Subtasks in waiting status (excluded from duration)
- ✅ Flow-level toolcalls without task binding (counted separately)
- ✅ Generator/Refiner without subtask binding (counted in task duration)
Troubleshooting
"My flow shows 0 duration but has toolcalls"
Possible causes:
- All subtasks are in
createdorwaitingstatus - Flow just started (no completed subtasks yet)
Check:
query CheckFlowStatus($flowId: ID!) {
flow(flowId: $flowId) {
status
}
tasks(flowId: $flowId) {
status
subtasks {
status
}
}
}"Task duration seems low compared to subtasks"
This is normal if:
- Subtasks were created in batch (overlap compensation applied)
- Example: 3 subtasks created at 10:00:00, finished at 10:00:10, 10:00:20, 10:00:30
- Naive sum: 10 + 20 + 30 = 60 seconds
- Actual time: 30 seconds (overlap compensated)
"Toolcalls duration > execution duration"
This is expected:
- Toolcalls duration: Sum of all toolcall times (includes nested calls)
- Execution duration: Wall-clock time (linear)
- Nested agent calls cause toolcalls > execution
- Example: Primary Agent (100s) calls Coder (30s), toolcalls = 130s, execution = 100s
"Count doesn't match my expectations"
Remember:
- Only completed toolcalls counted (finished/failed)
- Received (pending) and running (in-progress) excluded
- Check toolcall status distribution if counts seem low
Best Practices
Dashboard Design
Real-time monitoring:
- Use execution stats for user-facing progress
- Show flow/task/subtask hierarchy with durations
- Update as status changes (subscribe to updates)
Historical analysis:
- Use toolcalls stats for tool performance
- Use usage stats for cost tracking
- Group by period for trend analysis
Cost optimization:
- Compare cost per agent type
- Identify expensive models with low value
- Track cache hit rates for efficiency
Query Optimization
For large datasets:
# Don't fetch full hierarchy if not needed
query LightweightStats {
toolcallsStatsTotal { totalCount totalDurationSeconds }
usageStatsTotal { totalUsageCostIn totalUsageCostOut }
}
# Instead of:
query HeavyStats($period: UsageStatsPeriod!) {
flowsExecutionStatsByPeriod(period: $period) {
# Full hierarchy - expensive for many flows
tasks { subtasks { ... } }
}
}Batch requests:
query BatchedAnalytics {
# Fetch multiple metrics in one request
usageStatsTotal { ... }
toolcallsStatsTotal { ... }
flowsStatsTotal { ... }
}Data Refresh Strategy
Real-time (WebSocket subscriptions)
- Flow status changes
- Task/subtask creation
- Toolcall completion
- Use for: Live flow monitoring
Polling (every 1-5 minutes)
- Execution stats for running flows
- Toolcalls stats for active periods
- Use for: Dashboard auto-refresh
Cached (refresh every 10-30 minutes)
- Historical period stats
- Total stats (system-wide)
- Provider/model breakdowns
- Use for: Reports and analytics
On-demand (user action)
- Flow-specific deep dives
- Custom period queries
- Export operations
- Use for: Detailed investigation
Migration Notes
Duration Calculation Changes (20260129_120000)
Previous behavior:
- Durations calculated on-the-fly in SQL queries:
EXTRACT(EPOCH FROM (updated_at - created_at)) - Slower query performance due to real-time calculations
- Required complex SQL expressions in every analytics query
New behavior (improved performance):
- Pre-calculated
duration_secondscolumns intoolcallsandmsgchainstables - Duration calculated once during migration for existing records
- Analytics queries use simple column references:
tc.duration_seconds - Significant performance improvement for dashboard queries (simpler execution plans)
Migration steps:
- Add
duration_seconds DOUBLE PRECISION NULLcolumn - Calculate duration for existing records:
EXTRACT(EPOCH FROM (updated_at - created_at)) - Set remaining NULL values to 0.0
- Alter column to
NOT NULL - Set default value to 0.0 for future records
For developers:
- All SQL queries in
backend/sqlc/models/toolcalls.sqlupdated to useduration_seconds - Update queries accept
duration_deltaparameter from Go code - Updated queries with new signatures:
UpdateToolcallStatus(status, duration_delta, id): adds delta to duration when status changesUpdateToolcallFinishedResult(result, duration_delta, id): adds final delta when toolcall finishesUpdateToolcallFailedResult(result, duration_delta, id): adds final delta when toolcall failsUpdateMsgChain(chain, duration_delta, id): adds delta when chain is updatedUpdateMsgChainUsage(usage_in, usage_out, usage_cache_in, usage_cache_out, usage_cost_in, usage_cost_out, duration_delta, id): adds delta when usage updated
- Backend code must calculate
duration_deltain seconds before calling update - Use
time.Since(startTime).Seconds()or similar for accurate measurements - SQL simply adds the delta:
duration_seconds = duration_seconds + $duration_delta
Backend Docs Chain Ast (`backend/docs/chain_ast.md`)
ChainAST Documentation
Table of Contents
- ChainAST Documentation
Introduction
ChainAST is a structured representation of message chains used in Large Language Model (LLM) conversations. It organizes conversations into a logical hierarchy, making it easier to analyze, modify, and validate message sequences, especially when they involve tool calls and their responses.
The structure helps address common issues in LLM conversations such as:
- Validating proper conversation flow
- Managing tool calls and their responses
- Handling conversation sections and state changes
- Ensuring consistent conversation structure
- Efficient size tracking for summarization and context management
Structure Overview
ChainAST represents a message chain as an abstract syntax tree with the following components:
ChainAST
├── Sections[] (ChainSection)
├── Header
│ ├── SystemMessage (optional)
│ ├── HumanMessage (optional)
│ └── sizeBytes (total header size in bytes)
├── sizeBytes (total section size in bytes)
└── Body[] (BodyPair)
├── Type (RequestResponse, Completion, or Summarization)
├── AIMessage
├── ToolMessages[] (for RequestResponse and Summarization types)
└── sizeBytes (total body pair size in bytes)Components:
- ChainAST: The root structure containing an array of sections
- ChainSection: A logical unit of conversation, starting with a header and containing multiple body pairs
- Includes
sizeBytestracking total section size in bytes
- Includes
- Header: Contains system and/or human messages that initiate a section
- Includes
sizeBytestracking total header size in bytes
- Includes
- BodyPair: Represents an AI response, which may include tool calls and their responses
- Includes
sizeBytestracking total body pair size in bytes
- Includes
- RequestResponse: A type of body pair where the AI message contains tool calls requiring responses
- Completion: A simple AI message without tool calls
- Summarization: A special type of body pair containing a tool call to the summarization tool
Constants and Default Values
ChainAST provides several important constants:
fallbackRequestArgs: Default arguments ({}) for tool calls without specified argumentsFallbackResponseContent: Default response content ("the call was not handled, please try again") for missing tool responses when using force=trueSummarizationToolName: Name of the special summarization tool ("execute_task_and_return_summary")SummarizationToolArgs: Default arguments for the summarization tool ({"question": "delegate and execute the task, then return the summary of the result"})
Size Tracking Features
ChainAST includes built-in size tracking to support efficient summarization algorithms and context management:
// Get the size of a section in bytes
sizeInBytes := section.Size()
// Get the size of a body pair in bytes
sizeInBytes := bodyPair.Size()
// Get the size of a header in bytes
sizeInBytes := header.Size()
// Get the total size of the entire ChainAST
totalSize := ast.Size()Size calculation considers all content types including:
- Text content (string length)
- Image URLs (URL string length)
- Binary data (byte count)
- Tool calls (ID, type, name, and arguments length)
- Tool call responses (ID, name, and content length)
The sizeBytes values are automatically maintained when:
- Creating a new ChainAST from a message chain
- Appending human messages
- Adding tool responses
- Creating elements with constructors
Creating and Using ChainAST
Basic Creation
// Create from an existing message chain
ast, err := NewChainAST(messageChain, false)
if err != nil {
// Handle validation error
}
// Get messages (flattened chain)
flatChain := ast.Messages()The force parameter in NewChainAST determines how the function handles inconsistencies:
force=false: Strict validation, returns errors for any inconsistencyforce=true: Attempts to repair problems by:- Merging consecutive human messages into a single message with multiple content parts
- Adding missing tool responses with placeholder content ("the call was not handled, please try again")
- Skipping invalid messages like unexpected tool messages without preceding AI messages
During creation, the size of all components is calculated automatically.
Using Constructors
ChainAST provides constructors to create elements with automatic size calculation:
/* Detailed source-code truncated for AI context efficiency. */Getting Messages
Each component provides a method to get its messages in the correct order:
// Get all messages from a header (system first, then human)
headerMsgs := header.Messages()
// Get all messages from a body pair (AI first, then tools)
bodyPairMsgs := bodyPair.Messages()
// Get all messages from a section
sectionMsgs := section.Messages()
// Get all messages from the ChainAST
allMsgs := ast.Messages()Body Pair Validation
The IsValid() method checks if a BodyPair follows the structure rules:
// Check if a body pair is valid
isValid := bodyPair.IsValid()Validation rules depend on the body pair type:
- For Completion: No tool messages allowed
- For RequestResponse: Must have at least one tool message
- For Summarization: Must have exactly one tool message
- For all types: All tool calls must have matching responses and vice versa
The GetToolCallsInfo() method returns detailed information about tool calls:
// Get information about tool calls and responses
toolCallsInfo := bodyPair.GetToolCallsInfo()
// Check for pending or unmatched tool calls
if len(toolCallsInfo.PendingToolCallIDs) > 0 {
// There are tool calls without responses
}
if len(toolCallsInfo.UnmatchedToolCallIDs) > 0 {
// There are tool responses without matching tool calls
}
// Access completed tool calls
for id, pair := range toolCallsInfo.CompletedToolCalls {
// Use tool call and response information
toolCall := pair.ToolCall
response := pair.Response
}Common Validation Rules
When force=false, NewChainAST enforces these rules:
- First message must be System or Human
- No consecutive Human messages
- Tool calls must have matching responses
- Tool responses must reference valid tool calls
- System messages can't appear in the middle of a chain
- AI messages with tool calls must have responses before another AI message
- Summarization body pairs must have exactly one tool message
Modifying Message Chains
Adding Elements
// Add a section to the ChainAST
ast.AddSection(section)
// Add a body pair to a section
section.AddBodyPair(bodyPair)Adding Human Messages
// Append a new human message
ast.AppendHumanMessage("Tell me more about this topic")The function follows these rules:
- If chain is empty: Creates a new section with this message as HumanMessage
- If the last section has body pairs (AI responses): Creates a new section with this message
- If the last section has no body pairs and no HumanMessage: Adds this message to that section
- If the last section has no body pairs but has HumanMessage: Appends content to the existing message
Section and header sizes are automatically updated when human messages are added or modified.
Working with Tool Calls
// Add a response to a tool call
err := ast.AddToolResponse("tool-call-id", "tool-name", "Response content")
if err != nil {
// Handle error (tool call not found)
}
// Find all responses for a specific tool call
responses := ast.FindToolCallResponses("tool-call-id")The AddToolResponse function:
- Searches for the specified tool call ID in AI messages
- If the tool call is found and already has a response, updates the existing response content
- If the tool call is found but doesn't have a response, adds a new response
- If the tool call is not found, returns an error
Body pair and section sizes are automatically updated when tool responses are added or modified.
Testing Utilities
The package tests build their message chains from pkg/cast/fixtures_test.go. Every helper there is a function that returns a fresh chain on each call: parsing shares the Parts of the input messages with the AST, and AddToolResponse, NormalizeToolCallIDs, ClearReasoning and SanitizeToolCallArguments write into those parts, so a chain kept in a shared variable would change under the next test. The file is a _test.go file of package cast, so only the tests of this package can use it.
Message Builders
chainASTSystem(text) // a system message with one text part
chainASTHuman(text) // a human message with one text part
chainASTAnswer(text) // an AI message with one text part: a completion
chainASTText(role, text) // a message of any role with one text part
chainASTAI(parts...) // an AI message with the given parts
chainASTCall(id, name) // a "function" tool call with the arguments {}
chainASTCallWith(id, name, args) // the same call with the given arguments
chainASTTool(id, name, content) // a tool message holding one responsePredefined Test Chains
// Basic chains
emptyChain() // no messages
systemOnlyChain() // only a system message
humanOnlyChain() // only a human message
systemHumanChain() // system + human messages
basicConversationChain() // system + human + AI completion
// Tool-related chains
chainWithTool() // a call to get_weather (tool-1) with no response
chainWithSingleToolResponse() // the same call and its response
chainWithMultipleTools() // calls to get_weather (tool-1) and get_time (tool-2), neither answered
// Complex chains
chainWithMultipleSections() // a completion, then a second section with a call and its response
chainWithMissingToolResponse() // the two calls of chainWithMultipleTools with only tool-1 answered
chainWithUnexpectedTool() // a completion followed by a tool message that answers no call
// Summarization chains
chainWithSummarizationAndOtherPairs() // a summarization pair, a completion, then a call and its response
// Provider switching
providerSwitchChain(withReasoning) // a scan in three sections, described belowproviderSwitchChain is the chain a provider switch is tested against: NormalizeToolCallIDs and ClearReasoning run on it each on its own and, clearing first, one after the other; NormalizeToolCallIDs, SanitizeToolCallArguments and ClearReasoning run in the order pkg/providers runs them on a variant of it that needs a forced repair. Its first section holds a text preamble beside two parallel calls, one with an id already in the Anthropic format (toolu_…) and one with an OpenAI-style id, their two responses and a completion; the second section opens with a completion before a call whose tool message carries a text note beside the response; the third holds a summarization pair. With withReasoning set, the preamble, the first completion, the note and every call except the one already in the Anthropic format carry reasoning with a signature. Without it the chain is otherwise identical, which is what ClearReasoning must turn the first form into; both forms have the same sizes, because reasoning is not counted.
Generating Custom Test Chains
For chains with more sections, pairs and calls than the predefined ones, use the generators:
// A system prompt, one question and its completion
config := DefaultChainConfig()
// Custom configuration
config := ChainConfig{
IncludeSystem: true,
Sections: 3, // 3 conversation turns
BodyPairsPerSection: []int{1, 2, 2}, // AI responses per section
ToolsForBodyPairs: []bool{false, true, false, true, true}, // which responses, counted across the chain, make calls
ToolCallsPerBodyPair: []int{0, 2, 0, 3, 1}, // how many calls each of those responses makes
IncludeAllToolResponses: true, // whether every call gets its response
}
// Generate chain based on config
chain := GenerateChain(config)
// Five sections, a response with three calls in the first, third and fifth and a completion in the
// others, with the first seven tool responses of the chain dropped
complexChain := GenerateComplexChain(
5, // number of sections
3, // number of calls per tool-using response
7, // number of tool responses to drop, counted from the start of the chain
)The ChainConfig struct controls the generated chain:
IncludeSystem: whether the chain opens with the system message "You are a helpful assistant."Sections: the number of conversation turns; turn n opens with the human message "Question n"BodyPairsPerSection: the number of AI responses in each section; a section past the end of the slice has oneToolsForBodyPairs: which AI responses make tool calls, indexed by the position of the response in the whole chain; a response past the end of the slice is the completion "Response to question n"ToolCallsPerBodyPair: how many calls each tool-using response makes, indexed the same way; a response past the end of the slice makes oneIncludeAllToolResponses: whether every call is followed by its response; without it no call is answered
Calls are numbered tool-1, tool-2, … across the whole chain. The k-th call of a response is get_data_k with the arguments {"query": "Test query k"}, and its response, one tool message per call in the order of the calls, reads "Response for tool-N".
GenerateComplexChain(sections, calls, missing) gives every section one AI response, a response with calls calls in the first, third, fifth… section and a completion in the others, answers every call, and then drops the first missing tool messages of the chain.
Message Chain Structure in LLM Providers
ChainAST is designed to work with message chains that follow common conventions in LLM providers:
Message Roles
- System: Provides context or instructions to the model
- Human/User: User input messages
- AI/Assistant: Model responses
- Tool: Results of tool calls executed by the system
Message Content
Messages can contain different types of content:
- TextContent: Simple text messages
- ToolCall: Function call requests from the model
- ToolCallResponse: Results returned from executing tools
Provider-Specific Requirements
Reasoning Signatures
Different LLM providers have specific requirements for reasoning content in function calls:
Gemini (Google AI)
Gemini requires thought signatures (thought_signature) for function calls, especially in multi-turn conversations with tool use. These signatures:
- Are cryptographic representations of the model's internal reasoning process
- Are strictly validated only for the current turn (defined as all messages after the last user message with text content)
- Must be preserved when summarizing content that contains them
- Can use fake signatures when creating summarized content:
"skip_thought_signature_validator"
Example:
// Check if original content had reasoning
hasReasoning := ContainsReasoning(originalMessages)
// Create summarized content with fake signature if needed
summaryPair := NewBodyPairFromSummarization(summaryText, tcIDTemplate, hasReasoning)Anthropic (Claude)
Anthropic uses extended thinking with cryptographic signatures that:
- Are automatically removed from previous turns (not counted in context window)
- Are only required for the current tool use loop
Kimi/Moonshot (OpenAI-compatible)
Kimi reasoning models require reasoning_content in TextContent before ToolCall:
- Reasoning must be present in a TextContent part before any ToolCall when thinking is enabled
- Error: "thinking is enabled but reasoning_content is missing in assistant tool call message"
- Use
ExtractReasoningMessage()to preserve reasoning TextContent when summarizing - Combine with fake ToolCall signatures for full multi-provider compatibility
Example structure:
AIMessage.Parts = [
TextContent{Text: "...", Reasoning: {Content: "..."}}, // Required by Kimi
ToolCall{..., Reasoning: {Signature: []byte("...")}}, // Required by Gemini
]Critical Rule: Never summarize the last body pair in a section, as this preserves reasoning signatures required by Gemini, Anthropic, and Kimi.
Helper Functions
// Check if messages contain reasoning signatures in ToolCall parts
// Returns true if any message contains Reasoning in ToolCall (NOT TextContent)
// This is specific to function calling scenarios which require thought_signature
hasToolCallReasoning := ContainsToolCallReasoning(messages)
// Extract reasoning message from AI messages
// Returns the first AI message with TextContent containing reasoning (or nil)
// Useful for preserving reasoning content for providers like Kimi (Moonshot)
reasoningMsg := ExtractReasoningMessage(messages)
// Create summarization with conditional fake signature and reasoning message
addFakeSignature := ContainsToolCallReasoning(originalMessages)
reasoningMsg := ExtractReasoningMessage(originalMessages)
summaryPair := NewBodyPairFromSummarization(summaryText, tcIDTemplate, addFakeSignature, reasoningMsg)Best Practices
- Validation First: Use
NewChainASTwithforce=falseto validate chains before processing - Defensive Programming: Always check for errors from ChainAST functions
- Complete Tool Calls: Ensure all tool calls have corresponding responses before sending to an LLM
- Section Management: Use sections to organize conversation turns logically
- Testing: Use the predefined chains and the generators of
fixtures_test.goto test the code of this package that manipulates message chains - Size Management: Leverage size tracking to maintain efficient context windows
- Reasoning Preservation:
- Use
ContainsToolCallReasoning()to check if fake signatures are needed (checks only ToolCall.Reasoning) - Use
ExtractReasoningMessage()to preserve reasoning TextContent for Kimi/Moonshot
- Use
- Last Pair Protection: Never summarize the last (most recent) body pair in a section to preserve reasoning signatures
- Multi-Provider Support: When summarizing for current turn, preserve both ToolCall and TextContent reasoning for maximum compatibility
Common Use Cases
1. Chain Validation and Repair
// Try to parse with strict validation
ast, err := NewChainAST(chain, false)
if err != nil {
// If validation fails, try with repair enabled
ast, err = NewChainAST(chain, true)
if err != nil {
// Handle severe structural errors
}
// Log that the chain was repaired
}2. Chain Summarization
// Create AST from chain
ast, _ := NewChainAST(chain, true)
// Analyze total size and section sizes
totalSize := ast.Size()
if totalSize > maxContextSize {
// Select sections to summarize
oldestSections := ast.Sections[:len(ast.Sections)-1] // Keep last section
// Summarize sections
summaryText := generateSummary(oldestSections)
// Create a new AST with the summary
newAST := &ChainAST{Sections: []*ChainSection{}}
// Copy system message if exists
var systemMsg *llms.MessageContent
if len(ast.Sections) > 0 && ast.Sections[0].Header.SystemMessage != nil {
systemMsgCopy := *ast.Sections[0].Header.SystemMessage
systemMsg = &systemMsgCopy
}
// Create header and section
header := NewHeader(systemMsg, nil)
section := NewChainSection(header, []*BodyPair{})
newAST.AddSection(section)
// Add summarization body pair
summaryPair := NewBodyPairFromSummarization(summaryText)
section.AddBodyPair(summaryPair)
// Copy the most recent section
lastSection := ast.Sections[len(ast.Sections)-1]
// Add appropriate logic to copy the last section
// Get the summarized chain
summarizedChain := newAST.Messages()
}3. Adding Tool Responses
// Parse a chain with tool calls
ast, _ := NewChainAST(chain, false)
// Find unresponded tool calls and add responses
for _, section := range ast.Sections {
for _, bodyPair := range section.Body {
if bodyPair.Type == RequestResponse {
for _, part := range bodyPair.AIMessage.Parts {
if toolCall, ok := part.(llms.ToolCall); ok {
// Execute the tool
result := executeToolCall(toolCall)
// Add the response
ast.AddToolResponse(toolCall.ID, toolCall.FunctionCall.Name, result)
}
}
}
}
}
// Get the updated chain
updatedChain := ast.Messages()4. Building a Conversation
// Create an empty AST
ast := &ChainAST{Sections: []*ChainSection{}}
// Add system message
sysMsg := &llms.MessageContent{
Role: llms.ChatMessageTypeSystem,
Parts: []llms.ContentPart{llms.TextContent{Text: "You are a helpful assistant"}},
}
header := NewHeader(sysMsg, nil)
section := NewChainSection(header, []*BodyPair{})
ast.AddSection(section)
// Add a human message
ast.AppendHumanMessage("Hello, how can you help me?")
// Add an AI response
aiMsg := &llms.MessageContent{
Role: llms.ChatMessageTypeAI,
Parts: []llms.ContentPart{llms.TextContent{Text: "I can answer questions, help with tasks, and more."}},
}
bodyPair := NewBodyPair(aiMsg, nil)
section.AddBodyPair(bodyPair)
// Continue the conversation
ast.AppendHumanMessage("Can you help me find information?")
// Get the message chain
chain := ast.Messages()5. Using Summarization in Conversation
// Create an empty AST
ast := &ChainAST{Sections: []*ChainSection{}}
// Create a new header with a system message
sysMsg := &llms.MessageContent{
Role: llms.ChatMessageTypeSystem,
Parts: []llms.ContentPart{llms.TextContent{Text: "You are a helpful assistant."}},
}
header := NewHeader(sysMsg, nil)
// Create a new section with the header
section := NewChainSection(header, []*BodyPair{})
ast.AddSection(section)
// Add a human message requesting a summary
ast.AppendHumanMessage("Can you summarize our discussion?")
// Create a summarization body pair
summaryPair := NewBodyPairFromSummarization("This is a summary of our previous conversation about weather and travel plans.")
section.AddBodyPair(summaryPair)
// Get the message chain
chain := ast.Messages()Example Usage
// Parse a conversation chain with summarization
ast, err := NewChainAST(conversationChain, true)
if err != nil {
log.Fatalf("Failed to parse chain: %v", err)
}
// Check if any body pairs are summarization pairs
for _, section := range ast.Sections {
for _, bodyPair := range section.Body {
if bodyPair.Type == Summarization {
fmt.Println("Found a summarization body pair")
// Extract the summary text from the tool response
for _, toolMsg := range bodyPair.ToolMessages {
for _, part := range toolMsg.Parts {
if resp, ok := part.(llms.ToolCallResponse); ok &&
resp.Name == SummarizationToolName {
fmt.Printf("Summary content: %s\n", resp.Content)
}
}
}
}
}
}Backend Docs Chain Summary (`backend/docs/chain_summary.md`)
Enhanced Chain Summarization Algorithm
Table of Contents
- Enhanced Chain Summarization Algorithm
Overview
The Enhanced Chain Summarization Algorithm manages context growth in conversation chains by selectively summarizing older message content while preserving recent interactions. The algorithm maintains conversation coherence by creating summarized body pairs rather than modifying existing messages. It uses configurable parameters to optimize context retention based on use cases and introduces byte-size tracking for precise content management.
Key features of the enhanced algorithm:
- Size-aware processing - Tracks byte size of all content to make optimal retention decisions
- Section summarization - Ensures all sections except the last
KeepQASectionsones consist of a header and a single body pair - Last section rotation - Intelligently manages active conversation sections with size limits
- QA pair summarization - Focuses on question-answer sections when enabled, preserving last
KeepQASectionssections unconditionally - Body pair type preservation - Maintains appropriate type for summarized content based on original types
- Keep QA Sections - Preserves a configurable number of recent QA sections without summarization, even if they exceed
MaxQABytes(critical for agent state preservation) - Concurrent processing - Uses goroutines for efficient parallel summarization of sections and body pairs
- Idempotent operation - Multiple consecutive calls do not modify already summarized content
- Last BodyPair protection - The last BodyPair in a section is never summarized to preserve reasoning signatures
Architectural Overview
flowchart TD
A[Input Message Chain] --> B[Convert to ChainAST]
B --> C{Empty chain or\nsingle section?}
C -->|Yes| O[Return Original Chain]
C -->|No| D[Apply Section Summarization]
D --> E{PreserveLast\nenabled?}
E -->|Yes| F[Apply Last Section Rotation]
E -->|No| G{UseQA\nenabled?}
F --> G
G -->|Yes| H[Apply QA Summarization]
G -->|No| I[Convert AST to Messages]
H --> I
I --> O[Return Summarized Chain]Fundamental Concepts
ChainAST Structure with Size Tracking
The algorithm operates on ChainAST structure from the pentagi/pkg/cast package that includes size tracking:
ChainAST
├── Sections[] (ChainSection)
├── Header
│ ├── SystemMessage (optional)
│ ├── HumanMessage (optional)
│ └── Size() method
├── Body[] (BodyPair)
│ ├── Type (Completion | RequestResponse | Summarization)
│ ├── AIMessage
│ ├── ToolMessages[] (for RequestResponse type)
│ └── Size() method
└── Size() methodclassDiagram
class ChainAST {
+Sections[] ChainSection
+Size() int
+Messages() []MessageContent
+AddSection(section)
}
class ChainSection {
+Header Header
+Body[] BodyPair
+Size() int
+Messages() []MessageContent
}
class Header {
+SystemMessage *MessageContent
+HumanMessage *MessageContent
+Size() int
+Messages() []MessageContent
}
class BodyPair {
+Type BodyPairType
+AIMessage *MessageContent
+ToolMessages[] MessageContent
+Size() int
+Messages() []MessageContent
}
class BodyPairType {
<<enumeration>>
Completion
RequestResponse
Summarization
}
ChainAST "1" *-- "*" ChainSection : contains
ChainSection "1" *-- "1" Header : has
ChainSection "1" *-- "*" BodyPair : contains
BodyPair --> BodyPairType : has typeEach component (ChainAST, ChainSection, Header, BodyPair) provides a Size() method that enables precise content management decisions. Size calculation is handled internally by the cast package and considers all content types including text, binary data, and images.
The body pair types are critical for understanding the structure:
- Completion: Contains a single AI message with text content
- RequestResponse: Contains an AI message with tool calls and corresponding tool response messages
- Summarization: Contains a summary of previous messages
The algorithm leverages the cast package's constructor methods to ensure proper size calculation:
// Creating components with automatic size calculation
header := cast.NewHeader(systemMsg, humanMsg) // New header with size tracking
section := cast.NewChainSection(header, bodyPairs) // New section with size tracking
summaryPair := cast.NewBodyPairFromCompletion(text) // New Completion pair with text content
summaryPair := cast.NewBodyPairFromSummarization(text) // New Summarization pair with text contentChainAST Construction Process
flowchart TD
A[Input MessageContent Array] --> B[Create Empty ChainAST]
B --> C[Process Messages Sequentially]
C --> D{Is System Message?}
D -->|Yes| E[Add to Current/New Section Header]
D -->|No| F{Is Human Message?}
F -->|Yes| G[Create New Section with Human Message in Header]
F -->|No| H{Is AI or Tool Message?}
H -->|Yes| I[Add to Current Section's Body]
H -->|No| J[Skip Message]
E --> C
G --> C
I --> C
J --> C
C --> K[Calculate Sizes for All Components]
K --> L[Return Populated ChainAST]The ChainAST construction process analyzes the roles and types of messages in the chain, grouping them into logical sections with headers and body pairs.
Tool Call ID Normalization
When switching between different LLM providers (e.g., from Gemini to Anthropic), tool call IDs may have different formats that are incompatible with the new provider's API. The NormalizeToolCallIDs method addresses this by validating and replacing incompatible IDs:
flowchart TD
A[ChainAST with Tool Calls] --> B[Iterate Through Sections]
B --> C{Has RequestResponse or\nSummarization?}
C -->|No| D[Skip Section]
C -->|Yes| E[Extract Tool Call IDs]
E --> F{Validate ID Against\nNew Template}
F -->|Valid| G[Keep Existing ID]
F -->|Invalid| H[Generate New ID]
H --> I[Create ID Mapping]
I --> J[Update Tool Call ID]
J --> K[Update Corresponding\nTool Response IDs]
G --> L[Continue to Next]
K --> L
D --> L
L --> M{More Sections?}
M -->|Yes| B
M -->|No| N[Return Normalized AST]The normalization process:
- Validates each tool call ID against the new provider's template using
ValidatePattern - Generates new IDs only for those that don't match the template
- Preserves IDs that already match to avoid unnecessary changes
- Updates both tool calls and their corresponding responses to maintain consistency
- Supports all body pair types: RequestResponse and Summarization
Example Usage:
// After restoring a chain that may contain tool calls from a different provider
ast, err := cast.NewChainAST(chain, true)
if err != nil {
return err
}
// Normalize to new provider's format (e.g., from "call_*" to "toolu_*")
err = ast.NormalizeToolCallIDs("toolu_{r:24:b}")
if err != nil {
return err
}
// Chain now has compatible tool call IDs
normalizedChain := ast.Messages()Template Format Examples:
| Provider | Template Format | Example ID |
|---|---|---|
| OpenAI/Gemini | call_{r:24:x} |
call_abc123def456ghi789jkl |
| Anthropic | toolu_{r:24:b} |
toolu_A1b2C3d4E5f6G7h8I9j0K1l2 |
| Custom | {prefix}_{r:N:charset} |
Defined per provider |
This feature is critical for assistant providers that may switch between different LLM providers while maintaining conversation history.
Reasoning Content Cleanup
When switching between providers, reasoning content must also be cleared because it contains provider-specific data:
flowchart TD
A[ChainAST with Reasoning] --> B[Iterate Through Sections]
B --> C[Process Header Messages]
C --> D[Clear SystemMessage Reasoning]
D --> E[Clear HumanMessage Reasoning]
E --> F[Process Body Pairs]
F --> G[Clear AI Message Reasoning]
G --> H[Clear Tool Messages Reasoning]
H --> I{More Sections?}
I -->|Yes| B
I -->|No| J[Return Cleaned AST]The cleanup process:
- Iterates through all sections, headers, and body pairs
- Clears
Reasoningfield fromTextContentparts - Clears
Reasoningfield fromToolCallparts - Preserves all other content (text, arguments, function names, etc.)
Why this is needed:
- Reasoning content includes cryptographic signatures (especially Anthropic's extended thinking)
- These signatures are validated by the provider and will fail if sent to a different provider
- Reasoning blocks may contain provider-specific metadata
Example Usage:
// After restoring and normalizing a chain
ast, err := cast.NewChainAST(chain, true)
if err != nil {
return err
}
// First normalize tool call IDs
err = ast.NormalizeToolCallIDs(newTemplate)
if err != nil {
return err
}
// Then clear provider-specific reasoning
err = ast.ClearReasoning()
if err != nil {
return err
}
// Chain is now safe to use with the new provider
cleanedChain := ast.Messages()What gets cleared:
TextContent.Reasoning- Extended thinking signatures and contentToolCall.Reasoning- Per-tool reasoning (used by some providers)
What stays preserved:
- All text content
- Tool call IDs (after normalization)
- Function names and arguments
- Tool responses
This operation is automatically performed in restoreChain() when switching providers, ensuring compatibility across different LLM providers.
Summarization Types
The algorithm supports three types of summarization:
- Section Summarization - Ensures all sections except the last N ones consist of a header and a single body pair
- Last Section Rotation - Manages size of the last (active) section by summarizing oldest pairs when size limits are exceeded
- QA Pair Summarization - Creates a summary section containing essential question-answer exchanges when enabled
Configuration Parameters
Summarization behavior is controlled through the SummarizerConfig structure:
type SummarizerConfig struct {
PreserveLast bool // Whether to manage the last section size
UseQA bool // Whether to use QA pair summarization
SummHumanInQA bool // Whether to summarize human messages in QA pairs
LastSecBytes int // Maximum byte size for last section
MaxBPBytes int // Maximum byte size for a single body pair
MaxQASections int // Maximum QA pair sections to preserve
MaxQABytes int // Maximum byte size for QA pair sections
KeepQASections int // Number of recent QA sections to keep without summarization
}These parameters have default values defined as constants:
| Parameter | Field in SummarizerConfig | Default Constant | Default Value | Description |
|---|---|---|---|---|
| Max last section size | LastSecBytes |
maxLastSectionByteSize |
50 KB | Maximum size for the last section |
| Max single body pair size | MaxBPBytes |
maxSingleBodyPairByteSize |
16 KB | Maximum size for a single body pair |
| Max QA sections | MaxQASections |
maxQAPairSections |
10 | Maximum QA sections to keep |
| Max QA byte size | MaxQABytes |
maxQAPairByteSize |
64 KB | Maximum size for QA sections |
| Last section reserve percentage | N/A | lastSectionReservePercentage |
25% | Percentage of section size to reserve for future messages |
| Keep QA sections | KeepQASections |
keepMinLastQASections |
1 | Number of most recent QA sections to preserve without summarization, even if they exceed MaxQABytes |
Algorithm Operation
The enhanced algorithm operates in these sequential phases:
- Convert input chain to ChainAST with size tracking
- Apply section summarization to all sections except the last
KeepQASectionssections (with concurrent processing) - Apply last section rotation to multiple recent sections if enabled and size limits are exceeded
- Apply QA pair summarization if enabled and limits are exceeded, preserving the last
KeepQASectionssections - Return the modified chain if it saves space
Critical Guarantees:
- The last
KeepQASectionssections are NEVER summarized by section or QA summarization, even if they exceedMaxQABytes - The last BodyPair in a section is NEVER summarized by
summarizeOversizedBodyPairsorsummarizeLastSectionto preserve reasoning signatures - Idempotent: calling
SummarizeChainmultiple times on already summarized content does not change it further
The primary algorithm is implemented through the Summarizer interface in the pentagi/pkg/csum package:
// Summarizer interface for chain summarization
type Summarizer interface {
SummarizeChain(
ctx context.Context,
handler tools.SummarizeHandler,
chain []llms.MessageContent,
) ([]llms.MessageContent, error)
}
// Implementation is created using the NewSummarizer constructor
func NewSummarizer(config SummarizerConfig) Summarizer {
// Sets defaults if not specified
if config.PreserveLast {
if config.LastSecBytes <= 0 {
config.LastSecBytes = maxLastSectionByteSize
}
}
if config.UseQA {
if config.MaxQASections <= 0 {
config.MaxQASections = maxQAPairSections
}
if config.MaxQABytes <= 0 {
config.MaxQABytes = maxQAPairByteSize
}
}
if config.MaxBPBytes <= 0 {
config.MaxBPBytes = maxSingleBodyPairByteSize
}
if config.KeepQASections <= 0 {
config.KeepQASections = keepMinLastQASections
}
return &summarizer{config: config}
}The main algorithm flow:
/* Detailed source-code truncated for AI context efficiency. */Key Algorithm Components
1. Section Summarization
For all sections except the last KeepQASections sections, ensure they consist of a header and a single body pair:
flowchart TD
A[For each section except last KeepQASections] --> B{Has single body pair that\nis already summarized?}
B -->|Yes| A
B -->|No| C[Collect all messages from body pairs]
C --> D[Add human message if it exists]
D --> E[Start concurrent goroutine for summary generation]
E --> F[Determine appropriate body pair type]
F --> G[Create new body pair with summary]
G --> H[Replace all body pairs with summary pair]
H --> I[Wait for all goroutines to complete]
I --> J[Check for any errors from parallel processing]
J --> A
A --> K[Return updated AST]/* Detailed source-code truncated for AI context efficiency. */The determineTypeToSummarizedSection function decides which type to use for the summarized content based on the original section's body pair types:
// determineTypeToSummarizedSection determines the type of each body pair to summarize
// based on the type of the body pairs in the section
// if all body pairs are Completion, return Completion, otherwise return Summarization
func determineTypeToSummarizedSection(section *cast.ChainSection) cast.BodyPairType {
summarizedType := cast.Completion
for _, pair := range section.Body {
if pair.Type == cast.Summarization || pair.Type == cast.RequestResponse {
summarizedType = cast.Summarization
break
}
}
return summarizedType
}Reasoning Signature Handling
When summarizing content that originally contained reasoning, the algorithm:
- Detects ToolCall Reasoning: Uses
cast.ContainsToolCallReasoning()to check if messages contain reasoning in ToolCall parts (NOT TextContent) - Extracts TextContent Reasoning: Uses
cast.ExtractReasoningMessage()to preserve reasoning TextContent for providers like Kimi/Moonshot - Adds Fake Signatures: Adds a fake signature to the summarized ToolCall if ToolCall reasoning was present
- Preserves Reasoning Message: Prepends reasoning TextContent before ToolCall in the summarized content
- Provider Compatibility: Ensures the summarized chain remains compatible with all provider APIs
// Check if the original pair contained reasoning signatures in ToolCall parts
addFakeSignature := cast.ContainsToolCallReasoning(pairMessages)
// Extract reasoning message for Kimi/Moonshot compatibility
reasoningMsg := cast.ExtractReasoningMessage(pairMessages)
// Create summarization with conditional fake signature AND preserved reasoning
bodyPairsSummarized[i] = cast.NewBodyPairFromSummarization(summaryText, tcIDTemplate, addFakeSignature, reasoningMsg)Why this is needed:
- Gemini: Validates
thought_signaturepresence for function calls in the current turn. Removing signatures would cause 400 errors: "Function call is missing a thought_signature". Fake signatures satisfy the API validation. - Kimi/Moonshot: Requires
reasoning_contentin TextContent before ToolCall when thinking is enabled. Without it: "thinking is enabled but reasoning_content is missing". Preserving reasoning message satisfies this requirement. - Anthropic: Extended thinking with cryptographic signatures, automatically removed from previous turns.
Important: This reasoning preservation is only applied to current turn (last section). Previous turns are summarized without fake signatures to save tokens, as they are not validated by provider APIs.
2. Individual Body Pair Size Management
Before handling the overall last section size, manage individual oversized body pairs:
CRITICAL PRESERVATION RULES:
Never Summarize Last Pair: The last (most recent) body pair in a section is NEVER summarized to preserve reasoning signatures required by providers like Gemini (thought_signature) and Anthropic (cryptographic signatures). Summarizing the last pair would remove these signatures and cause API errors.
Preserve Reasoning Requirements: When summarizing body pairs that contain reasoning signatures:
- The algorithm checks if the original content contained reasoning using
cast.ContainsReasoning() - If reasoning was present, a fake signature is added to the summarized content
- For Gemini: uses
"skip_thought_signature_validator" - This ensures API compatibility when the chain continues with the same provider
- The algorithm checks if the original content contained reasoning using
flowchart TD
A[Start with Section's Body Pairs] --> B[Initialize concurrent processing]
B --> C[For each body pair]
C --> CA{Is this the\nlast body pair?}
CA -->|Yes| CB[SKIP - Never summarize last pair]
CA -->|No| D{Is pair oversized AND\nnot already summarized?}
CB --> C
D -->|Yes| E[Start goroutine for pair processing]
D -->|No| C
E --> F[Get messages from pair]
F --> G[Add human message if exists]
G --> H[Generate summary]
H --> I{Was summary generation successful?}
I -->|No| J[Skip this pair - handled by next step]
I -->|Yes| K{What is the original pair type?}
K -->|RequestResponse| L[Create Summarization pair]
K -->|Other| M[Create Completion pair]
L --> N[Add to modified pairs map with mutex]
M --> N
N --> O[Wait for all goroutines to complete]
O --> P{Any pairs summarized?}
P -->|Yes| Q[Update section with new pairs]
P -->|No| R[Return unchanged]
Q --> S[Return updated section]/* Detailed source-code truncated for AI context efficiency. */3. Last Section Rotation
For the specified section (which can be any of the last N sections in the active conversation), when it exceeds size limits:
CRITICAL PRESERVATION RULE: The last (most recent) body pair in a section is ALWAYS kept without summarization. This ensures:
- Reasoning signatures (Gemini's thought_signature, Anthropic's cryptographic signatures) are preserved
- Latest tool calls maintain their complete context including thinking content
- API compatibility when the chain continues with the same provider
flowchart TD
A[Get Specified Section by Index] --> B[Summarize Oversized Individual Pairs]
B --> C{Is section size still\nexceeding limit?}
C -->|No| Z[Return Unchanged]
C -->|Yes| D[Determine which pairs to keep vs. summarize]
D --> E{Any pairs to summarize?}
E -->|No| Z
E -->|Yes| F[Collect messages from pairs to summarize]
F --> G[Add human message if it exists]
G --> H[Generate summary text]
H --> I{Summary generation successful?}
I -->|No| J[Keep only recent pairs]
I -->|Yes| K[Determine type for summary pair]
K --> L[Create appropriate body pair]
L --> M[Create new body with summary pair first]
M --> N[Add kept pairs after summary]
N --> O[Create new section with updated body]
O --> P[Update specified section in AST]
J --> P
P --> Z[Return Updated AST]/* Detailed source-code truncated for AI context efficiency. */The determineLastSectionPairs function is a critical piece that decides which pairs to keep and which to summarize:
flowchart TD
A[Start with Section] --> B[Calculate header size]
B --> C{Are there any body pairs?}
C -->|No| D[Return empty arrays]
C -->|Yes| E[Always keep last pair]
E --> F[Calculate threshold with reserve]
F --> G[Process remaining pairs in reverse order]
G --> H{Would pair fit within threshold?}
H -->|Yes| I[Add to pairs to keep]
H -->|No| J[Add to pairs to summarize]
I --> G
J --> G
G --> K[Return pairs to keep and summarize]// determineLastSectionPairs splits the last section's pairs into those to keep and those to summarize
func determineLastSectionPairs(
section *cast.ChainSection,
maxBytes int,
reservePercent int,
) ([]*cast.BodyPair, []*cast.BodyPair) {
// Implementation details...
// Returns two slices: pairsToKeep and pairsToSummarize
}4. QA Pair Management
When QA pair summarization is enabled, the algorithm preserves the last KeepQASections sections without summarization, even if they exceed MaxQABytes. This ensures that:
- Recent reasoning blocks are preserved for AI agent continuation
- Tool calls in the most recent sections maintain their full context
- Agent state remains intact for multi-turn conversations
flowchart TD
A[Check limits] --> B{Do QA sections exceed limits?}
B -->|No| Z[Return unchanged]
B -->|Yes| C[Prepare sections for summarization]
C --> D{Any human/AI messages to summarize?}
D -->|No| Z
D -->|Yes| E{Human messages exist?}
E -->|Yes| F{Summarize human messages?}
F -->|Yes| G[Generate human summary]
F -->|No| H[Concatenate human messages]
E -->|No| I[No human message needed]
G --> J[Generate AI summary]
H --> J
I --> J
J --> K[Determine summary pair type]
K --> L[Create new AST with summary section]
L --> M[Add system message if it exists]
M --> N[Create summary section header]
N --> O[Create summary section with summary pair]
O --> P[Determine how many recent sections to keep]
P --> Q[Add those sections to new AST]
Q --> R[Replace original sections with new ones]
R --> Z/* Detailed source-code truncated for AI context efficiency. */Summary Generation
The algorithm uses a GenerateSummary function to create summaries:
flowchart TD
A[GenerateSummary Called] --> B{Handler is nil?}
B -->|Yes| C[Return error]
B -->|No| D{No messages to summarize?}
D -->|Yes| C
D -->|No| E[Convert messages to prompt]
E --> F[Call handler to generate summary]
F --> G{Handler error?}
G -->|Yes| C
G -->|No| H[Return summary text]// GenerateSummary generates a summary of the provided messages
func GenerateSummary(
ctx context.Context,
handler tools.SummarizeHandler,
humanMessages []llms.MessageContent,
aiMessages []llms.MessageContent,
) (string, error) {
if handler == nil {
return "", fmt.Errorf("summarizer handler cannot be nil")
}
if len(humanMessages) == 0 && len(aiMessages) == 0 {
return "", fmt.Errorf("cannot summarize empty message list")
}
// Convert messages to text format optimized for summarization
text := messagesToPrompt(humanMessages, aiMessages)
// Generate the summary using provided summarizer handler
summary, err := handler(ctx, text)
if err != nil {
return "", fmt.Errorf("summarization failed: %w", err)
}
return summary, nil
}The messagesToPrompt function handles different summarization scenarios:
flowchart TD
A[messagesToPrompt] --> B[Convert human messages to text]
A --> C[Convert AI messages to text]
B --> D{Both human and AI messages exist?}
C --> D
D -->|Yes| E[Use Case 1: Human as context for AI]
D -->|No| F{Only AI messages?}
F -->|Yes| G[Use Case 2: AI without context]
F -->|No| H{Only human messages?}
H -->|Yes| I[Use Case 3: Human as instructions]
H -->|No| J[Return empty string]
E --> K[Format with appropriate instructions]
G --> K
I --> K
K --> L[Return formatted prompt]// messagesToPrompt converts a slice of messages to a text representation
func messagesToPrompt(humanMessages []llms.MessageContent, aiMessages []llms.MessageContent) string {
var buffer strings.Builder
humanMessagesText := humanMessagesToText(humanMessages)
aiMessagesText := aiMessagesToText(aiMessages)
// Different cases based on available messages
// case 1: use human messages as a context for ai messages
if len(humanMessages) > 0 && len(aiMessages) > 0 {
instructions := getSummarizationInstructions(1)
buffer.WriteString(fmt.Sprintf("<instructions>%s</instructions>\n\n", instructions))
buffer.WriteString(humanMessagesText)
buffer.WriteString(aiMessagesText)
}
// case 2: use ai messages as a content to summarize without context
if len(aiMessages) > 0 && len(humanMessages) == 0 {
instructions := getSummarizationInstructions(2)
buffer.WriteString(fmt.Sprintf("<instructions>%s</instructions>\n\n", instructions))
buffer.WriteString(aiMessagesText)
}
// case 3: use human messages as a instructions to summarize them
if len(humanMessages) > 0 && len(aiMessages) == 0 {
instructions := getSummarizationInstructions(3)
buffer.WriteString(fmt.Sprintf("<instructions>%s</instructions>\n\n", instructions))
buffer.WriteString(humanMessagesText)
}
return buffer.String()
}The algorithm includes detailed instructions for each summarization scenario through the getSummarizationInstructions function, which ensures appropriate summaries for different contexts.
Helper Functions
The algorithm includes several important helper functions that support the summarization process:
Content Detection Functions
// containsSummarizedContent checks if a body pair contains summarized content
// Local helper function to avoid naming conflicts with test utilities
func containsSummarizedContent(pair *cast.BodyPair) bool {
if pair == nil {
return false
}
switch pair.Type {
case cast.Summarization:
return true
case cast.RequestResponse:
return false
case cast.Completion:
if pair.AIMessage == nil || len(pair.AIMessage.Parts) == 0 {
return false
}
textContent, ok := pair.AIMessage.Parts[0].(llms.TextContent)
if !ok {
return false
}
if strings.HasPrefix(textContent.Text, SummarizedContentPrefix) {
return true
}
return false
default:
return false
}
}This function is crucial for:
- Avoiding double summarization: Prevents already summarized content from being summarized again
- Type-aware detection: Handles different body pair types appropriately
- Content prefix detection: Recognizes summarized content by checking for the
SummarizedContentPrefixmarker - Robust checking: Safely handles nil values and missing content
The function replaces the previous logic that only checked for cast.Summarization type, providing more comprehensive detection of summarized content across all body pair types.
Code Architecture
classDiagram
class SummarizerConfig {
+bool PreserveLast
+bool UseQA
+bool SummHumanInQA
+int LastSecBytes
+int MaxBPBytes
+int MaxQASections
+int MaxQABytes
+int KeepQASections
}
class Summarizer {
<<interface>>
+SummarizeChain(ctx, handler, chain) []llms.MessageContent, error
}
class summarizer {
-config SummarizerConfig
+SummarizeChain(ctx, handler, chain) []llms.MessageContent, error
}
class SummarizeHandler {
<<function>>
+invoke(ctx, text) string, error
}
class tools.SummarizeHandler {
<<function>>
+invoke(ctx, text) string, error
}
Summarizer <|.. summarizer : implements
summarizer -- SummarizerConfig : uses
summarizer -- SummarizeHandler : calls
SummarizeHandler -- tools.SummarizeHandler : aliasThe algorithm is implemented through the Summarizer interface in the pentagi/pkg/csum package, which provides the SummarizeChain method. The implementation leverages the ChainAST structure from the pentagi/pkg/cast package for managing the chain structure.
Full Process Overview
/* Detailed source-code truncated for AI context efficiency. */Usage Example
// Create a summarizer with custom configuration
config := csum.SummarizerConfig{
PreserveLast: true,
LastSecBytes: 40 * 1024,
MaxBPBytes: 16 * 1024,
UseQA: true,
MaxQASections: 5,
MaxQABytes: 30 * 1024,
SummHumanInQA: false,
KeepQASections: 2,
}
summarizer := csum.NewSummarizer(config)
// Define a summary handler function
summaryHandler := func(ctx context.Context, text string) (string, error) {
// Use your preferred LLM or summarization method here
return llmClient.Summarize(ctx, text)
}
// Apply summarization to a message chain
newChain, err := summarizer.SummarizeChain(ctx, summaryHandler, originalChain)
if err != nil {
log.Fatalf("Failed to summarize chain: %v", err)
}
// Use the summarized chain
for _, msg := range newChain {
fmt.Printf("[%s] %s\n", msg.Role, getMessageText(msg))
}Edge Cases and Handling
| Edge Case | Handling Strategy |
|---|---|
| Empty chain | Return unchanged immediately without processing |
| Very short chains | Return unchanged after section count check |
| Single section chains | Return unchanged after section count check |
| Empty sections to process | Skip summarization |
| Last section over size limit | Create a new section with summary pair followed by recent pairs |
| QA pairs over limit | Create summary section and keep most recent sections |
| KeepQASections larger than number of sections | No summarization performed, preserves all sections |
| Last KeepQASections sections exceed MaxQABytes | Sections are kept anyway to preserve reasoning and agent state |
| Summary generation fails | Keep the most recent content and log the error |
| Chain with already summarized content | Detected during processing and handled appropriately (idempotent) |
| Multiple consecutive summarization calls | Idempotent - no changes after first summarization |
Performance Considerations
Token Efficiency
- Summarization creates body pairs that reduce overall token count
- Size-aware decisions prevent context growth while maintaining conversation coherence
- Multiple last section rotation prevents unbounded growth in active conversations
- Individual oversized pair handling prevents single large pairs from affecting summarization decisions
- KeepQASections parameter preserves recent context while summarizing older content
Memory Efficiency
- Leverages cast package's size tracking for precise memory management
- Creates new components only when needed (using constructors)
- Uses Messages() methods to extract content without duplication
Processing Optimization
- Concurrent Processing: Uses goroutines for parallel summarization of sections and body pairs, significantly improving performance for large chains
- Error Handling: Robust error collection and handling from parallel operations using channels and error joining
- Short-circuit logic avoids unnecessary processing for simple chains
- Handles empty or single-section chains efficiently
- Uses built-in size tracking methods rather than recalculating sizes
- Selective summarization with KeepQASections avoids redundant processing
- Multiple Last Sections: Processes multiple recent sections in sequence for better active conversation management
Limitations
Semantic Coherence
- Quality of summaries depends entirely on the provided summarizer handler
- Summarized content may lose detailed reasoning or discussion context
Content Processing
- Binary and image content has size tracked but content isn't semantically analyzed
- Tool calls and responses are included in text representation for summarization
Implementation Considerations
- Depends on ChainAST's accuracy for section and message management
- API changes in the cast package may require updates to summarization code
- KeepQASections parameter may need balancing between context preservation and token efficiency
Backend Docs Charm (`backend/docs/charm.md`)
Charm.sh Ecosystem - Personal Cheat Sheet
Personal reference for building TUI applications with the Charm stack.
📦 **Core Libraries Overview**
Core Packages
bubbletea: Event-driven TUI framework (MVU pattern)lipgloss: Styling and layout enginebubbles: Pre-built components (viewport, textinput, etc.)huh: Advanced form builderglamour: Markdown renderer
🫧 **BubbleTea (MVU Pattern)**
Model-View-Update Lifecycle
// Model holds all state
type Model struct {
content string
ready bool
}
// Update handles events and returns new state
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
// Handle resize
return m, nil
case tea.KeyMsg:
// Handle keyboard input
return m, nil
}
return m, nil
}
// View renders current state
func (m Model) View() string {
return "content"
}
// Init returns initial command
func (m Model) Init() tea.Cmd {
return nil
}Commands and Messages
// Commands return future messages
func loadDataCmd() tea.Msg {
return DataLoadedMsg{data: "loaded"}
}
// Async operations
return m, tea.Cmd(func() tea.Msg {
time.Sleep(time.Second)
return TimerMsg{}
})Critical Patterns
// Model interface implementation
type Model struct {
styles *styles.Styles // ALWAYS use shared styles
}
func (m Model) Init() tea.Cmd {
// ALWAYS reset state completely
m.content = ""
m.ready = false
return m.loadContent
}
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
// NEVER store dimensions in model - use styles.SetSize()
// Model gets dimensions via m.styles.GetSize()
case tea.KeyMsg:
switch msg.String() {
case "enter": return m, navigateCmd
}
}
}🎨 **Lipgloss (Styling & Layout)**
Purpose: CSS-like styling for terminal interfaces
Key Insight: Height() vs MaxHeight() behavior difference!
Critical Height Control
// ❌ WRONG: Height() sets MINIMUM height (can expand!)
style := lipgloss.NewStyle().Height(1).Border(lipgloss.NormalBorder())
// ✅ CORRECT: MaxHeight() + Inline() for EXACT height
style := lipgloss.NewStyle().MaxHeight(1).Inline(true)
// ✅ PRODUCTION: Background approach for consistent 1-line footers
footer := lipgloss.NewStyle().
Width(width).
Background(borderColor).
Foreground(textColor).
Padding(0, 1, 0, 1). // Only horizontal padding
Render(text)
// FOOTER APPROACH - PRODUCTION READY (✅ PROVEN SOLUTION)
// ❌ WRONG: Border approach (inconsistent height)
style.BorderTop(true).Height(1)
// ✅ CORRECT: Background approach (always 1 line)
style.Background(color).Foreground(textColor).Padding(0,1,0,1)Layout Patterns
// LAYOUT COMPOSITION
lipgloss.JoinVertical(lipgloss.Left, header, content, footer)
lipgloss.JoinHorizontal(lipgloss.Top, left, right)
lipgloss.Place(width, height, lipgloss.Center, lipgloss.Top, content)
// Horizontal layout
left := lipgloss.NewStyle().Width(leftWidth).Render(leftContent)
right := lipgloss.NewStyle().Width(rightWidth).Render(rightContent)
combined := lipgloss.JoinHorizontal(lipgloss.Top, left, right)
// Vertical layout with consistent spacing
sections := []string{header, content, footer}
combined := lipgloss.JoinVertical(lipgloss.Left, sections...)
// Centering content
centered := lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, content)
// Responsive design
verticalStyle := lipgloss.NewStyle().Width(width).Padding(0, 2, 0, 2)
if width < 80 {
// Vertical layout for narrow screens
}Responsive Patterns
// Breakpoint-based layout
width, height := m.styles.GetSize() // ALWAYS from styles
if width < 80 {
return lipgloss.JoinVertical(lipgloss.Left, panels...)
} else {
return lipgloss.JoinHorizontal(lipgloss.Top, panels...)
}
// Dynamic width allocation
leftWidth := width / 3
rightWidth := width - leftWidth - 4📺 **Bubbles (Interactive Components)**
Purpose: Pre-built interactive components
Key Components: viewport, textinput, list, table
Viewport - Critical for Scrolling
import "github.com/charmbracelet/bubbles/viewport"
// Setup
viewport := viewport.New(width, height)
viewport.Style = lipgloss.NewStyle() // Clean style prevents conflicts
// Modern scroll methods (use these!)
viewport.ScrollUp(1) // Replaces LineUp()
viewport.ScrollDown(1) // Replaces LineDown()
viewport.ScrollLeft(2) // Horizontal, 2 steps for forms
viewport.ScrollRight(2)
// Deprecated (avoid)
vp.LineUp(lines) // ❌ Deprecated
vp.LineDown(lines) // ❌ Deprecated
// Status tracking
viewport.ScrollPercent() // 0.0 to 1.0
viewport.AtBottom() // bool
viewport.AtTop() // bool
// State checking
isScrollable := !(vp.AtTop() && vp.AtBottom())
progress := vp.ScrollPercent()
// Content management
viewport.SetContent(content)
viewport.View() // Renders visible portion
// Update in message handling
var cmd tea.Cmd
m.viewport, cmd = m.viewport.Update(msg)TextInput
import "github.com/charmbracelet/bubbles/textinput"
ti := textinput.New()
ti.Placeholder = "Enter text..."
ti.Focus()
ti.EchoMode = textinput.EchoPassword // For masked input
ti.CharLimit = 100📝 **Huh (Forms)**
Purpose: Advanced form builder for complex user input
import "github.com/charmbracelet/huh"
form := huh.NewForm(
huh.NewGroup(
huh.NewInput().
Key("api_key").
Title("API Key").
Password(). // Masked input
Validate(func(s string) error {
if len(s) < 10 {
return errors.New("API key too short")
}
return nil
}),
huh.NewSelect[string]().
Key("provider").
Title("Provider").
Options(
huh.NewOption("OpenAI", "openai"),
huh.NewOption("Anthropic", "anthropic"),
),
),
)
// Integration with bubbletea
func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var cmd tea.Cmd
m.form, cmd = m.form.Update(msg)
if m.form.State == huh.StateCompleted {
// Form submitted - access values
apiKey := m.form.GetString("api_key")
provider := m.form.GetString("provider")
}
return m, cmd
}✨ **Glamour (Markdown Rendering)**
Purpose: Beautiful markdown rendering in terminal
CRITICAL: Create renderer ONCE in styles.New(), reuse everywhere
// ✅ CORRECT: Single renderer instance (prevents freezing)
// styles.go
type Styles struct {
renderer *glamour.TermRenderer
}
func New() *Styles {
renderer, _ := glamour.NewTermRenderer(
glamour.WithAutoStyle(),
glamour.WithWordWrap(80),
)
return &Styles{renderer: renderer}
}
func (s *Styles) GetRenderer() *glamour.TermRenderer {
return s.renderer
}
// Usage in models
rendered, err := m.styles.GetRenderer().Render(markdown)
// ❌ WRONG: Creating new renderer each time (can freeze!)
renderer, _ := glamour.NewTermRenderer(...)Safe Rendering with Fallback
// Safe rendering with fallback
rendered, err := renderer.Render(content)
if err != nil {
// Fallback to plain text
rendered = fmt.Sprintf("# Content\n\n%s\n\n*Render error: %v*", content, err)
}🏗️ **Production Architecture Patterns**
1. Centralized Styles & Dimensions
CRITICAL: Never store width/height in models - use styles singleton
// ✅ CORRECT: Centralized in styles
type Styles struct {
width int
height int
renderer *glamour.TermRenderer
// ... all styles
}
func (s *Styles) SetSize(width, height int) {
s.width = width
s.height = height
s.updateStyles() // Recalculate responsive styles
}
func (s *Styles) GetSize() (int, int) {
return s.width, s.height
}
// Models use styles for dimensions
func (m *Model) updateViewport() {
width, height := m.styles.GetSize()
if width <= 0 || height <= 0 {
return // Graceful handling
}
// ... viewport setup
}2. TUI-Safe Logging System
Problem: fmt.Printf breaks TUI rendering
Solution: File-based logger
// logger.Log() writes to log.json
logger.Log("[Component] ACTION: details %v", value)
logger.Errorf("[Component] ERROR: %v", err)
// Development monitoring (separate terminal)
tail -f log.json
// ❌ WRONG: Console output in TUI
fmt.Printf("Debug: %v\n", value) // Breaks rendering3. Unified Header/Footer Management
// app.go - Central layout control
func (a *App) View() string {
header := a.renderHeader()
footer := a.renderFooter()
content := a.currentModel.View()
contentHeight := max(height - headerHeight - footerHeight, 0)
contentArea := a.styles.Content.Height(contentHeight).Render(content)
return lipgloss.JoinVertical(lipgloss.Left, header, contentArea, footer)
}
func (a *App) renderHeader() string {
switch a.navigator.Current() {
case WelcomeScreen:
return a.styles.RenderASCIILogo()
default:
return a.styles.Header.Render(title)
}
}
func (a *App) renderFooter() string {
actions := []string{}
// Dynamic actions based on screen state
if canContinue {
actions = append(actions, "Enter: Continue")
}
if hasScrollableContent {
actions = append(actions, "↑↓: Scroll")
}
return lipgloss.NewStyle().
Width(width).
Background(borderColor).
Foreground(textColor).
Padding(0, 1, 0, 1).
Render(strings.Join(actions, " • "))
}
// locale.go - Helper functions
func BuildCommonActions() []string {
return []string{NavBack, NavExit}
}
func BuildEULAActions(atEnd bool) []string {
if !atEnd {
return []string{EULANavScrollInstructions}
}
return []string{EULANavAcceptReject}
}
// Usage
actions := locale.BuildCommonActions()
actions = append(actions, specificActions...)4. Type-Safe Navigation with Composite ScreenIDs
Critical Pattern: Use typed screen IDs with argument support
type ScreenID string
const (
WelcomeScreen ScreenID = "welcome"
EULAScreen ScreenID = "eula"
MainMenuScreen ScreenID = "main_menu"
LLMProviderFormScreen ScreenID = "llm_provider_form"
)
// ScreenID methods for composite support
func (s ScreenID) GetScreen() string {
parts := strings.Split(string(s), "§")
return parts[0]
}
func (s ScreenID) GetArgs() []string {
parts := strings.Split(string(s), "§")
if len(parts) <= 1 {
return []string{}
}
return parts[1:]
}
func CreateScreenID(screen string, args ...string) ScreenID {
if len(args) == 0 {
return ScreenID(screen)
}
parts := append([]string{screen}, args...)
return ScreenID(strings.Join(parts, "§"))
}
type NavigationMsg struct {
Target ScreenID // Can be simple or composite!
GoBack bool
}
// Usage - Simple screen
return m, func() tea.Msg {
return NavigationMsg{Target: EULAScreen}
}
// Usage - Composite screen with arguments
return m, func() tea.Msg {
return NavigationMsg{Target: CreateScreenID("llm_provider_form", "openai")}
}5. Model State Management
Pattern: Complete reset on Init() for predictable behavior
func (m *Model) Init() tea.Cmd {
logger.Log("[Model] INIT")
// ALWAYS reset ALL state
m.content = ""
m.ready = false
m.scrolled = false
m.scrolledToEnd = false
m.error = nil
return m.loadContent
}
// Force re-render after async operations
func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case ContentLoadedMsg:
m.content = msg.Content
m.ready = true
// Force view update
return m, func() tea.Msg { return nil }
}
return m, nil
}🐛 **Key Debugging Techniques**
1. TUI-Safe Debug Output
// ❌ NEVER: Breaks TUI rendering
fmt.Println("debug")
log.Println("debug")
// ✅ ALWAYS: File-based logging
logger.Log("[Component] Event: %v", msg)
logger.Log("[Model] UPDATE: key=%s", msg.String())
logger.Log("[Model] VIEWPORT: %dx%d ready=%v", width, height, m.ready)
// Monitor in separate terminal
tail -f log.json2. Dimension Handling
func (m Model) View() string {
width, height := m.styles.GetSize()
if width <= 0 || height <= 0 {
return "Loading..." // Graceful fallback
}
// ... normal rendering
}
// Log dimension changes
logger.Log("[Model] RESIZE: %dx%d", width, height)3. Content Loading Debug
func (m *Model) loadContent() tea.Msg {
logger.Log("[Model] LOAD: start")
content, err := source.GetContent()
if err != nil {
logger.Errorf("[Model] LOAD: error: %v", err)
return ErrorMsg{err}
}
logger.Log("[Model] LOAD: success (%d chars)", len(content))
return ContentLoadedMsg{content}
}🎯 **Advanced Navigation with Composite ScreenIDs**
Composite ScreenID Pattern
Problem: Need to pass parameters to screens (e.g., which provider to configure)
Solution: Composite ScreenIDs with § separator
// Format: "screen§arg1§arg2§..."
type ScreenID string
// Methods for parsing composite IDs
func (s ScreenID) GetScreen() string {
parts := strings.Split(string(s), "§")
return parts[0]
}
func (s ScreenID) GetArgs() []string {
parts := strings.Split(string(s), "§")
if len(parts) <= 1 {
return []string{}
}
return parts[1:]
}
// Helper for creating composite IDs
func CreateScreenID(screen string, args ...string) ScreenID {
if len(args) == 0 {
return ScreenID(screen)
}
parts := append([]string{screen}, args...)
return ScreenID(strings.Join(parts, "§"))
}Usage Examples
// Simple screen (no arguments)
welcome := WelcomeScreen // "welcome"
// Composite screen (with arguments)
providerForm := CreateScreenID("llm_provider_form", "openai") // "llm_provider_form§openai"
// Navigation with arguments
return m, func() tea.Msg {
return NavigationMsg{
Target: CreateScreenID("llm_provider_form", "anthropic"),
Data: FormData{ProviderID: "anthropic"},
}
}
// In createModelForScreen - extract arguments
func (a *App) createModelForScreen(screenID ScreenID, data any) tea.Model {
baseScreen := screenID.GetScreen()
args := screenID.GetArgs()
switch ScreenID(baseScreen) {
case LLMProviderFormScreen:
providerID := "openai" // default
if len(args) > 0 {
providerID = args[0]
}
return NewLLMProviderFormModel(providerID, ...)
}
}State Persistence
// Stack automatically preserves composite IDs
navigator.Push(CreateScreenID("llm_provider_form", "gemini"))
// State contains: ["welcome", "main_menu", "llm_providers", "llm_provider_form§gemini"]
// On restore: user returns to Gemini provider form, not default OpenAI🎯 **Advanced Form Scrolling with Viewport**
Auto-Scrolling Forms Pattern
Problem: Forms with many fields don't fit on smaller terminals, focused fields go off-screen
Solution: Viewport component with automatic scroll-to-focus behavior
/* Detailed source-code truncated for AI context efficiency. */Key Benefits of Viewport Forms
- Automatic Clipping: Viewport handles content that exceeds available space
- Smooth Scrolling: Fields slide into view without jarring jumps
- Focus Preservation: Focused field always remains visible
- No Extra Hotkeys: Uses standard navigation (Tab, arrows)
- Terminal Friendly: Works on any terminal size
Critical Implementation Details
- Field Height Tracking: Must calculate actual rendered height of each field
- Scroll Timing: Call
ensureFocusVisible()after every focus change - Content Updates: Re-render form content when input values change
- Viewport Sizing: Account for padding, headers, footers in size calculation
This pattern is essential for professional TUI applications with complex forms.
⚠️ **Common Pitfalls & Solutions**
1. Glamour Renderer Freezing
Problem: Creating new renderer instances can freeze
Solution: Single shared renderer in styles.New()
// ❌ WRONG: New renderer each time
func (m *Model) renderMarkdown(content string) string {
renderer, _ := glamour.NewTermRenderer(...) // Can freeze!
return renderer.Render(content)
}
// ✅ CORRECT: Shared renderer instance
func (m *Model) renderMarkdown(content string) string {
return m.styles.GetRenderer().Render(content)
}2. Footer Height Inconsistency
Problem: Border-based footers vary in height
Solution: Background approach with padding
// ❌ WRONG: Border approach (height varies)
footer := lipgloss.NewStyle().
Height(1).
Border(lipgloss.Border{Top: true}).
Render(text)
// ✅ CORRECT: Background approach (exactly 1 line)
footer := lipgloss.NewStyle().
Background(borderColor).
Foreground(textColor).
Padding(0, 1, 0, 1).
Render(text)3. Dimension Synchronization
Problem: Models store their own width/height, get out of sync
Solution: Centralize dimensions in styles singleton
// ❌ WRONG: Models managing their own dimensions
type Model struct {
width, height int
}
// ✅ CORRECT: Centralized dimension management
type Model struct {
styles *styles.Styles // Access via styles.GetSize()
}4. TUI Rendering Corruption
Problem: Console output breaks rendering
Solution: File-based logger, never fmt.Printf
// ❌ NEVER: Use tea.ClearScreen during navigation
return a, tea.Batch(cmd, tea.ClearScreen)
// ✅ CORRECT: Let model Init() handle clean state
return a, a.currentModel.Init()5. Navigation State Issues
Problem: Models retain state between visits
Solution: Complete state reset in Init()
// ❌ WRONG: String-based navigation (typo-prone)
return NavigationMsg{Target: "main_menu"}
// ❌ WRONG: Manual string concatenation for arguments
return NavigationMsg{Target: ScreenID("llm_provider_form/openai")}
// ✅ CORRECT: Type-safe constants
return NavigationMsg{Target: MainMenuScreen}
// ✅ CORRECT: Composite ScreenID with helper
return NavigationMsg{Target: CreateScreenID("llm_provider_form", "openai")}🚀 **Performance & Best Practices**
Proven Patterns
// ✅ DO: Shared renderer
rendered, _ := m.styles.GetRenderer().Render(content)
// ✅ DO: Centralized dimensions
width, height := m.styles.GetSize()
// ✅ DO: File logging
logger.Log("[Component] ACTION: %v", data)
// ✅ DO: Complete state reset
func (m *Model) Init() tea.Cmd {
m.resetAllState()
return m.loadContent
}
// ✅ DO: Graceful dimension handling
if width <= 0 || height <= 0 {
return "Loading..."
}Anti-Patterns to Avoid
// ❌ DON'T: New renderer instances
renderer, _ := glamour.NewTermRenderer(...)
// ❌ DON'T: Model dimensions
type Model struct {
width int // Store in styles instead
height int // Store in styles instead
}
// ❌ DON'T: Console output
fmt.Printf("Debug: %v\n", value)
// ❌ DON'T: Partial state reset
func (m *Model) Init() tea.Cmd {
// Only resetting some fields - incomplete!
m.content = ""
// Missing: m.ready, m.scrolled, etc.
}Key Best Practices Summary
- Single glamour renderer: Prevents freezing, faster rendering
- Centralized dimensions: Eliminates sync issues, simplifies models
- Background footer: Consistent height, modern appearance
- Type-safe navigation: Compile-time error prevention
- File-based logging: Debug without breaking TUI
- Complete state reset: Predictable model behavior
- Graceful fallbacks: Handle edge cases elegantly
- Resource estimation: Real-time calculation of token/memory usage
- Environment integration: Proper EnvVar handling with cleanup
- Value formatting: Consistent human-readable displays (formatBytes, formatNumber)
This cheat sheet contains battle-tested solutions for TUI development in the Charm ecosystem, proven in production use.
🎯 **Advanced Form Field Patterns**
**Boolean Fields with Tab Completion**
Innovation: Auto-completion for boolean values with suggestions
import "github.com/charmbracelet/bubbles/textinput"
func createBooleanField() textinput.Model {
input := textinput.New()
input.Prompt = ""
input.ShowSuggestions = true
input.SetSuggestions([]string{"true", "false"}) // Enable tab completion
// Show default value in placeholder
input.Placeholder = "true (default)" // Or "false (default)"
return input
}
// Tab completion handler in Update()
func (m *FormModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.KeyMsg:
switch msg.String() {
case "tab":
// Complete boolean suggestion
if m.focusedField.Input.ShowSuggestions {
suggestion := m.focusedField.Input.CurrentSuggestion()
if suggestion != "" {
m.focusedField.Input.SetValue(suggestion)
m.focusedField.Input.CursorEnd()
return m, nil
}
}
}
}
return m, nil
}**Integer Fields with Range Validation**
Innovation: Real-time validation with human-readable formatting
/* Detailed source-code truncated for AI context efficiency. */**Value Formatting Utilities**
Critical: Consistent formatting across all forms
// Universal byte formatting for configuration values
func formatBytes(bytes int) string {
if bytes >= 1048576 {
return fmt.Sprintf("%.1fMB", float64(bytes)/1048576)
} else if bytes >= 1024 {
return fmt.Sprintf("%.1fKB", float64(bytes)/1024)
}
return fmt.Sprintf("%d bytes", bytes)
}
// Universal number formatting for display
func formatNumber(num int) string {
if num >= 1000000 {
return fmt.Sprintf("%.1fM", float64(num)/1000000)
} else if num >= 1000 {
return fmt.Sprintf("%.1fK", float64(num)/1000)
}
return strconv.Itoa(num)
}
// Usage in forms and info panels
sections = append(sections, fmt.Sprintf("• Memory Limit: %s", formatBytes(memoryLimit)))
sections = append(sections, fmt.Sprintf("• Estimated tokens: ~%s", formatNumber(tokenCount)))**Environment Variable Integration Pattern**
Innovation: Direct EnvVar integration with presence detection
// EnvVar wrapper (from loader package)
type EnvVar struct {
Key string
Value string // Current value in environment
Default string // Default value from config
}
func (e EnvVar) IsPresent() bool {
return e.Value != "" // Check if actually set in environment
}
// Form field creation from EnvVar
func (m *FormModel) addFieldFromEnvVar(envVarName, fieldKey, title, description string) {
envVar, _ := m.controller.GetVar(envVarName)
// Track initially set fields for cleanup logic
m.initiallySetFields[fieldKey] = envVar.IsPresent()
input := textinput.New()
input.Prompt = ""
// Show default in placeholder if not set
if !envVar.IsPresent() {
input.Placeholder = fmt.Sprintf("%s (default)", envVar.Default)
} else {
input.SetValue(envVar.Value) // Set current value
}
field := FormField{
Key: fieldKey,
Title: title,
Description: description,
Input: input,
EnvVarName: envVarName,
}
m.fields = append(m.fields, field)
}**Smart Field Cleanup Pattern**
Innovation: Environment variable cleanup for empty values
func (m *FormModel) saveConfiguration() error {
// First pass: Remove cleared fields from environment
for _, field := range m.fields {
value := strings.TrimSpace(field.Input.Value())
// If field was initially set but now empty, remove it
if value == "" && m.initiallySetFields[field.Key] {
if err := m.controller.SetVar(field.EnvVarName, ""); err != nil {
return fmt.Errorf("failed to clear %s: %w", field.EnvVarName, err)
}
logger.Log("[FormModel] SAVE: cleared %s", field.EnvVarName)
}
}
// Second pass: Save only non-empty values
for _, field := range m.fields {
value := strings.TrimSpace(field.Input.Value())
if value == "" {
continue // Skip empty - use defaults
}
// Validate before saving
if err := m.validateFieldValue(field, value); err != nil {
return fmt.Errorf("validation failed for %s: %w", field.Key, err)
}
// Save validated value
if err := m.controller.SetVar(field.EnvVarName, value); err != nil {
return fmt.Errorf("failed to set %s: %w", field.EnvVarName, err)
}
logger.Log("[FormModel] SAVE: set %s=%s", field.EnvVarName, value)
}
return nil
}**Resource Estimation Pattern**
Innovation: Real-time calculation of resource usage
/* Detailed source-code truncated for AI context efficiency. */**Current Configuration Preview Pattern**
Innovation: Live display of current settings in info panel
/* Detailed source-code truncated for AI context efficiency. */**Type-Based Dynamic Forms**
Innovation: Conditional field generation based on selection
func (m *FormModel) buildDynamicForm() {
m.fields = []FormField{} // Reset
// Common fields for all types
m.addFieldFromEnvVar("ENABLED", "enabled", "Enable Service", "Enable or disable this service")
m.addFieldFromEnvVar("MAX_MEMORY", "max_memory", "Memory Limit", "Maximum memory usage in bytes")
// Type-specific fields
switch m.configType {
case "database":
m.addFieldFromEnvVar("MAX_CONNECTIONS", "max_connections", "Max Connections", "Maximum database connections")
m.addFieldFromEnvVar("CACHE_SIZE", "cache_size", "Cache Size", "Database cache size in bytes")
case "worker":
m.addFieldFromEnvVar("WORKER_COUNT", "worker_count", "Worker Count", "Number of worker processes")
m.addFieldFromEnvVar("QUEUE_SIZE", "queue_size", "Queue Size", "Maximum queue size")
case "api":
m.addFieldFromEnvVar("RATE_LIMIT", "rate_limit", "Rate Limit", "API requests per minute")
m.addFieldFromEnvVar("TIMEOUT", "timeout", "Request Timeout", "Request timeout in seconds")
}
// Set focus on first field
if len(m.fields) > 0 {
m.fields[0].Input.Focus()
}
}
// Environment variable naming helper
func (m *FormModel) getEnvVarName(configType, suffix string) string {
prefix := strings.ToUpper(configType) + "_"
return prefix + suffix
}These advanced patterns enable:
- Smart Validation: Real-time feedback with user-friendly error messages
- Resource Awareness: Live estimation of memory, CPU, or token usage
- Environment Integration: Proper handling of defaults, presence detection, and cleanup
- Type Safety: Compile-time validation and runtime error handling
- User Experience: Auto-completion, formatting, and intuitive navigation
🎯 **Production Form Architecture Patterns**
Form Model Structure (Latest Pattern)
Based on successful llm_provider_form.go and summarizer_form.go implementations
type FormModel struct {
controller *controllers.StateController
styles *styles.Styles
window *window.Window
// Core form state
fields []FormField
focusedIndex int
showValues bool
hasChanges bool
args []string // Arguments from composite ScreenID
// Enhanced state tracking (from summarizer implementation)
initialized bool
configType string
typeName string
initiallySetFields map[string]bool // Track fields for cleanup
// Viewport as permanent property for forms
viewport viewport.Model
formContent string
fieldHeights []int
}
// Constructor pattern - args from composite ScreenID
func NewFormModel(
controller *controllers.StateController, styles *styles.Styles,
window *window.Window, args []string,
) *FormModel {
// Extract primary argument (e.g., provider ID)
primaryArg := "default"
if len(args) > 0 && args[0] != "" {
primaryArg = args[0]
}
return &FormModel{
controller: controller,
styles: styles,
window: window,
args: args,
viewport: viewport.New(window.GetContentSize()), // Permanent viewport
}
}Key Form Implementation Patterns
1. Proper Navigation Hotkeys
// Modern form navigation (Production Pattern - from summarizer_form.go)
switch msg.String() {
case "down": // ↓: Next field
m.focusNext()
m.ensureFocusVisible()
case "up": // ↑: Previous field
m.focusPrev()
m.ensureFocusVisible()
case "tab": // Tab: Complete suggestion (boolean auto-complete)
m.completeSuggestion()
case "ctrl+h": // Ctrl+H: Toggle show/hide masked values
m.toggleShowValues()
case "ctrl+s": // Ctrl+S: Save configuration only
return m.saveConfiguration()
case "ctrl+r": // Ctrl+R: Reset form to defaults
m.resetForm()
return m, nil
case "enter": // Enter: Save and return (GoBack navigation)
return m.saveAndReturn()
}
// Enhanced field navigation with auto-scroll
func (m *FormModel) focusNext() {
if len(m.fields) == 0 {
return
}
m.fields[m.focusedIndex].Input.Blur()
m.focusedIndex = (m.focusedIndex + 1) % len(m.fields)
m.fields[m.focusedIndex].Input.Focus()
m.updateFormContent()
}2. Suggestions and Auto-completion
// Boolean field with suggestions (from summarizer_form.go)
func (m *FormModel) addBooleanField(key, title, description string, envVar loader.EnvVar) {
input := textinput.New()
input.Prompt = ""
input.PlaceholderStyle = m.styles.FormPlaceholder
input.ShowSuggestions = true
input.SetSuggestions([]string{"true", "false"})
// Show default in placeholder
if envVar.Default == "true" {
input.Placeholder = "true (default)"
} else {
input.Placeholder = "false (default)"
}
// Set value only if actually present in environment
if envVar.Value != "" && envVar.IsPresent() {
input.SetValue(envVar.Value)
}
}
// Tab completion handler
func (m *FormModel) completeSuggestion() {
if m.focusedIndex < len(m.fields) {
suggestion := m.fields[m.focusedIndex].Input.CurrentSuggestion()
if suggestion != "" {
m.fields[m.focusedIndex].Input.SetValue(suggestion)
m.fields[m.focusedIndex].Input.CursorEnd()
m.fields[m.focusedIndex].Value = suggestion
m.hasChanges = true
m.updateFormContent()
}
}
}3. Dynamic Input Width Calculation
// Adaptive input sizing
func (m *FormModel) getInputWidth() int {
viewportWidth, _ := m.getViewportSize()
inputWidth := viewportWidth - 6 // Account for padding
if m.isVerticalLayout() {
inputWidth = viewportWidth - 4 // Less padding in vertical
}
return inputWidth
}
func (m *FormModel) getViewportSize() (int, int) {
contentWidth, contentHeight := m.window.GetContentSize()
if contentWidth <= 0 || contentHeight <= 0 {
return 0, 0
}
if m.isVerticalLayout() {
return contentWidth - PaddingWidth/2, contentHeight - PaddingHeight
} else {
leftWidth := MinMenuWidth
extraWidth := contentWidth - leftWidth - MinInfoWidth - PaddingWidth
if extraWidth > 0 {
leftWidth = min(leftWidth+extraWidth/2, MaxMenuWidth)
}
return leftWidth, contentHeight - PaddingHeight
}
}4. Viewport as Permanent Form Property
// ✅ CORRECT: Viewport as permanent property for forms
type FormModel struct {
viewport viewport.Model // Permanent - preserves scroll position
}
// Update viewport dimensions on resize
func (m *FormModel) updateViewport() {
formContentHeight := lipgloss.Height(m.formContent) + 2
viewportWidth, viewportHeight := m.getViewportSize()
m.viewport.Width = viewportWidth
m.viewport.Height = min(viewportHeight, formContentHeight)
m.viewport.SetContent(m.formContent)
}
// ❌ WRONG: Creating viewport in View() - loses scroll state
func (m *FormModel) View() string {
vp := viewport.New(width, height) // State lost on re-render!
return vp.View()
}Layout Architecture (Two-Column Pattern)
1. Layout Constants (Production Values)
const (
MinMenuWidth = 38 // Minimum left panel width
MaxMenuWidth = 66 // Maximum left panel width (prevents too wide)
MinInfoWidth = 34 // Minimum right panel width
PaddingWidth = 8 // Total horizontal padding
PaddingHeight = 2 // Vertical padding
)2. Adaptive Layout Logic
func (m *Model) isVerticalLayout() bool {
contentWidth := m.window.GetContentWidth()
return contentWidth < (MinMenuWidth + MinInfoWidth + PaddingWidth)
}
// Horizontal layout with dynamic width allocation
func (m *Model) renderHorizontalLayout(leftPanel, rightPanel string, width, height int) string {
leftWidth, rightWidth := MinMenuWidth, MinInfoWidth
extraWidth := width - leftWidth - rightWidth - PaddingWidth
// Distribute extra space, but cap left panel at MaxMenuWidth
if extraWidth > 0 {
leftWidth = min(leftWidth+extraWidth/2, MaxMenuWidth)
rightWidth = width - leftWidth - PaddingWidth/2
}
leftStyled := lipgloss.NewStyle().Width(leftWidth).Padding(0, 2, 0, 2).Render(leftPanel)
rightStyled := lipgloss.NewStyle().Width(rightWidth).PaddingLeft(2).Render(rightPanel)
// Use viewport for final layout rendering
viewport := viewport.New(width, height-PaddingHeight)
viewport.SetContent(lipgloss.JoinHorizontal(lipgloss.Top, leftStyled, rightStyled))
return viewport.View()
}3. Content Hiding When Space Insufficient
// Vertical layout with conditional content hiding
func (m *Model) renderVerticalLayout(leftPanel, rightPanel string, width, height int) string {
verticalStyle := lipgloss.NewStyle().Width(width).Padding(0, 4, 0, 2)
leftStyled := verticalStyle.Render(leftPanel)
rightStyled := verticalStyle.Render(rightPanel)
// Hide right panel if both don't fit
if lipgloss.Height(leftStyled)+lipgloss.Height(rightStyled)+2 < height {
return lipgloss.JoinVertical(lipgloss.Left,
leftStyled,
verticalStyle.Height(1).Render(""),
rightStyled,
)
}
// Show only essential left panel
return leftStyled
}Composite ScreenID Navigation (Production Pattern)
1. Proper ScreenID Creation for Navigation
// Navigation from menu with argument preservation
func (m *MenuModel) handleSelection() (tea.Model, tea.Cmd) {
selectedItem := m.getSelectedItem()
// Create composite ScreenID with current selection for stack preservation
return m, func() tea.Msg {
return NavigationMsg{
Target: CreateScreenID(string(targetScreen), selectedItem.ID),
}
}
}
// Form navigation back - use GoBack to avoid stack loops
func (m *FormModel) saveAndReturn() (tea.Model, tea.Cmd) {
model, cmd := m.saveConfiguration()
if cmd != nil {
return model, cmd
}
// ✅ CORRECT: Use GoBack to return to previous screen
return m, func() tea.Msg {
return NavigationMsg{GoBack: true}
}
}
// ❌ WRONG: Direct navigation creates stack loops
return m, func() tea.Msg {
return NavigationMsg{Target: LLMProvidersScreen} // Creates loop!
}2. Constructor with Args Pattern
// Model constructor receives args from composite ScreenID
func NewModel(
controller *controllers.StateController, styles *styles.Styles,
window *window.Window, args []string,
) *Model {
// Initialize with selection from args
selectedIndex := 0
if len(args) > 1 && args[1] != "" {
// Find matching item and set selectedIndex
for i, item := range items {
if item.ID == args[1] {
selectedIndex = i
break
}
}
}
return &Model{
controller: controller,
selectedIndex: selectedIndex,
args: args,
}
}
// No separate SetSelected* methods needed
func (m *Model) Init() tea.Cmd {
logger.Log("[Model] INIT: args=%s", strings.Join(m.args, " § "))
// Selection already set in constructor from args
m.loadData()
return nil
}Viewport Usage Patterns
1. Forms: Permanent Viewport Property
// ✅ For forms with user interaction and scroll state
type FormModel struct {
viewport viewport.Model // Permanent - preserves scroll position
}
func (m *FormModel) ensureFocusVisible() {
// Auto-scroll to focused field
focusY := m.calculateFieldPosition(m.focusedIndex)
if focusY < m.viewport.YOffset {
m.viewport.YOffset = focusY
}
// ... scroll logic
}2. Layout: Temporary Viewport Creation
// ✅ For final layout rendering only
func (m *Model) renderHorizontalLayout(left, right string, width, height int) string {
content := lipgloss.JoinHorizontal(lipgloss.Top, leftStyled, rightStyled)
// Create viewport just for layout rendering
vp := viewport.New(width, height-PaddingHeight)
vp.SetContent(content)
return vp.View()
}Screen Architecture (App.go Integration)
1. Content Area Only Pattern
// Screen models ONLY handle content area
func (m *Model) View() string {
// ✅ CORRECT: Only content, no header/footer
leftPanel := m.renderForm()
rightPanel := m.renderHelp()
if m.isVerticalLayout() {
return m.renderVerticalLayout(leftPanel, rightPanel, width, height)
}
return m.renderHorizontalLayout(leftPanel, rightPanel, width, height)
}
// ❌ WRONG: Handling header/footer in screen
func (m *Model) View() string {
header := m.renderHeader() // App.go handles this!
footer := m.renderFooter() // App.go handles this!
// ...
}2. App.go Layout Management
// App.go manages complete layout structure
func (a *App) View() string {
header := a.renderHeader() // Screen-specific header
footer := a.renderFooter() // Dynamic footer with actions
content := a.currentModel.View() // Content from model
// Calculate content area size
contentWidth, contentHeight := a.window.GetContentSize()
contentArea := a.styles.Content.
Width(contentWidth).
Height(contentHeight).
Render(content)
return lipgloss.JoinVertical(lipgloss.Left, header, contentArea, footer)
}Field Configuration Best Practices
1. Clean Input Setup
// Modern input field setup
func (m *FormModel) addInputField(config *Config, fieldType string) {
input := textinput.New()
input.Prompt = "" // Clean appearance
input.PlaceholderStyle = m.styles.FormPlaceholder
// Dynamic width - set during rendering
// input.Width NOT set here - calculated in updateFormContent()
if fieldType == "password" {
input.EchoMode = textinput.EchoPassword
}
if fieldType == "boolean" {
input.ShowSuggestions = true
input.SetSuggestions([]string{"true", "false"})
}
// Set value from config
if config != nil {
input.SetValue(config.GetValue(fieldType))
}
}2. Dynamic Width Application
// Apply width during content update
func (m *FormModel) updateFormContent() {
inputWidth := m.getInputWidth()
for i, field := range m.fields {
// Apply dynamic width to input
field.Input.Width = inputWidth - 3 // Account for borders
field.Input.SetValue(field.Input.Value()) // Trigger width update
// Render with consistent styling
inputStyle := m.styles.FormInput.Width(inputWidth)
if i == m.focusedIndex {
inputStyle = inputStyle.BorderForeground(styles.Primary)
}
renderedInput := inputStyle.Render(field.Input.View())
sections = append(sections, renderedInput)
}
}State Management Best Practices
1. Configuration vs Status Separation
// ✅ SIMPLIFIED: Single status field
type ProviderInfo struct {
ID string
Name string
Description string
Configured bool // Single status - provider has required fields
}
// Load status logic
func (m *Model) loadProviders() {
configs := m.controller.GetLLMProviders()
provider := ProviderInfo{
ID: "openai",
Name: locale.LLMProviderOpenAI,
Configured: configs["openai"].Configured, // From controller
}
}
// ❌ COMPLEX: Multiple status fields (removed)
type ProviderInfo struct {
Configured bool
Enabled bool // Removed - controller handles this
}2. GoBack Navigation Pattern
// ✅ CORRECT: GoBack prevents navigation loops
func (m *FormModel) saveAndReturn() (tea.Model, tea.Cmd) {
if err := m.saveConfiguration(); err != nil {
return m, nil // Stay on form if save fails
}
// Return to previous screen (from navigation stack)
return m, func() tea.Msg {
return NavigationMsg{GoBack: true}
}
}
// Navigation stack automatically maintained:
// ["main_menu§llm_providers", "llm_providers§openai", "llm_provider_form§openai"]
// GoBack removes current and returns to: "llm_providers§openai"This production architecture ensures:
- Clean separation: Forms handle content, app.go handles layout
- Persistent state: Viewport scroll positions maintained
- Adaptive design: Content hides gracefully when space insufficient
- Type-safe navigation: Arguments preserved in composite ScreenIDs
- No navigation loops: GoBack pattern prevents stack corruption
Backend Docs Config (`backend/docs/config.md`)
PentAGI Configuration Guide
This document serves as a comprehensive guide to the configuration system in PentAGI, primarily aimed at developers. It details all available configuration options, their purposes, default values, and how they're used throughout the application.
Table of Contents
- PentAGI Configuration Guide
- Table of Contents
- Configuration Basics
- General Settings
- Multi-Instance Deployment (
TENANT_ID)- What the application namespaces automatically
- What stays the operator's responsibility
- Deployment topologies
- Extensions installed outside
public(DATABASE_EXTENSIONS_SCHEMA) - Multi-tenant PostgreSQL access through PgBouncer
- Multi-tenant PostgreSQL through Supabase's Supavisor pooler (
DATABASE_SEARCH_PATH_VIA_OPTIONS)
- PentAGI Cloud API Settings
- Usage Details
- Multi-Instance Deployment (
- Docker Settings
- Server Settings
- Frontend Settings
- Authentication Settings
- Web Scraper Settings
- LLM Provider Settings
- Embedding Settings
- Summarizer Settings
- Assistant Settings
- Functions Configuration
- Search Engine Settings
- Network and Proxy Settings
- Graphiti Knowledge Graph Settings
- Agent Supervision Settings
- Observability Settings
Configuration Basics
PentAGI uses environment variables for configuration, with support for .env files through the godotenv package. The configuration is defined in the Config struct in pkg/config/config.go and is loaded using the NewConfig() function.
func NewConfig() (*Config, error) {
godotenv.Load()
var config Config
if err := env.ParseWithOptions(&config, env.Options{
RequiredIfNoDef: false,
FuncMap: map[reflect.Type]env.ParserFunc{
reflect.TypeOf(&url.URL{}): func(s string) (interface{}, error) {
if s == "" {
return nil, nil
}
return url.Parse(s)
},
},
}); err != nil {
return nil, err
}
return &config, nil
}This function automatically loads environment variables from a .env file if present, then parses them into the Config struct using the env package from github.com/caarlos0/env/v10.
Current Web Settings Coverage
The running PentAGI instance already exposes several settings areas in the web UI:
- Settings -> Providers: Manage user-defined provider profiles, per-agent model and runtime options, and provider test actions for provider types supported by the running server.
- Settings -> Prompts: Manage system, human, and tool prompt templates.
- Settings -> PentAGI API: Create, revoke, and delete PentAGI API tokens.
- Other UI-managed preferences: Favorite flows are stored as user preferences, and theme selection is handled client-side from the main sidebar/profile controls.
These web-console features do not replace the environment variables in this guide for provider credentials, endpoints, or external integrations.
Still Server-Managed
The environment variables documented below remain the source of truth for configuration that is not currently editable from the web console:
- LLM credentials and connection settings: API keys, base URLs, auth modes, and provider-specific connection settings for OpenAI, Anthropic, Bedrock, Ollama, custom providers, and similar backends; config-path settings apply only where supported, such as
OLLAMA_SERVER_CONFIG_PATH,LLM_SERVER_CONFIG_PATH, andBEDROCK_CONFIG_PATH. - Search provider credentials and options: DuckDuckGo, Google, Tavily, Traversaal, Perplexity, SearXNG, Sploitus, and related search configuration.
- Third-party integrations: Langfuse, Graphiti, and other external observability or knowledge services.
General Settings
These settings control basic application behavior and are foundational for the system's operation.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| DatabaseURL | DATABASE_URL |
postgres://pentagiuser:pentagipass@pgvector:5432/pentagidb?sslmode=disable |
Connection string for the PostgreSQL database with pgvector extension |
| DatabaseExtensionsSchema | DATABASE_EXTENSIONS_SCHEMA |
public |
Schema every tenant's search_path must include for shared extensions (vector, pg_trgm) to resolve. Only used when TenantID is set. Override for databases that install extensions elsewhere by convention, e.g. Supabase uses extensions. See Extensions installed outside public. |
| DatabaseSearchPathViaOptions | DATABASE_SEARCH_PATH_VIA_OPTIONS |
false |
Sends the tenant search_path as options=--search_path=<value> instead of a bare search_path parameter. Only used when TenantID is set. For poolers that forward options but drop an unrecognized bare search_path (e.g. some Supabase Supavisor versions). See Multi-tenant PostgreSQL through Supabase's Supavisor pooler. |
| DBMaxOpenConns | DATABASE_MAX_OPEN_CONNS |
25 |
Maximum open connections in the shared sql.DB pool (sqlc + GORM combined). See database.md §Connection Pooling. |
| DBMaxIdleConns | DATABASE_MAX_IDLE_CONNS |
5 |
Maximum idle connections kept open between requests |
| DBVectorMaxConns | DATABASE_VECTOR_MAX_CONNS |
10 |
Maximum connections in the shared pgxpool for all pgvector stores |
| Debug | DEBUG |
false |
Enables debug mode with additional logging |
| DataDir | DATA_DIR |
./data |
Directory for storing persistent data |
| AskUser | ASK_USER |
false |
When enabled, requires explicit user confirmation for certain operations |
| TenantID | TENANT_ID |
(empty) | Namespaces every externally-visible artifact this instance creates so several PentAGI instances can share one host and one set of backing services. Empty = single-instance behavior, unchanged. See Multi-Instance Deployment. |
| DockerPortsBase | DOCKER_PORTS_BASE |
0 (means 28000) |
First host port for per-flow sandbox port publishing; each instance owns [base, base+2000) |
| InstallationID | INSTALLATION_ID |
(none) | Unique installation identifier for PentAGI Cloud API communication |
| LicenseKey | LICENSE_KEY |
(none) | License key for PentAGI Cloud API authentication and feature activation |
| UpdateStrategy | UPDATE_STRATEGY |
preview |
Which builds this installation is offered: nightly, preview or stable. An unrecognised value falls back to preview. See PentAGI Cloud API Settings. |
| UpdateCheckInterval | UPDATE_CHECK_INTERVAL |
3h |
How often the running server reports its own state to the Cloud API. 0 disables the report. See PentAGI Cloud API Settings. |
| UpdateServerHost | UPDATE_SERVER_HOST |
update.pentagi.com |
PentAGI Cloud API endpoint as host[:port], without a scheme — the connection is always HTTPS. See PentAGI Cloud API Settings. |
| SupportServerHost | SUPPORT_SERVER_HOST |
support.pentagi.com |
PentAGI Support API endpoint as host[:port], same shape as UPDATE_SERVER_HOST. See PentAGI Cloud API Settings. |
Multi-Instance Deployment (`TENANT_ID`)
TENANT_ID namespaces the artifacts a PentAGI instance creates in shared external services, so that several independent installations can use one PostgreSQL server, one worker node (Docker daemon), one Neo4j/Graphiti and one Langfuse without colliding.
Primary use case: several management instances, shared resources. The typical deployment puts each PentAGI management backend on its own server, while the heavy shared resources — the worker node where sandbox containers run, and the database — are common. TENANT_ID is what keeps those instances from writing over each other.
Running several management instances on one server is also possible (for example behind an nginx reverse proxy), but that is not what the installer or the stock docker-compose.yml are built for — see Deployment topologies below.
When empty (the default) nothing changes. Every helper degrades to an identity function: container names stay pentagi-terminal-, the database schema stays public, Graphiti group ids stay flow-, and the session cookie stays auth. This is enforced in one place (backend/pkg/config/tenant.go) and covered by tests.
Validation. TENANT_ID must match ^[a-z][a-z0-9_]{0,31}$ — the intersection of the constraints imposed by every consumer:
| Consumer | Constraint |
|---|---|
| Docker object name | [a-zA-Z0-9][a-zA-Z0-9_.-]* |
| PostgreSQL identifier | ≤ 63 bytes; unquoted-safe as [a-z_][a-z0-9_]* |
| Graphiti / Neo4j group id | parsed on a hyphen boundary, so no hyphens |
Hyphens are excluded deliberately: they separate the tenant from the rest of a group id or object name. An invalid value aborts startup rather than being normalised, because collapsing two distinct tenants onto one namespace is exactly the collision tenancy exists to prevent.
What the application namespaces automatically
| Area | Effect |
|---|---|
| PostgreSQL | A schema named after the tenant is created on boot and search_path is set to <tenant>,<DATABASE_EXTENSIONS_SCHEMA>; the DSN is rewritten once so sqlc, GORM, goose and the pgvector pool all follow. Extensions (vector, pg_trgm) stay shared in DATABASE_EXTENSIONS_SCHEMA (default public). |
| Worker containers | Sandbox container names become <tenant>-pentagi-terminal-<flow>; the per-flow volume and the container hostname derive from that name automatically. Both are labelled pentagi.tenant, so daemon-wide sweeps can filter by owner. |
| Host ports | Per-flow sandbox ports are allocated from DOCKER_PORTS_BASE (default 28000), giving each instance the window [base, base+2000). |
| Knowledge graph | Graphiti/Neo4j group ids become <tenant>-flow-. The GraphQL API contract is unchanged — clients still send flow- and the server rebuilds the namespaced key internally. |
| Auth | Cookie and JWT keys are derived from COOKIE_SIGNING_SALT plus the tenant, and the session cookie is renamed, so a session or API token minted by one instance is rejected by another. |
| Telemetry | OTel resources carry service.instance.id and, with a tenant set, tenant_id; Langfuse traces carry the native environment field plus a tenant: tag and tenant-prefixed trace/session names. |
What stays the operator's responsibility
TENANT_ID does not rewrite infrastructure-level settings. The following must be given distinct values per instance by whoever provisions it:
| Setting | Why it is not derived from TENANT_ID |
|---|---|
DATA_DIR |
The data directory is chosen by the operator. Flow file caches, the container /work bind mount, screenshots and installation_id all live under it, and two instances pointed at the same directory will overwrite each other's flow data. Give each instance its own path or its own volume. |
DOCKER_NETWORK |
Instances may legitimately share one Docker network; the application never renames it. Set it explicitly if you want separate networks. |
| Published ports | PENTAGI_LISTEN_PORT, PGVECTOR_LISTEN_PORT, SCRAPER_LISTEN_PORT, PPROF_ADDR and DOCKER_PORTS_BASE are host-level resources, not string namespaces. |
INSTALLATION_ID |
Unique per installation, or left empty to be generated once and cached in DATA_DIR. |
| Database privileges | The configured user needs CREATE SCHEMA, and on first boot CREATE EXTENSION — unless an administrator pre-installed vector and pg_trgm into DATABASE_EXTENSIONS_SCHEMA (default public). |
The values actually in effect are written to the startup log under Instance identity (tenant_id, data_dir, schema, installation_id), which is the quickest way to confirm two instances are not sharing something they should not.
DOCKER_INSIDE is orthogonal to tenancy and may be enabled alongside it.
Deployment topologies
| Topology | Supported by |
|---|---|
| One instance per server, shared PostgreSQL and/or worker node | The installer and the stock docker-compose.yml, with TENANT_ID set per server. This is the intended setup. |
| Several instances on one server | Manual configuration only. The stock docker-compose.yml uses fixed container_name and network names, so copying it verbatim will not start a second stack. Adapt it to your network and infrastructure — for example, distinct container names behind a shared nginx — and give each instance its own DATA_DIR and ports. |
The installer provisions one instance per server; it does not manage several side by side. Note that it does scope the sandbox resources it cleans up — worker containers and volumes are matched by the tenant prefix — so a purge run for one management instance will not remove another's sandboxes from a shared worker node.
Upgrading an existing deployment. Leave TENANT_ID empty. The instance keeps using public and its current data directory; no migration is required. Setting TENANT_ID on an existing installation points it at a new, empty schema — the data in public is not migrated and will appear to be gone. Do not point an existing public deployment at a search_path that lists another tenant's schema.
Extensions installed outside `public` (`DATABASE_EXTENSIONS_SCHEMA`)
ensureTenantSchema requires vector and pg_trgm to already live in (or be creatable in) one schema common to every tenant, because it must be part of every tenant's search_path. That schema defaults to public, which is where a stock PostgreSQL/docker-compose.yml install keeps them.
Managed providers do not always follow that convention. Supabase (cloud and self-hosted) installs its bundled extensions into a dedicated extensions schema instead, and its default roles get extensions added to their search_path for exactly that reason. Pointing DATABASE_URL at such a database with TENANT_ID set fails fast on boot:
Tenant schema initialization failed: extension "vector" is installed in schema "extensions",
but multi-tenant mode requires it in "public" so every tenant can reach it; either run
ALTER EXTENSION vector SET SCHEMA public, or set DATABASE_EXTENSIONS_SCHEMA=extensions
to match where it already livesSet DATABASE_EXTENSIONS_SCHEMA=extensions (or whatever schema the error reports) instead of moving the extension with ALTER EXTENSION ... SET SCHEMA — the schema only needs to be part of the search_path PentAGI computes (<tenant>,<DATABASE_EXTENSIONS_SCHEMA>), moving a provider-managed extension out of its documented location is unnecessary and risks breaking whatever else that provider expects to find it there.
Multi-tenant PostgreSQL access through PgBouncer
DATABASE_URL can point at a PgBouncer instance instead of PostgreSQL directly, but three things have to be true, independent of each other:
pool_mode = sessionon the PgBouncer side. PentAGI holds apg_advisory_lock/pg_advisory_unlockpair on one dedicated connection across the whole tenant-bootstrap + migration sequence (backend/pkg/database/tenant.go), andpgx(used by the pgvector pool) caches server-side prepared statements by default. Both break silently undertransaction/statementpooling, because PgBouncer is then free to hand the client a different backend connection between statements. This requirement is unrelated to tenancy — it applies even withTENANT_IDempty.ignore_startup_parameters = search_pathinpgbouncer.ini. With a tenant configured, PentAGI's own DSN already carries?search_path=<tenant>,<DATABASE_EXTENSIONS_SCHEMA>(see the table above). PgBouncer validates startup parameters from the client against a small built-in allowlist and rejects anything else withunsupported startup parameter: search_pathunless it is explicitly ignored. Ignoring it does not apply the value — it only stops PgBouncer from rejecting the connection — so this step alone is not sufficient; see the next point.A
connect_queryper tenant in PgBouncer's[databases]section, since PentAGI's ownsearch_pathstartup parameter is ignored per point 2 above.connect_queryruns on PgBouncer's own connection to PostgreSQL before any client statement, so it is not subject to the client-facing startup-parameter allowlist and works regardless of pool mode:ini[databases] pentagi_testing = host=pgvector port=5432 dbname=pentagidb pool_mode=session connect_query='SET search_path TO testing,public' pentagi_acme = host=pgvector port=5432 dbname=pentagidb pool_mode=session connect_query='SET search_path TO acme,public'Point each instance's
DATABASE_URLat its own virtual database name (pentagi_testing,pentagi_acme, ...) rather than the sharedpentagidb— PgBouncer pools per(user, dbname)pair, so distinct virtual names are what keeps the tenants' pools, and therefore theirconnect_query, apart.(
track_extra_parameters = search_pathis PgBouncer's other mechanism for this, but it only works when PostgreSQL reportssearch_pathchanges back to the client, which requires PostgreSQL 18+ or Citus 12+ — not an option against the PostgreSQL 16/17 that ships indocker-compose.yml.)
Multi-tenant PostgreSQL through Supabase's Supavisor pooler (`DATABASE_SEARCH_PATH_VIA_OPTIONS`)
Supabase's shared/self-hosted pooler (Supavisor) is not PgBouncer and none of its [databases]/connect_query configuration exists for it, so the PgBouncer recipe above does not apply. Reports on whether Supavisor forwards a tenant's search_path at all are inconsistent — see supabase/supavisor#206 — and depend on the Supavisor version (a parsing fix landed in PR #768).
If bypassing the pooler entirely (connecting straight to the underlying PostgreSQL, or to a Supabase project's "Direct connection"/IPv4-add-on string) is not an option, set:
DATABASE_SEARCH_PATH_VIA_OPTIONS=trueThis sends the tenant's search_path as options=--search_path=<tenant>,<DATABASE_EXTENSIONS_SCHEMA> instead of a bare search_path= parameter. Some poolers forward the options startup parameter through to the real backend while silently dropping an unrecognized bare search_path — this is exactly the workaround reported to work against some Supavisor versions. It is not guaranteed — verify it actually took effect by checking that the app starts (verifySearchPath fails fast with a clear error if it did not) rather than assuming success from the flag alone.
This flag changes nothing for a direct PostgreSQL connection or a PgBouncer setup already following the recipe above; both accept search_path and options equally, so there is no reason to enable it outside a Supavisor-fronted deployment.
PentAGI Cloud API Settings
These four are shared: one value in one .env, read by the installer and by the running PentAGI server alike. The server parses them into the Config struct as UpdateServerHost, UpdateStrategy, UpdateCheckInterval and SupportServerHost, and docker-compose.yml passes all four into the container; the installer's own calls read them straight from the file, and its Server Settings form edits every one of them except UPDATE_CHECK_INTERVAL, which concerns only the server and is set by hand. There is deliberately no per-component variant of any of them — an installation that pointed the installer at one endpoint and the server at another would be talking about itself in two places at once.
UPDATE_SERVER_HOST, together with INSTALLATION_ID and LICENSE_KEY, is behind every call either side makes to the PentAGI Cloud API: the update check the installer runs while gathering the state of the machine, the package info and download calls behind the installer build it offers, the same pair of calls used to fetch the Jaeger ClickHouse storage plugin whenever the observability stack is updated — that plugin is a file mounted into the container rather than an image, so a broken UPDATE_SERVER_HOST stops it from being installed as surely as it stops a check — and the running server's own periodic report, described under UPDATE_CHECK_INTERVAL. UPDATE_STRATEGY travels only in the update check; the package calls name a component, a version and a platform, and nothing else.
Both sides honour PROXY_URL. The installer's own proxy setting takes precedence over HTTP_PROXY/HTTPS_PROXY in the environment, because it is the one the operator configured deliberately and often the only one carrying credentials. See Network and Proxy Settings.
`UPDATE_STRATEGY`
Which builds this installation is offered. The value is trimmed and lower-cased before it is matched, so NIGHTLY and preview are accepted. Empty means preview; an unrecognised value logs a warning and also falls back to preview, deliberately, because a typo must not leave an installation quietly unable to hear about security updates.
| Value | What the installation is offered |
|---|---|
preview (default) |
Pre-release builds from the channels this installation already follows, offered as soon as they are published, without waiting for a release to be cut. The version, changelog and release notes that come with the answer still describe the stack's latest release, so they name the last release crossed and not the preview build itself. |
stable |
Only artefacts belonging to the latest curated stable release. The whole installation moves onto one released set, which is what lets it be described by a single version. A component that release does not cover is answered unknown with a reason rather than a newer build from elsewhere — offering part of an installation from outside the curated set is exactly the promise stable makes. |
nightly |
The newest build published, with no release attached to it. The answer carries no version metadata, so the update overview can report that a newer build exists and little else about it. |
The default is preview rather than stable, and the difference is not caution versus eagerness. Under stable the answer is measured against curated releases, so a stack with no published release is offered nothing at all — which reads as "no updates exist" when it means "we are not publishing releases for this yet". preview follows the channels the client is already on, which is what an installation wants until releases are something it can rely on.
The strategy is sent with each update check and applies to every stack in the answer, including the installer's own build — so it also decides which installer version is offered for download. Offered, not applied: the installer downloads that build next to the environment file under a name carrying its version, verifies it, and prints the command that moves it into place. It never writes over the binary it is running — a process cannot reliably replace its own executable, so "download the installer" and "update the installer" are the same operation, and putting the new build in place stays a deliberate step the operator takes.
`UPDATE_CHECK_INTERVAL`
How often the running server reports its own state to the Cloud API, as a Go duration (3h by default, 90m and 1h30m are the same value). 0, or any negative value, disables the report entirely.
This is the other half of the picture the update check cannot see. An installer's check describes the artefacts on the machine and says nothing about the server itself, so an installation deployed once and never started looks exactly like one in daily use. The report closes that gap: on each tick it sends the build that is running, the platform it was compiled for, the sha256 of that binary, and a summary of how the server is configured — quantities and closed vocabularies produced by GetInstanceSummary, nothing that identifies a person and nothing anybody wrote. Those first three are read off this process and travel inside the summary as well as beside it, because the same summary is what -info prints for a caller that will forward it: whoever carries a description cannot produce the platform of the process that wrote it, and the executable's digest is not something an image digest can stand in for.
The answer is kept in memory and shown in the web UI as a version badge — in the main sidebar header and at the bottom of the settings sidebar — read through the versionInfo GraphQL query (permission settings.view). The badge shows a yellow ↓ when the service names a newer version, and a yellow ? whenever no verdict is available: the first check has not completed yet, the service could not be reached, it answered without naming a version to move to, it could not match the running build to a published release, or checks are disabled. A verdict, once reached, outlives the failures that follow it — an update that was available an hour ago is still available — so a badge keeps its arrow through an outage and the popover dates both the last answer and the last failure. Nothing is applied from the UI: the tooltip and the popover say to run the installer.
Two properties are worth knowing when reading logs. The first send is delayed by a random wait of up to two minutes, so a fleet restarted at once does not arrive as one burst. And nothing here can interrupt the product: a missing or malformed INSTALLATION_ID means the service is never built, every later failure is a debug line, and the next tick simply tries again — an endpoint that is down or a licence that has expired are ordinary states of the world for something the server does not depend on.
`UPDATE_SERVER_HOST`
The address is host[:port] with no scheme — the connection is always TLS, so writing one is redundant. Empty, which is the normal case, means update.pentagi.com.
A full URL is still accepted because this setting historically held one: an https:// prefix is stripped, so is anything after the host (path, query string, fragment), surrounding whitespace is trimmed, and the remainder is lower-cased. What has to be left over is a host, and five shapes are refused outright rather than repaired:
| Value | Result |
|---|---|
http://update.example.com |
Rejected. The transport is HTTPS-only, and silently upgrading a value someone deliberately wrote as http would hide the misconfiguration instead of reporting it. |
Any other scheme (ftp://, tcp://, ...) |
Rejected as unsupported. |
https:// or https:///some/path |
Rejected — dropping the scheme and the path leaves nothing, so there is no host to connect to. |
user:[email protected] |
Rejected — credentials in the host are not supported. Authentication to the Cloud API is LICENSE_KEY, and a proxy's credentials belong in PROXY_URL. |
update .example.com (a space or a tab inside the host) |
Rejected. Whitespace around the value is trimmed, but whitespace inside it is a typo rather than an address. |
Writing a scheme is the common mistake here, and https://… being tolerated makes http://… look like it should work too. A rejected value does not abort the installer: the client is never built, the update check is recorded as failed with the reason unreachable, and the reason is written to the installer log. The visible symptom is therefore an unreachable update server, which is worth checking the log about before concluding the server is actually down.
The repairs and the refusals above are the installer's. The running server hands the value to the Cloud SDK as written — nothing normalises it and nothing reports it — so a value the SDK cannot build a client from costs one debug line at startup and then no product report at all. Write a bare host and both sides agree.
`SUPPORT_SERVER_HOST`
The PentAGI Support API endpoint, in exactly the shape UPDATE_SERVER_HOST takes: host[:port], no scheme, empty meaning support.pentagi.com. It is a separate service from the update endpoint and is configured separately, so that an installation reaching the Cloud API through a mirror or an internal proxy can point the two at different places.
It is written by the installer's Server Settings form and parsed by the server, and no call targets it yet — the support integration that will use it is not in place. Setting it today changes nothing; leaving it empty costs nothing either.
Usage Details
DatabaseURL: This is a critical setting used throughout the application for all database connections. It is used to:
- Initialize the single shared
sql.DBconnection pool inmain.go(used by both sqlcQueriesand GORM) - Seed the shared
pgxpool.Poolfor all pgvector stores (agent memory + knowledge API)
For connection pool sizing and operational monitoring commands see database.md §Connection Pooling.
- Initialize the single shared
// In main.go — one pool shared by sqlc Queries and GORM
db, err := sql.Open("postgres", cfg.DatabaseURL)
db.SetMaxOpenConns(cfg.DBMaxOpenConns)
db.SetMaxIdleConns(cfg.DBMaxIdleConns)
db.SetConnMaxLifetime(time.Hour)
queries := database.New(db)
orm, err := database.NewGorm(db) // GORM wraps the same *sql.DB
// Shared pgxpool for all pgvector stores
pgPoolConfig, _ := pgxpool.ParseConfig(cfg.DatabaseURL)
pgPoolConfig.MaxConns = int32(cfg.DBVectorMaxConns)
pgPool, _ := pgxpool.NewWithConfig(ctx, pgPoolConfig)
cfg.PgxPool = pgPool // passed to tools and router- Debug: Controls debug mode throughout the application, enabling additional logging and development features:
- Activates detailed logging in the router setup
- Can enable development endpoints and tools
// In router.go for enabling debug mode
if cfg.Debug {
// Enable debug features
}- DataDir: Specifies where PentAGI stores persistent data. This is used across multiple components:
- In
docker/client.gofor container volume mapping - For screenshots storage in
services.NewScreenshotService - In tools for file operations and data persistence
- In Docker container management for mapping volumes
- In
// In docker/client.go
dataDir, err := filepath.Abs(cfg.DataDir)
// In router.go for screenshot service
screenshotService := services.NewScreenshotService(orm, cfg.DataDir)
// In tools.go for various tools
dataDir: fte.cfg.DataDir- AskUser: A safety feature that, when enabled, requires explicit user confirmation before executing potentially destructive operations:
- Used in tools to prompt for confirmation before executing commands
- Serves as a safeguard for sensitive operations
// In tools.go
if fte.cfg.AskUser {
// Prompt user for confirmation before executing
}- InstallationID: A unique identifier for the PentAGI installation used for cloud API communication:
- Generated automatically during installation or can be manually set
- Required for certain cloud-based features and integrations
// Used in cloud SDK initialization
if cfg.InstallationID != "" {
// Initialize cloud API client with installation ID
}- LicenseKey: Authentication key for PentAGI Cloud API and premium feature activation:
- Validates license and enables licensed features
- Required for enterprise features and support
- Used for authentication with PentAGI Cloud services
// Used in cloud SDK initialization
if cfg.LicenseKey != "" {
// Validate license and activate premium features
}Docker Settings
These settings control how PentAGI interacts with Docker, which is used for terminal isolation and executing commands in a controlled environment. They're crucial for the security and functionality of tool execution.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| DockerInside | DOCKER_INSIDE |
false |
Set to true if PentAGI runs inside Docker and needs to access the host Docker daemon. |
| DockerNetAdmin | DOCKER_NET_ADMIN |
false |
Set to true to grant the primary container NET_ADMIN capability for advanced networking. |
| DockerSocket | DOCKER_SOCKET |
(none) | Path to Docker socket for container management |
| DockerInsideHost | DOCKER_INSIDE_HOST |
(none) | Docker daemon endpoint given to worker containers; also disables host-socket autodetection. See Worker Docker Access |
| DockerInsideTLSVerify | DOCKER_INSIDE_TLS_VERIFY |
(none) | TLS verification for the worker container's Docker connection |
| DockerInsideCertPath | DOCKER_INSIDE_CERT_PATH |
(none) | TLS certificate directory on the worker node, mounted read-only into worker containers |
| DockerInsidePolicyTests | DOCKER_INSIDE_POLICY_TESTS |
false |
Test the sandbox once at startup: the daemon must be separate from PentAGI's and must refuse host-escape requests. Its only effect is whether agents get Docker — a failure turns the sandbox off and PentAGI still starts. Off by default |
| DockerDefaultImageForTest | DOCKER_DEFAULT_IMAGE_FOR_TEST |
vxcontrol/kali-linux:test |
Worker the startup test runs in, and the image it pulls into the sandbox |
| DockerNetwork | DOCKER_NETWORK |
(none) | Docker network name for bridge mode, or host for host network mode. See network modes below. |
| DockerPublicIP | DOCKER_PUBLIC_IP |
0.0.0.0 |
Public IP address for Docker containers' port bindings (bridge mode only) |
| DockerWorkDir | DOCKER_WORK_DIR |
(none) | Custom working directory inside Docker containers |
| DockerDefaultImage | DOCKER_DEFAULT_IMAGE |
debian:latest |
Default Docker image for containers when specific images fail |
| DockerDefaultImageForPentest | DOCKER_DEFAULT_IMAGE_FOR_PENTEST |
vxcontrol/kali-linux |
Default Docker image for penetration testing tasks |
| TerminalToolTimeout | TERMINAL_TOOL_TIMEOUT |
1200 |
Default execution timeout in seconds applied when an agent requests timeout=0 or a negative value. Accepted range: 1–10800 (3 hours). Values <= 0 or above 10800 are clamped to the 3-hour maximum. Negative values are treated identically to 0. |
Worker Docker Access (`DOCKER_INSIDE_*`)
DOCKER_HOST, DOCKER_TLS_VERIFY and DOCKER_CERT_PATH tell PentAGI itself which daemon to create worker containers on. The DOCKER_INSIDE_* trio is the mirror image: it tells a worker container which daemon it may talk to. Both sets are independent — sandboxes can be pointed at a different daemon than the one that spawned them.
They are read only when DOCKER_INSIDE=true; with it disabled the sandbox gets no Docker access and no Docker configuration whatsoever.
How the socket is chosen. With DOCKER_INSIDE=true:
DOCKER_SOCKET |
DOCKER_INSIDE_HOST |
Result |
|---|---|---|
| set | empty | That socket is bind-mounted at /var/run/docker.sock; this legacy mode does not run the worker policy preflight. |
| set | set | Configuration error: the socket could bypass the designated worker daemon. |
| empty | set | Nothing is mounted. The sandbox reaches DOCKER_INSIDE_HOST and must pass the Docker API policy preflight before agent use. |
| empty | empty | A local Docker host keeps historical socket autodetection; with a remote DOCKER_HOST, initialization fails until a worker endpoint or explicit socket is configured. |
The separate-endpoint row is the point of the feature: mounting the host socket into a sandbox can give an autonomous agent control of the daemon running PentAGI itself, including every other flow's containers. A designated worker daemon must be distinct from the orchestration daemon and enforce the Docker API policy checked at worker startup. An explicit socket remains an unverified legacy exception.
What is injected. Every non-empty DOCKER_INSIDE_* value is passed into the container as an environment variable with the _INSIDE_ segment removed, so the Docker CLI inside picks it up with no extra configuration:
| Configured | Seen inside the worker container |
|---|---|
DOCKER_INSIDE_HOST=tcp://dind:2376 |
DOCKER_HOST=tcp://dind:2376 |
DOCKER_INSIDE_TLS_VERIFY=1 |
DOCKER_TLS_VERIFY=1 |
DOCKER_INSIDE_CERT_PATH=/certs/client |
DOCKER_CERT_PATH=/certs/client |
Empty values are omitted rather than injected blank.
When DOCKER_INSIDE_CERT_PATH is set, that directory is additionally bind-mounted read-only at the same path inside the container, so the injected DOCKER_CERT_PATH resolves unchanged. The path is resolved on the worker node — the machine whose daemon creates sandboxes — which may not be the machine running PentAGI or the installer. For that reason the installer does not verify it exists.
Example — sandboxes use a DinD sidecar over mutual TLS:
DOCKER_INSIDE=true
DOCKER_SOCKET= # leave empty so the host socket is not mounted
DOCKER_INSIDE_HOST=tcp://dind:2376
DOCKER_INSIDE_TLS_VERIFY=1
DOCKER_INSIDE_CERT_PATH=/certs/clientUsage Details
The Docker settings are primarily used in pkg/docker/client.go which implements the Docker client interface used throughout the application. This client is responsible for creating, managing, and executing commands in Docker containers:
DockerInside: Signals whether PentAGI is running inside a Docker container itself, which affects how volumes and sockets are mounted:
goinside := cfg.DockerInsideDockerSocket: Specifies the path to the Docker socket, which is crucial for container management:
goif cfg.DockerSocket != "" { socket = cfg.DockerSocket }TerminalToolTimeout: Sets the default execution timeout for terminal tool commands when the tool call uses
timeout=0or a negative value:goterm := NewTerminalTool( flowID, taskID, subtaskID, containerID, containerLID, dockerClient, termLogProvider, time.Duration(cfg.TerminalToolTimeout)*time.Second, )The value is clamped inside the terminal tool: values
<= 0or above10800s (3 hours) are silently raised/capped to the 3-hour maximum — agents always receive a finite timeout. Negative values are accepted at the environment level and treated identically to0(both resolve to the 3-hour ceiling). Explicittimeoutvalues provided by the tool call override this default when they are within the1–10800s range.DockerNetwork: Controls the network isolation mode for containers. Supports two modes:
Bridge Mode (custom network name, e.g.,
pentagi-network):- Containers run in an isolated bridge network
- Port forwarding maps container ports to host ports
- Enhanced security through network isolation
- Recommended for most deployments
Host Mode (special value:
host):- Containers share the host's network stack directly
- No port forwarding - services bind directly to host interfaces
- Required for advanced network testing (raw packets, custom protocols)
- Reduced isolation - use with caution
gonetwork := cfg.DockerNetwork // Host network mode if dc.network == "host" { hostConfig.NetworkMode = container.NetworkMode("host") // No port bindings needed } else if dc.network != "" { // Bridge mode with custom network networkingConfig = &network.NetworkingConfig{ EndpointsConfig: map[string]*network.EndpointSettings{ dc.network: {}, }, } // Port bindings are configured }DockerPublicIP: Defines the IP address to bind container ports to, making services accessible:
gopublicIP := cfg.DockerPublicIP // Used when setting up port bindings hostConfig.PortBindings[natPort] = []nat.PortBinding{ { HostIP: dc.publicIP, HostPort: fmt.Sprintf("%d", port), }, }DockerWorkDir: Provides a custom working directory path to use inside containers:
gohostDir := getHostDataDir(ctx, cli, dataDir, cfg.DockerWorkDir)DockerDefaultImage: Specifies the fallback image to use when requested images aren't available:
godefImage := strings.ToLower(cfg.DockerDefaultImage) if defImage == "" { defImage = defaultImage }
This client is used by the tools executor to run commands in isolated containers, providing a secure environment for AI agents to execute terminal commands.
Server Settings
These settings control the HTTP and GraphQL server that forms the backend API of PentAGI.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| ServerPort | SERVER_PORT |
8080 |
Port for the HTTP server |
| ServerHost | SERVER_HOST |
0.0.0.0 |
Host address for the HTTP server |
| ServerUseSSL | SERVER_USE_SSL |
false |
Enable SSL for the HTTP server |
| ServerSSLKey | SERVER_SSL_KEY |
(none) | Path to SSL key file |
| ServerSSLCrt | SERVER_SSL_CRT |
(none) | Path to SSL certificate file |
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| TrustedProxies | TRUSTED_PROXIES |
(none) | Comma-separated proxies whose X-Forwarded-For the server believes |
Left empty behind a reverse proxy, every request is attributed to the proxy's own
address, so the per-address login rate limit becomes a single budget shared by all
users. Set it to the addresses or CIDRs of the proxies in front of PentAGI.
Usage Details
These settings are used in main.go to configure and start the HTTP server:
// Build the listen address from host and port
listen := net.JoinHostPort(cfg.ServerHost, strconv.Itoa(cfg.ServerPort))
// Conditionally use TLS based on SSL configuration
if cfg.ServerUseSSL && cfg.ServerSSLCrt != "" && cfg.ServerSSLKey != "" {
err = r.RunTLS(listen, cfg.ServerSSLCrt, cfg.ServerSSLKey)
} else {
err = r.Run(listen)
}The settings determine:
- The IP address and port the server listens on
- Whether to use HTTPS (SSL/TLS) for secure connections
- The location of the SSL certificate and key files (when SSL is enabled)
These configurations are crucial for production deployments where proper server binding and secure communication are required.
Frontend Settings
These settings control how the server serves frontend assets and handles Cross-Origin Resource Sharing (CORS) for API requests from browsers.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| StaticURL | STATIC_URL |
(none) | URL to serve static frontend assets from (enables reverse proxy mode) |
| StaticDir | STATIC_DIR |
./fe |
Directory containing frontend static files (used when not in proxy mode) |
| CorsOrigins | CORS_ORIGINS |
* |
Allowed origins for CORS requests (comma-separated) |
Usage Details
The frontend settings are extensively used in pkg/server/router.go for configuring how the application serves the frontend:
StaticURL: When set, enables reverse proxy mode where static assets are served from an external URL:
goif cfg.StaticURL != nil && cfg.StaticURL.Scheme != "" && cfg.StaticURL.Host != "" { // Set up reverse proxy for static assets router.NoRoute(func(c *gin.Context) { req := c.Request.Clone(c.Request.Context()) req.URL.Scheme = cfg.StaticURL.Scheme req.URL.Host = cfg.StaticURL.Host // ... }) }StaticDir: When StaticURL is not set, specifies the local directory containing static frontend assets:
go// Serve static files from local directory router.Use(static.Serve("/", static.LocalFile(cfg.StaticDir, true))) // Also used for finding index.html for SPA routes indexPath := filepath.Join(cfg.StaticDir, "index.html")CorsOrigins: Configures CORS policy for the API, controlling which origins can make requests:
go// In GraphQL service initialization graphqlService := services.NewGraphqlService(db, baseURL, cfg.CorsOrigins, providers, controller, subscriptions) // In CORS middleware configuration if !slices.Contains(cfg.CorsOrigins, "*") { config.AllowCredentials = true } config.AllowOrigins = cfg.CorsOrigins
These settings are essential for:
- Supporting different deployment architectures (single server vs. separate frontend/backend)
- Enabling proper SPA routing for frontend applications
- Configuring security policies for cross-origin requests
Authentication Settings
These settings control authentication mechanisms, including cookie-based sessions and OAuth providers for user login.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| CookieSigningSalt | COOKIE_SIGNING_SALT |
(none) | Salt for signing and securing cookies used in sessions |
| PublicURL | PUBLIC_URL |
(none) | Public origin/base URL used to build OAuth callback URLs such as /api/v1/auth/login-callback |
| OAuthGoogleClientID | OAUTH_GOOGLE_CLIENT_ID |
(none) | Google OAuth client ID for authentication |
| OAuthGoogleClientSecret | OAUTH_GOOGLE_CLIENT_SECRET |
(none) | Google OAuth client secret |
| OAuthGithubClientID | OAUTH_GITHUB_CLIENT_ID |
(none) | GitHub OAuth client ID for authentication |
| OAuthGithubClientSecret | OAUTH_GITHUB_CLIENT_SECRET |
(none) | GitHub OAuth client secret |
Usage Details
The authentication settings are used in pkg/server/router.go to set up authentication middleware and OAuth providers:
CookieSigningSalt: Used to secure cookies for session management:
go// Used in auth middleware for authentication checks authMiddleware := auth.NewAuthMiddleware(baseURL, cfg.CookieSigningSalt, tokenCache, userCache) // Used for cookie store creation cookieStore := cookie.NewStore(auth.MakeCookieStoreKey(cfg.CookieSigningSalt)...) router.Use(sessions.Sessions("auth", cookieStore))PublicURL: The public origin/base URL for OAuth callback endpoints, crucial for redirects after authentication:
gopublicURL, err := url.Parse(cfg.PublicURL)The router builds the login callback path under the API base URL:
gooauthLoginCallbackURL := "/auth/login-callback" publicURL, err := url.Parse(cfg.PublicURL) if err == nil { publicURL.Path = path.Join(baseURL, oauthLoginCallbackURL) }In the default deployment, configure your OAuth providers with:
- Homepage URL:
PUBLIC_URL - Authorization callback URL / Redirect URI:
${PUBLIC_URL}/api/v1/auth/login-callback
Example:
bashPUBLIC_URL=https://pentagi.example.com OAUTH_GITHUB_CLIENT_ID=your_github_client_id OAUTH_GITHUB_CLIENT_SECRET=your_github_client_secret OAUTH_GOOGLE_CLIENT_ID=your_google_client_id OAUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret- Homepage URL:
OAuth Provider Settings: Used to configure authentication with Google and GitHub:
go// Google OAuth setup if publicURL != nil && cfg.OAuthGoogleClientID != "" && cfg.OAuthGoogleClientSecret != "" { googleClient := oauth.NewGoogleOAuthClient( cfg.OAuthGoogleClientID, cfg.OAuthGoogleClientSecret, publicURL.String(), ) // ... } // GitHub OAuth setup if publicURL != nil && cfg.OAuthGithubClientID != "" && cfg.OAuthGithubClientSecret != "" { githubClient := oauth.NewGithubOAuthClient( cfg.OAuthGithubClientID, cfg.OAuthGithubClientSecret, publicURL.String(), ) // ... }Google and GitHub both use the same PentAGI login callback endpoint.
PUBLIC_URLshould be the externally reachable base URL only, without an extra path suffix. If the URL configured in the provider console does not exactly match the generated callback URL, authentication will fail with a redirect URI mismatch error.
These settings are essential for:
- Secure user authentication and session management
- Supporting social login through OAuth providers
- Enabling proper redirects in the authentication flow
Web Scraper Settings
These settings control the web scraper service used for browsing websites and taking screenshots, which allows AI agents to interact with web content.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| ScraperPublicURL | SCRAPER_PUBLIC_URL |
(none) | Public URL for accessing the scraper service from clients |
| ScraperPrivateURL | SCRAPER_PRIVATE_URL |
(none) | Private URL for internal scraper service access |
Usage Details
The scraper settings are extensively used in the tools executor to provide web browsing capabilities to AI agents:
// In various tool functions in pkg/tools/tools.go
browseTool = &functions.BrowseFunc{
scPrvURL: fte.cfg.ScraperPrivateURL,
scPubURL: fte.cfg.ScraperPublicURL,
// ...
}
screenshotTool = &functions.ScreenshotFunc{
scPrvURL: fte.cfg.ScraperPrivateURL,
scPubURL: fte.cfg.ScraperPublicURL,
// ...
}These URLs serve different purposes:
- ScraperPublicURL: Used when generating URLs that will be accessed by the client (browser)
- ScraperPrivateURL: Used for internal communication between the backend and the scraper service
The scraper settings enable critical functionality:
- Web browsing capabilities for AI agents
- Screenshot capturing for web content analysis
- Web information gathering for research tasks
LLM Provider Settings
These settings control the integration with various Large Language Model (LLM) providers, including OpenAI, Anthropic, and custom providers.
OpenAI
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| OpenAIKey | OPEN_AI_KEY |
(none) | API key for OpenAI services |
| OpenAIServerURL | OPEN_AI_SERVER_URL |
https://api.openai.com/v1 |
Server URL for OpenAI API requests |
Anthropic
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| AnthropicAPIKey | ANTHROPIC_API_KEY |
(none) | API key for Anthropic Claude services |
| AnthropicServerURL | ANTHROPIC_SERVER_URL |
https://api.anthropic.com/v1 |
Server URL for Anthropic API requests |
Workload Identity Federation
Instead of a long-lived key, the Anthropic provider can exchange a short-lived identity token issued by your identity provider (AWS, Google Cloud, Azure, GitHub Actions, Kubernetes, any OIDC issuer) for a Claude API access token, refreshed before it expires. The variables are the ones the vendor SDKs read; see Workload Identity Federation for creating the service account, issuer and rule.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| AnthropicFederationRuleID | ANTHROPIC_FEDERATION_RULE_ID |
(none) | Federation rule to exchange under (fdrl_...) |
| AnthropicOrganizationID | ANTHROPIC_ORGANIZATION_ID |
(none) | Anthropic organization UUID |
| AnthropicServiceAccountID | ANTHROPIC_SERVICE_ACCOUNT_ID |
(none) | Service account the minted token acts as (svac_...) |
| AnthropicWorkspaceID | ANTHROPIC_WORKSPACE_ID |
(none) | Workspace to scope the token to; required when the rule covers more than one workspace |
| AnthropicIdentityTokenFile | ANTHROPIC_IDENTITY_TOKEN_FILE |
(none) | Path to the identity token, re-read on every exchange; takes precedence over the token below |
| AnthropicIdentityToken | ANTHROPIC_IDENTITY_TOKEN |
(none) | The identity token itself; read once, so only for runs shorter than its lifetime |
Federation is used only while ANTHROPIC_API_KEY is empty: a non-empty key shadows it, and the server log says so at startup. A partial federation config leaves the provider disabled and the log names the missing variables. The token file path is read inside the pentagi container, so mount the file there. The exchange goes to ANTHROPIC_SERVER_URL + /oauth/token, so federation works against the Anthropic API itself and not through a gateway.
Every provider, flow and settings test that uses the same federation rule, organization, service account, workspace and server URL shares one access token, exchanged once and refreshed the way the vendor SDKs do: from two minutes before it expires a failed exchange keeps serving the cached token and logs a warning that names the time calls start to fail, and from 30 seconds before it the call fails. Both points move closer for a token that lives only a few minutes, so it is not exchanged again on every call.
An identity token that carries a jti claim, as Kubernetes service-account and GitHub Actions tokens do, can be exchanged only once; presenting it again is refused with jti_reused. The token file must therefore hold a new token before each refresh, so the platform has to rotate it well within the lifetime of the minted token: the rule's token_lifetime_seconds, capped at twice the remaining lifetime of the identity token. A Kubernetes projected token is rotated at 80% of its expirationSeconds, about 48 minutes by default, which is too slow for a rule that mints ten-minute tokens.
ANTHROPIC_IDENTITY_TOKEN is read once at startup and cannot be rotated: once the identity token expires, and after its first exchange if it carries jti, the provider cannot mint a new access token. It suits only runs shorter than the identity token's lifetime, the server log warns about it at startup and names the expiry, and a long-running server needs the token file.
Note on Google Vertex AI: PentAGI does not currently expose a dedicated Vertex AI configuration path for Anthropic Claude in .env. The variables above target the direct Anthropic API. To run Claude through a non-Anthropic-hosted backend, use one of:
- AWS Bedrock: see the AWS Bedrock LLM Provider section below and configure the
BEDROCK_*variables. - OpenAI-compatible gateway in front of Vertex AI: expose Vertex AI through a proxy or gateway that translates requests into the Chat Completions format while preserving the chat and tool-call behavior PentAGI requires, then configure it as a custom LLM provider (
LLM_SERVER_URL,LLM_SERVER_KEY,LLM_SERVER_MODEL). Reliability of this path depends on the gateway you choose.
There is no VERTEX_API_KEY or GOOGLE_APPLICATION_CREDENTIALS variable wired into PentAGI's provider initialization today.
Ollama LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| OllamaServerURL | OLLAMA_SERVER_URL |
(none) | Ollama server URL (local or cloud https://ollama.com) |
| OllamaServerAPIKey | OLLAMA_SERVER_API_KEY |
(none) | Ollama Cloud API key (optional, required for https://ollama.com) |
| OllamaServerModel | OLLAMA_SERVER_MODEL |
(none) | Default model to use for inference |
| OllamaServerConfig | OLLAMA_SERVER_CONFIG_PATH |
(none) | Path to config file for Ollama provider options |
| OllamaServerPullModelsTimeout | OLLAMA_SERVER_PULL_MODELS_TIMEOUT |
600 |
Timeout in seconds for model downloads |
| OllamaServerPullModelsEnabled | OLLAMA_SERVER_PULL_MODELS_ENABLED |
false |
Automatically download required models on startup |
| OllamaServerLoadModelsEnabled | OLLAMA_SERVER_LOAD_MODELS_ENABLED |
false |
Load available models list from server API |
Deployment Scenarios:
- Local Server: Set
OLLAMA_SERVER_URLto local endpoint (e.g.,http://ollama-server:11434), leaveOLLAMA_SERVER_API_KEYempty - Ollama Cloud: Set
OLLAMA_SERVER_URL=https://ollama.comand provideOLLAMA_SERVER_API_KEYfrom https://ollama.com/settings/keys
Note: When OllamaServerLoadModelsEnabled=false, only the default model is available. Enable this to see all installed models in the UI.
Google AI (Gemini) LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| GeminiAPIKey | GEMINI_API_KEY |
(none) | API key for Google AI Gemini services |
| GeminiServerURL | GEMINI_SERVER_URL |
https://generativelanguage.googleapis.com |
Server URL for Gemini API requests |
AWS Bedrock LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| BedrockRegion | BEDROCK_REGION |
us-east-1 |
AWS region for Bedrock service |
| BedrockDefaultAuth | BEDROCK_DEFAULT_AUTH |
false |
Use default AWS SDK credential chain (environment variables, EC2 role, ~/.aws/credentials) - highest priority |
| BedrockBearerToken | BEDROCK_BEARER_TOKEN |
(none) | Bearer token for authentication - takes priority over static credentials |
| BedrockAccessKey | BEDROCK_ACCESS_KEY_ID |
(none) | AWS access key ID for static credentials authentication |
| BedrockSecretKey | BEDROCK_SECRET_ACCESS_KEY |
(none) | AWS secret access key for static credentials authentication |
| BedrockSessionToken | BEDROCK_SESSION_TOKEN |
(none) | AWS session token for temporary credentials (optional, used with static credentials for STS/assumed roles) |
| BedrockServerURL | BEDROCK_SERVER_URL |
(none) | Optional custom endpoint URL for Bedrock service (VPC endpoints, local testing) |
| BedrockConfig | BEDROCK_CONFIG_PATH |
(none) | Path to a custom YAML config that replaces the built-in Bedrock per-agent config (model assignments, prices) |
Authentication Priority: BedrockDefaultAuth (highest) → BedrockBearerToken → BedrockAccessKey+BedrockSecretKey (lowest)
DeepSeek LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| DeepSeekAPIKey | DEEPSEEK_API_KEY |
(none) | DeepSeek API key for authentication |
| DeepSeekServerURL | DEEPSEEK_SERVER_URL |
https://api.deepseek.com |
DeepSeek API endpoint URL |
| DeepSeekProvider | DEEPSEEK_PROVIDER |
(none) | Provider name prefix for LiteLLM integration (optional) |
LiteLLM Integration: Set DEEPSEEK_PROVIDER=deepseek to enable model prefixing (e.g., deepseek/deepseek-flash) when using LiteLLM proxy with default PentAGI configs.
GLM LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| GLMAPIKey | GLM_API_KEY |
(none) | GLM API key for authentication |
| GLMServerURL | GLM_SERVER_URL |
https://api.z.ai/api/paas/v4 |
GLM API endpoint URL (international) |
| GLMProvider | GLM_PROVIDER |
(none) | Provider name prefix for LiteLLM integration (optional) |
Alternative Endpoints:
- International:
https://api.z.ai/api/paas/v4(default) - China:
https://open.bigmodel.cn/api/paas/v4 - Coding-specific:
https://api.z.ai/api/coding/paas/v4
LiteLLM Integration: Set GLM_PROVIDER=zai to enable model prefixing (e.g., zai/glm-4) when using LiteLLM proxy with default PentAGI configs.
Kimi LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| KimiAPIKey | KIMI_API_KEY |
(none) | Kimi API key for authentication |
| KimiServerURL | KIMI_SERVER_URL |
https://api.moonshot.ai/v1 |
Kimi API endpoint URL (international) |
| KimiProvider | KIMI_PROVIDER |
(none) | Provider name prefix for LiteLLM integration (optional) |
Alternative Endpoints:
- International:
https://api.moonshot.ai/v1(default) - China:
https://api.moonshot.cn/v1
LiteLLM Integration: Set KIMI_PROVIDER=moonshot to enable model prefixing (e.g., moonshot/kimi-k2.6) when using LiteLLM proxy with default PentAGI configs.
Qwen LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| QwenAPIKey | QWEN_API_KEY |
(none) | Qwen Cloud, DashScope, or gateway API key |
| QwenServerURL | QWEN_SERVER_URL |
https://dashscope-us.aliyuncs.com/compatible-mode/v1 |
Direct DashScope or OpenAI-compatible gateway URL |
| QwenProvider | QWEN_PROVIDER |
(none) | Gateway model prefix: qwen_cloud or dashscope |
Alternative Endpoints:
- US:
https://dashscope-us.aliyuncs.com/compatible-mode/v1(default) - Singapore:
https://dashscope-intl.aliyuncs.com/compatible-mode/v1 - China:
https://dashscope.aliyuncs.com/compatible-mode/v1
LiteLLM Integration: Set QWEN_PROVIDER=qwen_cloud for Qwen Cloud routes or QWEN_PROVIDER=dashscope for Alibaba Cloud routes. The bundled role configuration uses only model IDs shared by both platforms.
MiniMax LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| MiniMaxAPIKey | MINIMAX_API_KEY |
(none) | MiniMax API key for authentication |
| MiniMaxServerURL | MINIMAX_SERVER_URL |
https://api.minimax.io/v1 |
MiniMax API endpoint URL |
| MiniMaxProvider | MINIMAX_PROVIDER |
(none) | Provider name prefix for LiteLLM integration (optional) |
LiteLLM Integration: Set MINIMAX_PROVIDER=minimax to enable model prefixing (e.g., minimax/MiniMax-M3) when using LiteLLM proxy with default PentAGI configs.
Mistral LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| MistralAPIKey | MISTRAL_API_KEY |
(none) | Mistral API key for authentication |
| MistralServerURL | MISTRAL_SERVER_URL |
https://api.mistral.ai/v1 |
Mistral API endpoint URL |
| MistralProvider | MISTRAL_PROVIDER |
(none) | Provider name prefix for LiteLLM integration (optional) |
LiteLLM Integration: Set MISTRAL_PROVIDER=mistral to enable model prefixing (e.g., mistral/mistral-medium-latest) when using LiteLLM proxy with default PentAGI configs.
xAI (Grok) LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| XAIAPIKey | XAI_API_KEY |
(none) | xAI API key for authentication |
| XAIServerURL | XAI_SERVER_URL |
https://api.x.ai/v1 |
xAI API endpoint URL |
| XAIProvider | XAI_PROVIDER |
(none) | Provider name prefix for LiteLLM integration (optional) |
LiteLLM Integration: Set XAI_PROVIDER=xai to enable model prefixing (e.g., xai/grok-4.3) when using LiteLLM proxy with default PentAGI configs.
Custom LLM Provider
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| LLMServerURL | LLM_SERVER_URL |
(none) | Server URL for custom LLM provider |
| LLMServerKey | LLM_SERVER_KEY |
(none) | API key for custom LLM provider |
| LLMServerModel | LLM_SERVER_MODEL |
(none) | Model name for custom LLM provider |
| LLMServerConfig | LLM_SERVER_CONFIG_PATH |
(none) | Path to config file for custom LLM provider options |
| LLMServerProvider | LLM_SERVER_PROVIDER |
(none) | Provider name prefix for model names (useful for LiteLLM proxy) |
| LLMServerPreserveReasoning | LLM_SERVER_PRESERVE_REASONING |
false |
Preserve reasoning content in multi-turn conversations (required by some providers) |
| LLMServerAPIType | LLM_SERVER_API_TYPE |
(none) | azure or azure_ad to talk to an Azure OpenAI deployment; empty for a plain OpenAI-compatible endpoint |
| LLMServerAPIVersion | LLM_SERVER_API_VERSION |
2024-10-21 |
Azure OpenAI API version, used only when LLM_SERVER_API_TYPE names an Azure mode |
Usage Details
The LLM provider settings are used in pkg/providers modules to initialize and configure the appropriate language model providers:
OpenAI Settings: Used in
pkg/providers/openai/openai.goto create the OpenAI client:gobaseURL := cfg.OpenAIServerURL client, err := openai.New( openai.WithToken(cfg.OpenAIKey), openai.WithModel(OpenAIAgentModel), openai.WithBaseURL(baseURL), // ... )Anthropic Settings: Used in
pkg/providers/anthropic/anthropic.goto create the Anthropic client:gobaseURL := cfg.AnthropicServerURL client, err := anthropic.New( anthropic.WithToken(cfg.AnthropicAPIKey), anthropic.WithBaseURL(baseURL), // ... )Ollama Settings: Used in
pkg/providers/ollama/ollama.goto create the Ollama client:goserverURL := cfg.OllamaServerURL client, err := ollama.New( ollama.WithServerURL(serverURL), ollama.WithHTTPClient(httpClient), ollama.WithModel(OllamaAgentModel), ollama.WithPullModel(), ) // Load provider options from config file if specified if cfg.OllamaServerConfig != "" { configData, err := os.ReadFile(cfg.OllamaServerConfig) providerConfig, err := BuildProviderConfig(cfg, configData) // ... }Gemini Settings: Used in
pkg/providers/gemini/gemini.goto create the Google AI client:goopts := []googleai.Option{ googleai.WithRest(), googleai.WithAPIKey(cfg.GeminiAPIKey), googleai.WithEndpoint(cfg.GeminiServerURL), googleai.WithDefaultModel(GeminiAgentModel), } client, err := googleai.New(context.Background(), opts...)Bedrock Settings: Used in
pkg/providers/bedrock/bedrock.goto create the AWS Bedrock client:goopts := []func(*bconfig.LoadOptions) error{ bconfig.WithRegion(cfg.BedrockRegion), bconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider( cfg.BedrockAccessKey, cfg.BedrockSecretKey, cfg.BedrockSessionToken, )), } if cfg.BedrockServerURL != "" { opts = append(opts, bconfig.WithBaseEndpoint(cfg.BedrockServerURL)) } bcfg, err := bconfig.LoadDefaultConfig(context.Background(), opts...) bclient := bedrockruntime.NewFromConfig(bcfg) client, err := bedrock.New( bedrock.WithClient(bclient), bedrock.WithModel(BedrockAgentModel), bedrock.WithConverseAPI(), )The
BedrockSessionTokenis optional and only required when using temporary AWS credentials (e.g., from STS, assumed roles, or MFA-enabled IAM users). For permanent IAM user credentials, leave this field empty.Custom LLM Settings: Used in
pkg/providers/custom/custom.goto create a custom LLM client:gobaseKey := cfg.LLMServerKey baseURL := cfg.LLMServerURL baseModel := cfg.LLMServerModel client, err := openai.New( openai.WithToken(baseKey), openai.WithModel(baseModel), openai.WithBaseURL(baseURL), // ... ) // Load provider options from config file if specified if cfg.LLMServerConfig != "" { providerConfig, err := LoadConfig(cfg.LLMServerConfig, simple) // ... }LLMServerPreserveReasoning: Controls whether reasoning content is preserved and sent back in multi-turn conversations:
go// Used in custom provider to preserve reasoning content if cfg.LLMServerPreserveReasoning { // Preserves and returns reasoning_content in assistant messages }false(default): Reasoning content is not preserved in conversation historytrue: Reasoning content is preserved and sent in subsequent API calls
This setting is required by some LLM providers (e.g., Moonshot) that return errors like "thinking is enabled but reasoning_content is missing in assistant tool call message" when reasoning content is not included in multi-turn conversations. Enable this setting if your provider requires reasoning content to be preserved across conversation turns.
Provider registration is table-driven. pkg/providers/registry.go holds one entry per
provider type and pkg/providers/providers.go walks it; nothing enumerates providers by hand:
// pkg/providers/registry.go
type registryEntry struct {
Type provider.ProviderType
Name provider.ProviderName
Enabled func(*config.Config) bool
NewConfig func(*config.Config) (*pconfig.ProviderConfig, error)
New func(*config.Config, provider.ProviderName, *pconfig.ProviderConfig) (provider.Provider, error)
BuildConfig func(*config.Config, []byte) (*pconfig.ProviderConfig, error)
}
var providerRegistry = []registryEntry{
{
Type: provider.ProviderOpenAI,
Name: provider.DefaultProviderNameOpenAI,
Enabled: func(c *config.Config) bool { return c.OpenAIKey != "" },
NewConfig: ignoreConfig(openai.DefaultProviderConfig),
New: openai.New,
BuildConfig: fromData(openai.BuildProviderConfig),
},
// ... one entry per provider type
}Enabled is where the credential gate lives — a deployment without a given key simply ends
up with fewer providers rather than a failed startup:
// pkg/providers/providers.go
for _, e := range providerRegistry {
if !e.Enabled(cfg) {
continue
}
p, err := e.New(cfg, e.Name, defaultConfigs[e.Type])
if err != nil {
return nil, fmt.Errorf("failed to create %s provider: %w", e.Type, err)
}
providers[e.Name] = p
}defaultConfigs comes from the same table: buildDefaultConfigs calls every entry'sNewConfig beforehand, and a config that fails to load aborts startup unless that provider
is disabled anyway, in which case the reason is recorded and reported later. BuildConfig
is the same construction from stored bytes, used through entryForType when a user-defined
provider or a provider test rebuilds one at request time.
An entry may also wrap its constructor. The custom provider's does, to hand the transport the
same catalogue enrichment the picker sees — its models come from the gateway at runtime, so
the reasoning metadata the bundled models.yml files carry has to be folded in on the way.
Adding a provider is therefore adding one entry; the full checklist is in the repository rootCLAUDE.md under "Adding a New LLM Provider".
These settings are critical for:
- Connecting to various LLM providers for AI capabilities
- Supporting multiple model options for different tasks
- Enabling custom or self-hosted LLM solutions
- Configuring specific model behaviors and parameters
Embedding Settings
These settings control the vector embedding service used for semantic search and similarity matching, which is fundamental for PentAGI's intelligent search capabilities.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| EmbeddingURL | EMBEDDING_URL |
(none) | Server URL for embedding provider (overrides provider-specific URLs) |
| EmbeddingKey | EMBEDDING_KEY |
(none) | API key for embedding provider (overrides provider-specific keys) |
| EmbeddingModel | EMBEDDING_MODEL |
(none) | Model name for embedding generation |
| EmbeddingStripNewLines | EMBEDDING_STRIP_NEW_LINES |
true |
Whether to strip newlines before embedding (improves quality) |
| EmbeddingBatchSize | EMBEDDING_BATCH_SIZE |
512 |
Batch size for embedding operations (affects memory usage and performance) |
| EmbeddingProvider | EMBEDDING_PROVIDER |
openai |
Provider for embeddings (openai, ollama, mistral, jina, huggingface) |
| EmbeddingMaxTextBytes | EMBEDDING_MAX_TEXT_BYTES |
8192 |
Maximum byte size of text sent to the embedding model per document. Acts as a byte-level proxy for token limits (e.g. 8192 tokens for OpenAI models). When a stored document exceeds this limit the heavy content field (Guide/Answer/Code) is truncated to fit before computing the vector; the full original text is always preserved in the database. Reduce if your model has a smaller context window. |
Usage Details
The embedding settings are extensively used in pkg/providers/embeddings/embedder.go to configure the vector embedding service:
EmbeddingProvider: Determines which embedding provider to use:
goswitch cfg.EmbeddingProvider { case "openai": return newOpenAIEmbedder(ctx, cfg) case "ollama": return newOllamaEmbedder(ctx, cfg) case "mistral": return newMistralEmbedder(ctx, cfg) case "jina": return newJinaEmbedder(ctx, cfg) case "huggingface": return newHuggingFaceEmbedder(ctx, cfg) default: return &embedder{nil}, fmt.Errorf("unsupported embedding provider: %s", cfg.EmbeddingProvider) }Provider-specific configurations: Used to configure each embedding provider with appropriate options:
go// Example for OpenAI embeddings if cfg.EmbeddingURL != "" { opts = append(opts, openai.WithBaseURL(cfg.EmbeddingURL)) } else if cfg.OpenAIServerURL != "" { opts = append(opts, openai.WithBaseURL(cfg.OpenAIServerURL)) } if cfg.EmbeddingKey != "" { opts = append(opts, openai.WithToken(cfg.EmbeddingKey)) } else if cfg.OpenAIKey != "" { opts = append(opts, openai.WithToken(cfg.OpenAIKey)) } if cfg.EmbeddingModel != "" { opts = append(opts, openai.WithEmbeddingModel(cfg.EmbeddingModel)) }Embedding behavior configuration: Controls how text is processed for embeddings:
goembeddings.WithStripNewLines(cfg.EmbeddingStripNewLines), embeddings.WithBatchSize(cfg.EmbeddingBatchSize),
These settings are essential for:
- Configuring semantic search capabilities
- Determining which embedding model to use
- Optimizing embedding performance and quality
- Supporting multiple embedding providers for flexibility
Summarizer Settings
These settings control the text summarization behavior used for condensing long conversations and improving context management in AI interactions. The summarization system is a critical component that allows PentAGI to maintain coherent, long-running conversations while managing token usage effectively.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| SummarizerPreserveLast | SUMMARIZER_PRESERVE_LAST |
true |
Preserve the last message in summarization |
| SummarizerUseQA | SUMMARIZER_USE_QA |
true |
Use question-answer format for summarization |
| SummarizerSumHumanInQA | SUMMARIZER_SUM_MSG_HUMAN_IN_QA |
false |
Include human messages in QA summaries |
| SummarizerLastSecBytes | SUMMARIZER_LAST_SEC_BYTES |
51200 |
Bytes to preserve from the last section (50KB) |
| SummarizerMaxBPBytes | SUMMARIZER_MAX_BP_BYTES |
16384 |
Maximum bytes for bullet points summarization (16KB) |
| SummarizerMaxQASections | SUMMARIZER_MAX_QA_SECTIONS |
10 |
Maximum QA sections to include |
| SummarizerMaxQABytes | SUMMARIZER_MAX_QA_BYTES |
65536 |
Maximum bytes for QA summarization (64KB) |
| SummarizerKeepQASections | SUMMARIZER_KEEP_QA_SECTIONS |
1 |
Number of recent QA sections to keep without summarization |
Usage Details and Impact on System Behavior
The summarizer settings map directly to the SummarizerConfig structure that controls the chain summarization algorithm in pkg/csum. These settings work together to implement a sophisticated, multi-strategy approach to managing conversation context:
Core Summarization Strategies and Their Parameters
Section Summarization - Always active, ensures all older sections (except the last one) consist of a single summarized pair
- No specific parameters control this as it's a fundamental part of the algorithm
- Prevents unbounded growth by consolidating completed conversation sections
Last Section Management (
SummarizerPreserveLastandSummarizerLastSecBytes)- Controls how the current/active conversation section is managed
- When
SummarizerPreserveLast = true, older messages within the last section will be summarized when the section exceedsSummarizerLastSecBytesbytes - A reserve space of 25% is automatically maintained to accommodate new messages without triggering frequent re-summarization
- Individual oversized pairs are summarized separately if they exceed
SummarizerMaxBPBytes
QA Pair Summarization (
SummarizerUseQA,SummarizerMaxQASections,SummarizerMaxQABytes,SummarizerSumHumanInQA)- When
SummarizerUseQA = true, creates larger summarization units focused on question-answer patterns - Preserves the most recent
SummarizerMaxQASectionssections as long as they don't exceedSummarizerMaxQABytestotal - If
SummarizerSumHumanInQA = true, human messages are also summarized; otherwise, they're preserved verbatim
- When
Deep Dive: Parameter Impact and Recommendations
SummarizerPreserveLast (Default: true)
- Purpose: Controls whether the last (active) section has size management applied
- Impact: When enabled, prevents the active conversation from growing indefinitely
- When to adjust:
- Enable (default) for production systems and long-running conversations
- Disable only for debugging or when you need to preserve the complete conversation history regardless of size
SummarizerLastSecBytes (Default: 51200 - 50KB)
- Purpose: Maximum byte size for the last (active) section before summarization begins
- Impact: Directly controls how much conversation history is preserved verbatim in the active section
- When to adjust:
- Increase for models with larger context windows to maintain more conversation detail
- Decrease for models with smaller context to prevent token limits from being exceeded
- Balance with
SummarizerMaxBPBytesto ensure coherent summarization
SummarizerMaxBPBytes (Default: 16384 - 16KB)
- Purpose: Maximum byte size for individual body pairs (typically AI responses)
- Impact: Controls when individual large responses get summarized, even if the overall section is under limit
- When to adjust:
- Increase if your use case involves long but important AI responses that shouldn't be summarized
- Decrease if you want more aggressive summarization of lengthy responses
SummarizerUseQA (Default: true)
- Purpose: Enables question-answer style summarization that creates more cohesive summaries
- Impact: When enabled, creates a new first section with a summary of older interactions, preserving recent sections
- When to adjust:
- Enable (default) for more coherent, organized summaries focused on main topics
- Disable if you prefer simpler, section-by-section summarization without cross-section analysis
SummarizerMaxQASections (Default: 10)
- Purpose: Maximum number of recent sections to preserve when using QA-style summarization
- Impact: Directly controls how many conversation turns remain intact after QA summarization
- When to adjust:
- Increase to preserve more recent conversation context (at the cost of token usage)
- Decrease to create more compact conversation histories, focusing on only the very recent exchanges
SummarizerMaxQABytes (Default: 65536 - 64KB)
- Purpose: Maximum total byte size for preserved sections in QA-style summarization
- Impact: Sets an upper bound on memory used by preserved sections, regardless of section count
- When to adjust:
- Increase for models with larger context windows or when detailed context is essential
- Decrease for smaller context models or when prioritizing efficiency over context preservation
SummarizerSumHumanInQA (Default: false)
- Purpose: Controls whether human messages are summarized in QA-style summarization
- Impact: When false, human messages are preserved verbatim; when true, they are also summarized
- When to adjust:
- Keep disabled (default) to preserve the exact wording of user queries
- Enable only when human messages are very verbose and token efficiency is critical
SummarizerKeepQASections (Default: 1)
- Purpose: Controls the number of recent QA sections to keep without summarization
- Impact: Directly controls how many recent conversation turns are preserved verbatim
- When to adjust:
- Increase to preserve more recent conversation context
- Decrease to create more compact conversation histories, focusing on only the very recent exchanges
Summarization Effects on Agent Behavior
The summarization settings have significant effects on agent behavior:
Context Retention vs. Token Efficiency
- More aggressive summarization (smaller byte limits) reduces token usage but may lose context details
- More permissive settings (larger byte limits) preserve more context but increase token consumption
Conversation Coherence
- Appropriate summarization helps the agent maintain a coherent understanding of the conversation
- Over-aggressive summarization may cause the agent to lose important details or previous instructions
- Under-aggressive summarization may lead to context overflow in longer conversations
Response Quality
- QA-style summarization (
SummarizerUseQA = true) typically improves response quality for complex tasks - Preserving human messages (
SummarizerSumHumanInQA = false) helps maintain alignment with user intent - Appropriate
SummarizerMaxBPBytesprevents loss of detailed information from complex AI responses
- QA-style summarization (
Implementation Details
The summarizer settings are used in pkg/providers/providers.go to configure the summarization behavior:
summarizer := provider.SummarizerSettings{
PreserveLast: cfg.SummarizerPreserveLast,
UseQA: cfg.SummarizerUseQA,
SummHumanInQA: cfg.SummarizerSumHumanInQA,
LastSecBytes: cfg.SummarizerLastSecBytes,
MaxBPBytes: cfg.SummarizerMaxBPBytes,
MaxQASections: cfg.SummarizerMaxQASections,
MaxQABytes: cfg.SummarizerMaxQABytes,
}These settings are passed to various components through the chain summarization system:
// In csum/chain_summary.go
func NewSummarizer(config SummarizerConfig) Summarizer {
if config.PreserveLast {
if config.LastSecBytes <= 0 {
config.LastSecBytes = maxLastSectionByteSize
}
}
if config.UseQA {
if config.MaxQASections <= 0 {
config.MaxQASections = maxQAPairSections
}
if config.MaxQABytes <= 0 {
config.MaxQABytes = maxQAPairByteSize
}
}
if config.MaxBPBytes <= 0 {
config.MaxBPBytes = maxSingleBodyPairByteSize
}
return &summarizer{config: config}
}Recommended Settings for Different Use Cases
Long-running Assistant Conversations
textSummarizerPreserveLast: true SummarizerLastSecBytes: 51200 (50KB) SummarizerMaxBPBytes: 16384 (16KB) SummarizerUseQA: true SummarizerMaxQASections: 10 SummarizerMaxQABytes: 65536 (64KB) SummarizerSumHumanInQA: false SummarizerKeepQASections: 1The default settings are optimized for assistant-style conversations. They maintain a good balance between context retention and token efficiency.
Technical Problem-Solving with Large Context Models
textSummarizerPreserveLast: true SummarizerLastSecBytes: 81920 (80KB) SummarizerMaxBPBytes: 32768 (32KB) SummarizerUseQA: true SummarizerMaxQASections: 15 SummarizerMaxQABytes: 102400 (100KB) SummarizerSumHumanInQA: false SummarizerKeepQASections: 1Increased limits to preserve more technical details when using models with large context windows (e.g., GPT-4).
Limited Context Models
textSummarizerPreserveLast: true SummarizerLastSecBytes: 25600 (25KB) SummarizerMaxBPBytes: 8192 (8KB) SummarizerUseQA: true SummarizerMaxQASections: 5 SummarizerMaxQABytes: 32768 (32KB) SummarizerSumHumanInQA: true SummarizerKeepQASections: 1More aggressive summarization for models with smaller context windows (e.g., smaller or older LLMs).
Debugging or Analysis (Maximum Context Preservation)
textSummarizerPreserveLast: false SummarizerUseQA: false SummarizerKeepQASections: 0Disables active summarization to preserve the complete conversation history for debugging purposes. Note that this can lead to context overflow in long conversations.
Assistant Settings
These settings control the behavior of the AI assistant functionality, including whether to use multi-agent delegation and assistant-specific summarization settings.
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| AssistantUseAgents | ASSISTANT_USE_AGENTS |
false |
Controls the default value for agent usage when creating new assistants |
| AssistantSummarizerPreserveLast | ASSISTANT_SUMMARIZER_PRESERVE_LAST |
true |
Whether to preserve all messages in the assistant's last section |
| AssistantSummarizerLastSecBytes | ASSISTANT_SUMMARIZER_LAST_SEC_BYTES |
76800 |
Maximum byte size for assistant's last section (75KB) |
| AssistantSummarizerMaxBPBytes | ASSISTANT_SUMMARIZER_MAX_BP_BYTES |
16384 |
Maximum byte size for a single body pair in assistant context (16KB) |
| AssistantSummarizerMaxQASections | ASSISTANT_SUMMARIZER_MAX_QA_SECTIONS |
7 |
Maximum QA sections to preserve in assistant context |
| AssistantSummarizerMaxQABytes | ASSISTANT_SUMMARIZER_MAX_QA_BYTES |
76800 |
Maximum byte size for assistant's QA sections (75KB) |
| AssistantSummarizerKeepQASections | ASSISTANT_SUMMARIZER_KEEP_QA_SECTIONS |
3 |
Number of recent QA sections to preserve without summarization |
Usage Details
The assistant settings are used to configure the behavior of the AI assistant and its context management:
AssistantUseAgents: Controls the default state of the "Use Agents" toggle when creating new assistants in the UI:
go// This setting affects the initial state when creating assistants // Users can always override this by toggling the "Use Agents" button in the UIfalse(default): New assistants are created with agent delegation disabled by defaulttrue: New assistants are created with agent delegation enabled by default
Assistant Summarizer Settings: These provide dedicated summarization configuration for assistant instances, typically allowing for more memory retention compared to the global settings:
go// Assistant summarizer configuration provides more context retention // compared to global settings, preserving more recent conversation history // while still ensuring efficient token usage
The assistant summarizer configuration is designed to provide more memory for context retention compared to the global settings, preserving more recent conversation history while still ensuring efficient token usage.
Recommended Assistant Settings for Different Use Cases
Standard Assistant Conversations
textAssistantUseAgents: false AssistantSummarizerPreserveLast: true AssistantSummarizerLastSecBytes: 76800 (75KB) AssistantSummarizerMaxBPBytes: 16384 (16KB) AssistantSummarizerMaxQASections: 7 AssistantSummarizerMaxQABytes: 76800 (75KB) AssistantSummarizerKeepQASections: 3The default settings provide a balance between context retention and performance for typical assistant interactions.
Multi-Agent Assistant Workflows
textAssistantUseAgents: true AssistantSummarizerPreserveLast: true AssistantSummarizerLastSecBytes: 102400 (100KB) AssistantSummarizerMaxBPBytes: 32768 (32KB) AssistantSummarizerMaxQASections: 10 AssistantSummarizerMaxQABytes: 102400 (100KB) AssistantSummarizerKeepQASections: 5Enhanced settings for complex workflows that benefit from agent delegation with increased context preservation.
Resource-Constrained Assistant
textAssistantUseAgents: false AssistantSummarizerPreserveLast: true AssistantSummarizerLastSecBytes: 51200 (50KB) AssistantSummarizerMaxBPBytes: 16384 (16KB) AssistantSummarizerMaxQASections: 5 AssistantSummarizerMaxQABytes: 51200 (50KB) AssistantSummarizerKeepQASections: 2More conservative settings for environments with limited resources or smaller context models.
Functions Configuration
These settings control which tools are available to AI agents and allow adding custom external functions. The Functions API enables fine-grained control over agent capabilities by selectively disabling built-in tools or extending functionality with custom integrations.
For provider-neutral OSINT enrichment ideas that could be exposed through external functions, see OSINT Integration Scenarios for PentAGI Agents.
| Field | Type | Description |
|---|---|---|
| token | string (optional) | API token for authenticating external function calls |
| disabled | DisableFunction[] | List of built-in functions to disable for specific agent types |
| functions | ExternalFunction[] | List of custom external functions to add to agent capabilities |
DisableFunction Structure
Allows disabling specific built-in functions for certain agent contexts, providing security and control over agent capabilities.
| Field | Type | Description |
|---|---|---|
| name | string | Name of the built-in function to disable (e.g., terminal, browser, file) |
| context | string[] | Agent contexts where the function should be disabled (optional) |
Available Agent Contexts: agent, adviser, coder, searcher, generator, memorist, enricher, reporter, assistant
When context is empty or omitted, the function is disabled for all agents.
ExternalFunction Structure
Allows adding custom external functions that agents can call via HTTP endpoints, enabling integration with external tools and services.
| Field | Type | Description |
|---|---|---|
| name | string | Name of the custom function (must be unique) |
| url | string | HTTP(S) URL endpoint for the function |
| timeout | int64 | Timeout in seconds for function execution (default: 60) |
| context | string[] | Agent contexts where the function is available (optional) |
| schema | Schema object | JSON schema defining function parameters and description (OpenAI format) |
Available Agent Contexts: Same as DisableFunction (agent, adviser, coder, searcher, generator, memorist, enricher, reporter, assistant)
When context is empty or omitted, the function is available to all agents.
Usage Details
The Functions configuration is typically provided when creating a flow through the API:
// Example from pkg/tools/tools.go
type Functions struct {
Token *string `json:"token,omitempty"`
Disabled []DisableFunction `json:"disabled,omitempty"`
Function []ExternalFunction `json:"functions,omitempty"`
}These settings are used in pkg/tools/tools.go to configure available tools for each agent type:
Token: Used for authenticating requests to external function endpoints:
go// The token is passed in the Authorization header when calling external functions req.Header.Set("Authorization", "Bearer " + *functions.Token)Disabled: Filters out built-in functions for specific agent contexts:
go// Check if function is disabled for current agent context for _, disabled := range functions.Disabled { if disabled.Name == functionName && (len(disabled.Context) == 0 || contains(disabled.Context, agentType)) { // Skip this function } }Functions: Adds custom external functions to agent capabilities:
go// Register external functions as available tools for _, externalFunc := range functions.Function { if len(externalFunc.Context) == 0 || contains(externalFunc.Context, agentType) { definitions = append(definitions, externalFunc.Schema) handlers[externalFunc.Name] = createExternalHandler(externalFunc) } }
Example Configuration
/* Detailed source-code truncated for AI context efficiency. */Security Considerations
- Token Security: Store the
tokenvalue securely and use HTTPS endpoints for external functions - Function Validation: External functions should validate all inputs and return structured error messages
- Timeout Configuration: Set appropriate timeouts to prevent long-running operations from blocking agents
- Context Restriction: Use the
contextfield to limit which agents can access sensitive functions - URL Validation: Ensure external function URLs are trusted and properly secured
Built-in Functions Reference
Common built-in functions that can be disabled:
terminal- Execute shell commands in containersfile- Read and write files in containersbrowser- Browse websites and take screenshotssearch_in_memory- Search vector memory storesearch_guide- Search knowledge guidessearch_answer- Search for answerssearch_code- Search code repositoriesstore_guide- Store knowledge guidesstore_answer- Store answersstore_code- Store code snippetsgoogle- Google Searchduckduckgo- DuckDuckGo Searchtavily- Tavily Searchfirecrawl- Firecrawl Searchtraversaal- Traversaal Searchperplexity- Perplexity Searchsearxng- SearXNG Searchsploitus- Sploitus Exploit Searchgraphiti_search- Graphiti Knowledge Graph Search
The specific functions available depend on the agent type and system configuration.
Search Engine Settings
These settings control the integration with various search engines used for web search capabilities, providing AI agents with up-to-date information from the internet.
DuckDuckGo Search
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| DuckDuckGoEnabled | DUCKDUCKGO_ENABLED |
true |
Enable or disable DuckDuckGo Search engine |
| DuckDuckGoRegion | DUCKDUCKGO_REGION |
(none) | Region code for search results (e.g., us-en, uk-en, cn-zh) |
| DuckDuckGoSafeSearch | DUCKDUCKGO_SAFESEARCH |
(none) | Safe search filter (off, moderate, strict) |
| DuckDuckGoTimeRange | DUCKDUCKGO_TIME_RANGE |
(none) | Time range for search results (d: day, w: week, m: month, y: year) |
Sploitus Search
| Option | Environment Variable | Default Value | Description |
|---|---|---|---|
| SploitusEnabled | SPLOITUS_ENABLED |
true |
Enable or disable Sploitus exploit and vulnerability search |
Google Search
| Option | Environment Variable | Default Value | Description |
|---|