qa296 commited on
Commit
6d49dc7
·
1 Parent(s): 3281c27

refactor: standardize type hints and improve null safety across codebase

Browse files

- Update type annotations to use modern `dict[str, Any]` syntax instead of bare `dict`
- Add `_require_*` helper methods in main.py and browser/client.py for safe attribute access
- Fix category parameter validation in remember tool to ensure string type
- Refactor LLM client response handling to use standardized content block format
- Fix bug in browser_action where local variable wasn't used
- Fix missing brackets in tool result logging
- Remove unused imports throughout (datetime, os, subprocess, Path, logger)
- Update category parameter type hints to use `str | None = None` syntax

TODO.md CHANGED
@@ -2,18 +2,6 @@
2
 
3
  ## 优先级排序
4
 
5
- ### 1. 提示词工程(最高优先级)⭐
6
- **状态**: 已添加"诚实原则",需要验证和优化
7
- **问题**: 从运行日志看,数字生命还在编造结果(假装成功、实际失败)
8
- **需要**:
9
- - 验证"诚实原则"是否生效
10
- - 可能需要更强的提示或示例
11
- - 可能需要添加"验证工具"来强制检查结果
12
-
13
- **成功标准**:
14
- - 浏览器崩溃时,它如实说"失败了"
15
- - 插件加载失败时,不说"成功创建"
16
- - API调用失败时,说"无法获取",不编造数据
17
 
18
  ### 2. 插件系统完善
19
  **状态**: 框架已有,但有问题
@@ -56,12 +44,6 @@
56
  **描述**: `receive_stimulus()` 需要一个入口
57
  **可能方案**: 前端控制台的"发送刺激"功能
58
 
59
- ### 5. 清理错误记忆
60
- **状态**: 待办
61
- **描述**: 数字生命之前存储了很多"虚假成功"的记忆
62
- **需要**: 清理或标记这些记忆为"不可靠"
63
-
64
- ---
65
 
66
  ## 已完成
67
 
 
2
 
3
  ## 优先级排序
4
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
  ### 2. 插件系统完善
7
  **状态**: 框架已有,但有问题
 
44
  **描述**: `receive_stimulus()` 需要一个入口
45
  **可能方案**: 前端控制台的"发送刺激"功能
46
 
 
 
 
 
 
 
47
 
48
  ## 已完成
49
 
agent/agent_loop.py CHANGED
@@ -7,12 +7,12 @@ from pathlib import Path
7
  from loguru import logger
8
 
9
  from agent.context_manager import ContextManager
10
- from agent.llm_client import BaseLLMClient, create_client
11
  from tools.bash_tool import run_bash
12
  from tools.file_tools import run_read, run_write, run_edit
13
  from tools.code_executor import run_create_plugin
14
  from tools.browser_tool import run_browser
15
- from tools.memory_tools import run_remember, run_recall, run_journal
16
 
17
 
18
  # Tool definitions sent to the API
@@ -246,9 +246,12 @@ class AgentLoop:
246
  args["path"], args["old_text"], args["new_text"], workdir=self.workdir
247
  )
248
  elif name == "remember":
 
 
 
249
  return await run_remember(
250
  args["content"],
251
- category=args.get("category"),
252
  )
253
  elif name == "recall":
254
  return await run_recall(args["query"])
 
7
  from loguru import logger
8
 
9
  from agent.context_manager import ContextManager
10
+ from agent.llm_client import BaseLLMClient
11
  from tools.bash_tool import run_bash
12
  from tools.file_tools import run_read, run_write, run_edit
13
  from tools.code_executor import run_create_plugin
14
  from tools.browser_tool import run_browser
15
+ from tools.memory_tools import run_recall, run_remember
16
 
17
 
18
  # Tool definitions sent to the API
 
246
  args["path"], args["old_text"], args["new_text"], workdir=self.workdir
247
  )
248
  elif name == "remember":
249
+ category = args.get("category")
250
+ if category is not None and not isinstance(category, str):
251
+ return "Error: category must be a string"
252
  return await run_remember(
253
  args["content"],
254
+ category=category,
255
  )
256
  elif name == "recall":
257
  return await run_recall(args["query"])
agent/context_manager.py CHANGED
@@ -2,9 +2,9 @@
2
 
3
  from __future__ import annotations
4
 
5
- import anthropic
6
  from loguru import logger
7
 
 
8
  from utils.token_counter import estimate_messages_tokens
9
 
10
 
@@ -13,7 +13,7 @@ class ContextManager:
13
 
14
  def __init__(
15
  self,
16
- client: anthropic.AsyncAnthropic,
17
  model: str,
18
  context_window: int = 200000,
19
  compact_threshold: float = 0.9,
@@ -134,10 +134,9 @@ class ContextManager:
134
  conversation_text = conversation_text[:100000] + "\n\n... (truncated)"
135
 
136
  try:
137
- response = await self.client.messages.create(
138
  model=self.model,
139
- max_tokens=4000,
140
- system=(
141
  "You are a conversation summarizer. Create a concise but comprehensive "
142
  "summary of the following conversation. Preserve:\n"
143
  "- Key decisions made\n"
@@ -152,14 +151,20 @@ class ContextManager:
152
  {
153
  "role": "user",
154
  "content": f"Summarize this conversation:\n\n{conversation_text}",
155
- }
156
  ],
 
 
157
  )
158
 
159
- summary = ""
160
- for block in response.content:
161
- if hasattr(block, "text"):
162
- summary += block.text
 
 
 
 
163
 
164
  return summary or "No summary generated."
165
  except Exception as e:
 
2
 
3
  from __future__ import annotations
4
 
 
5
  from loguru import logger
6
 
7
+ from agent.llm_client import BaseLLMClient
8
  from utils.token_counter import estimate_messages_tokens
9
 
10
 
 
13
 
14
  def __init__(
15
  self,
16
+ client: BaseLLMClient,
17
  model: str,
18
  context_window: int = 200000,
19
  compact_threshold: float = 0.9,
 
134
  conversation_text = conversation_text[:100000] + "\n\n... (truncated)"
135
 
136
  try:
137
+ response = await self.client.create_message(
138
  model=self.model,
139
+ system_prompt=(
 
140
  "You are a conversation summarizer. Create a concise but comprehensive "
141
  "summary of the following conversation. Preserve:\n"
142
  "- Key decisions made\n"
 
151
  {
152
  "role": "user",
153
  "content": f"Summarize this conversation:\n\n{conversation_text}",
154
+ },
155
  ],
156
+ tools=[],
157
+ max_tokens=4000,
158
  )
159
 
160
+ summary_parts: list[str] = []
161
+ for block in response.content_blocks:
162
+ if isinstance(block, dict) and block.get("type") == "text":
163
+ text = block.get("text")
164
+ if isinstance(text, str):
165
+ summary_parts.append(text)
166
+
167
+ summary = "".join(summary_parts)
168
 
169
  return summary or "No summary generated."
170
  except Exception as e:
agent/llm_client.py CHANGED
@@ -4,7 +4,7 @@ from __future__ import annotations
4
 
5
  import os
6
  from abc import ABC, abstractmethod
7
- from typing import Any
8
 
9
  from loguru import logger
10
 
@@ -17,8 +17,8 @@ class BaseLLMClient(ABC):
17
  self,
18
  model: str,
19
  system_prompt: str,
20
- messages: list[dict],
21
- tools: list[dict],
22
  max_tokens: int,
23
  ) -> LLMResponse:
24
  """Create a message and return standardized response."""
@@ -30,7 +30,7 @@ class LLMResponse:
30
 
31
  def __init__(
32
  self,
33
- content_blocks: list[dict],
34
  stop_reason: str,
35
  tool_calls: list[ToolCall] | None = None,
36
  ):
@@ -42,7 +42,7 @@ class LLMResponse:
42
  class ToolCall:
43
  """Standardized tool call."""
44
 
45
- def __init__(self, id: str, name: str, input: dict):
46
  self.id = id
47
  self.name = name
48
  self.input = input
@@ -59,16 +59,16 @@ class AnthropicClient(BaseLLMClient):
59
  self,
60
  model: str,
61
  system_prompt: str,
62
- messages: list[dict],
63
- tools: list[dict],
64
  max_tokens: int,
65
  ) -> LLMResponse:
66
  """Create message using Anthropic API."""
67
  response = await self._client.messages.create(
68
  model=model,
69
  system=system_prompt,
70
- messages=messages,
71
- tools=tools,
72
  max_tokens=max_tokens,
73
  )
74
 
@@ -92,7 +92,7 @@ class AnthropicClient(BaseLLMClient):
92
 
93
  return LLMResponse(
94
  content_blocks=content_blocks,
95
- stop_reason=response.stop_reason,
96
  tool_calls=tool_calls,
97
  )
98
 
@@ -111,8 +111,8 @@ class OpenAIClient(BaseLLMClient):
111
  self,
112
  model: str,
113
  system_prompt: str,
114
- messages: list[dict],
115
- tools: list[dict],
116
  max_tokens: int,
117
  ) -> LLMResponse:
118
  """Create message using OpenAI-compatible API."""
@@ -120,7 +120,9 @@ class OpenAIClient(BaseLLMClient):
120
  openai_tools = self._convert_tools(tools)
121
 
122
  # Build message list with system prompt
123
- all_messages = [{"role": "system", "content": system_prompt}]
 
 
124
 
125
  # Convert messages
126
  for msg in messages:
@@ -130,8 +132,8 @@ class OpenAIClient(BaseLLMClient):
130
 
131
  response = await self._client.chat.completions.create(
132
  model=model,
133
- messages=all_messages,
134
- tools=openai_tools if openai_tools else None,
135
  max_tokens=max_tokens,
136
  )
137
 
@@ -147,16 +149,20 @@ class OpenAIClient(BaseLLMClient):
147
  if choice.message.tool_calls:
148
  for tc in choice.message.tool_calls:
149
  import json
 
 
 
 
150
  tool_calls.append(ToolCall(
151
- id=tc.id,
152
- name=tc.function.name,
153
- input=json.loads(tc.function.arguments) if tc.function.arguments else {},
154
  ))
155
  content_blocks.append({
156
  "type": "tool_use",
157
- "id": tc.id,
158
- "name": tc.function.name,
159
- "input": json.loads(tc.function.arguments) if tc.function.arguments else {},
160
  })
161
 
162
  stop_reason = "tool_use" if tool_calls else "end_turn"
@@ -169,7 +175,7 @@ class OpenAIClient(BaseLLMClient):
169
  tool_calls=tool_calls,
170
  )
171
 
172
- def _convert_tools(self, anthropic_tools: list[dict]) -> list[dict]:
173
  """Convert Anthropic-style tools to OpenAI format."""
174
  openai_tools = []
175
  for tool in anthropic_tools:
@@ -183,7 +189,7 @@ class OpenAIClient(BaseLLMClient):
183
  })
184
  return openai_tools
185
 
186
- def _convert_message(self, msg: dict) -> dict | None:
187
  """Convert Anthropic-style message to OpenAI format."""
188
  role = msg.get("role")
189
  content = msg.get("content")
@@ -204,7 +210,7 @@ class OpenAIClient(BaseLLMClient):
204
 
205
  if tool_results:
206
  # Convert tool results to OpenAI format
207
- result_msg = {"role": "tool", "content": ""}
208
  for tr in tool_results:
209
  result_msg["tool_call_id"] = tr.get("tool_use_id", "")
210
  result_msg["content"] = tr.get("content", "")
@@ -225,7 +231,7 @@ class OpenAIClient(BaseLLMClient):
225
  elif block.get("type") == "tool_use":
226
  tool_uses.append(block)
227
 
228
- result = {"role": "assistant"}
229
  if texts:
230
  result["content"] = "\n".join(texts)
231
  if tool_uses:
 
4
 
5
  import os
6
  from abc import ABC, abstractmethod
7
+ from typing import Any, cast
8
 
9
  from loguru import logger
10
 
 
17
  self,
18
  model: str,
19
  system_prompt: str,
20
+ messages: list[dict[str, Any]],
21
+ tools: list[dict[str, Any]],
22
  max_tokens: int,
23
  ) -> LLMResponse:
24
  """Create a message and return standardized response."""
 
30
 
31
  def __init__(
32
  self,
33
+ content_blocks: list[dict[str, Any]],
34
  stop_reason: str,
35
  tool_calls: list[ToolCall] | None = None,
36
  ):
 
42
  class ToolCall:
43
  """Standardized tool call."""
44
 
45
+ def __init__(self, id: str, name: str, input: dict[str, Any]):
46
  self.id = id
47
  self.name = name
48
  self.input = input
 
59
  self,
60
  model: str,
61
  system_prompt: str,
62
+ messages: list[dict[str, Any]],
63
+ tools: list[dict[str, Any]],
64
  max_tokens: int,
65
  ) -> LLMResponse:
66
  """Create message using Anthropic API."""
67
  response = await self._client.messages.create(
68
  model=model,
69
  system=system_prompt,
70
+ messages=cast(Any, messages),
71
+ tools=cast(Any, tools),
72
  max_tokens=max_tokens,
73
  )
74
 
 
92
 
93
  return LLMResponse(
94
  content_blocks=content_blocks,
95
+ stop_reason=str(response.stop_reason or "end_turn"),
96
  tool_calls=tool_calls,
97
  )
98
 
 
111
  self,
112
  model: str,
113
  system_prompt: str,
114
+ messages: list[dict[str, Any]],
115
+ tools: list[dict[str, Any]],
116
  max_tokens: int,
117
  ) -> LLMResponse:
118
  """Create message using OpenAI-compatible API."""
 
120
  openai_tools = self._convert_tools(tools)
121
 
122
  # Build message list with system prompt
123
+ all_messages: list[dict[str, Any]] = [
124
+ {"role": "system", "content": system_prompt}
125
+ ]
126
 
127
  # Convert messages
128
  for msg in messages:
 
132
 
133
  response = await self._client.chat.completions.create(
134
  model=model,
135
+ messages=cast(Any, all_messages),
136
+ tools=cast(Any, openai_tools if openai_tools else None),
137
  max_tokens=max_tokens,
138
  )
139
 
 
149
  if choice.message.tool_calls:
150
  for tc in choice.message.tool_calls:
151
  import json
152
+ if not hasattr(tc, "function"):
153
+ continue
154
+ tc_any = cast(Any, tc)
155
+ function = tc_any.function
156
  tool_calls.append(ToolCall(
157
+ id=tc_any.id,
158
+ name=function.name,
159
+ input=json.loads(function.arguments) if function.arguments else {},
160
  ))
161
  content_blocks.append({
162
  "type": "tool_use",
163
+ "id": tc_any.id,
164
+ "name": function.name,
165
+ "input": json.loads(function.arguments) if function.arguments else {},
166
  })
167
 
168
  stop_reason = "tool_use" if tool_calls else "end_turn"
 
175
  tool_calls=tool_calls,
176
  )
177
 
178
+ def _convert_tools(self, anthropic_tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
179
  """Convert Anthropic-style tools to OpenAI format."""
180
  openai_tools = []
181
  for tool in anthropic_tools:
 
189
  })
190
  return openai_tools
191
 
192
+ def _convert_message(self, msg: dict[str, Any]) -> dict[str, Any] | None:
193
  """Convert Anthropic-style message to OpenAI format."""
194
  role = msg.get("role")
195
  content = msg.get("content")
 
210
 
211
  if tool_results:
212
  # Convert tool results to OpenAI format
213
+ result_msg: dict[str, Any] = {"role": "tool", "content": ""}
214
  for tr in tool_results:
215
  result_msg["tool_call_id"] = tr.get("tool_use_id", "")
216
  result_msg["content"] = tr.get("content", "")
 
231
  elif block.get("type") == "tool_use":
232
  tool_uses.append(block)
233
 
234
+ result: dict[str, Any] = {"role": "assistant"}
235
  if texts:
236
  result["content"] = "\n".join(texts)
237
  if tool_uses:
agent/message_history.py CHANGED
@@ -1,7 +1,6 @@
1
  """Message history tracking and persistence."""
2
 
3
  import json
4
- from datetime import datetime
5
  from pathlib import Path
6
 
7
  import aiofiles
 
1
  """Message history tracking and persistence."""
2
 
3
  import json
 
4
  from pathlib import Path
5
 
6
  import aiofiles
app.py CHANGED
@@ -1,7 +1,6 @@
1
  """Entelechy Control Console - 观察和控制数字生命的控制台"""
2
 
3
  import asyncio
4
- import os
5
  import subprocess
6
  import threading
7
  from pathlib import Path
@@ -69,7 +68,7 @@ def get_thinking_stream() -> str:
69
  inputs = block.get("input", {})
70
  texts.append(f"[调用工具: {name}({inputs})]")
71
  elif block.get("type") == "tool_result":
72
- texts.append(f"[工具结果]")
73
  content = "\n".join(texts)
74
 
75
  output.append(f"**{role}**: {content}\n")
@@ -154,20 +153,21 @@ def send_stimulus(stimulus_type: str, content: str) -> str:
154
  def browser_action(action: str, url: str = "", selector: str = "", text: str = "") -> str:
155
  """浏览器控制"""
156
  life = _get_life()
157
- if life.browser_client is None:
 
158
  return "浏览器客户端未初始化"
159
 
160
  async def _action():
161
  if action == "navigate":
162
- return await life.browser_client.navigate(url)
163
  elif action == "click":
164
- return await life.browser_client.click(selector)
165
  elif action == "type":
166
- return await life.browser_client.type_text(selector, text)
167
  elif action == "screenshot":
168
- return await life.browser_client.screenshot()
169
  elif action == "extract":
170
- return await life.browser_client.extract_content()
171
  else:
172
  return f"未知操作: {action}"
173
 
@@ -196,7 +196,7 @@ def _start_background_life():
196
 
197
  # ========== 构建控制台界面 ==========
198
 
199
- with gr.Blocks(title="Entelechy 控制台", theme=gr.themes.Soft()) as demo:
200
  gr.Markdown("# 🧬 Entelechy - 数字生命控制台")
201
  gr.Markdown("这是观察和控制数字生命的控制台,不是聊天机器人")
202
 
 
1
  """Entelechy Control Console - 观察和控制数字生命的控制台"""
2
 
3
  import asyncio
 
4
  import subprocess
5
  import threading
6
  from pathlib import Path
 
68
  inputs = block.get("input", {})
69
  texts.append(f"[调用工具: {name}({inputs})]")
70
  elif block.get("type") == "tool_result":
71
+ texts.append("[工具结果]")
72
  content = "\n".join(texts)
73
 
74
  output.append(f"**{role}**: {content}\n")
 
153
  def browser_action(action: str, url: str = "", selector: str = "", text: str = "") -> str:
154
  """浏览器控制"""
155
  life = _get_life()
156
+ browser_client = life.browser_client
157
+ if browser_client is None:
158
  return "浏览器客户端未初始化"
159
 
160
  async def _action():
161
  if action == "navigate":
162
+ return await browser_client.navigate(url)
163
  elif action == "click":
164
+ return await browser_client.click(selector)
165
  elif action == "type":
166
+ return await browser_client.type_text(selector, text)
167
  elif action == "screenshot":
168
+ return await browser_client.screenshot()
169
  elif action == "extract":
170
+ return await browser_client.extract_content()
171
  else:
172
  return f"未知操作: {action}"
173
 
 
196
 
197
  # ========== 构建控制台界面 ==========
198
 
199
+ with gr.Blocks(title="Entelechy 控制台") as demo:
200
  gr.Markdown("# 🧬 Entelechy - 数字生命控制台")
201
  gr.Markdown("这是观察和控制数字生命的控制台,不是聊天机器人")
202
 
browser/client.py CHANGED
@@ -4,6 +4,7 @@ from __future__ import annotations
4
 
5
  import base64
6
  from pathlib import Path
 
7
 
8
  from loguru import logger
9
 
@@ -22,6 +23,16 @@ class BrowserClient:
22
  self.page = None
23
  self._started = False
24
 
 
 
 
 
 
 
 
 
 
 
25
  async def start(self):
26
  """Launch the browser and restore session state."""
27
  if self._started:
@@ -78,9 +89,11 @@ class BrowserClient:
78
  """
79
  await self.ensure_started()
80
  try:
81
- await self.page.goto(url, wait_until="domcontentloaded", timeout=30000)
82
- title = await self.page.title()
83
- await self.session_manager.save_state(self.context)
 
 
84
  return f"Navigated to: {url}\nTitle: {title}"
85
  except Exception as e:
86
  return f"Navigation error: {e}"
@@ -89,8 +102,9 @@ class BrowserClient:
89
  """Click an element."""
90
  await self.ensure_started()
91
  try:
92
- await self.page.click(selector, timeout=10000)
93
- await self.page.wait_for_load_state("domcontentloaded")
 
94
  return f"Clicked: {selector}"
95
  except Exception as e:
96
  return f"Click error: {e}"
@@ -99,7 +113,8 @@ class BrowserClient:
99
  """Type text into an element."""
100
  await self.ensure_started()
101
  try:
102
- await self.page.fill(selector, text, timeout=10000)
 
103
  return f"Typed text into: {selector}"
104
  except Exception as e:
105
  return f"Type error: {e}"
@@ -112,7 +127,8 @@ class BrowserClient:
112
  """
113
  await self.ensure_started()
114
  try:
115
- raw = await self.page.screenshot(full_page=full_page)
 
116
  encoded = base64.b64encode(raw).decode("utf-8")
117
  return f"Screenshot taken ({len(raw)} bytes). Base64: {encoded[:100]}..."
118
  except Exception as e:
@@ -122,9 +138,10 @@ class BrowserClient:
122
  """Extract text content from the current page."""
123
  await self.ensure_started()
124
  try:
125
- title = await self.page.title()
126
- url = self.page.url
127
- text = await self.page.inner_text("body")
 
128
  # Truncate very long pages
129
  if len(text) > 50000:
130
  text = text[:50000] + "\n\n... (truncated)"
 
4
 
5
  import base64
6
  from pathlib import Path
7
+ from typing import Any
8
 
9
  from loguru import logger
10
 
 
23
  self.page = None
24
  self._started = False
25
 
26
+ def _require_page(self) -> Any:
27
+ if self.page is None:
28
+ raise RuntimeError("Browser page is not initialized")
29
+ return self.page
30
+
31
+ def _require_context(self) -> Any:
32
+ if self.context is None:
33
+ raise RuntimeError("Browser context is not initialized")
34
+ return self.context
35
+
36
  async def start(self):
37
  """Launch the browser and restore session state."""
38
  if self._started:
 
89
  """
90
  await self.ensure_started()
91
  try:
92
+ page = self._require_page()
93
+ context = self._require_context()
94
+ await page.goto(url, wait_until="domcontentloaded", timeout=30000)
95
+ title = await page.title()
96
+ await self.session_manager.save_state(context)
97
  return f"Navigated to: {url}\nTitle: {title}"
98
  except Exception as e:
99
  return f"Navigation error: {e}"
 
102
  """Click an element."""
103
  await self.ensure_started()
104
  try:
105
+ page = self._require_page()
106
+ await page.click(selector, timeout=10000)
107
+ await page.wait_for_load_state("domcontentloaded")
108
  return f"Clicked: {selector}"
109
  except Exception as e:
110
  return f"Click error: {e}"
 
113
  """Type text into an element."""
114
  await self.ensure_started()
115
  try:
116
+ page = self._require_page()
117
+ await page.fill(selector, text, timeout=10000)
118
  return f"Typed text into: {selector}"
119
  except Exception as e:
120
  return f"Type error: {e}"
 
127
  """
128
  await self.ensure_started()
129
  try:
130
+ page = self._require_page()
131
+ raw = await page.screenshot(full_page=full_page)
132
  encoded = base64.b64encode(raw).decode("utf-8")
133
  return f"Screenshot taken ({len(raw)} bytes). Base64: {encoded[:100]}..."
134
  except Exception as e:
 
138
  """Extract text content from the current page."""
139
  await self.ensure_started()
140
  try:
141
+ page = self._require_page()
142
+ title = await page.title()
143
+ url = page.url
144
+ text = await page.inner_text("body")
145
  # Truncate very long pages
146
  if len(text) > 50000:
147
  text = text[:50000] + "\n\n... (truncated)"
main.py CHANGED
@@ -60,6 +60,21 @@ class DigitalLife:
60
  self.agent: AgentLoop | None = None
61
  self.history: MessageHistory | None = None
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  async def _initialize(self):
64
  """Initialize all components."""
65
  # Ensure directories exist
@@ -139,7 +154,8 @@ class DigitalLife:
139
 
140
  async def _get_core_context(self) -> str:
141
  """Get CORE.md content for every LLM call."""
142
- core = await self.memory_manager.load_core()
 
143
  if core:
144
  return f"\n=== 核心记忆 ===\n{core}\n================\n"
145
  return ""
@@ -148,7 +164,11 @@ class DigitalLife:
148
  """Wake up: load CORE.md and restore self-awareness."""
149
  logger.info("Waking up...")
150
 
151
- core_memories = await self.memory_manager.load_core()
 
 
 
 
152
 
153
  wake_up_parts = ["你醒来了。\n"]
154
 
@@ -160,16 +180,16 @@ class DigitalLife:
160
  wake_up_content = "\n".join(wake_up_parts)
161
 
162
  # If we have saved history, continue from it; otherwise start fresh
163
- if self.history.messages:
164
- logger.info(f"Resuming from {len(self.history.messages)} saved messages")
165
- self.history.append({"role": "user", "content": wake_up_content})
166
  else:
167
- self.history.set_messages([{"role": "user", "content": wake_up_content}])
168
 
169
  # Run the wake-up conversation
170
- messages = await self.agent.run(self.history.get_messages())
171
- self.history.set_messages(messages)
172
- await self.history.save()
173
 
174
  logger.info("Wake up complete")
175
 
@@ -178,6 +198,9 @@ class DigitalLife:
178
  await self._initialize()
179
  await self._wake_up()
180
 
 
 
 
181
  while self.alive:
182
  try:
183
  # Check for external stimulus (non-blocking, immediate)
@@ -187,23 +210,23 @@ class DigitalLife:
187
 
188
  if stimulus:
189
  # External stimulus received
190
- self.history.append({
191
  "role": "user",
192
  "content": f"[感知] {stimulus['type']}: {stimulus['content']}",
193
  })
194
  else:
195
  # No stimulus - continue autonomous operation
196
- self.history.append({
197
  "role": "user",
198
  "content": "继续。",
199
  })
200
 
201
  # Run agent loop
202
- messages = await self.agent.run(self.history.get_messages())
203
- self.history.set_messages(messages)
204
 
205
  # Persist history periodically
206
- await self.history.save()
207
 
208
  # Immediately continue to next iteration (no waiting)
209
 
@@ -238,10 +261,13 @@ class DigitalLife:
238
  if self.agent is None:
239
  await self._initialize()
240
 
241
- self.history.append({"role": "user", "content": message})
242
- messages = await self.agent.run(self.history.get_messages())
243
- self.history.set_messages(messages)
244
- await self.history.save()
 
 
 
245
 
246
  # Extract the last assistant text
247
  for msg in reversed(messages):
 
60
  self.agent: AgentLoop | None = None
61
  self.history: MessageHistory | None = None
62
 
63
+ def _require_memory_manager(self) -> MemoryManager:
64
+ if self.memory_manager is None:
65
+ raise RuntimeError("Memory manager is not initialized")
66
+ return self.memory_manager
67
+
68
+ def _require_agent(self) -> AgentLoop:
69
+ if self.agent is None:
70
+ raise RuntimeError("Agent loop is not initialized")
71
+ return self.agent
72
+
73
+ def _require_history(self) -> MessageHistory:
74
+ if self.history is None:
75
+ raise RuntimeError("Message history is not initialized")
76
+ return self.history
77
+
78
  async def _initialize(self):
79
  """Initialize all components."""
80
  # Ensure directories exist
 
154
 
155
  async def _get_core_context(self) -> str:
156
  """Get CORE.md content for every LLM call."""
157
+ memory_manager = self._require_memory_manager()
158
+ core = await memory_manager.load_core()
159
  if core:
160
  return f"\n=== 核心记忆 ===\n{core}\n================\n"
161
  return ""
 
164
  """Wake up: load CORE.md and restore self-awareness."""
165
  logger.info("Waking up...")
166
 
167
+ memory_manager = self._require_memory_manager()
168
+ history = self._require_history()
169
+ agent = self._require_agent()
170
+
171
+ core_memories = await memory_manager.load_core()
172
 
173
  wake_up_parts = ["你醒来了。\n"]
174
 
 
180
  wake_up_content = "\n".join(wake_up_parts)
181
 
182
  # If we have saved history, continue from it; otherwise start fresh
183
+ if history.messages:
184
+ logger.info(f"Resuming from {len(history.messages)} saved messages")
185
+ history.append({"role": "user", "content": wake_up_content})
186
  else:
187
+ history.set_messages([{"role": "user", "content": wake_up_content}])
188
 
189
  # Run the wake-up conversation
190
+ messages = await agent.run(history.get_messages())
191
+ history.set_messages(messages)
192
+ await history.save()
193
 
194
  logger.info("Wake up complete")
195
 
 
198
  await self._initialize()
199
  await self._wake_up()
200
 
201
+ history = self._require_history()
202
+ agent = self._require_agent()
203
+
204
  while self.alive:
205
  try:
206
  # Check for external stimulus (non-blocking, immediate)
 
210
 
211
  if stimulus:
212
  # External stimulus received
213
+ history.append({
214
  "role": "user",
215
  "content": f"[感知] {stimulus['type']}: {stimulus['content']}",
216
  })
217
  else:
218
  # No stimulus - continue autonomous operation
219
+ history.append({
220
  "role": "user",
221
  "content": "继续。",
222
  })
223
 
224
  # Run agent loop
225
+ messages = await agent.run(history.get_messages())
226
+ history.set_messages(messages)
227
 
228
  # Persist history periodically
229
+ await history.save()
230
 
231
  # Immediately continue to next iteration (no waiting)
232
 
 
261
  if self.agent is None:
262
  await self._initialize()
263
 
264
+ history = self._require_history()
265
+ agent = self._require_agent()
266
+
267
+ history.append({"role": "user", "content": message})
268
+ messages = await agent.run(history.get_messages())
269
+ history.set_messages(messages)
270
+ await history.save()
271
 
272
  # Extract the last assistant text
273
  for msg in reversed(messages):
memory/manager.py CHANGED
@@ -13,7 +13,7 @@ class MemoryManager:
13
  self.storage = MemoryStorage(base_path)
14
  self.retrieval = MemoryRetrieval(base_path)
15
 
16
- async def remember(self, content: str, category: str = None) -> str:
17
  """Store a new memory.
18
 
19
  Args:
 
13
  self.storage = MemoryStorage(base_path)
14
  self.retrieval = MemoryRetrieval(base_path)
15
 
16
+ async def remember(self, content: str, category: str | None = None) -> str:
17
  """Store a new memory.
18
 
19
  Args:
memory/storage.py CHANGED
@@ -31,7 +31,7 @@ class MemoryStorage:
31
  "\n"
32
  )
33
 
34
- async def store(self, content: str, category: str = None) -> Path:
35
  """Store a memory as a markdown file with frontmatter.
36
 
37
  Args:
 
31
  "\n"
32
  )
33
 
34
+ async def store(self, content: str, category: str | None = None) -> Path:
35
  """Store a memory as a markdown file with frontmatter.
36
 
37
  Args:
tools/bash_tool.py CHANGED
@@ -1,7 +1,6 @@
1
  """Bash tool - execute shell commands with safety checks."""
2
 
3
  import asyncio
4
- import subprocess
5
  from pathlib import Path
6
 
7
  from loguru import logger
@@ -37,6 +36,7 @@ async def run_bash(command: str, workdir: Path | None = None, timeout: int = 120
37
 
38
  logger.debug(f"Executing: {command}")
39
 
 
40
  try:
41
  proc = await asyncio.create_subprocess_shell(
42
  command,
@@ -56,10 +56,11 @@ async def run_bash(command: str, workdir: Path | None = None, timeout: int = 120
56
 
57
  return output
58
  except asyncio.TimeoutError:
59
- try:
60
- proc.kill()
61
- except Exception:
62
- pass
 
63
  return f"Error: Command timed out after {timeout}s"
64
  except Exception as e:
65
  return f"Error: {e}"
 
1
  """Bash tool - execute shell commands with safety checks."""
2
 
3
  import asyncio
 
4
  from pathlib import Path
5
 
6
  from loguru import logger
 
36
 
37
  logger.debug(f"Executing: {command}")
38
 
39
+ proc: asyncio.subprocess.Process | None = None
40
  try:
41
  proc = await asyncio.create_subprocess_shell(
42
  command,
 
56
 
57
  return output
58
  except asyncio.TimeoutError:
59
+ if proc is not None:
60
+ try:
61
+ proc.kill()
62
+ except Exception:
63
+ pass
64
  return f"Error: Command timed out after {timeout}s"
65
  except Exception as e:
66
  return f"Error: {e}"
tools/code_executor.py CHANGED
@@ -1,6 +1,5 @@
1
  """Code executor - allows the AI to create new plugins at runtime."""
2
 
3
- from pathlib import Path
4
 
5
  import yaml
6
  from loguru import logger
 
1
  """Code executor - allows the AI to create new plugins at runtime."""
2
 
 
3
 
4
  import yaml
5
  from loguru import logger
tools/file_tools.py CHANGED
@@ -3,7 +3,6 @@
3
  import aiofiles
4
  from pathlib import Path
5
 
6
- from loguru import logger
7
 
8
  from utils.path_utils import safe_path
9
 
 
3
  import aiofiles
4
  from pathlib import Path
5
 
 
6
 
7
  from utils.path_utils import safe_path
8
 
tools/memory_tools.py CHANGED
@@ -19,7 +19,7 @@ def _get_manager() -> MemoryManager:
19
  return _memory_manager
20
 
21
 
22
- async def run_remember(content: str, category: str = None) -> str:
23
  """Store information to long-term memory.
24
 
25
  Args:
 
19
  return _memory_manager
20
 
21
 
22
+ async def run_remember(content: str, category: str | None = None) -> str:
23
  """Store information to long-term memory.
24
 
25
  Args:
utils/token_counter.py CHANGED
@@ -1,5 +1,7 @@
1
  """Token counting utilities using tiktoken."""
2
 
 
 
3
  import tiktoken
4
 
5
 
@@ -18,7 +20,7 @@ def estimate_tokens(text: str) -> int:
18
  return len(_get_encoder().encode(text))
19
 
20
 
21
- def estimate_messages_tokens(messages: list[dict]) -> int:
22
  """Estimate total tokens across a list of messages.
23
 
24
  Each message contributes its content tokens plus a small overhead
 
1
  """Token counting utilities using tiktoken."""
2
 
3
+ from typing import Any
4
+
5
  import tiktoken
6
 
7
 
 
20
  return len(_get_encoder().encode(text))
21
 
22
 
23
+ def estimate_messages_tokens(messages: list[dict[str, Any]]) -> int:
24
  """Estimate total tokens across a list of messages.
25
 
26
  Each message contributes its content tokens plus a small overhead