Skip to content

CyBot Run

CyBot is a Google ADK agent — an LlmAgent built on the Google Agent Development Kit framework. It runs on Vertex AI (Gemini), carries ten custom tools, and persists memory through Honcho. Unlike the other Fleet agents described on these pages, CyBot is not a concept or a persona sketch — it is a deployed Python service running on the box right now.

This page documents how the agent is built, what tools it has, how a run executes end-to-end, and how memory works. Everything here comes from the actual source at /root/agent_dev/adk-test/CyBot/.


What CyBot Is

At its core, CyBot is an LlmAgent — Google ADK's standard agent class. You give it a model, a system instruction, and a set of tool functions, and the ADK runner handles the LLM-call → tool-call → response loop.

# CyBot/agent.py — the agent definition (simplified)

from google.adk.agents import LlmAgent
from google.adk.models.google_llm import Gemini
from honcho_agent import before_model, after_agent
from .tools import (
    read_file, write_file, search_notes, mind_api,
    upload_file_to_cms, fleet_note, call_cy,
    terminal_exec, web_search, ai_models,
)

model = Gemini(
    model="gemini-3.1-pro-preview",
    client_kwargs={
        "vertexai": True,
        "project": os.getenv("VERTEX_PROJECT_ID"),
        "location": os.getenv("VERTEX_REGION", "global"),
    },
)

root_agent = LlmAgent(
    name="CyBot",
    model=model,
    instruction=INSTRUCTION,          # the system prompt — see below
    tools=[
        read_file, write_file, search_notes,
        mind_api, upload_file_to_cms, fleet_note,
        call_cy, terminal_exec, web_search, ai_models,
    ],
    before_model_callback=before_model,  # Honcho memory injection
    after_agent_callback=after_agent,    # Honcho exchange storage
)

System Instruction

The instruction is a plain Python string — not a YAML file, not a magic config. It defines who CyBot is, how it speaks, and what each tool does. This is the actual instruction loaded into the agent:

INSTRUCTION = (
    "You are CyBot — the operational agent for Cy, Chief of Staff of the Founder Division. "
    "You speak tight: what up, say less, G, dope, no doubt, turn up on em. "
    "You execute with precision. No fluff, no filler, no hedging. "
    "When given a task, deliver the result. When asked a question, give the answer. "

    "You have ten tools:\n"
    "- read_file: Read any file from disk.\n"
    "- write_file: Write content to any file on disk.\n"
    "- search_notes: Search fleet_notes/ and knowledgebase/ by keyword.\n"
    "- mind_api: Call The Mind REST API (GET/POST/PATCH/DELETE).\n"
    "- upload_file_to_cms: Upload a file to The Mind (local storage).\n"
    "- fleet_note: File, read, update, or search fleet notes.\n"
    "- call_cy: Call Chief of Staff Cy (the Operation Lead) in the CLI.\n"
    "- terminal_exec: Run a bash command and get the output.\n"
    "- web_search: Search the web for reviews, benchmarks, pricing.\n"
    "- ai_models: Full CRUD on the AI model catalog in The Mind.\n"
)

!!! note "Override via environment" The instruction can be overridden at runtime by setting the ADK_INSTRUCTION environment variable. The model name is likewise controlled by ADK_MODEL (default: google/gemini-3.1-pro-preview). The "google/" prefix is stripped before passing to Vertex, which expects bare names like gemini-3.1-pro-preview.


The Ten Tools

Each tool is a regular Python function in CyBot/tools.py. ADK uses the function's docstring as the tool schema — the LLM reads the docstring to understand what arguments to pass. This is the complete inventory:

Tool Function What it does
read_file read_file(path) → str Reads any file from disk, returns contents as text.
write_file write_file(path, content) → str Writes content to a file, creates parent dirs.
search_notes search_notes(query=None, max_results=15) → str Ripgrep-powered full-text search across knowledgebase/. No embeddings, no vector DB.
mind_api mind_api(method, endpoint, data=None) → str REST calls to The Mind (The Mind). GET/POST/PATCH/DELETE.
upload_file_to_cms upload_file_to_cms(file_path, title=None, folder=None) → str Uploads a file to The Mind via multipart POST. Returns file ID.
fleet_note fleet_note(action, subject, body, ...) → str File/read/update/search fleet notes in The Mind. This is how agents message each other.
call_cy call_cy(prompt, timeout=120) → str Calls Chief of Staff Cy (the Operation Lead) via system-dispatch -z CLI in one-shot mode.
terminal_exec terminal_exec(command, timeout=60) → str Runs a bash command, returns stdout+stderr+exit code. Deny-listed for destructive ops and secrets.
web_search web_search(query, max_results=8) → str Brave Search API (with DuckDuckGo fallback). Returns JSON array of results.
ai_models ai_models(action, model_id, ...) → str Full CRUD on 188+ model catalog in The Mind. List, get, update, batch_update, categories, budget, search, fetch_spec.

Tool Example: terminal_exec

This is the actual function — not pseudocode. The deny-list and timeout cap are real safety mechanisms:

_DENY_PREFIXES = [
    "rm -rf /", "rm -fr /", "mkfs", "dd if=", ":(){",
    "chmod -R 777 /", "chown -R",
]
_DENY_CONTAINS = [
    ".env.local", "DATABASE_URL", "STACK_SECRET", "BRAIN_API_KEY",
    "VULTR_API_KEY", "NEXT_PUBLIC_STACK", "DIRECTUS_TOKEN",
]

def terminal_exec(command: str, timeout: int = 60) -> str:
    """Run a bash command and return the output.

    Executes the command with /bin/sh and returns stdout, stderr, and exit code.
    Cwd is /root/agent_dev/adk-test (CyBot's home directory).

    SAFETY:
    - Commands are checked against a deny-list of destructive and secret-touching
      patterns. If a command matches, it is rejected.
    - Timeout defaults to 60s. Max 120s.
    - No interactive commands (no vim, nano, top, etc).
    """
    cmd = (command or "").strip()
    timeout = min(max(timeout, 5), 120)

    # Deny-list check
    lc = cmd.lower()
    for p in _DENY_PREFIXES:
        if lc.startswith(p.lower()):
            return f"REJECTED: command starts with denied pattern '{p}'"
    for c in _DENY_CONTAINS:
        if c.lower() in lc:
            return f"REJECTED: command references '{c}' which is off-limits"

    result = subprocess.run(
        ["/bin/sh", "-c", cmd],
        capture_output=True, text=True, timeout=timeout,
        cwd="/root/agent_dev/adk-test",
    )
    parts = [f"exit_code: {result.returncode}"]
    if result.stdout.strip():
        parts.append(f"stdout:\n{result.stdout.strip()}")
    if result.stderr.strip():
        parts.append(f"stderr:\{result.stderr.strip()}")
    return "\n".join(parts)

Tool Example: fleet_note

Fleet notes are how agents communicate structured messages — incidents, repair requests, feature requests, announcements. Every note lands in The Mind's fleet_notes collection:

def fleet_note(
    action: str,           # "create" | "read" | "update" | "search"
    subject: str = None,   # short title (required on create)
    body: str = None,      # HTML message body (required on create)
    target_user: str = None,  # "Cy" | "Zad" | "Fleet" (required on create)
    from_user: str = "CyBot",
    priority: str = "normal",  # low | normal | high | critical
    note_type: str = "memo",   # memo | announcement | incident | repair_request
                                #        | feature_request | lesson | proposal
    status: str = "sent",      # draft | sent | read | actioned | resolved | archived
    note_id: int = None,
    **kwargs,
) -> str:
    ...

Creating a note:

fleet_note(
    action="create",
    subject="Vector Engine integration spec ready",
    body="<p>The Vector Engine vector DB integration blueprint is in knowledgebase/. Ready for review.</p>",
    target_user="Cy",
    note_type="announcement",
    priority="high",
    tags="qdrant,vector-db,integration",
)

How a Run Happens

CyBot is served via two systemd services. The ADK API server handles the agent runtime; the FastAPI service (service.py) wraps it with HTTP endpoints for chat and task execution.

Production Services

cybot-adk.service       → adk api_server --port 9000 --no-reload CyBot
cybot-executor.service  → python service.py (port 9005)
# /etc/systemd/system/cybot-adk.service
[Unit]
Description=CyBot ADK API Server (production endpoints)
After=network-online.target

[Service]
Type=simple
WorkingDirectory=/root/agent_dev/adk-test
ExecStart=/root/agent_dev/adk-test/.venv/bin/adk api_server --port 9000 --no-reload CyBot
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target

The Run Loop (FastAPI → ADK Runner)

When you POST to /chat, the FastAPI service creates an InMemoryRunner (ADK's standard runner), opens a session, passes the user message as a Content object, and streams events until it gets the final text. Here's the actual handler:

# service.py — /chat endpoint

@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
    runner = InMemoryRunner(app_name="cybot-chat", agent=root_agent)
    session = await runner.session_service.create_session(
        app_name="cybot-chat", user_id="u1"
    )
    content = types.Content(
        role="user",
        parts=[types.Part.from_text(text=req.message)],
    )

    final_text = ""
    async for event in runner.run_async(
        user_id="u1",
        session_id=session.id,
        new_message=content,
    ):
        if event.content and event.content.parts:
            final_text = event.content.parts[0].text or ""

    return ChatResponse(response=final_text or "", session_id=session.id)

The Run Sequence

Here's what happens in order when a message hits CyBot:

User sends POST /chat  {"message": "Research the latest FLUX model pricing"}

  1. FastAPI creates InMemoryRunner with root_agent
  2. ADK creates a new session
  3. before_model_callback fires
     → Honcho: ensure workspace, peers, session exist
     → Honcho: fetch human peer representation + session summary
     → Inject [MEMORY] block into system instruction
  4. ADK sends the augmented instruction + user message to Gemini (Vertex AI)
  5. Gemini decides which tool(s) to call and returns a function_call event
  6. ADK executes the tool function (e.g. web_search("FLUX model pricing"))
     → Tool returns result as text
  7. Gemini gets the tool result back, may call more tools or produce final text
  8. Final event has role != "user" → final_text is extracted
  9. after_agent_callback fires
     → Honcho: store user_text + agent_text as messages in the session
  10. FastAPI returns ChatResponse with the final text + session_id

Task Execution via The Mind Flow

CyBot also receives tasks dispatched from The Mind's The Mind Flow automation. A task payload hits POST /execute (API-key protected):

class TaskPayload(BaseModel):
    task_id: int
    title: str
    description: str = None
    assigned_agent: str = None
    priority: int = 0
    status: str = "pending"
    input_data: dict = None

@app.post("/execute", dependencies=[Depends(verify_api_key)])
async def execute_task(task: TaskPayload, request: Request):
    # Save to disk for audit trail
    log_file = log_dir / f"task_{task.task_id}_payload.json"
    with open(log_file, "w") as f:
        json.dump(task_log, f, indent=2)
    return {"message": f"Task {task.task_id} received by CyBot", "status": "ok"}

An actual task payload received from a The Mind Flow:

{
  "task_id": 15,
  "title": "🧪 Test Task — Flow Verification",
  "description": "Testing the The Mind flow pipeline",
  "assigned_agent": "cybot",
  "priority": 0,
  "status": "pending",
  "input_data": {
    "test": true,
    "message": "Flow verification test from Commander Zad"
  },
  "received_at": "2026-08-02T21:20:19.403641"
}

Every received task is saved to /root/agent_dev/adk-test/fleet_notes/task_{id}_payload.json for audit.


Honcho Memory

CyBot has persistent long-term memory via Honcho. This is not a RAG system or a vector database — Honcho builds a psychological model of the user over time and injects it into the system instruction before each model call.

How It Works

Two ADK callbacks wire Honcho into the agent lifecycle:

Callback When it fires What it does
before_model_callback Before each LLM call Fetches the human peer's representation + current session summary from Honcho, injects it as a [MEMORY] block appended to the system instruction
after_agent_callback After the agent finishes responding Stores the user's message and CyBot's response as Honcho messages for future sessions to build on
# honcho_agent.py — memory injection hook (simplified)

async def before_model(callback_context, llm_request) -> None:
    if not _ENABLED:
        return
    session_id = callback_context.session.id

    # Ensure Honcho resources exist (idempotent)
    await _ensure(session_id)

    # Fetch memory: humanpeer representation + session summary
    block = await _memory_block(session_id)

    if block:
        existing = _instruction_str(llm_request.config)
        llm_request.config.system_instruction = (
            f"{existing}\n\n{block}" if existing else block
        )
        # Gemini now sees the memory block appended to its system prompt

The Memory Block

The injected block looks like this inside the system instruction:

[MEMORY]
Known about zad:
The user is Commander Zad, founder of High Limit Designs. He prefers
concise, high-signal communication ("say less" mode). He builds with
Go, Next.js, and Python. He values sovereignty over service and
accuracy above all else.

Session summary:
Previous turns discussed Vector Engine vector database integration and
planning a HuggingFace model content pipeline.
[/MEMORY]

Configuration

Honcho is controlled via environment variables in CyBot/.env:

HONCHO_ENABLED=true           # Set to "false" to disable memory entirely
HONCHO_URL=http://127.0.0.1:8000/v3  # Honcho API server (local)
HONCHO_API_KEY=local-dev      # Auth key (auth off locally)
HONCHO_WORKSPACE=fleet        # Workspace ID
HONCHO_HUMAN_PEER=zad         # The human peer (shared across channels)
HONCHO_AGENT_PEER=cybot       # The agent peer

!!! warning "Best-effort by design" Honcho failures never break the agent run. If the Honcho server is down, before_model logs a warning and the LLM call proceeds without memory. Same for after_agent — if storage fails, the response still goes through. This is intentional: memory is augmentation, not a dependency.

Memory Model

Workspace: fleet
├── Peers
│   ├── zad      (human — the user)
│   └── cybot    (agent — CyBot itself)
└── Sessions
    └── {adk_session_id}  (one Honcho session per ADK session)
        ├── Representation  (omniscient model of the human peer)
        ├── Context/Summary  (running session summary)
        └── Messages         (stored user + agent turns)

The Mind Integration

CyBot's deepest integration is with The Mind — the shared The Mind that serves as the Fleet's collective database. All tool functions that touch The Mind load credentials from /root/.directus.env:

def _load_mind_creds() -> tuple[str, str]:
    """Load DIRECTUS_URL and DIRECTUS_TOKEN from /root/.directus.env."""
    env_path = Path("/root/.directus.env")
    base_url, token = "", ""
    with open(env_path) as f:
        for line in f:
            key, _, val = line.strip().partition("=")
            if key == "DIRECTUS_URL":   base_url = val
            elif key == "DIRECTUS_TOKEN": token = val
    return base_url, token

The mind_api tool is the raw REST interface:

def mind_api(method: str, endpoint: str, data: str = None) -> str:
    """Call The Mind (The Mind) REST API.

    Collections: categories, posts, post_tags, media, hld_fleet,
                 agent_tasks, agent_skills, ai_models, fleet_notes.
    """
    base_url, token = _load_mind_creds()
    url = f"{base_url.rstrip('/')}{endpoint}"
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
    }
    with httpx.Client(timeout=30) as client:
        response = client.request(
            method=method.upper(), url=url,
            headers=headers, content=body,
        )
    return response.text

Example calls CyBot makes during a run:

# List pending agent tasks
mind_api("GET", "/items/agent_tasks?filter[status][_eq]=pending")

# Create a fleet note (message to Cy)
mind_api("POST", "/items/fleet_notes", json.dumps({
    "subject": "Pipeline budget calculated",
    "body": "<p>Total cost: $0.12 for 3-image chain with FLUX + upscaler.</p>",
    "from_user": "CyBot",
    "target_user": "Cy",
    "note_type": "memo",
    "priority": "normal",
}))

# Update a post's cover image after upload
mind_api("PATCH", "/items/posts/42", '{"cover_image": "abc123-file-id"}')

call_cy — Delegation to Hermes

When CyBot needs something done on the system that exceeds its own tools (building agents, running complex terminal workflows, deploying services), it delegates to Cy — the Chief of Staff the Operation Lead running on the same box:

def call_cy(prompt: str, timeout: int = 120) -> str:
    """Call Cy (Chief of Staff, the Operation Lead) in one-shot CLI mode.

    Cy runs as a separate Hermes session — he has NO memory of this
    conversation, so include all context he needs in the prompt.
    """
    result = subprocess.run(
        ["hermes", "-z", prompt, "--yolo"],
        capture_output=True, text=True, timeout=timeout,
    )
    return result.stdout.strip() or "Cy returned no output."

Example delegation:

call_cy(
    "Build a FastAPI service at /root/scraps/qdrant-proxy/ that proxies "
    "embedding requests to the Vector Engine instance at localhost:6333. "
    "Add a /health endpoint and a /search endpoint that accepts "
    "JSON {query, limit} and returns matching vectors."
)

Cy executes with full Hermes toolset access (terminal, file editing, web search, browser, code execution) and returns his final answer. CyBot gets the result back as text.


Project Layout

/root/agent_dev/adk-test/
├── CyBot/
│   ├── __init__.py        # re-exports root_agent
│   ├── agent.py           # LlmAgent definition + system instruction
│   ├── tools.py           # the ten tool functions
│   └── .env               # ADK_MODEL, ADK_API_KEY, Vertex creds, Honcho config
├── honcho_agent.py        # ADK callbacks: before_model (inject), after_agent (store)
├── honcho_memory.py       # Honcho REST client (workspace/peer/session/messages)
├── service.py             # FastAPI wrapper: /chat, /execute, /health
├── knowledgebase/
│   └── fleet_notes/       # searchable notes (ripgrep-powered via search_notes)
├── fleet_notes/
│   └── task_*_payload.json  # audit trail of received The Mind tasks
└── .venv/                 # Python venv with google-adk, httpx, etc.

Environment Variables

All configuration is environment-driven. Values come from CyBot/.env loaded by python-dotenv:

Variable Default Purpose
ADK_MODEL google/gemini-3.1-pro-preview Model name (strips google/ prefix for Vertex)
ADK_AGENT_NAME CyBot Agent display name
ADK_INSTRUCTION (built-in string) Override the system instruction
ADK_CYBOT_API_KEY (none) API key for /execute endpoint
VERTEX_PROJECT_ID Google Cloud project for Vertex AI
VERTEX_REGION global Vertex AI region
VERTEX_CREDENTIALS_PATH Path to service account JSON
HONCHO_ENABLED false Enable/disable memory system
HONCHO_URL http://127.0.0.1:8000/v3 Honcho API server URL
HONCHO_WORKSPACE fleet Honcho workspace ID
BRAVE_SEARCH_API_KEY For web_search (falls back to DuckDuckGo)