Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,18 @@ rust/debug
.artifacts/
rbnx-deploy
tmp*
.libero/
.libero/

# memory service
agent_milvus.db*
agent_memory/

# python venv
venv/
.venv/

# rbnx build
rbnx-build/

# trae IDE
.trae/
2 changes: 1 addition & 1 deletion docs
46 changes: 44 additions & 2 deletions rust/crates/robonix-agent/src/react.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,16 @@ where
let mut vlm = vlm.lock().await;
let mut history = history.lock().await;

if user_input.trim() == "quit" {
// Trigger memory compaction at session end if available
let mcp_tools_for_quit = load_mcp_tools(&mut sdk).await.unwrap_or_default();
if mcp_tools_for_quit.contains_key("compact_memory") {
log::info!("[mcp] Compacting memory session before exit...");
let _ = execute_mcp_tool(&mut sdk, &mcp_tools_for_quit, "compact_memory", "{}").await;
}
return Ok(String::new());
}

let mcp_tools = load_mcp_tools(&mut sdk).await?;
let merged_skills = match skills::load_merged_skills(&mut sdk).await {
Ok(s) => s,
Expand All @@ -116,11 +126,28 @@ where
Vec::new()
}
};

let mut memory_context = String::new();
let is_casual = user_input.trim().to_lowercase();
let skip_memory = is_casual == "hi" || is_casual == "hello" || is_casual.starts_with("who are you") || is_casual == "你是谁" || is_casual == "你好";

if !skip_memory && mcp_tools.contains_key("search_memory") {
let args = serde_json::json!({
"query": user_input
});
let args_str = args.to_string();
if let Ok(result) = execute_mcp_tool(&mut sdk, &mcp_tools, "search_memory", &args_str).await {
if !result.contains("No relevant memories found") {
memory_context = result;
}
}
}

let mcp_tool_defs = build_mcp_tool_defs(&mcp_tools);
let mut all_tools = tools::builtin_tool_defs();
all_tools.extend(mcp_tool_defs);
let soul = skills::load_agent_soul();
let system_prompt = build_system_prompt(soul.as_deref(), &mcp_tools, &merged_skills);
let system_prompt = build_system_prompt(soul.as_deref(), &mcp_tools, &merged_skills, &memory_context);
log::info!(
"tools for this turn: {} total ({} builtin + {} mcp), skills={}",
all_tools.len(),
Expand Down Expand Up @@ -324,7 +351,12 @@ fn truncate_log(s: &str, max: usize) -> String {
if s.len() <= max {
s.to_string()
} else {
format!("{}...", &s[..max])
// Find the nearest valid character boundary before `max`
let mut max_idx = max;
while max_idx > 0 && !s.is_char_boundary(max_idx) {
max_idx -= 1;
}
format!("{}...", &s[..max_idx])
}
}

Expand Down Expand Up @@ -370,6 +402,7 @@ fn build_system_prompt(
soul: Option<&str>,
mcp_tools: &HashMap<String, McpToolSpec>,
skills: &[AgentSkill],
memory_context: &str,
) -> String {
let mut p = String::new();
if let Some(s) = soul {
Expand All @@ -380,6 +413,13 @@ fn build_system_prompt(
p.push_str("\n\n---\n\n");
}
}

if !memory_context.is_empty() {
p.push_str("## Relevant past memories (System Context)\n\n");
p.push_str(memory_context);
p.push_str("\n\n---\n\n");
}

p.push_str(
"\
You are the Robonix system agent running on a robotic platform.
Expand All @@ -393,6 +433,8 @@ Rules:
- If a request requires multiple steps, execute them all before responding.
- When the user references something from earlier in the conversation, use that context.
- Keep responses concise and direct.
- If you are unsure about historical context, user preferences, or past decisions, use the `search_memory` tool.
- When you learn an important user preference or complete a major milestone, use `save_memory` to record it.
- When a tool returns a JSON result with a clear outcome (success/failure, error, etc.),
report that result to the user immediately.
- Use **list_dir** / **read_file** for skill playbooks (see Skill workflow) or when the user
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Memsearch MCP Service package."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import json
import os
import sys
import asyncio
import logging
from pathlib import Path

# Suppress verbose gRPC / absl warnings from milvus-lite
os.environ["GRPC_VERBOSITY"] = "ERROR"
os.environ["GLOG_minloglevel"] = "2"
logging.getLogger("absl").setLevel(logging.ERROR)

def _ensure_proto_gen() -> None:
d = Path(__file__).resolve().parent
while d.parent != d:
pg = d / "proto_gen"
if pg.is_dir() and (pg / "robonix_runtime_pb2.py").exists():
sys.path.insert(0, str(pg))
return
d = d.parent

_ensure_proto_gen()
import grpc
import robonix_runtime_pb2 as pb
import robonix_runtime_pb2_grpc as pb_grpc

from mcp.server.fastmcp import FastMCP
from memsearch import MemSearch

mcp = FastMCP("memsearch_provider")

# Initialize MemSearch
MEMORY_DIR = os.environ.get("AGENT_MEMORY_DIR", "./agent_memory")
MILVUS_URI = os.environ.get("AGENT_MILVUS_URI", "./agent_milvus.db")

mem = MemSearch(
paths=[MEMORY_DIR],
embedding_provider="onnx",
milvus_uri=MILVUS_URI
)

@mcp.tool()
async def search_memory(query: str) -> str:
"""Search the agent's long-term memory for relevant past context, decisions, or user preferences."""
results = await mem.search(query, top_k=2)
if not results:
return "No relevant memories found."
context = "\n\n".join(f"- {m['content']}" for m in results)
return f"Relevant memories:\n{context}"

@mcp.tool()
async def compact_memory() -> str:
"""Compact and summarize recent memories. Call this at the end of a session."""
# Note: requires llm_provider setup for compacting, defaulting to openai if not set
try:
summary_path = await mem.compact(llm_provider="openai")
return f"Memory compacted successfully to {summary_path}."
except Exception as e:
return f"Failed to compact memory: {e}"
Comment on lines +54 to +59

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

compact_memory hardcodes llm_provider="openai"; if openai isn’t installed/configured this tool will always fail (currently only returning an exception string). Consider either adding an explicit dependency/config requirement or detecting missing configuration and returning a clearer actionable message.

Copilot uses AI. Check for mistakes.

@mcp.tool()
async def save_memory(content: str) -> str:
"""Save an important fact, user preference, or decision to long-term memory."""
from datetime import date
p = Path(MEMORY_DIR) / f"{date.today()}_notes.md"
p.parent.mkdir(parents=True, exist_ok=True)
with open(p, "a") as f:

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

save_memory appends to a markdown file without specifying an encoding. On systems with non-UTF-8 locale defaults this can corrupt or fail when saving non-ASCII (e.g., Chinese) content. Open the file with an explicit UTF-8 encoding.

Suggested change
with open(p, "a") as f:
with open(p, "a", encoding="utf-8") as f:

Copilot uses AI. Check for mistakes.
f.write(f"\n{content}\n")
await mem.index()
return "Memory saved and indexed."

def main():
import threading

# 1. Register with Robonix Control Plane
channel = grpc.insecure_channel(os.environ.get("ROBONIX_SERVER", "localhost:50051"))
stub = pb_grpc.RobonixRuntimeStub(channel)

# Pick a port for FastMCP SSE
port = int(os.environ.get("MEMSEARCH_PORT", "50105"))
endpoint = f"http://127.0.0.1:{port}"

metadata = {
"endpoint": endpoint,
"tools": [
{
"name": "search_memory",
"description": "Search the agent's long-term memory for relevant past context, decisions, or user preferences.",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string"}
},
"required": ["query"]
}
},
{
"name": "compact_memory",
"description": "Compact and summarize recent memories. Call this at the end of a session.",
"input_schema": {"type": "object", "properties": {}}
},
{
"name": "save_memory",
"description": "Save an important fact, user preference, or decision to long-term memory.",
"input_schema": {
"type": "object",
"properties": {
"content": {"type": "string"}
},
"required": ["content"]
}
}
]
}

try:
stub.RegisterNode(
pb.RegisterNodeRequest(
node_id="com.robonix.services.memsearch",
namespace="robonix/prm/memsearch",
kind="service",
skill_md="# Memsearch MCP\nProvides memory operations."
)
)

stub.DeclareInterface(
pb.DeclareInterfaceRequest(
node_id="com.robonix.services.memsearch",
name="mcp_interface",
supported_transports=["mcp"],
metadata_json=json.dumps(metadata),
listen_port=port,
)
)
print(f"[memsearch-service] Registered node robonix/prm/memsearch with endpoint {endpoint}")
except Exception as e:
print(f"[memsearch-service] Warning: Failed to register with control plane: {e}")

# Initialize memory index once
asyncio.run(mem.index())

# Start FastMCP server using streamable HTTP
print(f"[memsearch-service] Starting MCP Streamable HTTP server on port {port}...")
import uvicorn
import logging
# Suppress verbose gRPC / absl warnings from milvus-lite
logging.getLogger("absl").setLevel(logging.ERROR)

app = mcp.streamable_http_app()
uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning")

if __name__ == "__main__":
main()
6 changes: 6 additions & 0 deletions rust/examples/packages/memsearch_service/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
mcp
fastmcp
memsearch[onnx]
grpcio
protobuf
uvicorn
20 changes: 20 additions & 0 deletions rust/examples/packages/memsearch_service/robonix_manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
manifestVersion: 1

build:
script: scripts/build.sh

package:
id: com.robonix.example.memsearch_service
name: memsearch_service
version: 0.1.0
vendor: robonix
description: Memsearch MCP Server for Agent memory
license: MulanPSL-2.0

nodes:
- id: com.robonix.services.memsearch
type: python
start: |
export PYTHONPATH="$(pwd):$(pwd)/../../proto_gen:${PYTHONPATH:-}"
# FastMCP internal logs can be verbose, redirect them to a file or suppress
exec python -m memsearch_service.service > memsearch.log 2>&1

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using exec python ... can fail on systems where python isn’t available (only python3 is). Prefer python3 (or a configurable python executable) for portability. Also consider whether redirecting all stdout/stderr to memsearch.log is desirable, since it can hide startup failures from rbnx logs.

Suggested change
exec python -m memsearch_service.service > memsearch.log 2>&1
exec "${PYTHON_EXEC:-python3}" -m memsearch_service.service > memsearch.log 2>&1

Copilot uses AI. Check for mistakes.
13 changes: 13 additions & 0 deletions rust/examples/packages/memsearch_service/scripts/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# SPDX-License-Identifier: MulanPSL-2.0
set -euo pipefail
PKG="${RBNX_PACKAGE_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
if [[ "${RBNX_BUILD_CLEAN:-}" == "1" ]]; then
rm -rf "$PKG/rbnx-build"
fi
EXAMPLES="$(cd "$PKG/../.." && pwd)"
PROTO_GEN="$EXAMPLES/proto_gen"
mkdir -p "$PKG/rbnx-build/ws/install"
cat >"$PKG/rbnx-build/ws/install/setup.bash" <<EOF
export PYTHONPATH="$PKG:$PROTO_GEN:\${PYTHONPATH:-}"
EOF
4 changes: 3 additions & 1 deletion rust/examples/packages/vlm_service/robonix_manifest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ package:
nodes:
- id: com.robonix.services.vlm
type: python
start: exec python3 -m vlm_service.service
start: |
export PYTHONPATH="$(pwd):$(pwd)/../../proto_gen:${PYTHONPATH:-}"
exec python -m vlm_service.service

Copilot AI Mar 30, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using exec python ... can fail on systems where python isn’t available (only python3 is). For consistency with other example scripts and better portability, prefer python3 (or a configurable python executable) in the start command.

Suggested change
exec python -m vlm_service.service
exec "${PYTHON:-python3}" -m vlm_service.service

Copilot uses AI. Check for mistakes.
14 changes: 10 additions & 4 deletions rust/examples/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Robonix examples (vlm_service, tiago_node, smoke scripts).
# grpcio-tools requires protobuf<7 (see https://github.com/grpc/grpc/issues/39750 — tooling lags protobuf 7).
# proto_gen/*_grpc.py is generated with grpcio>=1.78; older grpcio raises RuntimeError at import.
grpcio>=1.78.0
grpcio-tools>=1.78.0
# proto_gen/*_grpc.py is generated with grpcio>=1.78; older grpcio raises RuntimeError at import.# Core
grpcio>=1.60.0
grpcio-tools>=1.60.0
protobuf>=6.30,<7
openai>=1.0
mcp>=1.0
jinja2
typeguard

# Memory Service
fastmcp>=0.1.0
mcp>=1.0.0
memsearch[onnx]
numpy
Pillow
uvicorn
Loading
Loading