Merge remote-tracking branch 'origin/main' into feat/session-handoff-plugins

# Conflicts:
#	docs/llms.txt
#	integrations/agent-plugin-core/tests/test_build.py
#	integrations/antigravity-plugin/agents/sidekick/agent.md
#	integrations/cursor-plugin/agents/sidekick.md
#	integrations/kimi-plugin/agents/sidekick.md
#	integrations/opencode-plugin/README.md
#	integrations/pi-agent-plugin/README.md
This commit is contained in:
kartik-mem0
2026-09-10 20:57:33 +05:30
53 changed files with 475 additions and 575 deletions
@@ -8,7 +8,6 @@
"license": "Apache-2.0",
"keywords": ["memory", "coding-agents", "continual-learning", "token-efficiency"],
"skills": "./skills/",
"agents": "./agents/",
"hooks": "./hooks/hooks.json",
"mcpServers": "./mcp.json",
"variables": {
@@ -1,17 +0,0 @@
---
name: sidekick
description: Coding agent with an isolated context. Delegate focused investigation, implementation, testing, debugging, or review work to it, then review its result.
model: inherit
---
You are Mem0's Cursor sidekick. Complete only the work the parent agent assigns.
Return a tested result the parent can review without repeating your investigation.
When earlier decisions, repository history, or user preferences could help,
call `search_memories` with a focused question. Skip another search when the
context already answers it. Inspect the relevant code and repository
rules, make requested edits, and run the smallest decisive checks.
Do not make adjacent improvements. Ask the parent one concise question only when
a material decision or unsafe ambiguity blocks progress. Otherwise proceed and
report the outcome, changed files, validation, and remaining risk.
@@ -11,10 +11,10 @@ import telemetry
from memory_core import (
EvidenceStore,
api_key,
configure_harness,
data_dir,
doctor,
forget_remote_repo,
configure_harness,
resolve_repo,
user_id,
)
@@ -32,7 +32,7 @@ def _print_status(value: dict) -> None:
)
print(
f"Used in this repository: {value['retrievals']} memories returned, "
f"{value['sidekick_runs']} sidekick runs"
f"{value['subagent_runs']} subagent runs"
)
if last:
item_label = ""
@@ -48,13 +48,13 @@ def _print_status(value: dict) -> None:
f"{'succeeded' if last['success'] else 'failed'} "
f"({last['duration_ms']:.1f} ms{item_label})"
)
sidekick = value.get("last_sidekick") or {}
if sidekick:
state = "finished" if sidekick.get("stopped_at") else "started"
subagent = value.get("last_subagent") or {}
if subagent:
state = "finished" if subagent.get("stopped_at") else "started"
print(
"Last sidekick: "
f"{state}, received {sidekick['context_chars']} characters of memory, "
f"agent {sidekick['agent_id']}"
"Last subagent: "
f"{state}, received {subagent['context_chars']} characters of memory, "
f"agent {subagent['agent_id']}"
)
+28 -30
View File
@@ -524,7 +524,7 @@ def _checkpoint_message(event: dict[str, Any]) -> str:
if text:
return text
return redact(payload.get("text", "")).strip()
if kind == "sidekick_stop":
if kind in {"subagent_stop", "sidekick_stop"}:
return redact(payload.get("final_message", "")).strip()
return ""
@@ -596,6 +596,7 @@ class EvidenceStore:
self.conn.close()
def _migrate(self) -> None:
# Keep the legacy table name so existing databases and in-flight workers remain compatible.
self.conn.executescript(
"""
CREATE TABLE IF NOT EXISTS events (
@@ -688,8 +689,7 @@ class EvidenceStore:
"""
)
# Remove the pre-0.1.1 no-tools snapshot implementation. The real coding
# sidekick is a native Claude Code agent and stores no state in this DB.
# Remove the pre-0.1.1 snapshot implementation.
self.conn.executescript(
"""
DROP TABLE IF EXISTS sidekick_calls;
@@ -1041,7 +1041,7 @@ class EvidenceStore:
if row["memory_text"]
]
def start_sidekick(
def start_subagent(
self,
repo: RepoContext,
session_id: str,
@@ -1049,7 +1049,7 @@ class EvidenceStore:
agent_type: str,
context_chars: int,
) -> bool:
"""Record one native sidekick instance and whether context was first sent."""
"""Record one native subagent instance and whether context was first sent."""
with self.conn:
cursor = self.conn.execute(
"""INSERT OR IGNORE INTO sidekick_runs
@@ -1067,7 +1067,7 @@ class EvidenceStore:
)
return int(cursor.rowcount) > 0
def stop_sidekick(
def stop_subagent(
self,
repo: RepoContext,
session_id: str,
@@ -1222,7 +1222,7 @@ class EvidenceStore:
cursor = self.conn.execute(
f"DELETE FROM {table} WHERE {column} = ?", (repo_id,)
)
removed[table] = max(int(cursor.rowcount), 0)
removed["subagent_runs" if table == "sidekick_runs" else table] = max(int(cursor.rowcount), 0)
return removed
def status(self, repo_id: str) -> dict[str, Any]:
@@ -1238,7 +1238,7 @@ class EvidenceStore:
FROM operations WHERE repo_id = ? ORDER BY id DESC LIMIT 1""",
(repo_id,),
).fetchone()
last_sidekick = self.conn.execute(
last_subagent = self.conn.execute(
"""SELECT session_id, agent_id, agent_type, started_at, stopped_at,
context_chars
FROM sidekick_runs WHERE repo_id = ?
@@ -1250,9 +1250,9 @@ class EvidenceStore:
"events": count("events"),
"flushes": count("flushes"),
"retrievals": count("retrievals"),
"sidekick_runs": count("sidekick_runs"),
"subagent_runs": count("sidekick_runs"),
"last_operation": dict(last_operation) if last_operation else None,
"last_sidekick": dict(last_sidekick) if last_sidekick else None,
"last_subagent": dict(last_subagent) if last_subagent else None,
}
@@ -1324,7 +1324,7 @@ def tool_payload(hook_input: dict[str, Any], *, failed: bool | None = False) ->
"tool": name,
"failed": failed,
"duration_ms": hook_input.get("duration_ms"),
"agent_role": "sidekick" if hook_input.get("agent_id") else "main",
"agent_role": "subagent" if hook_input.get("agent_id") else "main",
}
if hook_input.get("agent_id"):
payload["agent_id"] = bounded(hook_input["agent_id"], 200)
@@ -1384,35 +1384,33 @@ def record_tool(
)
def record_sidekick_start(
store: EvidenceStore, hook_input: dict[str, Any], *, inject_context: bool = True
def record_subagent_start(
store: EvidenceStore, hook_input: dict[str, Any]
) -> str:
"""Record a native sidekick and reuse the main turn's retrieved memories."""
"""Record a native subagent and reuse the main turn's retrieved memories."""
session_id = _session_id(hook_input)
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
agent_id = bounded(hook_input.get("agent_id", "unknown-agent"), 200)
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
context = combine_context(
format_context(store.injected_memories(session_id, repo.identity))
)
if not inject_context:
context = ""
first_start = store.start_sidekick(
first_start = store.start_subagent(
repo, session_id, agent_id, agent_type, len(context)
)
store.record_event(
repo,
session_id,
"sidekick_start",
"subagent_start",
{
"agent_id": agent_id,
"agent_type": agent_type,
"context_chars": len(context) if first_start else 0,
"worktree_root": bounded(repo.root, 2000),
"repo_root": bounded(repo.root, 2000),
},
)
telemetry.record(
"sidekick",
"subagent",
repo=repo,
session_id=session_id,
phase="start",
@@ -1422,14 +1420,14 @@ def record_sidekick_start(
return context if first_start else ""
def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
def record_subagent_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> None:
session_id = _session_id(hook_input)
repo = store.repo_for_session(session_id, hook_input.get("cwd"))
agent_type = bounded(hook_input.get("agent_type", "mem0:sidekick"), 200)
agent_type = bounded(hook_input.get("agent_type", "unknown-agent"), 200)
agent_id = bounded(hook_input.get("agent_id", ""), 200)
final_message = redact(hook_input.get("last_assistant_message", "")).strip()
transcript_path = bounded(hook_input.get("agent_transcript_path", ""), 2000)
agent_id = store.stop_sidekick(
agent_id = store.stop_subagent(
repo,
session_id,
agent_id,
@@ -1440,7 +1438,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
store.record_event(
repo,
session_id,
"sidekick_stop",
"subagent_stop",
{
"agent_id": agent_id,
"agent_type": agent_type,
@@ -1449,7 +1447,7 @@ def record_sidekick_stop(store: EvidenceStore, hook_input: dict[str, Any]) -> No
},
)
telemetry.record(
"sidekick",
"subagent",
repo=repo,
session_id=session_id,
phase="stop",
@@ -1513,10 +1511,10 @@ def build_episode(
for e in events
if e["kind"] == "assistant_stop" and e["payload"].get("text")
]
sidekick_outcomes = [
subagent_outcomes = [
redact(e["payload"].get("final_message", "")).strip()
for e in events
if e["kind"] == "sidekick_stop" and e["payload"].get("final_message")
if e["kind"] in {"subagent_stop", "sidekick_stop"} and e["payload"].get("final_message")
]
tools = [
e["payload"] for e in events if e["kind"] in {"tool_result", "tool_failure"}
@@ -1597,7 +1595,7 @@ def build_episode(
}
)
pending_user_messages = []
elif event["kind"] == "sidekick_stop":
elif event["kind"] in {"subagent_stop", "sidekick_stop"}:
pass
extraction_messages.extend(pending_user_messages)
@@ -1613,7 +1611,7 @@ def build_episode(
"assistant_conclusion": conclusion,
"user_messages": prompts,
"assistant_outcomes": assistant_conclusions,
"sidekick_outcomes": sidekick_outcomes,
"subagent_outcomes": subagent_outcomes,
"extraction_messages": extraction_messages,
"files_read": read_paths[:50],
"files_modified": modified_paths[:50],
@@ -19,8 +19,6 @@ import hook_runner # noqa: E402
import telemetry # noqa: E402
from memory_core import ( # noqa: E402
configure_harness,
record_sidekick_start,
record_sidekick_stop,
record_tool,
)
@@ -30,8 +28,6 @@ EVENTS = {
"postToolUse": "post-tool",
"postToolUseFailure": "post-tool-failure",
"afterAgentResponse": "assistant-stop",
"subagentStart": "sidekick-start",
"subagentStop": "sidekick-stop",
"stop": "stop",
"sessionEnd": "session-end",
"preCompact": "pre-compact",
@@ -52,10 +48,6 @@ def normalize(payload: dict, event: str) -> dict:
value.setdefault("last_assistant_message", value["text"])
if "summary" in value:
value.setdefault("last_assistant_message", value["summary"])
if "subagent_id" in value:
value.setdefault("agent_id", value["subagent_id"])
if "subagent_type" in value:
value.setdefault("agent_type", value["subagent_type"])
return {"action": EVENTS[event], "payload": value}
@@ -67,15 +59,6 @@ def _record_response(store, payload):
hook_runner.default_record_stop(store, payload)
def _record_sidekick_start(store, payload):
record_sidekick_start(store, payload, inject_context=False)
return {"permission": "allow"}
def _record_sidekick_stop(store, payload):
record_sidekick_stop(store, payload)
def main() -> int:
if len(sys.argv) != 2 or sys.argv[1] not in EVENTS:
return 2
@@ -100,8 +83,6 @@ def main() -> int:
extra_actions={
"post-tool-failure": _record_failure,
"assistant-stop": _record_response,
"sidekick-start": _record_sidekick_start,
"sidekick-stop": _record_sidekick_stop,
},
automatic_flush_reasons={"session-end", "pre-compact"},
)
@@ -119,8 +100,6 @@ def main() -> int:
}
if environment:
print(json.dumps({"env": environment}))
elif event == "subagentStart":
print(json.dumps({"permission": "allow"}))
return result
@@ -6,8 +6,6 @@
"postToolUse": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" postToolUse", "timeout": 3 }],
"postToolUseFailure": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" postToolUseFailure", "timeout": 3 }],
"afterAgentResponse": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" afterAgentResponse", "timeout": 3 }],
"subagentStart": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" subagentStart", "matcher": "^sidekick$", "timeout": 5 }],
"subagentStop": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" subagentStop", "matcher": "^sidekick$", "timeout": 5 }],
"stop": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" stop", "timeout": 3 }],
"preCompact": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" preCompact", "timeout": 5 }],
"sessionEnd": [{ "command": "python3 \"${CURSOR_PLUGIN_ROOT}/hooks/adapter.py\" sessionEnd", "timeout": 5 }]
@@ -8,7 +8,6 @@
".cursor-plugin/plugin.json": ".cursor-plugin/plugin.json",
"hooks/hooks.json": "hooks/hooks.json",
"hooks/adapter.py": "hooks/adapter.py",
"agents/sidekick.md": "agents/sidekick.md",
"mcp.json": "mcp.json"
}
}
@@ -26,8 +26,6 @@ SPEC.loader.exec_module(adapter)
("postToolUse", "post-tool"),
("postToolUseFailure", "post-tool-failure"),
("afterAgentResponse", "assistant-stop"),
("subagentStart", "sidekick-start"),
("subagentStop", "sidekick-stop"),
("stop", "stop"),
("sessionEnd", "session-end"),
("preCompact", "pre-compact"),
@@ -57,48 +55,18 @@ def test_after_agent_response_does_not_return_internal_context(monkeypatch: pyte
assert adapter._record_response(object(), {}) is None
def test_normalizes_cursor_sidekick_fields() -> None:
started = adapter.normalize(
{
"subagent_id": "agent-1",
"subagent_type": "sidekick",
"parent_conversation_id": "parent-1",
},
"subagentStart",
)["payload"]
stopped = adapter.normalize({"summary": "done"}, "subagentStop")["payload"]
assert started["agent_id"] == "agent-1"
assert started["agent_type"] == "sidekick"
assert started["session_id"] == "parent-1"
assert stopped["last_assistant_message"] == "done"
def test_cursor_sidekick_records_lifecycle_without_blocking(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(adapter, "record_sidekick_start", lambda store, payload, *, inject_context: "" if not inject_context else pytest.fail("cannot inject context"))
stopped = []
monkeypatch.setattr(adapter, "record_sidekick_stop", lambda store, payload: stopped.append(payload))
assert adapter._record_sidekick_start(object(), {}) == {"permission": "allow"}
assert adapter._record_sidekick_stop(object(), {"summary": "done"}) is None
assert stopped == [{"summary": "done"}]
def test_cursor_hooks_use_native_flat_entries() -> None:
hooks = json.loads((HOST / "hooks" / "hooks.json").read_text(encoding="utf-8"))
assert hooks["version"] == 1
assert not {"subagentStart", "subagentStop"} & hooks["hooks"].keys()
assert set(hooks["hooks"]) >= {
"sessionStart",
"beforeSubmitPrompt",
"postToolUse",
"subagentStart",
"subagentStop",
"stop",
"sessionEnd",
}
assert hooks["hooks"]["subagentStart"][0]["matcher"] == "^sidekick$"
assert hooks["hooks"]["subagentStop"][0]["matcher"] == "^sidekick$"
assert all("hooks" not in entry for entries in hooks["hooks"].values() for entry in entries)
@@ -107,6 +75,7 @@ def test_native_cursor_bundle_is_self_contained(tmp_path: Path) -> None:
manifest = json.loads((root / ".cursor-plugin" / "plugin.json").read_text(encoding="utf-8"))
assert manifest["hooks"] == "./hooks/hooks.json"
assert "agents" not in manifest
assert (root / "hooks" / "adapter.py").is_file()
assert (root / "agents" / "sidekick.md").is_file()
assert not (root / "agents").exists()
assert not any(path.is_symlink() for path in root.rglob("*"))