memocode 0.2.3__tar.gz → 0.3.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memocode-0.2.3 → memocode-0.3.2}/PKG-INFO +36 -5
- {memocode-0.2.3 → memocode-0.3.2}/README.md +35 -4
- {memocode-0.2.3 → memocode-0.3.2}/control/brain.py +260 -41
- memocode-0.3.2/control/chatmem/compressor.py +176 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/context_manager.py +132 -10
- memocode-0.3.2/control/chatmem/memory/consolidation.py +114 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/memory/core_memory.py +70 -26
- memocode-0.3.2/control/chatmem/memory/recent_memory.py +165 -0
- {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/PKG-INFO +36 -5
- {memocode-0.2.3 → memocode-0.3.2}/pyproject.toml +1 -1
- {memocode-0.2.3 → memocode-0.3.2}/run.py +86 -54
- {memocode-0.2.3 → memocode-0.3.2}/safety/safety.py +9 -3
- memocode-0.2.3/control/chatmem/compressor.py +0 -288
- memocode-0.2.3/control/chatmem/memory/consolidation.py +0 -95
- memocode-0.2.3/control/chatmem/memory/recent_memory.py +0 -153
- {memocode-0.2.3 → memocode-0.3.2}/control/__init__.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/audit.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/__init__.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/cli.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/config.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/mcp_server.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/memory/__init__.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/memory/forgetting.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/server.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/chatmem/token_counter.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/fmt.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/llm.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/planner.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/control/project_manager.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/SOURCES.txt +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/dependency_links.txt +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/entry_points.txt +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/requires.txt +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/memocode.egg-info/top_level.txt +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/safety/__init__.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/safety/backup.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/safety/policy.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/setup.cfg +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/tools/__init__.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/tools/file.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/tools/loader.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/tools/registry.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/tools/shell.py +0 -0
- {memocode-0.2.3 → memocode-0.3.2}/tools/web.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memocode
|
|
3
|
-
Version: 0.2
|
|
3
|
+
Version: 0.3.2
|
|
4
4
|
Summary: Personal AI coding agent with memory, tool execution, and safety controls
|
|
5
5
|
Author: AssassinCHN
|
|
6
6
|
Requires-Python: >=3.11
|
|
@@ -18,9 +18,9 @@ Personal AI coding agent CLI with memory, tool execution, and safety controls.
|
|
|
18
18
|
## Features
|
|
19
19
|
|
|
20
20
|
- **Agentic tool-use loop** — Native function calling (OpenAI / Anthropic compatible), stuck detection, auto mode for fully autonomous task execution
|
|
21
|
-
- **
|
|
21
|
+
- **Multi-layer memory system** — Cross-session persistence with structured compression, on-demand recall, and forgetting curve
|
|
22
22
|
- **Project management** — Multiple projects with separate memory DBs and working directories
|
|
23
|
-
- **Built-in tools** — Shell execution, file read/write/edit, glob, grep, web fetch
|
|
23
|
+
- **Built-in tools** — Shell execution, file read/write/edit, glob, grep, web fetch, memory search
|
|
24
24
|
- **Safety layer** — Human mode (zone checks + confirmation prompts) / Auto mode (whitelist + work_dir hard boundaries) / Bypass mode (session-only, explicit confirmation required)
|
|
25
25
|
- **Extensible** — Drop Python files into `~/.mcode/tools/` for custom tools
|
|
26
26
|
|
|
@@ -46,7 +46,7 @@ Configure your model in `~/.mcode/agent.json` (created on first run):
|
|
|
46
46
|
"model": "MiniMax-M2.7",
|
|
47
47
|
"base_url": "https://api.minimaxi.com/v1",
|
|
48
48
|
"api_key_env": "MINIMAX_API_KEY",
|
|
49
|
-
"context_window":
|
|
49
|
+
"context_window": 65536,
|
|
50
50
|
"extra_body": {"reasoning_split": true}
|
|
51
51
|
}
|
|
52
52
|
}
|
|
@@ -93,11 +93,34 @@ mcode --verbose # Show full tracebacks on errors
|
|
|
93
93
|
| `/undo` | Undo the last run — restore code and/or rewind conversation |
|
|
94
94
|
| `/rollback` | Restore a specific backed-up file (file only, for audit) |
|
|
95
95
|
| `/rewind [N]` | Rewind conversation to turn N (conversation only) |
|
|
96
|
-
| `/
|
|
96
|
+
| `/memory` | Show core memory (user profile) |
|
|
97
|
+
| `/memory set <key> <value>` | Write a core memory entry |
|
|
98
|
+
| `/memory del <key>` | Delete a core memory entry |
|
|
99
|
+
| `/memory pin <key>` | Pin an entry (stability=999, never forgets) |
|
|
97
100
|
| | |
|
|
98
101
|
| `!<command>` | Run shell command directly (safety-checked) |
|
|
99
102
|
| `@<path>` | Attach file contents to your message |
|
|
100
103
|
|
|
104
|
+
## Memory System
|
|
105
|
+
|
|
106
|
+
Mcode maintains four memory layers that persist across sessions:
|
|
107
|
+
|
|
108
|
+
| Layer | Scope | Contents | Updated |
|
|
109
|
+
|-------|-------|----------|---------|
|
|
110
|
+
| **Core memory** | Global (all projects) | User traits: communication style, autonomy preference | Every compression + session end |
|
|
111
|
+
| **Project memory** | Per-project | Decisions, architecture, progress, conventions | Every compression + session end |
|
|
112
|
+
| **Recent memory** | Per-project | Compressed summaries of past sessions | Session end; grows indefinitely |
|
|
113
|
+
| **Session history** | Per-project | Current session verbatim + older turns compressed | Each turn |
|
|
114
|
+
|
|
115
|
+
**How recall works:**
|
|
116
|
+
- Recent session summaries are automatically injected into every turn (newest-first, 4k token cap)
|
|
117
|
+
- The LLM calls `memory_search` when it needs context from older sessions — it decides when and what to search
|
|
118
|
+
- Project memory and core memory are always present in the system prefix (prompt-cache eligible)
|
|
119
|
+
|
|
120
|
+
**Compression:** When the session context reaches 50% of `context_window`, old turns are replaced with a structured summary (topics, decisions, progress, pending, key context). Raw code and config values are excluded — only decisions and rationale are preserved.
|
|
121
|
+
|
|
122
|
+
**Forgetting:** Core memory entries decay over time (Ebbinghaus curve). Entries not reinforced by the judge gradually fade, preventing stale user traits from persisting indefinitely.
|
|
123
|
+
|
|
101
124
|
## Auto Mode
|
|
102
125
|
|
|
103
126
|
Auto mode runs the agent fully autonomously — no confirmation prompts, hard safety boundaries (whitelist-only shell commands, writes restricted to `work_dir`).
|
|
@@ -131,6 +154,7 @@ Verify by running: <test command>
|
|
|
131
154
|
| `glob` | Find files by pattern (`**/*.py`) |
|
|
132
155
|
| `grep` | Search file content by regex |
|
|
133
156
|
| `web_fetch` | Fetch a URL, returns readable text (HTML stripped) |
|
|
157
|
+
| `memory_search` | Search past session summaries for relevant context |
|
|
134
158
|
|
|
135
159
|
## Custom Tools
|
|
136
160
|
|
|
@@ -167,6 +191,13 @@ mcode/
|
|
|
167
191
|
│ ├── project_manager.py # Project registry
|
|
168
192
|
│ ├── audit.py # Audit log
|
|
169
193
|
│ └── chatmem/ # Memory system
|
|
194
|
+
│ ├── context_manager.py # Session history, compression, injection
|
|
195
|
+
│ ├── compressor.py # LLM-based structured summarization
|
|
196
|
+
│ └── memory/
|
|
197
|
+
│ ├── core_memory.py # User traits (global, with forgetting)
|
|
198
|
+
│ ├── recent_memory.py # Cross-session summaries (per-project)
|
|
199
|
+
│ ├── consolidation.py # Periodic pattern extraction → core memory
|
|
200
|
+
│ └── forgetting.py # Ebbinghaus decay for core memory
|
|
170
201
|
├── tools/
|
|
171
202
|
│ ├── file.py # file_read/write/edit, glob, grep
|
|
172
203
|
│ ├── shell.py # shell_exec
|
|
@@ -6,9 +6,9 @@ Personal AI coding agent CLI with memory, tool execution, and safety controls.
|
|
|
6
6
|
## Features
|
|
7
7
|
|
|
8
8
|
- **Agentic tool-use loop** — Native function calling (OpenAI / Anthropic compatible), stuck detection, auto mode for fully autonomous task execution
|
|
9
|
-
- **
|
|
9
|
+
- **Multi-layer memory system** — Cross-session persistence with structured compression, on-demand recall, and forgetting curve
|
|
10
10
|
- **Project management** — Multiple projects with separate memory DBs and working directories
|
|
11
|
-
- **Built-in tools** — Shell execution, file read/write/edit, glob, grep, web fetch
|
|
11
|
+
- **Built-in tools** — Shell execution, file read/write/edit, glob, grep, web fetch, memory search
|
|
12
12
|
- **Safety layer** — Human mode (zone checks + confirmation prompts) / Auto mode (whitelist + work_dir hard boundaries) / Bypass mode (session-only, explicit confirmation required)
|
|
13
13
|
- **Extensible** — Drop Python files into `~/.mcode/tools/` for custom tools
|
|
14
14
|
|
|
@@ -34,7 +34,7 @@ Configure your model in `~/.mcode/agent.json` (created on first run):
|
|
|
34
34
|
"model": "MiniMax-M2.7",
|
|
35
35
|
"base_url": "https://api.minimaxi.com/v1",
|
|
36
36
|
"api_key_env": "MINIMAX_API_KEY",
|
|
37
|
-
"context_window":
|
|
37
|
+
"context_window": 65536,
|
|
38
38
|
"extra_body": {"reasoning_split": true}
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -81,11 +81,34 @@ mcode --verbose # Show full tracebacks on errors
|
|
|
81
81
|
| `/undo` | Undo the last run — restore code and/or rewind conversation |
|
|
82
82
|
| `/rollback` | Restore a specific backed-up file (file only, for audit) |
|
|
83
83
|
| `/rewind [N]` | Rewind conversation to turn N (conversation only) |
|
|
84
|
-
| `/
|
|
84
|
+
| `/memory` | Show core memory (user profile) |
|
|
85
|
+
| `/memory set <key> <value>` | Write a core memory entry |
|
|
86
|
+
| `/memory del <key>` | Delete a core memory entry |
|
|
87
|
+
| `/memory pin <key>` | Pin an entry (stability=999, never forgets) |
|
|
85
88
|
| | |
|
|
86
89
|
| `!<command>` | Run shell command directly (safety-checked) |
|
|
87
90
|
| `@<path>` | Attach file contents to your message |
|
|
88
91
|
|
|
92
|
+
## Memory System
|
|
93
|
+
|
|
94
|
+
Mcode maintains four memory layers that persist across sessions:
|
|
95
|
+
|
|
96
|
+
| Layer | Scope | Contents | Updated |
|
|
97
|
+
|-------|-------|----------|---------|
|
|
98
|
+
| **Core memory** | Global (all projects) | User traits: communication style, autonomy preference | Every compression + session end |
|
|
99
|
+
| **Project memory** | Per-project | Decisions, architecture, progress, conventions | Every compression + session end |
|
|
100
|
+
| **Recent memory** | Per-project | Compressed summaries of past sessions | Session end; grows indefinitely |
|
|
101
|
+
| **Session history** | Per-project | Current session verbatim + older turns compressed | Each turn |
|
|
102
|
+
|
|
103
|
+
**How recall works:**
|
|
104
|
+
- Recent session summaries are automatically injected into every turn (newest-first, 4k token cap)
|
|
105
|
+
- The LLM calls `memory_search` when it needs context from older sessions — it decides when and what to search
|
|
106
|
+
- Project memory and core memory are always present in the system prefix (prompt-cache eligible)
|
|
107
|
+
|
|
108
|
+
**Compression:** When the session context reaches 50% of `context_window`, old turns are replaced with a structured summary (topics, decisions, progress, pending, key context). Raw code and config values are excluded — only decisions and rationale are preserved.
|
|
109
|
+
|
|
110
|
+
**Forgetting:** Core memory entries decay over time (Ebbinghaus curve). Entries not reinforced by the judge gradually fade, preventing stale user traits from persisting indefinitely.
|
|
111
|
+
|
|
89
112
|
## Auto Mode
|
|
90
113
|
|
|
91
114
|
Auto mode runs the agent fully autonomously — no confirmation prompts, hard safety boundaries (whitelist-only shell commands, writes restricted to `work_dir`).
|
|
@@ -119,6 +142,7 @@ Verify by running: <test command>
|
|
|
119
142
|
| `glob` | Find files by pattern (`**/*.py`) |
|
|
120
143
|
| `grep` | Search file content by regex |
|
|
121
144
|
| `web_fetch` | Fetch a URL, returns readable text (HTML stripped) |
|
|
145
|
+
| `memory_search` | Search past session summaries for relevant context |
|
|
122
146
|
|
|
123
147
|
## Custom Tools
|
|
124
148
|
|
|
@@ -155,6 +179,13 @@ mcode/
|
|
|
155
179
|
│ ├── project_manager.py # Project registry
|
|
156
180
|
│ ├── audit.py # Audit log
|
|
157
181
|
│ └── chatmem/ # Memory system
|
|
182
|
+
│ ├── context_manager.py # Session history, compression, injection
|
|
183
|
+
│ ├── compressor.py # LLM-based structured summarization
|
|
184
|
+
│ └── memory/
|
|
185
|
+
│ ├── core_memory.py # User traits (global, with forgetting)
|
|
186
|
+
│ ├── recent_memory.py # Cross-session summaries (per-project)
|
|
187
|
+
│ ├── consolidation.py # Periodic pattern extraction → core memory
|
|
188
|
+
│ └── forgetting.py # Ebbinghaus decay for core memory
|
|
158
189
|
├── tools/
|
|
159
190
|
│ ├── file.py # file_read/write/edit, glob, grep
|
|
160
191
|
│ ├── shell.py # shell_exec
|
|
@@ -68,6 +68,8 @@ def _tool_hint(name: str, args: dict) -> str:
|
|
|
68
68
|
return dim(args.get("pattern", ""))
|
|
69
69
|
if name == "web_fetch":
|
|
70
70
|
return dim(args.get("url", ""))
|
|
71
|
+
if name == "memory_search":
|
|
72
|
+
return dim(args.get("query", ""))
|
|
71
73
|
return ""
|
|
72
74
|
|
|
73
75
|
|
|
@@ -93,42 +95,112 @@ def _print_tool_header_simple(name: str, args: dict):
|
|
|
93
95
|
print(dim(f" {extra}"))
|
|
94
96
|
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
_PROJECT_MEMORY_JSON = os.path.join(_DATA_DIR, "project_memory.json")
|
|
101
|
+
|
|
102
|
+
_DEFAULT_PROJECT_MEMORY = {
|
|
103
|
+
"dimensions": {
|
|
104
|
+
"architecture": {"label": "Tech stack, modules, structure", "stability": 40.0},
|
|
105
|
+
"decisions": {"label": "Confirmed design decisions and rationale", "stability": 35.0},
|
|
106
|
+
"conventions": {"label": "Naming, interface, format conventions", "stability": 35.0},
|
|
107
|
+
"context": {"label": "Project background, goals, constraints", "stability": 30.0},
|
|
108
|
+
"progress": {"label": "Current phase, pending key tasks", "stability": 20.0},
|
|
109
|
+
"completed": {"label": "Completed tasks and milestones", "stability": 25.0},
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
def _build_project_judge_prompt(dimensions: dict) -> str:
|
|
114
|
+
dim_lines = "\n".join(f"- {name}: {cfg.label}" for name, cfg in dimensions.items())
|
|
115
|
+
dim_names = ", ".join(f'"{name}"' for name in dimensions)
|
|
116
|
+
return f"""You are a project memory editor. Review the conversation and update the project's knowledge base.
|
|
117
|
+
|
|
118
|
+
Project memory dimensions:
|
|
119
|
+
{dim_lines}
|
|
120
|
+
|
|
121
|
+
Existing project memory:
|
|
122
|
+
{{existing}}
|
|
123
|
+
|
|
124
|
+
New conversation:
|
|
125
|
+
{{text}}
|
|
126
|
+
|
|
127
|
+
Rules:
|
|
128
|
+
- One entry per dimension. Key must equal the dimension name (e.g. key="decisions").
|
|
129
|
+
- For each dimension, choose one action:
|
|
130
|
+
SKIP — no new project-relevant information for this dimension.
|
|
131
|
+
SUPPLEMENT — new info extends existing (merge into one updated value).
|
|
132
|
+
UPDATE — existing info is clearly superseded or corrected.
|
|
133
|
+
DELETE — the entire dimension entry is explicitly invalidated with nothing to replace it
|
|
134
|
+
(e.g. "we're dropping the X feature entirely", "forget everything about the old auth system").
|
|
135
|
+
Use DELETE only when UPDATE with a replacement value is not possible.
|
|
136
|
+
- Special rules by dimension:
|
|
137
|
+
completed: always SUPPLEMENT (append new completions, never erase old ones). Never DELETE.
|
|
138
|
+
progress: UPDATE freely when the current phase or pending tasks change.
|
|
139
|
+
ONLY record what the user explicitly stated. Do NOT infer, extrapolate,
|
|
140
|
+
or add typical/expected follow-up tasks not mentioned in the conversation.
|
|
141
|
+
decisions: SUPPLEMENT by default; UPDATE only if the user makes an explicit statement reversing a prior decision
|
|
142
|
+
(e.g. "we're switching from X to Y", "decided to drop X"). Do NOT update based on the assistant
|
|
143
|
+
discussing alternatives, or the user asking hypothetical questions about other options.
|
|
144
|
+
architecture/conventions: SUPPLEMENT by default; UPDATE on explicit refactor or migration.
|
|
145
|
+
- Return ONLY entries that changed. Omit SKIPped ones.
|
|
146
|
+
- Exclude: user personal preferences, one-time questions, reasoning processes.
|
|
147
|
+
- Never fabricate or infer information not present in the conversation.
|
|
148
|
+
|
|
149
|
+
Return a JSON array (empty if nothing changed):
|
|
150
|
+
[{{"key": "<dimension_name>", "value": "<merged or new value>", "dimension": "<dimension_name>", "action": "supplement|update|delete"}}]
|
|
151
|
+
Dimension name must be one of: {dim_names}
|
|
152
|
+
|
|
153
|
+
Output JSON only:"""
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _build_project_judge_prompt_zh(dimensions: dict) -> str:
|
|
157
|
+
dim_lines = "\n".join(f"- {name}: {cfg.label}" for name, cfg in dimensions.items())
|
|
158
|
+
dim_names = ", ".join(f'"{name}"' for name in dimensions)
|
|
159
|
+
return f"""你是一个项目记忆编辑器。审查对话内容,更新项目知识库。
|
|
160
|
+
|
|
161
|
+
项目记忆维度:
|
|
162
|
+
{dim_lines}
|
|
163
|
+
|
|
164
|
+
当前项目记忆:
|
|
165
|
+
{{existing}}
|
|
166
|
+
|
|
167
|
+
新对话:
|
|
168
|
+
{{text}}
|
|
169
|
+
|
|
170
|
+
规则:
|
|
171
|
+
- 每个维度只有一条记录,key 等于维度名(如 key="decisions")。
|
|
172
|
+
- 对每个维度,选择以下操作之一:
|
|
173
|
+
跳过 — 该维度没有新的项目相关信息。
|
|
174
|
+
补充 — 新信息扩展了已有内容(合并为一个更新后的 value)。
|
|
175
|
+
修正 — 已有信息被明确推翻或替换。
|
|
176
|
+
删除 — 整个维度条目被明确作废且无替代内容
|
|
177
|
+
(如"我们整个X功能都不做了"、"忘掉旧认证系统的一切")。
|
|
178
|
+
仅在无法用修正+新值替代时才使用删除。
|
|
179
|
+
- 各维度特殊规则:
|
|
180
|
+
completed:始终补充(追加新完成内容,不删除旧记录)。不可删除。
|
|
181
|
+
progress:当前阶段或待办任务变化时,直接修正。
|
|
182
|
+
只记录用户明确说出的内容,严禁推断、外推或添加"典型后续任务"。
|
|
183
|
+
用户未提及的待办事项一律不写。
|
|
184
|
+
decisions:默认补充;仅当用户明确陈述推翻先前决策时才修正
|
|
185
|
+
(如"我们从X换成Y了"、"决定放弃X")。不得因为
|
|
186
|
+
AI 讨论替代方案、或用户询问假设性问题而修正。
|
|
187
|
+
architecture/conventions:默认补充;仅在明确重构或迁移时修正。
|
|
188
|
+
- 只返回发生变化的条目,跳过的不返回。
|
|
189
|
+
- 不包括:用户个人偏好、一次性问题、推理过程。
|
|
190
|
+
- 严禁捏造或推断对话中未出现的信息。
|
|
191
|
+
|
|
192
|
+
返回JSON数组(无变化则返回空数组):
|
|
193
|
+
[{{"key": "<维度名>", "value": "<合并后或新的内容>", "dimension": "<维度名>", "action": "supplement|update|delete"}}]
|
|
194
|
+
维度名必须是以下之一:{dim_names}
|
|
195
|
+
|
|
196
|
+
只输出JSON:"""
|
|
120
197
|
|
|
121
198
|
|
|
122
199
|
_DEFAULT_CHATMEM = {
|
|
123
200
|
"llm": {},
|
|
124
201
|
"dimensions": {
|
|
125
|
-
"
|
|
126
|
-
"
|
|
127
|
-
"goals": {"label": "What I want", "stability": 30.0},
|
|
128
|
-
"preferences": {"label": "Likes / dislikes", "stability": 25.0},
|
|
129
|
-
"capabilities": {"label": "What I can do", "stability": 30.0},
|
|
130
|
-
"emotional": {"label": "How I react", "stability": 20.0},
|
|
131
|
-
"autobiography": {"label": "Key experiences", "stability": 35.0},
|
|
202
|
+
"communication": {"label": "Expression style, language preference, tone, detail level", "stability": 45.0},
|
|
203
|
+
"autonomy": {"label": "How much decision authority delegated to LLM: executes directly vs presents options vs validates", "stability": 40.0},
|
|
132
204
|
},
|
|
133
205
|
}
|
|
134
206
|
|
|
@@ -170,11 +242,11 @@ _DEFAULT_AGENT = {
|
|
|
170
242
|
}
|
|
171
243
|
|
|
172
244
|
|
|
173
|
-
def _init_data_dir() -> tuple[str, str]:
|
|
245
|
+
def _init_data_dir() -> tuple[str, str, str]:
|
|
174
246
|
"""
|
|
175
|
-
Ensure ~/.mcode/ exists with agent.json
|
|
247
|
+
Ensure ~/.mcode/ exists with agent.json, chatmem.json, project_memory.json.
|
|
176
248
|
Migrates from control/ if old files exist and new ones don't.
|
|
177
|
-
Returns (agent_json_path, chatmem_json_path).
|
|
249
|
+
Returns (agent_json_path, chatmem_json_path, project_memory_json_path).
|
|
178
250
|
"""
|
|
179
251
|
import json as _json
|
|
180
252
|
import shutil as _shutil
|
|
@@ -184,6 +256,7 @@ def _init_data_dir() -> tuple[str, str]:
|
|
|
184
256
|
|
|
185
257
|
agent_path = os.path.join(_DATA_DIR, "agent.json")
|
|
186
258
|
chatmem_path = os.path.join(_DATA_DIR, "chatmem.json")
|
|
259
|
+
project_memory_path = _PROJECT_MEMORY_JSON
|
|
187
260
|
|
|
188
261
|
# Migrate agent.json from old location
|
|
189
262
|
old_agent = os.path.join(_CONTROL, "agent.json")
|
|
@@ -205,7 +278,13 @@ def _init_data_dir() -> tuple[str, str]:
|
|
|
205
278
|
_json.dump(_DEFAULT_CHATMEM, f, indent=2, ensure_ascii=False)
|
|
206
279
|
f.write("\n")
|
|
207
280
|
|
|
208
|
-
|
|
281
|
+
# Create project_memory.json with default project dimensions
|
|
282
|
+
if not os.path.exists(project_memory_path):
|
|
283
|
+
with open(project_memory_path, "w") as f:
|
|
284
|
+
_json.dump(_DEFAULT_PROJECT_MEMORY, f, indent=2, ensure_ascii=False)
|
|
285
|
+
f.write("\n")
|
|
286
|
+
|
|
287
|
+
return agent_path, chatmem_path, project_memory_path
|
|
209
288
|
|
|
210
289
|
|
|
211
290
|
def _sync_active_model(agent_cfg: dict, chatmem_json_path: str | None):
|
|
@@ -227,7 +306,7 @@ def _sync_active_model(agent_cfg: dict, chatmem_json_path: str | None):
|
|
|
227
306
|
profile = model_cfg
|
|
228
307
|
agent_cfg["active_model"] = active
|
|
229
308
|
# Save the updated active_model back to agent.json
|
|
230
|
-
agent_path, _ = _init_data_dir()
|
|
309
|
+
agent_path, *_ = _init_data_dir()
|
|
231
310
|
with open(agent_path) as f:
|
|
232
311
|
import json as _json
|
|
233
312
|
data = _json.load(f)
|
|
@@ -260,7 +339,7 @@ class Brain:
|
|
|
260
339
|
no_lock: bool = False,
|
|
261
340
|
):
|
|
262
341
|
# Ensure ~/.mcode/ exists; migrate old files if needed
|
|
263
|
-
agent_path, chatmem_path = _init_data_dir()
|
|
342
|
+
agent_path, chatmem_path, project_memory_path = _init_data_dir()
|
|
264
343
|
|
|
265
344
|
self._agent_cfg_path = agent_path
|
|
266
345
|
self._config_path = config_path or chatmem_path
|
|
@@ -320,6 +399,11 @@ class Brain:
|
|
|
320
399
|
else:
|
|
321
400
|
self.projects.find_or_create_for_cwd()
|
|
322
401
|
|
|
402
|
+
# Project memory — per-project CoreMemory in the same project DB
|
|
403
|
+
self.project_memory = self._make_project_memory(
|
|
404
|
+
cfg, _api_key, project_memory_path
|
|
405
|
+
)
|
|
406
|
+
|
|
323
407
|
# Memory layer — one DB per project, core DB shared
|
|
324
408
|
self.memory = ContextManager.create(
|
|
325
409
|
config=cfg,
|
|
@@ -327,6 +411,13 @@ class Brain:
|
|
|
327
411
|
db_path=self.projects.project_db_path,
|
|
328
412
|
core_db_path=self.projects.core_db_path,
|
|
329
413
|
no_lock=no_lock,
|
|
414
|
+
extra_context_fn=(
|
|
415
|
+
self.project_memory.to_context_string if self.project_memory else None
|
|
416
|
+
),
|
|
417
|
+
on_compress_fn=(
|
|
418
|
+
self._run_project_judge if self.project_memory else None
|
|
419
|
+
),
|
|
420
|
+
auto_recall=False,
|
|
330
421
|
)
|
|
331
422
|
|
|
332
423
|
# Audit log
|
|
@@ -341,6 +432,7 @@ class Brain:
|
|
|
341
432
|
self.registry.register(GLOB_TOOL)
|
|
342
433
|
self.registry.register(GREP_TOOL)
|
|
343
434
|
self.registry.register(WEB_FETCH_TOOL)
|
|
435
|
+
self.registry.register(self._make_memory_search_tool())
|
|
344
436
|
load_external_tools(self.registry)
|
|
345
437
|
|
|
346
438
|
self._max_tool_iter: int = agent_cfg.get("max_tool_iter", 40)
|
|
@@ -406,7 +498,8 @@ class Brain:
|
|
|
406
498
|
"If the user asks you to fetch a URL or HTTP endpoint, call web_fetch — never fabricate the response. "
|
|
407
499
|
"Before editing a file, always read it first with file_read, then proceed to make the edit without stopping. "
|
|
408
500
|
"For targeted changes use file_edit; for new files use file_write. "
|
|
409
|
-
"If a task requires multiple tool calls, keep calling tools until the task is fully done — do not report completion until all actions have been executed via tools."
|
|
501
|
+
"If a task requires multiple tool calls, keep calling tools until the task is fully done — do not report completion until all actions have been executed via tools. "
|
|
502
|
+
"If the user asks about something that may have been discussed in a previous session and you don't have that information in the current context, call memory_search before concluding it is unknown or undecided."
|
|
410
503
|
)
|
|
411
504
|
user_msg = [
|
|
412
505
|
{"role": "system", "content": _system},
|
|
@@ -627,7 +720,8 @@ class Brain:
|
|
|
627
720
|
return True
|
|
628
721
|
|
|
629
722
|
if self.auto_mode:
|
|
630
|
-
|
|
723
|
+
user_wl = self._agent_cfg.get("auto_whitelist", None)
|
|
724
|
+
whitelist = list(set(_safety.DEFAULT_AUTO_WHITELIST) | set(user_wl)) if user_wl else None
|
|
631
725
|
result = _safety.check_auto(task, work_dir=work_dir, whitelist=whitelist)
|
|
632
726
|
if result.violation:
|
|
633
727
|
print(red(f"\n🛑 AUTO-BLOCKED {result.reason}"))
|
|
@@ -789,20 +883,115 @@ class Brain:
|
|
|
789
883
|
# ------------------------------------------------------------------
|
|
790
884
|
|
|
791
885
|
def switch_project(self, name: str):
|
|
792
|
-
self.
|
|
886
|
+
self.end_session()
|
|
793
887
|
self.projects.switch_project(name)
|
|
794
888
|
self._reinit_memory()
|
|
795
889
|
|
|
890
|
+
def _make_memory_search_tool(self):
|
|
891
|
+
"""Build a memory_search Tool that searches recent_memory for relevant past context."""
|
|
892
|
+
from tools.registry import Tool, ToolSchema
|
|
893
|
+
recent_memory = self.memory.recent_memory
|
|
894
|
+
|
|
895
|
+
def _search(query: str, top_k: int = 5) -> str:
|
|
896
|
+
results = recent_memory.search(query, top_k=top_k, min_score=0.0)
|
|
897
|
+
if not results:
|
|
898
|
+
return "No relevant memory found."
|
|
899
|
+
return "\n\n---\n\n".join(results)
|
|
900
|
+
|
|
901
|
+
return Tool(
|
|
902
|
+
schema=ToolSchema(
|
|
903
|
+
name="memory_search",
|
|
904
|
+
description=(
|
|
905
|
+
"Search compressed summaries of past conversation sessions for relevant context. "
|
|
906
|
+
"ALWAYS call this before saying something is unknown, undecided, or not recorded — "
|
|
907
|
+
"the information may exist in a previous session even if it is not in the current context. "
|
|
908
|
+
"Use key terms from the user's question as the query.\n"
|
|
909
|
+
"搜索过去对话的压缩摘要,获取相关上下文。"
|
|
910
|
+
"在说某事未知、未决定或未记录之前,必须先调用此工具——"
|
|
911
|
+
"即使当前上下文中没有该信息,它也可能存在于之前的会话中。"
|
|
912
|
+
"用用户问题中的关键词作为查询词。"
|
|
913
|
+
),
|
|
914
|
+
parameters={
|
|
915
|
+
"type": "object",
|
|
916
|
+
"properties": {
|
|
917
|
+
"query": {
|
|
918
|
+
"type": "string",
|
|
919
|
+
"description": "Key terms to search for (e.g. '8080 端口', 'authentication design')",
|
|
920
|
+
},
|
|
921
|
+
"top_k": {
|
|
922
|
+
"type": "integer",
|
|
923
|
+
"description": "Max results to return (default 5)",
|
|
924
|
+
"default": 5,
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
"required": ["query"],
|
|
928
|
+
},
|
|
929
|
+
),
|
|
930
|
+
fn=_search,
|
|
931
|
+
)
|
|
932
|
+
|
|
933
|
+
def _make_project_memory(self, cfg: "ContextConfig", api_key: str, project_memory_path: str):
|
|
934
|
+
"""Instantiate a CoreMemory for project-scoped knowledge."""
|
|
935
|
+
import json as _json
|
|
936
|
+
from chatmem.memory.core_memory import CoreMemory
|
|
937
|
+
from chatmem.config import DimensionConfig
|
|
938
|
+
|
|
939
|
+
try:
|
|
940
|
+
with open(project_memory_path) as f:
|
|
941
|
+
pm_data = _json.load(f)
|
|
942
|
+
dims = {
|
|
943
|
+
k: DimensionConfig(label=v["label"], stability=float(v["stability"]))
|
|
944
|
+
for k, v in pm_data.get("dimensions", {}).items()
|
|
945
|
+
}
|
|
946
|
+
except Exception:
|
|
947
|
+
dims = {}
|
|
948
|
+
if not dims:
|
|
949
|
+
return None
|
|
950
|
+
|
|
951
|
+
compress_model = cfg.llm.compress_model or cfg.llm.model
|
|
952
|
+
_THINKING_KEYS = {"reasoning_split", "thinking", "reasoning_effort"}
|
|
953
|
+
extra_body = (
|
|
954
|
+
{k: v for k, v in cfg.llm.extra_body.items() if k not in _THINKING_KEYS}
|
|
955
|
+
if cfg.llm.extra_body else None
|
|
956
|
+
) or None
|
|
957
|
+
provider = getattr(cfg.llm, "provider", "openai") or "openai"
|
|
958
|
+
|
|
959
|
+
pm = CoreMemory(
|
|
960
|
+
db_path=self.projects.project_db_path,
|
|
961
|
+
api_key=api_key,
|
|
962
|
+
model=compress_model,
|
|
963
|
+
base_url=cfg.llm.base_url,
|
|
964
|
+
dimensions=dims,
|
|
965
|
+
extra_body=extra_body,
|
|
966
|
+
provider=provider,
|
|
967
|
+
context_label="Project Memory",
|
|
968
|
+
)
|
|
969
|
+
|
|
970
|
+
# Override judge prompts with project-specific framing
|
|
971
|
+
pm._judge_prompt_en = _build_project_judge_prompt(dims)
|
|
972
|
+
pm._judge_prompt_zh = _build_project_judge_prompt_zh(dims)
|
|
973
|
+
return pm
|
|
974
|
+
|
|
796
975
|
def _reinit_memory(self):
|
|
797
976
|
if getattr(self, "memory", None) is not None:
|
|
798
977
|
self.memory.close()
|
|
799
978
|
cfg = ContextConfig.from_json(self._config_path) if self._config_path else ContextConfig()
|
|
979
|
+
api_key = cfg.llm.resolve_api_key()
|
|
980
|
+
self.project_memory = self._make_project_memory(cfg, api_key, _PROJECT_MEMORY_JSON)
|
|
800
981
|
self.memory = ContextManager.create(
|
|
801
982
|
config=cfg,
|
|
802
|
-
api_key=
|
|
983
|
+
api_key=api_key,
|
|
803
984
|
db_path=self.projects.project_db_path,
|
|
804
985
|
core_db_path=self.projects.core_db_path,
|
|
986
|
+
extra_context_fn=(
|
|
987
|
+
self.project_memory.to_context_string if self.project_memory else None
|
|
988
|
+
),
|
|
989
|
+
on_compress_fn=(
|
|
990
|
+
self._run_project_judge if self.project_memory else None
|
|
991
|
+
),
|
|
992
|
+
auto_recall=False,
|
|
805
993
|
)
|
|
994
|
+
self.registry.register(self._make_memory_search_tool())
|
|
806
995
|
|
|
807
996
|
def _save_agent_cfg(self):
|
|
808
997
|
"""Persist current _agent_cfg to agent.json."""
|
|
@@ -815,5 +1004,35 @@ class Brain:
|
|
|
815
1004
|
# Session lifecycle
|
|
816
1005
|
# ------------------------------------------------------------------
|
|
817
1006
|
|
|
1007
|
+
def _run_project_judge(self, text: str):
|
|
1008
|
+
if not self.project_memory:
|
|
1009
|
+
return
|
|
1010
|
+
import logging as _logging
|
|
1011
|
+
logger = _logging.getLogger("chatmem")
|
|
1012
|
+
try:
|
|
1013
|
+
written = self.project_memory.judge_and_update(text)
|
|
1014
|
+
if written:
|
|
1015
|
+
for e in written:
|
|
1016
|
+
if e.get("action") == "delete":
|
|
1017
|
+
logger.info("[project] judge deleted key=%r", e["key"])
|
|
1018
|
+
else:
|
|
1019
|
+
logger.info("[project] judge wrote key=%r dim=%s value=%r",
|
|
1020
|
+
e["key"], e.get("dimension"), e["value"][:60])
|
|
1021
|
+
else:
|
|
1022
|
+
logger.info("[project] judge returned empty")
|
|
1023
|
+
except Exception as exc:
|
|
1024
|
+
logger.warning("[project] judge error: %s", exc)
|
|
1025
|
+
|
|
818
1026
|
def end_session(self):
|
|
1027
|
+
# Capture verbatim history before end_session clears it
|
|
1028
|
+
verbatim = [m for m in self.memory._history if not m.get("_is_summary")]
|
|
1029
|
+
conv_text = "\n".join(
|
|
1030
|
+
f"{m['role']}: {m.get('content', '')}" for m in verbatim
|
|
1031
|
+
) if verbatim else ""
|
|
1032
|
+
|
|
819
1033
|
self.memory.end_session()
|
|
1034
|
+
|
|
1035
|
+
if conv_text:
|
|
1036
|
+
threading.Thread(
|
|
1037
|
+
target=self._run_project_judge, args=(conv_text,), daemon=True
|
|
1038
|
+
).start()
|