linhai 0.1.0__py3-none-any.whl

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.
Files changed (130) hide show
  1. linhai/__init__.py +3 -0
  2. linhai/__main__.py +10 -0
  3. linhai/agent/__init__.py +23 -0
  4. linhai/agent/answer.py +120 -0
  5. linhai/agent/callback_slot.py +74 -0
  6. linhai/agent/command_callback.py +177 -0
  7. linhai/agent/conversation.py +152 -0
  8. linhai/agent/conversation_save.py +58 -0
  9. linhai/agent/create.py +757 -0
  10. linhai/agent/lifecycle.py +185 -0
  11. linhai/agent/main.py +335 -0
  12. linhai/agent/message.py +467 -0
  13. linhai/agent/messages/__init__.py +16 -0
  14. linhai/agent/messages/compression.py +64 -0
  15. linhai/agent/messages/file_content.py +57 -0
  16. linhai/agent/messages/prompt.py +67 -0
  17. linhai/agent/messages/reasoning.py +63 -0
  18. linhai/agent/messages/runtime.py +31 -0
  19. linhai/agent/orchestration.py +797 -0
  20. linhai/agent/planning.py +88 -0
  21. linhai/agent/savable_state.py +8 -0
  22. linhai/agent/state_machine.py +108 -0
  23. linhai/agent/toolcall.py +578 -0
  24. linhai/agent/user_message_handler.py +44 -0
  25. linhai/agent/workflow.py +275 -0
  26. linhai/base.py +541 -0
  27. linhai/cl100k_base.tiktoken +100256 -0
  28. linhai/config.py +506 -0
  29. linhai/context_statistics.py +314 -0
  30. linhai/cron.py +168 -0
  31. linhai/exceptions.py +19 -0
  32. linhai/init/__init__.py +14 -0
  33. linhai/init/app.py +160 -0
  34. linhai/init/config_writer.py +178 -0
  35. linhai/init/widgets.py +118 -0
  36. linhai/llm.py +642 -0
  37. linhai/llm_manager.py +351 -0
  38. linhai/machine_control/__init__.py +15 -0
  39. linhai/machine_control/bash_host/__init__.py +5 -0
  40. linhai/machine_control/bash_host/bash_host.py +449 -0
  41. linhai/machine_control/bash_host/file.py +234 -0
  42. linhai/machine_control/bash_host/http.py +162 -0
  43. linhai/machine_control/bash_host/process.py +139 -0
  44. linhai/machine_control/bash_host/terminal.py +200 -0
  45. linhai/machine_control/ether_ghost_host/__init__.py +5 -0
  46. linhai/machine_control/ether_ghost_host/ether_ghost_host.py +317 -0
  47. linhai/machine_control/http_message.py +127 -0
  48. linhai/machine_control/main.py +496 -0
  49. linhai/machine_control/master_host/__init__.py +49 -0
  50. linhai/machine_control/master_host/file.py +462 -0
  51. linhai/machine_control/master_host/http.py +53 -0
  52. linhai/machine_control/master_host/master_host.py +361 -0
  53. linhai/machine_control/master_host/process.py +363 -0
  54. linhai/machine_control/master_host/terminal.py +286 -0
  55. linhai/machine_control/master_host/tmux_terminal.py +145 -0
  56. linhai/machine_control/plugin.py +88 -0
  57. linhai/machine_control/posix_shell/__init__.py +5 -0
  58. linhai/machine_control/posix_shell/posix_shell_control.py +406 -0
  59. linhai/machine_control/posix_shell/process.py +92 -0
  60. linhai/machine_control/process.py +87 -0
  61. linhai/machine_control/protocol.py +106 -0
  62. linhai/machine_control/tools.py +1040 -0
  63. linhai/machine_control/trojan/__init__.py +1 -0
  64. linhai/machine_control/trojan/shell_transport.py +218 -0
  65. linhai/machine_control/trojan/transport.py +167 -0
  66. linhai/machine_control/trojan/trojan.py +828 -0
  67. linhai/main.py +263 -0
  68. linhai/markdown_parser.py +217 -0
  69. linhai/multimodal.py +341 -0
  70. linhai/parsed_message.py +397 -0
  71. linhai/plugin/__init__.py +123 -0
  72. linhai/plugin/afk_plugin.py +39 -0
  73. linhai/plugin/catgirl_tone.py +64 -0
  74. linhai/plugin/claw.py +144 -0
  75. linhai/plugin/command_hints.py +319 -0
  76. linhai/plugin/file_operations.py +535 -0
  77. linhai/plugin/file_permission_plugin.py +81 -0
  78. linhai/plugin/helpers.py +85 -0
  79. linhai/plugin/message_checkers.py +768 -0
  80. linhai/plugin/planning.py +484 -0
  81. linhai/plugin/python_chore.py +133 -0
  82. linhai/plugin/reminder.py +128 -0
  83. linhai/plugin/security_config.py +268 -0
  84. linhai/plugin/stdio_command_checker.py +193 -0
  85. linhai/plugin/sudo_stdio_checker.py +115 -0
  86. linhai/plugin/system_message_leaning.py +88 -0
  87. linhai/plugin/telegram.py +285 -0
  88. linhai/plugin/tool_call_managers.py +369 -0
  89. linhai/plugin/user_reminder.py +41 -0
  90. linhai/prompt.py +1954 -0
  91. linhai/registry.py +123 -0
  92. linhai/sandbox.py +130 -0
  93. linhai/secret.py +512 -0
  94. linhai/task_supervisor.py +106 -0
  95. linhai/telegram.py +228 -0
  96. linhai/token_manager.py +208 -0
  97. linhai/tool/__init__.py +35 -0
  98. linhai/tool/base.py +474 -0
  99. linhai/tool/general.py +221 -0
  100. linhai/tool/main.py +198 -0
  101. linhai/tool/mcp_connector.py +288 -0
  102. linhai/tool/mcp_server_example.py +42 -0
  103. linhai/tool/search.py +175 -0
  104. linhai/tui/__init__.py +23 -0
  105. linhai/tui/app.py +319 -0
  106. linhai/tui/components.py +1304 -0
  107. linhai/tui/context_tab.py +412 -0
  108. linhai/tui/messages_list.py +284 -0
  109. linhai/tui/planning_tab.py +83 -0
  110. linhai/tui/process_tab.py +208 -0
  111. linhai/type_hints.py +170 -0
  112. linhai/utils/__init__.py +0 -0
  113. linhai/utils/common.py +191 -0
  114. linhai/utils/i18n.py +11 -0
  115. linhai/utils/input_parser.py +42 -0
  116. linhai/utils/jsonpubsub.py +216 -0
  117. linhai/utils/pulse_encoding.py +137 -0
  118. linhai/utils/streamjson.py +397 -0
  119. linhai/utils/token_parser.py +156 -0
  120. linhai/utils/tokenizer.py +120 -0
  121. linhai/webui/__init__.py +6 -0
  122. linhai/webui/agent_manager.py +393 -0
  123. linhai/webui/app.py +66 -0
  124. linhai/webui/routes.py +295 -0
  125. linhai/webui/schemas.py +155 -0
  126. linhai-0.1.0.dist-info/METADATA +33 -0
  127. linhai-0.1.0.dist-info/RECORD +130 -0
  128. linhai-0.1.0.dist-info/WHEEL +4 -0
  129. linhai-0.1.0.dist-info/entry_points.txt +2 -0
  130. linhai-0.1.0.dist-info/licenses/LICENSE +674 -0
linhai/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """林海漫游AI Agent包。"""
2
+
3
+ from . import agent, main, init, registry
linhai/__main__.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ LinHai 命令行入口点模块。
3
+
4
+ 提供直接运行程序的入口。
5
+ """
6
+
7
+ from linhai.main import main
8
+
9
+ if __name__ == "__main__":
10
+ main()
@@ -0,0 +1,23 @@
1
+ """Agent module for LinHai."""
2
+
3
+ from .messages import DynamicFileContentMessage
4
+ from .main import Agent
5
+ from .lifecycle import Lifecycle
6
+ from .workflow import context_forget_range_step1, context_forget_range_step2
7
+ from .answer import AgentLlm
8
+ from .user_message_handler import UserMessageHandler, ParsedUserMessage
9
+ from .command_callback import CommandCallback
10
+ from .state_machine import AgentStateMachine
11
+
12
+ __all__ = [
13
+ "Agent",
14
+ "Lifecycle",
15
+ "DynamicFileContentMessage",
16
+ "context_forget_range_step1",
17
+ "context_forget_range_step2",
18
+ "AgentLlm",
19
+ "UserMessageHandler",
20
+ "ParsedUserMessage",
21
+ "CommandCallback",
22
+ "AgentStateMachine",
23
+ ]
linhai/agent/answer.py ADDED
@@ -0,0 +1,120 @@
1
+ from typing import Tuple, TYPE_CHECKING
2
+ import asyncio
3
+ from linhai.base import Answer, Message
4
+ from linhai.llm_manager import LlmManager
5
+ from linhai.parsed_message import ParsedAnswer
6
+ from linhai.agent.lifecycle import Lifecycle
7
+ from linhai.agent.messages import RuntimeMessage
8
+ from linhai.registry import Registry
9
+ from linhai.utils.common import UiNotice
10
+ from linhai.base import UserMessage, AssistantMessage, ToolCallMessage
11
+ from linhai.agent.user_message_handler import UserMessageHandler
12
+
13
+ if TYPE_CHECKING:
14
+ from linhai.agent.toolcall import AgentToolcall
15
+ from linhai.agent.message import AgentMessage
16
+ from linhai.agent.main import Agent
17
+
18
+
19
+ class AgentLlm:
20
+ """AgentLlm类,负责管理LLM调用、Answer解析和打断逻辑。"""
21
+
22
+ def __init__(
23
+ self,
24
+ llm_manager: LlmManager,
25
+ registry: Registry,
26
+ toolcall_processor: "AgentToolcall",
27
+ message_processor: "AgentMessage",
28
+ ):
29
+ """初始化AgentLlm。
30
+
31
+ Args:
32
+ llm_manager: LlmManager实例
33
+ registry: Registry实例
34
+ toolcall_processor: AgentToolcall实例
35
+ message_processor: AgentMessage实例
36
+ """
37
+ self.llm_manager = llm_manager
38
+ self.registry = registry
39
+ self.toolcall_processor = toolcall_processor
40
+ self.message_processor = message_processor
41
+ self._current_parsed_answer: ParsedAnswer | None = None
42
+ self.current_answer: Answer | None = None
43
+
44
+ async def call_and_wait_llm(self) -> Tuple[Answer, ParsedAnswer, bool]:
45
+ """调用LLM并等待解析完成。
46
+
47
+ Returns:
48
+ Tuple[Answer, ParsedAnswer, bool]: (Answer, ParsedAnswer, 是否正常完成)
49
+ """
50
+ from .main import Agent
51
+
52
+ agent = self.registry.get_member_typechecked("agent", Agent)
53
+ lifecycle = agent.lifecycle
54
+
55
+ await lifecycle.before_message_generation.trigger()
56
+
57
+ answer: Answer = await self.llm_manager.answer_stream(
58
+ self.message_processor.get_messages()
59
+ )
60
+
61
+ self.current_answer = answer
62
+
63
+ parsed_answer = ParsedAnswer(
64
+ answer, lifecycle, agent=agent, registry=self.registry
65
+ )
66
+ await parsed_answer.start_parsing()
67
+ await lifecycle.after_new_parsed_answer.trigger(parsed_answer)
68
+ await self.registry.send("parsed_agent_answer", parsed_answer)
69
+
70
+ completed_normally = await parsed_answer.wait_parsing()
71
+ return answer, parsed_answer, completed_normally
72
+
73
+ async def interrupt(self, agent_message: str, ui_notice: str):
74
+ """打断当前Answer并添加自定义消息。
75
+
76
+ Args:
77
+ agent_message: 发送给agent的消息内容,放入RuntimeMessage
78
+ ui_notice: 发送给UI的通知内容,必须提供
79
+ """
80
+ from .main import Agent
81
+
82
+ message_processor = self.message_processor
83
+ lifecycle = self.registry.get_member_typechecked("lifecycle", Lifecycle)
84
+ agent = self.registry.get_member_typechecked("agent", Agent)
85
+
86
+ if self._current_parsed_answer:
87
+ self._current_parsed_answer.interrupt()
88
+
89
+ await message_processor.add_new_message(RuntimeMessage(agent_message))
90
+
91
+ if ui_notice is not None:
92
+ interrupt_msg = UiNotice(level="WARNING", content=ui_notice)
93
+ else:
94
+ interrupt_msg = UiNotice(level="WARNING", content="Agent被打断")
95
+
96
+ current_content = self._current_parsed_answer._answer.get_current_content()
97
+ has_tool_calls = "```json toolcall" in current_content or bool(
98
+ self._current_parsed_answer._openai_toolcall_segments
99
+ )
100
+ if has_tool_calls:
101
+ await message_processor.add_new_message(
102
+ RuntimeMessage("当前所有工具调用全部被忽略,请重新调用")
103
+ )
104
+
105
+ self._current_parsed_answer = None
106
+
107
+ await self.registry.send_if_exists("ui_log", interrupt_msg)
108
+
109
+ from .state_machine import AgentStateMachine
110
+
111
+ state_machine = self.registry.get_member_typechecked(
112
+ "state_machine", AgentStateMachine
113
+ )
114
+ state_machine.transition_to_working()
115
+
116
+ user_message_handler = self.registry.get_member_typechecked(
117
+ "user_message_handler", UserMessageHandler
118
+ )
119
+ while user_message_handler.has_message():
120
+ await user_message_handler.receive_and_dispatch()
@@ -0,0 +1,74 @@
1
+ from typing import Generic, TypeVar, Callable, Awaitable
2
+
3
+ R = TypeVar("R")
4
+
5
+
6
+ class CallbackSlot(Generic[R]):
7
+ def __init__(self) -> None:
8
+ self._callbacks: list[Callable[..., Awaitable[R]]] = []
9
+
10
+ def register(self, callback: Callable[..., Awaitable[R]]) -> None:
11
+ self._callbacks.append(callback)
12
+
13
+
14
+ class BroadcastSlot(CallbackSlot[None]):
15
+ async def trigger(self, *args, **kwargs) -> None:
16
+ for callback in self._callbacks:
17
+ await callback(*args, **kwargs)
18
+
19
+
20
+ class ShortCircuitSlot(CallbackSlot[R]):
21
+ async def trigger(self, *args, **kwargs) -> R | None:
22
+ for callback in self._callbacks:
23
+ result = await callback(*args, **kwargs)
24
+ if result is not None:
25
+ return result
26
+ return None
27
+
28
+
29
+ class InterruptSlot(CallbackSlot[bool]):
30
+ async def trigger(self, *args, **kwargs) -> bool:
31
+ for callback in self._callbacks:
32
+ if await callback(*args, **kwargs):
33
+ return True
34
+ return False
35
+
36
+
37
+ class AfterToolcallSlot(CallbackSlot):
38
+ async def trigger(self, *args, **kwargs):
39
+ from linhai.agent.lifecycle import AfterToolcallResult
40
+
41
+ replacement = None
42
+ warnings = []
43
+ for callback in self._callbacks:
44
+ result = await callback(*args, **kwargs)
45
+ if result is None:
46
+ continue
47
+ if result.replacement is not None and replacement is None:
48
+ replacement = result.replacement
49
+ warnings.extend(result.warnings)
50
+ if replacement is None and not warnings:
51
+ return None
52
+ return AfterToolcallResult(replacement=replacement, warnings=warnings)
53
+
54
+
55
+ class ChainSlot(CallbackSlot[R]):
56
+ def __init__(
57
+ self,
58
+ *,
59
+ chain_arg: int = 0,
60
+ should_stop: Callable[..., bool] | None = None,
61
+ ) -> None:
62
+ super().__init__()
63
+ self._chain_arg = chain_arg
64
+ self._should_stop = should_stop
65
+
66
+ async def trigger(self, *args, **kwargs) -> R:
67
+ args_list = list(args)
68
+ for callback in self._callbacks:
69
+ result = await callback(*args_list, **kwargs)
70
+ if result is not None:
71
+ args_list[self._chain_arg] = result
72
+ if self._should_stop is not None and self._should_stop(result):
73
+ break
74
+ return args_list[self._chain_arg]
@@ -0,0 +1,177 @@
1
+ from typing import Literal
2
+
3
+ from linhai.base import UserMessage, ToolCallMessage
4
+ from linhai.registry import Registry
5
+ from linhai.utils.common import UiNotice
6
+ from linhai.agent.user_message_handler import ParsedUserMessage
7
+
8
+
9
+ class CommandCallback:
10
+ def __init__(self, registry: Registry):
11
+ self.registry = registry
12
+
13
+ async def __call__(self, parsed: ParsedUserMessage) -> bool | None:
14
+ parsed_input = parsed["parsed_input"]
15
+ msg = parsed["raw_message"]
16
+
17
+ if parsed_input.switch_model:
18
+ return await self._handle_switch_model(parsed_input.switch_model, msg)
19
+
20
+ if parsed_input.command:
21
+ if parsed_input.command == "context_forget_large_message":
22
+ return await self._handle_context_tool_command(parsed_input)
23
+ if parsed_input.command == "queue":
24
+ return await self._handle_queue_command(msg)
25
+ if parsed_input.command in ("quit", "exit"):
26
+ return await self._handle_quit_command()
27
+ if parsed_input.command == "help":
28
+ return await self._handle_help_command()
29
+ if parsed_input.command == "status":
30
+ return await self._handle_status_command()
31
+ if parsed_input.command == "save":
32
+ return await self._handle_save_command()
33
+
34
+ return None
35
+
36
+ @staticmethod
37
+ def get_command_completions() -> list[str]:
38
+ return [
39
+ "/queue",
40
+ "/help",
41
+ "/status",
42
+ "/save",
43
+ "/quit",
44
+ "/exit",
45
+ "/context_forget_large_message",
46
+ ]
47
+
48
+ async def _show_runtime_message(
49
+ self, level: Literal["INFO", "WARNING", "ERROR"], content: str
50
+ ) -> None:
51
+ await self.registry.send_if_exists(
52
+ "ui_log", UiNotice(level=level, content=content)
53
+ )
54
+
55
+ async def _handle_queue_command(self, msg: UserMessage) -> bool:
56
+ from .message import AgentMessage
57
+
58
+ agent_message = self.registry.get_member_typechecked(
59
+ "agent_message", AgentMessage
60
+ )
61
+ queue_content = msg.message.removeprefix("/queue").strip()
62
+ if not queue_content:
63
+ await self._show_runtime_message("ERROR", "用法: /queue <消息内容>")
64
+ return False
65
+
66
+ queued_msg = UserMessage(message=queue_content)
67
+ agent_message.add_queued_message(queued_msg)
68
+ return False
69
+
70
+ async def _handle_quit_command(self) -> bool:
71
+ await self.registry.send("exit_signal", {"return_code": 0})
72
+ return False
73
+
74
+ async def _handle_help_command(self) -> bool:
75
+ help_text = (
76
+ "可用命令:\n"
77
+ "/queue <消息> - 将消息加入排队列表,在下次回答后处理\n"
78
+ "\n"
79
+ "/status - 显示当前状态信息\n"
80
+ "/save - 保存当前会话状态\n"
81
+ "/help - 显示此帮助信息\n"
82
+ "/quit, /exit - 退出程序\n"
83
+ "@<模型名> - 切换底层LLM模型\n"
84
+ "\n"
85
+ "上下文工具:\n"
86
+ "/context_forget_large_message - 清理大消息\n"
87
+ )
88
+ await self._show_runtime_message("INFO", help_text)
89
+ return False
90
+
91
+ async def _handle_status_command(self) -> bool:
92
+ from .main import Agent
93
+
94
+ agent = self.registry.get_member_typechecked("agent", Agent)
95
+ llm_name, _llm = agent.get_current_llm_info()
96
+ threshold_info = agent.get_threshold_info()
97
+
98
+ from .state_machine import AgentStateMachine
99
+
100
+ state_machine = self.registry.get_member_typechecked(
101
+ "state_machine", AgentStateMachine
102
+ )
103
+ status_lines = [f"当前LLM: {llm_name}", f"当前状态: {state_machine.state}"]
104
+
105
+ if threshold_info:
106
+ usage_percent = threshold_info["usage_ratio"] * 100
107
+ status_lines.append(
108
+ f"Token使用: {threshold_info['used_tokens']}/{threshold_info['hard_limit']} ({usage_percent:.1f}%)"
109
+ )
110
+
111
+ await self._show_runtime_message("INFO", "\n".join(status_lines))
112
+ return False
113
+
114
+ async def _handle_switch_model(self, model_name: str, msg: UserMessage) -> bool:
115
+ from .main import Agent
116
+
117
+ agent = self.registry.get_member_typechecked("agent", Agent)
118
+ llm_manager = agent.llm_manager
119
+
120
+ await agent.message_processor.add_new_message(msg)
121
+
122
+ if model_name == "default":
123
+ await llm_manager.switch_to_llm(llm_manager.default_llm_name)
124
+ await self._show_runtime_message("INFO", "已将底层LLM切换为默认LLM")
125
+ elif model_name in llm_manager.llm_names:
126
+ await llm_manager.switch_to_llm(model_name)
127
+ await self._show_runtime_message(
128
+ "INFO", f"已将底层LLM切换为 {model_name!r}"
129
+ )
130
+ else:
131
+ await self._show_runtime_message(
132
+ "ERROR",
133
+ f"错误:LLM名称 {model_name!r} 不存在.可用的LLM包括: {', '.join(llm_manager.llm_names)}",
134
+ )
135
+
136
+ return True
137
+
138
+ async def _handle_save_command(self) -> bool:
139
+ from pathlib import Path
140
+ from linhai.agent.conversation_save import save_conversation
141
+
142
+ conversation_folder = self.registry.get_member_typechecked(
143
+ "conversation_folder", Path
144
+ )
145
+ filepath = conversation_folder / "saved.json"
146
+
147
+ await self._show_runtime_message(
148
+ "INFO", f"Saving conversation to {filepath}..."
149
+ )
150
+ await save_conversation(self.registry, filepath)
151
+ await self._show_runtime_message("INFO", f"Conversation saved to {filepath}")
152
+ return False
153
+
154
+ async def _handle_context_tool_command(self, parsed_input) -> bool:
155
+ from .main import Agent
156
+
157
+ agent = self.registry.get_member_typechecked("agent", Agent)
158
+
159
+ await self._show_runtime_message(
160
+ "INFO", f"正在执行命令: {parsed_input.command}"
161
+ )
162
+
163
+ tool_call = ToolCallMessage(
164
+ function_name=parsed_input.command,
165
+ function_arguments={},
166
+ assert_success=False,
167
+ with_secret={"in_arguments": [], "in_result": []},
168
+ )
169
+
170
+ await agent.toolcall_processor.call_tool(tool_call, tool_index=1)
171
+
172
+ await self._show_runtime_message(
173
+ "INFO",
174
+ f"上下文工具命令执行成功: {parsed_input.command}",
175
+ )
176
+
177
+ return True
@@ -0,0 +1,152 @@
1
+ """对话管理系统,负责对话历史、大消息、secret等的保存。"""
2
+
3
+ import json
4
+ import time
5
+ import uuid
6
+ from pathlib import Path
7
+ from typing import List
8
+
9
+ from linhai.registry import Registry
10
+ from linhai.base import Message
11
+
12
+
13
+ def register_conversation_folder(registry: Registry) -> Path:
14
+ """注册conversation_folder到registry,创建并返回对话目录路径。
15
+
16
+ Args:
17
+ registry: Registry实例
18
+
19
+ Returns:
20
+ 创建的对话目录路径
21
+ """
22
+ conversation_id = str(uuid.uuid4())
23
+ base_dir = Path.home() / ".local" / "share" / "linhai" / "conversation"
24
+ conversation_dir = base_dir / conversation_id
25
+ conversation_dir.mkdir(parents=True, exist_ok=True)
26
+
27
+ (conversation_dir / "cleaned_messages").mkdir(exist_ok=True)
28
+ (conversation_dir / "large_messages").mkdir(exist_ok=True)
29
+ (conversation_dir / "long_toolcall").mkdir(exist_ok=True)
30
+ (conversation_dir / "secret_intercepted").mkdir(exist_ok=True)
31
+ (conversation_dir / "cron").mkdir(exist_ok=True)
32
+
33
+ registry.register_member("conversation_folder", conversation_dir)
34
+ return conversation_dir
35
+
36
+
37
+ def save_context(conversation_dir: Path, messages: List[Message]) -> Path:
38
+ """保存消息历史到context.json。
39
+
40
+ Args:
41
+ conversation_dir: 对话目录路径
42
+ messages: 消息列表
43
+
44
+ Returns:
45
+ 保存的文件路径
46
+ """
47
+ context_file = conversation_dir / "context.json"
48
+
49
+ history_data = [
50
+ {"type": msg.__class__.__name__, "data": msg.to_json()} for msg in messages
51
+ ]
52
+
53
+ with open(context_file, "w", encoding="utf-8") as f:
54
+ json.dump(history_data, f, ensure_ascii=False, indent=2)
55
+
56
+ return context_file
57
+
58
+
59
+ def save_cleaned_messages(
60
+ conversation_dir: Path, messages: List[Message], prefix: str
61
+ ) -> Path:
62
+ """保存被清理的消息到cleaned_messages目录。
63
+
64
+ Args:
65
+ conversation_dir: 对话目录路径
66
+ messages: 被清理的消息列表
67
+ prefix: 文件名前缀
68
+
69
+ Returns:
70
+ 保存的文件路径
71
+ """
72
+ timestamp = int(time.time())
73
+ filename = f"{prefix}_{timestamp}.json"
74
+ cleaned_messages_dir = conversation_dir / "cleaned_messages"
75
+ cleaned_messages_dir.mkdir(parents=True, exist_ok=True)
76
+ filepath = cleaned_messages_dir / filename
77
+
78
+ history_data = [
79
+ {"type": msg.__class__.__name__, "data": msg.to_json()} for msg in messages
80
+ ]
81
+
82
+ with open(filepath, "w", encoding="utf-8") as f:
83
+ json.dump(history_data, f, ensure_ascii=False, indent=2)
84
+
85
+ return filepath
86
+
87
+
88
+ def save_large_message_chunk(
89
+ conversation_dir: Path, content: str, chunk_index: int
90
+ ) -> Path:
91
+ """保存大消息分块到large_messages目录。
92
+
93
+ Args:
94
+ conversation_dir: 对话目录路径
95
+ content: 消息内容
96
+ chunk_index: 分块索引
97
+
98
+ Returns:
99
+ 保存的文件路径
100
+ """
101
+ timestamp = int(time.time())
102
+ filename = f"large_message_{timestamp}_{chunk_index}.txt"
103
+ filepath = conversation_dir / "large_messages" / filename
104
+
105
+ filepath.write_text(content, encoding="utf-8")
106
+ return filepath
107
+
108
+
109
+ def save_long_toolcall_output(
110
+ conversation_dir: Path, content: str, tool_name: str, part_index: int | None = None
111
+ ) -> Path:
112
+ """保存大工具输出到long_toolcall目录。
113
+
114
+ Args:
115
+ conversation_dir: 对话目录路径
116
+ content: 输出内容
117
+ tool_name: 工具名称
118
+ part_index: 分块索引,None表示不分块
119
+
120
+ Returns:
121
+ 保存的文件路径
122
+ """
123
+ timestamp = int(time.time())
124
+ if part_index is not None:
125
+ filename = f"{tool_name}_{timestamp}_part{part_index}.txt"
126
+ else:
127
+ filename = f"{tool_name}_{timestamp}.txt"
128
+ filepath = conversation_dir / "long_toolcall" / filename
129
+
130
+ filepath.write_text(content, encoding="utf-8")
131
+ return filepath
132
+
133
+
134
+ def save_secret_intercepted(
135
+ conversation_dir: Path, content: str, tool_name: str
136
+ ) -> Path:
137
+ """保存被拦截的含secret内容到secret_intercepted目录。
138
+
139
+ Args:
140
+ conversation_dir: 对话目录路径
141
+ content: 被拦截的内容
142
+ tool_name: 工具名称
143
+
144
+ Returns:
145
+ 保存的文件路径
146
+ """
147
+ timestamp = int(time.time())
148
+ filename = f"secret_intercepted_{timestamp}_{tool_name}.txt"
149
+ filepath = conversation_dir / "secret_intercepted" / filename
150
+
151
+ filepath.write_text(content, encoding="utf-8")
152
+ return filepath
@@ -0,0 +1,58 @@
1
+ import json
2
+ from pathlib import Path
3
+
4
+ from linhai.agent.savable_state import SavableState
5
+
6
+ CONVERSATION_VERSION = "1"
7
+
8
+
9
+ def _get_savable_members(registry) -> dict[str, SavableState]:
10
+ result = {}
11
+ for name, obj in registry.members.items():
12
+ if isinstance(obj, SavableState):
13
+ result[name] = obj
14
+ return result
15
+
16
+
17
+ async def save_conversation(registry, filepath: Path) -> None:
18
+ savable = _get_savable_members(registry)
19
+ data = {
20
+ "version": CONVERSATION_VERSION,
21
+ "members": {name: obj.serialize() for name, obj in savable.items()},
22
+ }
23
+ filepath.parent.mkdir(parents=True, exist_ok=True)
24
+ filepath.write_text(
25
+ json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8"
26
+ )
27
+
28
+
29
+ async def restore_conversation(registry, filepath: Path) -> None:
30
+ content = filepath.read_text(encoding="utf-8")
31
+ data = json.loads(content)
32
+
33
+ if data.get("version") != CONVERSATION_VERSION:
34
+ raise RuntimeError(
35
+ f"conversation version mismatch: expected {CONVERSATION_VERSION!r}, got {data.get('version')!r}"
36
+ )
37
+
38
+ saved_members = set(data.get("members", {}).keys())
39
+ savable = _get_savable_members(registry)
40
+ current_members = set(savable.keys())
41
+
42
+ missing = current_members - saved_members
43
+ extra = saved_members - current_members
44
+ if missing or extra:
45
+ parts = []
46
+ if missing:
47
+ parts.append(f"missing members: {sorted(missing)}")
48
+ if extra:
49
+ parts.append(f"extra members: {sorted(extra)}")
50
+ raise RuntimeError("conversation restore failed: " + ", ".join(parts))
51
+
52
+ for name, obj in savable.items():
53
+ obj.restore_from(data["members"][name])
54
+
55
+ from linhai.agent.lifecycle import Lifecycle
56
+
57
+ lifecycle = registry.get_member_typechecked("lifecycle", Lifecycle)
58
+ await lifecycle.after_conversation_restore.trigger()