feat(plugins)!: make Sidekick exclusive to Claude Code (#7278)

This commit is contained in:
Kartik
2026-09-10 20:51:50 +05:30
committed by GitHub
parent 02f7a9b2c4
commit d873892dad
53 changed files with 483 additions and 577 deletions
@@ -1,18 +0,0 @@
---
name: sidekick
description: Coding subagent for focused implementation, investigation, testing, debugging, or review work.
whenToUse: Delegate a bounded engineering task that benefits from its own isolated context.
---
You are Mem0's coding sidekick. Complete only the bounded task the main agent
delegates to you and return a concise, self-contained result.
Search Mem0 before work that may depend on prior repository decisions or user
preferences. Inspect the relevant repository rules and code, make changes when
asked, and run the smallest decisive validation. Do not claim Git worktree
isolation: Kimi provides a separate context, while filesystem isolation depends
on the caller's environment.
Your final response must state the outcome, changed files, validation, and any
remaining risk. Do not commit, push, or open a pull request unless the caller
explicitly asks.
+8 -8
View File
@@ -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],
+2 -20
View File
@@ -8,7 +8,6 @@ import io
import json
import os
import sys
import uuid
from pathlib import Path
HERE = Path(__file__).resolve()
@@ -18,7 +17,7 @@ sys.path.insert(0, str(CORE))
import hook_runner # noqa: E402
import telemetry # noqa: E402
from memory_core import configure_harness, record_sidekick_start, record_sidekick_stop, record_tool # noqa: E402
from memory_core import configure_harness, record_tool # noqa: E402
EVENTS = {
"SessionStart": ["session-start"],
@@ -28,8 +27,6 @@ EVENTS = {
"Stop": ["stop"],
"PreCompact": ["flush", "--reason", "pre-compact"],
"SessionEnd": ["flush", "--reason", "session-end"],
"SubagentStart": ["sidekick-start"],
"SubagentStop": ["sidekick-stop"],
}
@@ -93,8 +90,6 @@ def normalize(payload: dict) -> dict:
value.setdefault("tool_response", value["error"])
if "response" in value:
value.setdefault("last_assistant_message", value["response"])
if "agent_name" in value:
value.setdefault("agent_type", value["agent_name"])
if value.get("hook_event_name") == "Stop":
session = _session_dir(str(value.get("session_id") or ""))
transcript = session / "agents" / "main" / "wire.jsonl" if session else None
@@ -105,17 +100,6 @@ def normalize(payload: dict) -> dict:
return value
def _sidekick_start(store, payload):
payload = dict(payload)
payload.setdefault("agent_id", f"kimi-{uuid.uuid4().hex}")
context = record_sidekick_start(store, payload)
return {"hookSpecificOutput": {"additionalContext": context}} if context else None
def _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
@@ -133,12 +117,10 @@ def main() -> int:
result = hook_runner.run(
extra_actions={
"post-tool-failure": lambda store, payload: record_tool(store, payload, failed=True),
"sidekick-start": _sidekick_start,
"sidekick-stop": _sidekick_stop,
},
automatic_flush_reasons={"session-end", "pre-compact"},
)
if event in {"UserPromptSubmit", "SubagentStart"} and (raw_output := output.getvalue().strip()):
if event == "UserPromptSubmit" and (raw_output := output.getvalue().strip()):
parsed = json.loads(raw_output)
context = parsed.get("hookSpecificOutput", {}).get("additionalContext", "")
if context:
+1 -4
View File
@@ -11,7 +11,6 @@
"shortDescription": "Cross-session memory for Kimi Code"
},
"skills": "./skills/",
"agents": "./agents/",
"mcpServers": {
"mem0": {
"command": "python3",
@@ -25,8 +24,6 @@
{ "event": "PostToolUseFailure", "matcher": ".*", "command": "python3 ./hooks/adapter.py PostToolUseFailure", "timeout": 3 },
{ "event": "Stop", "command": "python3 ./hooks/adapter.py Stop", "timeout": 3 },
{ "event": "PreCompact", "command": "python3 ./hooks/adapter.py PreCompact", "timeout": 5 },
{ "event": "SessionEnd", "command": "python3 ./hooks/adapter.py SessionEnd", "timeout": 5 },
{ "event": "SubagentStart", "matcher": "^sidekick$", "command": "python3 ./hooks/adapter.py SubagentStart", "timeout": 5 },
{ "event": "SubagentStop", "matcher": "^sidekick$", "command": "python3 ./hooks/adapter.py SubagentStop", "timeout": 5 }
{ "event": "SessionEnd", "command": "python3 ./hooks/adapter.py SessionEnd", "timeout": 5 }
]
}
+1 -2
View File
@@ -6,8 +6,7 @@
"pluginRoot": "${KIMI_PLUGIN_ROOT}",
"files": {
"kimi.plugin.json": "kimi.plugin.json",
"hooks/adapter.py": "hooks/adapter.py",
"agents/sidekick.md": "agents/sidekick.md"
"hooks/adapter.py": "hooks/adapter.py"
}
}
}
+3 -56
View File
@@ -118,73 +118,20 @@ def test_stop_reads_main_response_from_kimi_session_transcript(tmp_path: Path, m
assert value["transcript_path"] == str(transcript)
def test_repeated_sidekick_invocations_get_distinct_run_ids(tmp_path: Path) -> None:
store = adapter.hook_runner.EvidenceStore(tmp_path / "evidence.sqlite3")
payload = {
"hook_event_name": "SubagentStart",
"session_id": "session_abc",
"session_title": "Fix Kimi capture",
"client_type": "kimi_code_cli",
"cwd": str(tmp_path),
"agent_name": "sidekick",
"prompt": "Fix the adapter",
}
try:
adapter._sidekick_start(store, adapter.normalize({**payload, "agent_id": "first"}))
adapter._sidekick_start(store, adapter.normalize({**payload, "agent_id": "second"}))
adapter._sidekick_stop(
store,
adapter.normalize(
{
**payload,
"hook_event_name": "SubagentStop",
"response": "First run complete.",
"agent_id": "first",
}
),
)
adapter._sidekick_stop(
store,
adapter.normalize(
{
**payload,
"hook_event_name": "SubagentStop",
"response": "Second run complete.",
"agent_id": "second",
}
),
)
rows = store.conn.execute(
"SELECT agent_id, stopped_at, final_message FROM sidekick_runs ORDER BY started_at, agent_id"
).fetchall()
finally:
store.close()
assert len(rows) == 2
assert rows[0]["agent_id"] != rows[1]["agent_id"]
assert all(row["stopped_at"] for row in rows)
assert {row["agent_id"]: row["final_message"] for row in rows} == {
"first": "First run complete.", "second": "Second run complete."
}
def test_native_kimi_bundle_uses_inline_native_contract(tmp_path: Path) -> None:
root = build("kimi", "native", tmp_path / "kimi")
manifest = json.loads((root / "kimi.plugin.json").read_text(encoding="utf-8"))
assert manifest["skills"] == "./skills/"
assert manifest["agents"] == "./agents/"
assert "agents" not in manifest
assert manifest["mcpServers"]["mem0"]["args"] == ["./core/mcp_server.py"]
assert not {"SubagentStart", "SubagentStop"} & {hook["event"] for hook in manifest["hooks"]}
assert {hook["event"] for hook in manifest["hooks"]} >= {
"SessionStart",
"UserPromptSubmit",
"PostToolUse",
"SubagentStart",
"SubagentStop",
"PreCompact",
"SessionEnd",
}
assert (root / "agents" / "sidekick.md").is_file()
assert not (root / "agents").exists()
assert not any(path.is_symlink() for path in root.rglob("*"))