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) |