feat(plugins)!: make Sidekick exclusive to Claude Code (#7278)
This commit is contained in:
@@ -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.
|
||||
@@ -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']}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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("*"))
|
||||
|
||||
Reference in New Issue
Block a user