Skip to main content
Source Code: cpp/examples/process_agent.cpp — single-file, self-contained agent (~2,900 lines including 7 tools, action menu, and decision support).
Platform: Windows (Win32 APIs + PowerShell). Compiles on Linux/macOS for CI but tools require Windows to return real data. Prerequisite: Lemonade Server running with a model loaded. Recommended: Run as Administrator for full process visibility and quarantine access.

What This Agent Does

The Process Analyst is an AI agent that makes sense of what’s running on your PC. On startup it scans every process and service, classifies them by resource use and behavior, and shows you a plain-English summary. You can ask it to explain any task — what it is, what it does, and whether there’s reason to be concerned — or take action directly. Here’s what makes it interesting:
  • It explains what’s running — every process gets a plain-English description: what it is, what it does, and whether it looks normal
  • Ask about any task — describe an item from the summary and get a clear explanation without needing technical knowledge
  • Manage processes and services — stop, restart, or quarantine items; the agent always asks before acting
  • Background monitoring — toggle a background watcher that alerts you to memory spikes, new suspicious items, and health changes while you keep working
  • Everything is local — no data leaves your machine

See It In Action

The agent auto-runs a full system scan on startup, then waits for your input. You can ask about any item in plain English — or use the action menu to stop, restart, or quarantine it.

Quick Start

1

Build

cd cpp
cmake -B build -G "Visual Studio 17 2022" -A x64
If CMake reports a generator or platform mismatch, a stale build\ directory from a previous run is causing the conflict. Delete it and reconfigure:
Remove-Item -Recurse -Force build
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release --target process_agent
Binary: build\Release\process_agent.exe
2

Start Lemonade Server

lemonade-server serve
The agent connects to http://localhost:8000/api/v1 by default.
3

Run the agent

build\Release\process_agent.exe
The agent auto-analyzes on startup. After the scan completes, you’ll see a summary followed by an action menu:
A. Processes
A1  msedge.exe (x28) - 2.1 GB RAM - Microsoft Edge [NORMAL]
A2  Teams.exe (x8) - 1.4 GB RAM - Microsoft Teams [HIGH]
A3  cpptools-srv.exe (x2) - 890 MB RAM - C/C++ IntelliSense [NORMAL]

B. Services
All services running normally

C. Suspicious Items
C1  unknown_app.exe - C:\Users\user\Downloads\unknown_app.exe
    flags: unknown_company, unknown_description, temp_path

──────────────────────────────────────────────────────────────
Available Actions:

[1] Explain    -- Get details on a process or service
[2] Stop       -- Kill a process or stop a service
[3] Restart    -- Restart an app or service
[4] Quarantine -- Move a suspicious file to quarantine
[5] Reanalyze  -- Run a fresh system analysis
[6] Monitor    -- Toggle background health watcher

Shortcuts: '1 A3' = Explain A3,  '2 B1' = Stop B1,  '4 C1' = Quarantine C1,  '6 5' = Monitor every 5 min

>
Each item gets a label (A1, A2, C1, etc.) you can use in any command:
What you typeWhat happens
1Explain all items
1 A3Explain cpptools-srv.exe
2 A1Stop msedge.exe
4 C1Quarantine unknown_app.exe
5Reanalyze the system
6Toggle background monitor (default 5 min interval)
6 5Toggle monitor with 5-minute interval
What is svchost.exe?Ask a free-form question
For full process visibility and quarantine access, right-click your terminal and select Run as administrator before launching the agent.

Architecture

The agent runs in two phases. On startup it auto-scans the system — no menu shown first. The LLM calls system_snapshot and list_processes, classifies everything into three labeled sections (A: Processes, B: Services, C: Suspicious Items), then presents an action menu. Conversation history is preserved across all actions so the LLM can reference any labeled item without re-scanning. Only Reanalyze clears history, because the system state has genuinely changed.

How It Works

The entire agent is a single .cpp file with six sections. Let’s walk through each one.
  1. Shell Helper — bridges C++ and PowerShell for command execution and validates inputs
  2. Win32 Process Intelligence — native API for fast process snapshots with file version info
  3. Tool Registration — 7 tools the LLM can call (3 read-only + 4 destructive)
  4. System Prompt — teaches the LLM the A/B/C classification protocol
  5. ProcessConsole — custom TUI that formats A/B/C sections with color coding
  6. Monitor Mode — background thread that diffs snapshots and surfaces alerts
The agent subclasses gaia::Agent with three pieces: a config, registered tools, and a system prompt.
class ProcessAgent : public gaia::Agent {
public:
    explicit ProcessAgent(const std::string& modelId)
        : Agent(makeConfig(modelId)) {
        setOutputHandler(std::make_unique<ProcessConsole>());  // custom TUI
        init();  // calls registerTools() + composes system prompt
    }

protected:
    std::string getSystemPrompt() const override { /* ... */ }
    void registerTools() override { /* ... */ }

private:
    static gaia::AgentConfig makeConfig(const std::string& modelId) {
        gaia::AgentConfig config;
        config.maxSteps = 20;
        config.modelId = modelId;
        return config;
    }
};

1. The Shell Helper: Bridging C++ and PowerShell

The runShell() function wraps PowerShell execution, building a command string and reading output through a pipe:
static std::string runShell(const std::string& command) {
    std::string fullCmd = "powershell -NoProfile -NonInteractive -Command \"& { "
                          + command + " }\" 2>&1";
    FILE* pipe = _popen(fullCmd.c_str(), "r");
    if (!pipe) return R"({"error": "Failed to execute command"})";
    // ... code redacted for simplicity ...
    return result.empty()
        ? R"json({"status": "completed", "output": "(no output)"})json"
        : result;
}
The agent also includes a dedicated path validator for quarantine operations:
static bool isSafePath(const std::string& path) {
    if (path.size() < 3) return false;
    if (!std::isalpha(static_cast<unsigned char>(path[0])) ||
        path[1] != ':' || path[2] != '\\') return false;
    const std::string dangerous = "\"`;|&{}<>$";
    for (char c : path) {
        if (dangerous.find(c) != std::string::npos) return false;
    }
    return true;
}
Why a separate path validator? File paths need drive-letter validation and a different dangerous character set than shell arguments. isSafeShellArg() rejects single quotes (valid in Windows paths), while isSafePath() requires the X:\ prefix and allows single quotes but blocks shell metacharacters. Two validators, two threat models.

2. Win32 Process Intelligence

Rather than relying on PowerShell for everything, the Process Analyst uses native Win32 APIs for its hot path — process enumeration and memory measurement. The core of getTopProcesses() takes a snapshot, groups processes by exe name, then enriches each entry with version info and factual flags:
static gaia::json getTopProcesses(int topN = 20) {
    HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    // ... enumerate processes, measure memory via K32GetProcessMemoryInfo ...
    // ... group by exe name, accumulate total memory and instance count ...

    for (auto& g : sorted) {
        // ... enrich with version info (company, description) via GetFileVersionInfoW ...

        // Factual flags for LLM classification
        gaia::json flags = gaia::json::array();
        if (company.empty()) flags.push_back("unknown_company");
        if (lowerPath.find("\\Downloads\\") != std::string::npos)
            flags.push_back("temp_path");
        entry["flags"] = flags;
    }
    return result;
}
Three design choices worth understanding:
  • PerformanceCreateToolhelp32Snapshot + K32GetProcessMemoryInfo returns data in ~20ms for 200+ processes. Get-Process | ConvertTo-Json takes 2-3 seconds for the same data. When the agent needs to scan on every startup, that difference matters.
  • Process grouping — Instead of listing 45 separate chrome.exe entries, the agent groups by exe name with instance count and total memory. The LLM sees “chrome.exe x45 — 3.2 GB” which is far more actionable than 45 lines of individual PIDs.
  • Factual flagsunknown_company, unknown_description, temp_path are computed as structured metadata from Win32 GetFileVersionInfoW and path analysis. The LLM uses these as input to classification, not as final verdicts.

3. Tool Registration: Teaching the Agent What It Can Do

Tools are registered with a name, description, callback, and typed parameter list. The framework automatically includes registered tools in the system prompt sent to the LLM. Here’s system_snapshot, which demonstrates the hybrid Win32 + PowerShell approach:
toolRegistry().registerTool(
    "system_snapshot",
    "System overview: CPU load, memory, disk, uptime, top processes by memory "
    "(with company, description, path, and factual flags), problematic services, "
    "and network connection count. Use first during analysis.",
    [](const gaia::json& /*args*/) -> gaia::json {
        gaia::json result;
        result["tool"]          = "system_snapshot";
        result["disk"]          = getDiskUsageInfo();     // Win32: GetDiskFreeSpaceExW
        result["memory"]        = getMemoryInfo();        // Win32: GlobalMemoryStatusEx
        result["top_processes"] = getTopProcesses(30);    // Win32: CreateToolhelp32Snapshot

        // PowerShell: CPU load, uptime, services, connections
        std::string psCmd = "$o=@{}; $c=Get-CimInstance Win32_Processor; ...";
        // ... merge PowerShell results into result ...

        result["command"] = "Win32 API: memory, disk, processes + PowerShell: CPU, services, connections";
        return result;
    },
    {}  // no parameters
);
And kill_process, which shows parameter validation and the command + output contract:
toolRegistry().registerTool(
    "kill_process",
    "Terminate a running process by name. ONLY use after explicit user confirmation.",
    [](const gaia::json& args) -> gaia::json {
        std::string name = args.value("name", "");
        if (name.empty())
            return {{"error", "Process name is required"}};

        // Validate: only alphanumeric + . - _
        // ... code redacted for simplicity ...

        KillResult kr = killProcessesByName(name);
        return {
            {"tool", "kill_process"},
            {"terminated", kr.terminated},
            {"command", "Win32 API: TerminateProcess(" + name + ")"},
            {"output", "Found N instances, terminated N, freed X MB"}
        };
    },
    {{"name", gaia::ToolParamType::STRING, true, "Process name (e.g., 'chrome.exe')"}}
);
Why command + output keys? Every tool returns both a command string and an output string. The ProcessConsole uses these for its TUI display — the command appears in the tool header, and output appears in the preview box. This consistent contract means the output handler doesn’t need to understand tool-specific JSON structures.

4. The System Prompt: Classification Protocol

The system prompt teaches the LLM how to classify processes into three labeled sections and how to respond to user actions. Here are the core classification criteria:
### A. Processes (resource consumers)
Select the most significant resource consumers. Consider:
- Memory usage relative to total system RAM
- Instance count (many instances = noteworthy)
- Whether the resource usage is expected for that application type
Tag each: [NORMAL] expected, [HIGH] unexpectedly high, [SUSPICIOUS]

### B. Services (problematic)
Report services with Status not "OK" or Memory > 200 MB.
If none: "All services running normally"

### C. Suspicious Items
The flags array provides factual signals — use as INPUT, not verdicts:
- unknown_company + unknown_description = STRONG signal
- temp_path = STRONG signal for non-installers
- EXCEPTIONS: svchost.exe, csrss.exe, smss.exe may show empty company — NORMAL
The system prompt also defines the action behavior protocol, including the multi-step quarantine confirmation:
## ACTION BEHAVIOR

Users reference items by group letter + number (e.g., "Explain A3", "Stop B1").

- **Quarantine**: Before calling quarantine_item, you MUST:
  1. Call explain_item to get full details
  2. Present confirmation in Key: Value format
  3. Ask: Kill [name] and move it to quarantine? (yes / no)
  4. Wait for response. If NO, abort. If YES, call quarantine_item.
Why A/B/C labels? They create a shared vocabulary between the LLM and the user. “Explain A3” unambiguously maps to the third process in the resource consumers section — no re-scanning required. Why explicit quarantine protocol? Quarantine is irreversible — it kills the process AND moves the executable file. The multi-step protocol (explain, confirm, wait for yes/no) prevents accidental data loss from an LLM hallucination.

5. ProcessConsole: Section-Aware Formatting

The ProcessConsole overrides CleanConsole::printFinalAnswer() to render the LLM’s structured A/B/C output with visual hierarchy. It applies six line detection rules in order (first match wins):
  1. Blank line — suppresses consecutive blanks to keep output tight
  2. Section header — “A. Processes”, “B. Services”, “C. Suspicious Items” — rendered as bold white label with a gray description line underneath
  3. Item referenceA4: cpptools-srv.exe — bold cyan tag, bold white name
  4. Key: Value — first colon within 20 chars with alpha key — bold white key, normal value
  5. Numbered item1. chrome.exe ... — normal rendering with markdown bold support
  6. Default paragraph — everything else — gray (dimmed) prose
Here’s how Rule 3 handles item reference lines:
// Rule 3: Item reference title (A4: cpptools-srv.exe)
if (line.size() >= 4 && line[0] >= 'A' && line[0] <= 'C' &&
    std::isdigit(static_cast<unsigned char>(line[1]))) {
    auto colon = line.find(": ");
    if (colon != std::string::npos && colon <= 4) {
        std::string tag  = line.substr(0, colon);
        std::string rest = line.substr(colon + 2);
        std::cout << color::BOLD << color::CYAN << tag << color::RESET
                  << "  " << color::BOLD << color::WHITE << rest << color::RESET;
    }
}
Why override printFinalAnswer()? The LLM outputs structured sections that need visual hierarchy. Default CleanConsole treats all text equally — ProcessConsole adds section headers with descriptions, color-coded item tags, and Key: Value formatting that makes the output scannable at a glance.

6. Monitor Mode: Background Health Watcher

The monitor is a background thread that periodically re-scans the system and compares consecutive snapshots to detect changes. It runs alongside the interactive agent — you can keep explaining, stopping, or restarting processes while the monitor watches for anomalies. Starting and stopping. Monitor is a toggle on [6]. Press it once to start, again to stop. You can also specify an interval: 6 5 starts monitoring every 5 minutes. Typing monitor or monitor N at the prompt does the same thing. The default interval is 5 minutes. How it works internally. When started, the agent spawns a second ProcessAgent on a dedicated thread. This background agent uses a SilentConsole — it runs the same system_snapshot and list_processes tools but produces no visible output. After each scan it builds a MonitorSnapshot (memory usage, top process list, suspicious items, health status) and diffs it against the previous snapshot. Any meaningful changes become alerts. Alert types and thresholds:
AlertTriggerSeverity
Memory spikeSystem memory use jumps >10 percentage pointsWARNING (CRITICAL if >90%)
Per-process memory surgeA single process grows by >500 MBWARNING
New process in top-20A process appears that was not in the previous top-20INFO
Process left top-20A process drops out of the previous top-20INFO
New suspicious itemLLM flags a new item in section CCRITICAL
Health status changeOverall health classification changesWARNING or CRITICAL
Alert delivery. Pending alerts are displayed before the next menu prompt, so you see them naturally between actions. For CRITICAL and NEW_SUSPICIOUS alerts, the agent also sends a Windows notification so you get alerted even if the terminal is in the background. Design choices:
  • Separate agent instance — the monitor’s ProcessAgent has its own conversation history and LLM connection. This avoids corrupting the interactive agent’s context with monitoring chatter.
  • SilentConsole — suppresses all tool output and conclusions from the background scan. Only the diff-based alerts reach the user.
  • Deterministic scans — the monitor agent uses temperature=0 for consistent health classifications across consecutive scans when system state hasn’t changed.
  • Toggle, not a mode — the interactive menu stays fully functional while the monitor runs. There is no “monitor mode” to enter or exit.
  • NPU-friendly — background monitoring is an ideal workload for AMD Ryzen AI NPU inference. The monitor runs periodic scans without tying up the CPU or GPU, leaving them free for your other work.

Tool Reference

The agent registers 7 tools in two categories:

Analysis Tools (Read-Only)

These gather information without changing anything on the system.
ToolWhat It UsesParametersWhat the LLM Learns
system_snapshotWin32 API + PowerShellnoneCPU load, memory, disk, uptime, top 30 processes with flags, problematic services, connection count
list_processesWin32 API + PowerShellnoneTop 30 by memory (with company, description, flags), top 10 by CPU, background count
explain_itemWin32 API + PowerShellname (required)Full details: path, what the file is, what it does, memory, CPU, start time, network connections

Action Tools (Destructive — require user confirmation)

These modify the system. The system prompt mandates user approval before the LLM calls any of them.
ToolWhat It UsesParametersWhen to Use
kill_processCreateToolhelp32Snapshot + TerminateProcessname (required)Stop a resource hog or suspicious process
restart_processTerminateProcess + CreateProcessWname (required)Restart a hung application from the same exe path
restart_servicePowerShell Restart-Servicename (required)Restart a degraded Windows service
quarantine_itemkillProcessesByPath + MoveFileExWpath (required)Move suspicious exe to C:\ProgramData\GAIA\quarantine\

Diagnostics Flow

Diagnostic nodes (dark) gather data. Red nodes are destructive actions requiring confirmation. Blue nodes are the Reanalyze and Monitor paths. Orange is the alert display. Green nodes are conclusion displays where the user reads output.

Sample Session

  ========================================================================================
   Process Analyst  |  GAIA C++ Agent Framework  |  Local Inference
  ========================================================================================

  Select inference backend:
  [1] GPU  - Qwen3-4B-Instruct-2507-GGUF
  [2] NPU  - Qwen3-4B-Instruct-2507-FLM

  > 1
  Using GPU backend: Qwen3-4B-Instruct-2507-GGUF

  Analyzing your system...

  [1/20] system_snapshot
      Command: Win32 API: memory, disk, processes + PowerShell: CPU, services, connections
      Output:
      .----
      | CPU: 12% load  |  AMD Ryzen 9 7940HS  |  3893 / 5200 MHz
      | Memory: 12.4 GB / 30.4 GB  (41% used)
      | Uptime: 48.3 hrs  |  Processes: 312  |  Connections: 87
      |
      | Disk:
      |   C:\  52% used  |  224.8 GB free of 476.3 GB
      |
      | Top 30 by memory:
      |   msedge.exe                  x28  2.1 GB   <- explorer.exe
      |   Teams.exe                   x8   1.4 GB   <- explorer.exe
      |   cpptools-srv.exe            x2   890 MB   <- vsls-agent.exe
      |   ...
      '----

  [2/20] list_processes
      Command: Win32 API: top 30 by memory + PowerShell: top 10 by CPU time
      ...

  ========================================================================================
  Conclusion
  ========================================================================================

  A. Processes
  Top resource consumers grouped by name, sorted by memory and CPU usage.

  A1  msedge.exe (x28) - 2.1 GB RAM - Microsoft Edge <- explorer.exe [NORMAL]
  A2  Teams.exe (x8) - 1.4 GB RAM - Microsoft Teams <- explorer.exe [HIGH]
  A3  cpptools-srv.exe (x2) - 890 MB RAM - C/C++ IntelliSense <- vsls-agent.exe [NORMAL]
  A4  SearchHost.exe - 456 MB RAM - Windows Search <- svchost.exe [NORMAL]
  A5  explorer.exe - 312 MB RAM - Windows Explorer <- userinit.exe [NORMAL]

  B. Services
  System services with high memory usage (>200 MB) or error/degraded status.

  All services running normally

  C. Suspicious Items
  Unsigned binaries, unknown publishers, or processes running from unexpected locations.

  C1  unknown_app.exe - C:\Users\user\Downloads\unknown_app.exe
      flags: unknown_company, unknown_description, temp_path

  System Health: Warning - One suspicious executable found in Downloads folder

  ========================================================================================

  ========================================================================================
  Available Actions:

  [1] Explain  -- Get details on a process or service
  [2] Stop     -- Kill a process or stop a service
  [3] Restart  -- Restart an app or service
  [4] Quarantine -- Move a suspicious file to quarantine
  [5] Reanalyze  -- Run a fresh system analysis
  [6] Monitor    -- Toggle background health watcher
  ========================================================================================
  Shortcuts: '1 A3' = Explain A3,  '2 B1' = Stop B1,  '4 C1' = Quarantine C1,  '6 5' = Monitor every 5 min

  > 4 C1

  [3/20] explain_item
      Command: Win32 API + PowerShell: explain unknown_app.exe
      Output:
      .----
      | unknown_app.exe  (PID 14228, 1 instance)
      |   Path:        C:\Users\user\Downloads\unknown_app.exe
      |   Parent:      explorer.exe
      |   Memory:      45.2 MB
      |   CPU time:    2.3 sec
      |   Network:     no active connections
      '----

  ========================================================================================
  Conclusion
  ========================================================================================

  Process: unknown_app.exe
  Path: C:\Users\user\Downloads\unknown_app.exe
  Memory: 45.2 MB
  Publisher: Unknown
  Signature: Unsigned
  Reason: Unknown publisher, no description, running from Downloads folder

  Kill unknown_app.exe and quarantine this file? This will terminate the process
  immediately and move the file to C:\ProgramData\GAIA\quarantine\. (yes / no)

  ========================================================================================
  [1] Yes
  [2] No

  > 1

  [4/20] quarantine_item
      Command: Win32 API: TerminateProcess + MoveFileExW(... -> C:\ProgramData\GAIA\quarantine\...)
      Output:
      .----
      | Killed:      unknown_app.exe (1 instance, 45.2 MB freed)
      | Quarantined: C:\Users\user\Downloads\unknown_app.exe
      | Moved to:    C:\ProgramData\GAIA\quarantine\unknown_app.exe.20260309.quarantined
      | File size:   2.8 MB
      '----

  ========================================================================================
  Conclusion
  ========================================================================================

  Action: Quarantine
  Target: unknown_app.exe
  Result: Success

  Terminated 1 instance of unknown_app.exe, freed 45.2 MB. File moved to
  C:\ProgramData\GAIA\quarantine\unknown_app.exe.20260309.quarantined

  ========================================================================================

Extending the Agent

Want to add your own tools? The pattern is straightforward. Here’s an example that lists Windows startup items:
// Add this inside registerTools()
toolRegistry().registerTool(
    "check_startup_items",
    "List programs that run at Windows startup — registry Run keys and Startup folder.",
    [](const gaia::json& /*args*/) -> gaia::json {
        std::string output = runShell(
            "Get-CimInstance Win32_StartupCommand "
            "| Select-Object Name, Command, Location "
            "| ConvertTo-Json -Depth 2"
        );
        return {{"tool", "check_startup_items"},
                {"command", "PowerShell: Get-CimInstance Win32_StartupCommand"},
                {"output", output}};
    },
    {}
);
The framework automatically appends registered tools to the system prompt — the new tool is available to the LLM immediately. Optionally, add an ActionEntry to kActions[] if you want a new numbered menu item (e.g., [7] Startup Items).

Next Steps