my-pi-agent 0.1.0
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.
- package/README.md +318 -0
- package/package.json +45 -0
- package/pyproject.toml +50 -0
- package/src/my_agent_core/__init__.py +123 -0
- package/src/my_agent_core/agent.py +441 -0
- package/src/my_agent_core/background.py +121 -0
- package/src/my_agent_core/context.py +505 -0
- package/src/my_agent_core/events.py +153 -0
- package/src/my_agent_core/extensions/__init__.py +9 -0
- package/src/my_agent_core/extensions/core.py +197 -0
- package/src/my_agent_core/hooks.py +130 -0
- package/src/my_agent_core/loop.py +709 -0
- package/src/my_agent_core/main.py +134 -0
- package/src/my_agent_core/memory.py +241 -0
- package/src/my_agent_core/message_queue.py +110 -0
- package/src/my_agent_core/plugins.py +212 -0
- package/src/my_agent_core/registry.py +186 -0
- package/src/my_agent_core/session/__init__.py +79 -0
- package/src/my_agent_core/session/entries.py +197 -0
- package/src/my_agent_core/session/jsonl.py +60 -0
- package/src/my_agent_core/session/memory.py +137 -0
- package/src/my_agent_core/session/session.py +400 -0
- package/src/my_agent_core/session/storage.py +245 -0
- package/src/my_agent_core/session/store.py +131 -0
- package/src/my_agent_core/session/tree.py +86 -0
- package/src/my_agent_core/skills.py +149 -0
- package/src/my_agent_core/subagent_tasks.py +170 -0
- package/src/my_agent_core/subagents.py +148 -0
- package/src/my_agent_core/task_store.py +248 -0
- package/src/my_agent_core/tool_history.py +189 -0
- package/src/my_agent_core/tools/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/__init__.py +5 -0
- package/src/my_agent_core/tools/builtin/task.py +30 -0
- package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
- package/src/my_agent_core/tools/core.py +239 -0
- package/src/my_agent_llm/__init__.py +45 -0
- package/src/my_agent_llm/auth/__init__.py +46 -0
- package/src/my_agent_llm/auth/antigravity.py +209 -0
- package/src/my_agent_llm/auth/manager.py +259 -0
- package/src/my_agent_llm/auth/quota.py +56 -0
- package/src/my_agent_llm/auth/schema.py +94 -0
- package/src/my_agent_llm/client.py +116 -0
- package/src/my_agent_llm/config.py +17 -0
- package/src/my_agent_llm/events.py +84 -0
- package/src/my_agent_llm/models.py +195 -0
- package/src/my_agent_llm/providers/__init__.py +4 -0
- package/src/my_agent_llm/providers/_base.py +94 -0
- package/src/my_agent_llm/providers/anthropic.py +298 -0
- package/src/my_agent_llm/providers/antigravity.py +480 -0
- package/src/my_agent_llm/providers/deepseek.py +196 -0
- package/src/my_agent_llm/providers/openai.py +364 -0
- package/src/my_agent_llm/providers/registry.py +16 -0
- package/src/my_agent_llm/stream.py +218 -0
- package/src/my_coding_agent/__init__.py +66 -0
- package/src/my_coding_agent/agent.py +208 -0
- package/src/my_coding_agent/cli.py +78 -0
- package/src/my_coding_agent/file_reference.py +80 -0
- package/src/my_coding_agent/macro.py +408 -0
- package/src/my_coding_agent/mcp.py +243 -0
- package/src/my_coding_agent/mutation_queue.py +37 -0
- package/src/my_coding_agent/paths.py +119 -0
- package/src/my_coding_agent/permissions.py +84 -0
- package/src/my_coding_agent/prompt.py +54 -0
- package/src/my_coding_agent/rpc_server.py +2817 -0
- package/src/my_coding_agent/settings.py +126 -0
- package/src/my_coding_agent/tools/__init__.py +55 -0
- package/src/my_coding_agent/tools/base.py +58 -0
- package/src/my_coding_agent/tools/bash.py +206 -0
- package/src/my_coding_agent/tools/edit.py +226 -0
- package/src/my_coding_agent/tools/find.py +118 -0
- package/src/my_coding_agent/tools/grep.py +177 -0
- package/src/my_coding_agent/tools/ls.py +112 -0
- package/src/my_coding_agent/tools/read.py +113 -0
- package/src/my_coding_agent/tools/write.py +72 -0
- package/tui/README.md +27 -0
- package/tui/bin/my-agent.js +98 -0
- package/tui/dist/app.d.ts +41 -0
- package/tui/dist/app.js +110 -0
- package/tui/dist/bridge/event-translator.d.ts +92 -0
- package/tui/dist/bridge/event-translator.js +216 -0
- package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
- package/tui/dist/bridge/kernel-bridge.js +132 -0
- package/tui/dist/client.d.ts +63 -0
- package/tui/dist/client.js +239 -0
- package/tui/dist/components/assistant-message.d.ts +19 -0
- package/tui/dist/components/assistant-message.js +90 -0
- package/tui/dist/components/compaction-summary-message.d.ts +19 -0
- package/tui/dist/components/compaction-summary-message.js +46 -0
- package/tui/dist/components/custom-editor.d.ts +18 -0
- package/tui/dist/components/custom-editor.js +56 -0
- package/tui/dist/components/dynamic-border.d.ts +9 -0
- package/tui/dist/components/dynamic-border.js +14 -0
- package/tui/dist/components/footer.d.ts +39 -0
- package/tui/dist/components/footer.js +199 -0
- package/tui/dist/components/header.d.ts +4 -0
- package/tui/dist/components/header.js +21 -0
- package/tui/dist/components/keys.d.ts +5 -0
- package/tui/dist/components/keys.js +12 -0
- package/tui/dist/components/login-selector.d.ts +26 -0
- package/tui/dist/components/login-selector.js +181 -0
- package/tui/dist/components/logout-selector.d.ts +19 -0
- package/tui/dist/components/logout-selector.js +88 -0
- package/tui/dist/components/model-selector.d.ts +40 -0
- package/tui/dist/components/model-selector.js +268 -0
- package/tui/dist/components/session-selector.d.ts +54 -0
- package/tui/dist/components/session-selector.js +393 -0
- package/tui/dist/components/settings-selector.d.ts +24 -0
- package/tui/dist/components/settings-selector.js +146 -0
- package/tui/dist/components/status-indicator.d.ts +25 -0
- package/tui/dist/components/status-indicator.js +60 -0
- package/tui/dist/components/theme-selector.d.ts +14 -0
- package/tui/dist/components/theme-selector.js +77 -0
- package/tui/dist/components/thinking-selector.d.ts +21 -0
- package/tui/dist/components/thinking-selector.js +128 -0
- package/tui/dist/components/tool-execution.d.ts +31 -0
- package/tui/dist/components/tool-execution.js +206 -0
- package/tui/dist/components/tree-selector.d.ts +40 -0
- package/tui/dist/components/tree-selector.js +173 -0
- package/tui/dist/components/user-message-selector.d.ts +21 -0
- package/tui/dist/components/user-message-selector.js +103 -0
- package/tui/dist/components/user-message.d.ts +5 -0
- package/tui/dist/components/user-message.js +15 -0
- package/tui/dist/index.d.ts +11 -0
- package/tui/dist/index.js +11 -0
- package/tui/dist/interactive/chat-viewport.d.ts +19 -0
- package/tui/dist/interactive/chat-viewport.js +41 -0
- package/tui/dist/interactive/components.d.ts +1 -0
- package/tui/dist/interactive/components.js +1 -0
- package/tui/dist/interactive/interactive-mode.d.ts +89 -0
- package/tui/dist/interactive/interactive-mode.js +1625 -0
- package/tui/dist/interactive/theme.d.ts +1 -0
- package/tui/dist/interactive/theme.js +1 -0
- package/tui/dist/interactive/tui-renderer.d.ts +8 -0
- package/tui/dist/interactive/tui-renderer.js +10 -0
- package/tui/dist/protocol.d.ts +78 -0
- package/tui/dist/protocol.js +1 -0
- package/tui/dist/theme/dark.json +54 -0
- package/tui/dist/theme/light.json +71 -0
- package/tui/dist/theme/theme.d.ts +20 -0
- package/tui/dist/theme/theme.js +86 -0
- package/tui/package.json +25 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""工具注册表 —— 持有工具集合,按名字查表与执行。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Callable, Sequence
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from my_agent_core.tools import Tool, ToolResult, tool
|
|
11
|
+
|
|
12
|
+
__all__ = ["Tool", "ToolRegistry", "ToolResult", "tool"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ToolRegistry:
|
|
16
|
+
"""工具注册表:持有工具集合,按名字查表与执行。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._tools: dict[str, Tool] = {}
|
|
20
|
+
|
|
21
|
+
def register(self, tool: Tool) -> None:
|
|
22
|
+
self._tools[tool.name] = tool
|
|
23
|
+
|
|
24
|
+
def unregister(self, name: str) -> None:
|
|
25
|
+
self._tools.pop(name, None)
|
|
26
|
+
|
|
27
|
+
def get(self, name: str) -> Tool | None:
|
|
28
|
+
return self._tools.get(name)
|
|
29
|
+
|
|
30
|
+
def list(self) -> list[Tool]:
|
|
31
|
+
"""当前全部工具(发现顺序)。"""
|
|
32
|
+
return list(self._tools.values())
|
|
33
|
+
|
|
34
|
+
def get_schemas(self) -> list[dict[str, Any]]:
|
|
35
|
+
"""生成全部工具的 OpenAI tools 参数。"""
|
|
36
|
+
return [t.to_openai_schema() for t in self._tools.values()]
|
|
37
|
+
|
|
38
|
+
async def execute_tool(
|
|
39
|
+
self,
|
|
40
|
+
tool_call: Any = None,
|
|
41
|
+
*,
|
|
42
|
+
name: str | None = None,
|
|
43
|
+
args: dict[str, Any] | str | None = None,
|
|
44
|
+
signal: Any | None = None,
|
|
45
|
+
on_update: Callable[[Any], None] | None = None,
|
|
46
|
+
tool_call_id: str | None = None,
|
|
47
|
+
) -> ToolResult:
|
|
48
|
+
"""执行单个 tool 调用。
|
|
49
|
+
|
|
50
|
+
支持多种入参形式:
|
|
51
|
+
1. 原生结构化传参: execute_tool(name="add", args={"a": 1})
|
|
52
|
+
2. 元组传参: execute_tool(("add", {"a": 1})) 或 execute_tool(("add", {"a": 1}, on_update))
|
|
53
|
+
3. 实体传参: execute_tool(ToolCall(id=..., name="add", args={"a": 1}))
|
|
54
|
+
4. 字典传参: execute_tool({"name": "add", "args": {"a": 1}})
|
|
55
|
+
5. 协议传参: execute_tool({"function": {"name": "add", "arguments": "..."}})
|
|
56
|
+
任何错误都转成 ToolResult,永不抛。
|
|
57
|
+
"""
|
|
58
|
+
target_name = name
|
|
59
|
+
target_args = args
|
|
60
|
+
|
|
61
|
+
if target_name is None:
|
|
62
|
+
if isinstance(tool_call, tuple):
|
|
63
|
+
if len(tool_call) >= 2:
|
|
64
|
+
target_name, target_args = str(tool_call[0]), tool_call[1]
|
|
65
|
+
if len(tool_call) >= 3 and on_update is None and callable(tool_call[2]):
|
|
66
|
+
on_update = tool_call[2] # pyright: ignore[reportAssignmentType]
|
|
67
|
+
if len(tool_call) >= 4 and tool_call_id is None:
|
|
68
|
+
tool_call_id = str(tool_call[3])
|
|
69
|
+
elif isinstance(tool_call, dict):
|
|
70
|
+
if "function" in tool_call:
|
|
71
|
+
fn = tool_call.get("function") or {}
|
|
72
|
+
target_name = fn.get("name", "")
|
|
73
|
+
target_args = fn.get("arguments", "{}")
|
|
74
|
+
else:
|
|
75
|
+
target_name = tool_call.get("name", "")
|
|
76
|
+
target_args = tool_call.get("args", {})
|
|
77
|
+
if tool_call_id is None:
|
|
78
|
+
tool_call_id = tool_call.get("id")
|
|
79
|
+
elif isinstance(tool_call, str):
|
|
80
|
+
target_name = tool_call
|
|
81
|
+
elif tool_call is not None:
|
|
82
|
+
target_name = getattr(tool_call, "name", "") # pyright: ignore[reportAttributeAccessIssue]
|
|
83
|
+
target_args = getattr(tool_call, "args", {}) # pyright: ignore[reportAttributeAccessIssue]
|
|
84
|
+
if tool_call_id is None:
|
|
85
|
+
tool_call_id = getattr(tool_call, "id", None) # pyright: ignore[reportAttributeAccessIssue]
|
|
86
|
+
|
|
87
|
+
final_args: dict[str, Any]
|
|
88
|
+
if isinstance(target_args, str):
|
|
89
|
+
try:
|
|
90
|
+
parsed = json.loads(target_args) if target_args.strip() else {}
|
|
91
|
+
if not isinstance(parsed, dict):
|
|
92
|
+
return ToolResult(
|
|
93
|
+
ok=False,
|
|
94
|
+
error=f"Invalid JSON arguments for tool '{target_name}': expected object dict",
|
|
95
|
+
)
|
|
96
|
+
final_args = parsed
|
|
97
|
+
except (json.JSONDecodeError, TypeError) as exc:
|
|
98
|
+
return ToolResult(
|
|
99
|
+
ok=False,
|
|
100
|
+
error=f"Invalid JSON arguments for tool '{target_name}': {exc}",
|
|
101
|
+
)
|
|
102
|
+
elif isinstance(target_args, dict):
|
|
103
|
+
final_args = target_args
|
|
104
|
+
elif target_args is None:
|
|
105
|
+
final_args = {}
|
|
106
|
+
else:
|
|
107
|
+
return ToolResult(
|
|
108
|
+
ok=False,
|
|
109
|
+
error=f"Invalid arguments for tool '{target_name}': expected dict, got {type(target_args).__name__}",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
target = self._tools.get(target_name or "")
|
|
113
|
+
if target is None:
|
|
114
|
+
available = ", ".join(sorted(self._tools))
|
|
115
|
+
return ToolResult(
|
|
116
|
+
ok=False, error=f"Unknown tool '{target_name}'. Available: {available}"
|
|
117
|
+
)
|
|
118
|
+
return await target.execute(
|
|
119
|
+
final_args,
|
|
120
|
+
signal=signal,
|
|
121
|
+
on_update=on_update,
|
|
122
|
+
tool_call_id=tool_call_id,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def execute_batch(
|
|
126
|
+
self,
|
|
127
|
+
tool_calls: Sequence[Any],
|
|
128
|
+
signal: Any | None = None,
|
|
129
|
+
on_updates: Sequence[Callable[[Any], None] | None] | None = None,
|
|
130
|
+
on_update_factory: Callable[[int, Any], Callable[[Any], None] | None]
|
|
131
|
+
| None = None,
|
|
132
|
+
) -> list[ToolResult]:
|
|
133
|
+
"""批量执行工具调用(全员只读并发;只要包含一个写入则整批保序串行,防止因果时序倒置)。"""
|
|
134
|
+
if not tool_calls:
|
|
135
|
+
return []
|
|
136
|
+
|
|
137
|
+
def _get_name(tc: Any) -> str:
|
|
138
|
+
if isinstance(tc, tuple) and len(tc) >= 2:
|
|
139
|
+
return str(tc[0])
|
|
140
|
+
if isinstance(tc, dict):
|
|
141
|
+
if "function" in tc:
|
|
142
|
+
return str((tc.get("function") or {}).get("name", ""))
|
|
143
|
+
return str(tc.get("name", ""))
|
|
144
|
+
if tc is not None:
|
|
145
|
+
name_attr = getattr(tc, "name", None) # pyright: ignore[reportAttributeAccessIssue]
|
|
146
|
+
if name_attr is not None:
|
|
147
|
+
return str(name_attr)
|
|
148
|
+
return ""
|
|
149
|
+
|
|
150
|
+
def _get_update_cb(idx: int, tc: Any) -> Callable[[Any], None] | None:
|
|
151
|
+
if isinstance(tc, tuple) and len(tc) >= 3 and callable(tc[2]):
|
|
152
|
+
return tc[2] # pyright: ignore[reportReturnType]
|
|
153
|
+
if on_updates is not None and idx < len(on_updates):
|
|
154
|
+
return on_updates[idx]
|
|
155
|
+
if on_update_factory is not None:
|
|
156
|
+
return on_update_factory(idx, tc)
|
|
157
|
+
return None
|
|
158
|
+
|
|
159
|
+
# 检查这批工具中是否包含任何不安全的写工具(或未知工具)
|
|
160
|
+
has_sequential = any(
|
|
161
|
+
(t := self._tools.get(_get_name(tc))) is None or not t.is_parallel_safe
|
|
162
|
+
for tc in tool_calls
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if has_sequential:
|
|
166
|
+
# 只要包含一个写操作,整批严格按大模型输出的原始顺序串行执行,确保因果顺序绝对正确
|
|
167
|
+
results = []
|
|
168
|
+
for i, tc in enumerate(tool_calls):
|
|
169
|
+
results.append(
|
|
170
|
+
await self.execute_tool(
|
|
171
|
+
tc, signal=signal, on_update=_get_update_cb(i, tc)
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
return results
|
|
175
|
+
|
|
176
|
+
# 全部都是只读安全工具时,安全并发执行
|
|
177
|
+
return list(
|
|
178
|
+
await asyncio.gather(
|
|
179
|
+
*(
|
|
180
|
+
self.execute_tool(
|
|
181
|
+
tc, signal=signal, on_update=_get_update_cb(i, tc)
|
|
182
|
+
)
|
|
183
|
+
for i, tc in enumerate(tool_calls)
|
|
184
|
+
)
|
|
185
|
+
)
|
|
186
|
+
)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# pyright: reportAttributeAccessIssue=false
|
|
2
|
+
"""Session package: entries, tree, state projection, storage abstractions, and Session manager."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
from .entries import (
|
|
7
|
+
BaseSessionEntry,
|
|
8
|
+
BranchSummaryEntry,
|
|
9
|
+
CompactionEntry,
|
|
10
|
+
CustomEntry,
|
|
11
|
+
LabelEntry,
|
|
12
|
+
LeafEntry,
|
|
13
|
+
MessageEntry,
|
|
14
|
+
ModelChangeEntry,
|
|
15
|
+
SessionEntry,
|
|
16
|
+
SessionInfoEntry,
|
|
17
|
+
ThinkingLevelChangeEntry,
|
|
18
|
+
)
|
|
19
|
+
from .jsonl import (
|
|
20
|
+
SessionJsonlError,
|
|
21
|
+
entry_from_json_line,
|
|
22
|
+
entry_to_json_line,
|
|
23
|
+
)
|
|
24
|
+
from .memory import (
|
|
25
|
+
SessionState,
|
|
26
|
+
)
|
|
27
|
+
from .session import (
|
|
28
|
+
Session,
|
|
29
|
+
SessionTree,
|
|
30
|
+
)
|
|
31
|
+
from .storage import (
|
|
32
|
+
InMemorySessionStorage,
|
|
33
|
+
JsonlSessionStorage,
|
|
34
|
+
SessionStorage,
|
|
35
|
+
)
|
|
36
|
+
from .store import (
|
|
37
|
+
SessionMeta,
|
|
38
|
+
SessionStore,
|
|
39
|
+
)
|
|
40
|
+
from .tree import (
|
|
41
|
+
SessionTreeError,
|
|
42
|
+
entries_by_id,
|
|
43
|
+
lowest_common_ancestor,
|
|
44
|
+
path_to_entry,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
# Session manager & tree
|
|
49
|
+
"Session",
|
|
50
|
+
"SessionTree",
|
|
51
|
+
"SessionStore",
|
|
52
|
+
"SessionMeta",
|
|
53
|
+
# Modular entries
|
|
54
|
+
"BaseSessionEntry",
|
|
55
|
+
"SessionInfoEntry",
|
|
56
|
+
"MessageEntry",
|
|
57
|
+
"ModelChangeEntry",
|
|
58
|
+
"ThinkingLevelChangeEntry",
|
|
59
|
+
"CompactionEntry",
|
|
60
|
+
"BranchSummaryEntry",
|
|
61
|
+
"LabelEntry",
|
|
62
|
+
"LeafEntry",
|
|
63
|
+
"CustomEntry",
|
|
64
|
+
"SessionEntry",
|
|
65
|
+
# Tree algorithms
|
|
66
|
+
"SessionTreeError",
|
|
67
|
+
"entries_by_id",
|
|
68
|
+
"path_to_entry",
|
|
69
|
+
"lowest_common_ancestor",
|
|
70
|
+
# Memory projection
|
|
71
|
+
"SessionState",
|
|
72
|
+
# Storage protocol & driver
|
|
73
|
+
"SessionStorage",
|
|
74
|
+
"InMemorySessionStorage",
|
|
75
|
+
"JsonlSessionStorage",
|
|
76
|
+
"SessionJsonlError",
|
|
77
|
+
"entry_to_json_line",
|
|
78
|
+
"entry_from_json_line",
|
|
79
|
+
]
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Session entry definitions for the modular session subsystem.
|
|
2
|
+
|
|
3
|
+
Provides 9 polymorphic discriminated entry types backed by Pydantic v2,
|
|
4
|
+
supporting camelCase and snake_case aliasing and strict extra field rejection.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Annotated, Any, Literal
|
|
11
|
+
from uuid import uuid4
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
14
|
+
from pydantic.alias_generators import to_camel
|
|
15
|
+
|
|
16
|
+
from my_agent_llm.models import Message
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseSessionEntry(BaseModel):
|
|
20
|
+
"""会话树节点基类。
|
|
21
|
+
|
|
22
|
+
提供自增 ID、父节点指针、浮点时间戳,并开启 camelCase 互转与未知字段禁止。
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
model_config = ConfigDict(
|
|
26
|
+
extra="forbid",
|
|
27
|
+
alias_generator=to_camel,
|
|
28
|
+
populate_by_name=True,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
id: str = Field(default_factory=lambda: uuid4().hex)
|
|
32
|
+
parent_id: str | None = None
|
|
33
|
+
timestamp: float = Field(default_factory=time.time)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SessionInfoEntry(BaseSessionEntry):
|
|
37
|
+
"""记录会话元数据(工作目录、标题、创建时间),通常作为流首项。"""
|
|
38
|
+
|
|
39
|
+
type: Literal["session_info", "sessionInfo"] = "session_info"
|
|
40
|
+
cwd: str | None = None
|
|
41
|
+
title: str | None = None
|
|
42
|
+
name: str | None = None
|
|
43
|
+
created_at: float | None = None
|
|
44
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
45
|
+
|
|
46
|
+
@model_validator(mode="after")
|
|
47
|
+
def _sync_title_name(self) -> SessionInfoEntry:
|
|
48
|
+
if self.name and not self.title:
|
|
49
|
+
self.title = self.name
|
|
50
|
+
elif self.title and not self.name:
|
|
51
|
+
self.name = self.title
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class MessageEntry(BaseSessionEntry):
|
|
56
|
+
"""包装 Agent 对话消息(User / Assistant / Tool 交互)。"""
|
|
57
|
+
|
|
58
|
+
type: Literal["message"] = "message"
|
|
59
|
+
message: Message
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def role(self) -> str:
|
|
63
|
+
return self.message.role
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def content(self) -> str:
|
|
67
|
+
return self.message.content
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def metadata(self) -> dict[str, Any]:
|
|
71
|
+
return self.message.metadata or {}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class ModelChangeEntry(BaseSessionEntry):
|
|
75
|
+
"""记录运行时大模型变更(兼容 Pi 原厂 modelId 字段)。"""
|
|
76
|
+
|
|
77
|
+
type: Literal["model_change", "modelChange"] = "model_change"
|
|
78
|
+
model: str | None = None
|
|
79
|
+
model_id: str | None = Field(default=None, alias="modelId")
|
|
80
|
+
provider: str | None = None
|
|
81
|
+
|
|
82
|
+
@model_validator(mode="after")
|
|
83
|
+
def _sync_model_fields(self) -> ModelChangeEntry:
|
|
84
|
+
if self.model_id and not self.model:
|
|
85
|
+
self.model = self.model_id
|
|
86
|
+
elif self.model and not self.model_id:
|
|
87
|
+
self.model_id = self.model
|
|
88
|
+
return self
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ThinkingLevelChangeEntry(BaseSessionEntry):
|
|
92
|
+
"""记录推理思考等级调整。"""
|
|
93
|
+
|
|
94
|
+
type: Literal["thinking_level_change", "thinkingLevelChange"] = "thinking_level_change"
|
|
95
|
+
thinking_level: str
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class CompactionEntry(BaseSessionEntry):
|
|
99
|
+
"""记录上下文压缩覆盖的条目 ID 清单与折叠摘要(兼容 Pi 原厂 firstKeptEntryId 与 tokensBefore)。"""
|
|
100
|
+
|
|
101
|
+
type: Literal["compaction"] = "compaction"
|
|
102
|
+
summary: str
|
|
103
|
+
first_kept_entry_id: str | None = Field(default=None, alias="firstKeptEntryId")
|
|
104
|
+
tokens_before: int | None = Field(default=None, alias="tokensBefore")
|
|
105
|
+
replaces_entry_ids: list[str] = Field(default_factory=list)
|
|
106
|
+
metadata: dict[str, Any] = Field(default_factory=dict)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def role(self) -> str:
|
|
110
|
+
return "system"
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def content(self) -> str:
|
|
114
|
+
return self.summary
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class BranchSummaryEntry(BaseSessionEntry):
|
|
118
|
+
"""记录分支折叠探索摘要。"""
|
|
119
|
+
|
|
120
|
+
type: Literal["branch_summary", "branchSummary"] = "branch_summary"
|
|
121
|
+
summary: str
|
|
122
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class LabelEntry(BaseSessionEntry):
|
|
126
|
+
"""用户书签/检查点。"""
|
|
127
|
+
|
|
128
|
+
type: Literal["label"] = "label"
|
|
129
|
+
label: str
|
|
130
|
+
target_id: str | None = None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
class LeafEntry(BaseSessionEntry):
|
|
134
|
+
"""指向当前分支活动叶节点的指针(分支切换仅需追加一条 LeafEntry,零文件重写)。"""
|
|
135
|
+
|
|
136
|
+
type: Literal["leaf"] = "leaf"
|
|
137
|
+
leaf_id: str
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class CustomEntry(BaseSessionEntry):
|
|
141
|
+
"""扩展与遥测隔离槽位。"""
|
|
142
|
+
|
|
143
|
+
type: Literal["custom"] = "custom"
|
|
144
|
+
namespace: str | None = None
|
|
145
|
+
custom_type: str | None = Field(default=None, alias="customType")
|
|
146
|
+
data: dict[str, Any] = Field(default_factory=dict)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class CustomMessageEntry(BaseSessionEntry):
|
|
150
|
+
"""扩展上下文注入消息条目(对标 Pi 原厂 CustomMessageEntry)。"""
|
|
151
|
+
|
|
152
|
+
type: Literal["custom_message", "customMessage"] = "custom_message"
|
|
153
|
+
custom_type: str = Field(default="custom", alias="customType")
|
|
154
|
+
content: str | list[Any] = ""
|
|
155
|
+
display: bool = True
|
|
156
|
+
details: dict[str, Any] = Field(default_factory=dict)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SessionHeaderEntry(BaseSessionEntry):
|
|
160
|
+
"""Pi 原厂会话头节点。"""
|
|
161
|
+
|
|
162
|
+
type: Literal["session"] = "session"
|
|
163
|
+
version: int = 3
|
|
164
|
+
cwd: str = ""
|
|
165
|
+
parent_session: str | None = Field(default=None, alias="parentSession")
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
SessionEntry = Annotated[
|
|
169
|
+
SessionInfoEntry
|
|
170
|
+
| SessionHeaderEntry
|
|
171
|
+
| MessageEntry
|
|
172
|
+
| ModelChangeEntry
|
|
173
|
+
| ThinkingLevelChangeEntry
|
|
174
|
+
| CompactionEntry
|
|
175
|
+
| BranchSummaryEntry
|
|
176
|
+
| LabelEntry
|
|
177
|
+
| LeafEntry
|
|
178
|
+
| CustomEntry
|
|
179
|
+
| CustomMessageEntry,
|
|
180
|
+
Field(discriminator="type"),
|
|
181
|
+
]
|
|
182
|
+
|
|
183
|
+
__all__ = [
|
|
184
|
+
"BaseSessionEntry",
|
|
185
|
+
"SessionInfoEntry",
|
|
186
|
+
"SessionHeaderEntry",
|
|
187
|
+
"MessageEntry",
|
|
188
|
+
"ModelChangeEntry",
|
|
189
|
+
"ThinkingLevelChangeEntry",
|
|
190
|
+
"CompactionEntry",
|
|
191
|
+
"BranchSummaryEntry",
|
|
192
|
+
"LabelEntry",
|
|
193
|
+
"LeafEntry",
|
|
194
|
+
"CustomEntry",
|
|
195
|
+
"CustomMessageEntry",
|
|
196
|
+
"SessionEntry",
|
|
197
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""JSONL format serialization, parsing, and error definitions for session entries.
|
|
2
|
+
|
|
3
|
+
对齐 Tau (tau_agent.session.jsonl) 架构设计:
|
|
4
|
+
本模块纯粹负责 SessionEntry 与单行 JSONL 文本之间的双向序列化与反序列化,
|
|
5
|
+
以及行格式异常定义。持久化驱动与文件锁已归位至 storage.py。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
from pydantic import TypeAdapter
|
|
13
|
+
|
|
14
|
+
from .entries import SessionEntry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SessionJsonlError(ValueError):
|
|
18
|
+
"""JSONL 序列化、反序列化或格式损坏异常。"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_ENTRY_ADAPTER: TypeAdapter[SessionEntry] = TypeAdapter(SessionEntry)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def entry_to_json_line(entry: SessionEntry) -> str:
|
|
25
|
+
"""将 SessionEntry 序列化为单行 JSON 字符串(不带尾随换行符)。"""
|
|
26
|
+
try:
|
|
27
|
+
return entry.model_dump_json(by_alias=True, exclude_none=True)
|
|
28
|
+
except Exception as exc:
|
|
29
|
+
raise SessionJsonlError(
|
|
30
|
+
f"Failed to serialize entry to JSON line: {exc}"
|
|
31
|
+
) from exc
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def entry_from_json_line(line: str) -> SessionEntry:
|
|
35
|
+
"""从单行 JSON 字符串解析 SessionEntry。"""
|
|
36
|
+
stripped = line.strip()
|
|
37
|
+
if not stripped:
|
|
38
|
+
raise SessionJsonlError("JSONL line is empty or blank")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
raw = json.loads(stripped)
|
|
42
|
+
except json.JSONDecodeError as exc:
|
|
43
|
+
raise SessionJsonlError(f"Invalid JSON line: {exc}") from exc
|
|
44
|
+
|
|
45
|
+
if not isinstance(raw, dict):
|
|
46
|
+
raise SessionJsonlError(f"Expected JSON object, got {type(raw).__name__}")
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
return _ENTRY_ADAPTER.validate_python(raw)
|
|
50
|
+
except Exception as exc:
|
|
51
|
+
raise SessionJsonlError(
|
|
52
|
+
f"Cannot parse JSON object as SessionEntry: {exc}"
|
|
53
|
+
) from exc
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"SessionJsonlError",
|
|
58
|
+
"entry_to_json_line",
|
|
59
|
+
"entry_from_json_line",
|
|
60
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""Pure in-memory state fold projection for session entries.
|
|
2
|
+
|
|
3
|
+
Provides the immutable SessionState dataclass and pure function folding over
|
|
4
|
+
path entries from root to leaf, applying model changes, thinking levels,
|
|
5
|
+
labels, leaf pointers, and compaction history replacements.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
from my_agent_llm.models import Message
|
|
14
|
+
|
|
15
|
+
from .entries import (
|
|
16
|
+
CompactionEntry,
|
|
17
|
+
LabelEntry,
|
|
18
|
+
LeafEntry,
|
|
19
|
+
MessageEntry,
|
|
20
|
+
ModelChangeEntry,
|
|
21
|
+
SessionEntry,
|
|
22
|
+
ThinkingLevelChangeEntry,
|
|
23
|
+
)
|
|
24
|
+
from .tree import path_to_entry
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _apply_compaction(
|
|
28
|
+
items: list[tuple[str, Message]],
|
|
29
|
+
entry: CompactionEntry,
|
|
30
|
+
) -> list[tuple[str, Message]]:
|
|
31
|
+
"""将 replaces_entry_ids 范围内的消息条目折叠为一条 UserMessage 摘要。"""
|
|
32
|
+
summary_text = entry.summary
|
|
33
|
+
prefix = "Previous conversation summary:\n"
|
|
34
|
+
if summary_text.startswith(prefix):
|
|
35
|
+
content = summary_text
|
|
36
|
+
else:
|
|
37
|
+
content = f"{prefix}{summary_text}"
|
|
38
|
+
|
|
39
|
+
summary_msg = Message(role="user", content=content)
|
|
40
|
+
replaces_set = set(entry.replaces_entry_ids)
|
|
41
|
+
|
|
42
|
+
if not replaces_set:
|
|
43
|
+
return [*items, (entry.id, summary_msg)]
|
|
44
|
+
|
|
45
|
+
new_items: list[tuple[str, Message]] = []
|
|
46
|
+
inserted = False
|
|
47
|
+
for eid, msg in items:
|
|
48
|
+
if eid in replaces_set:
|
|
49
|
+
if not inserted:
|
|
50
|
+
new_items.append((entry.id, summary_msg))
|
|
51
|
+
inserted = True
|
|
52
|
+
else:
|
|
53
|
+
new_items.append((eid, msg))
|
|
54
|
+
|
|
55
|
+
if not inserted:
|
|
56
|
+
new_items.append((entry.id, summary_msg))
|
|
57
|
+
|
|
58
|
+
return new_items
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True, slots=True)
|
|
62
|
+
class SessionState:
|
|
63
|
+
"""不可变运行时会话状态快照。
|
|
64
|
+
|
|
65
|
+
通过事件溯源沿树状 DAG 路径纯函数折叠聚合生成,无锁且线程安全。
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
messages: tuple[Message, ...] = ()
|
|
69
|
+
model: str | None = None
|
|
70
|
+
provider: str | None = None
|
|
71
|
+
thinking_level: str | None = None
|
|
72
|
+
label: str | None = None
|
|
73
|
+
active_leaf_id: str | None = None
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_entries(
|
|
77
|
+
cls,
|
|
78
|
+
entries: Sequence[SessionEntry],
|
|
79
|
+
leaf_id: str | None = None,
|
|
80
|
+
) -> SessionState:
|
|
81
|
+
"""纯函数折叠投影:沿根至 leaf_id 的路径条目无锁计算出最新运行时状态。"""
|
|
82
|
+
if not entries:
|
|
83
|
+
return cls()
|
|
84
|
+
|
|
85
|
+
# 确定目标叶节点 ID
|
|
86
|
+
if leaf_id is not None:
|
|
87
|
+
target_leaf_id = leaf_id
|
|
88
|
+
else:
|
|
89
|
+
# 若存在 LeafEntry 指针,取最后一条 LeafEntry 指定的叶节点
|
|
90
|
+
target_leaf_id = None
|
|
91
|
+
for entry in reversed(entries):
|
|
92
|
+
if isinstance(entry, LeafEntry):
|
|
93
|
+
target_leaf_id = entry.leaf_id
|
|
94
|
+
break
|
|
95
|
+
if target_leaf_id is None:
|
|
96
|
+
target_leaf_id = entries[-1].id
|
|
97
|
+
|
|
98
|
+
# 提取从根至目标叶节点的有序单链路径
|
|
99
|
+
path = path_to_entry(entries, target_leaf_id)
|
|
100
|
+
|
|
101
|
+
# 沿路径纯函数折叠投影
|
|
102
|
+
items: list[tuple[str, Message]] = []
|
|
103
|
+
model: str | None = None
|
|
104
|
+
provider: str | None = None
|
|
105
|
+
thinking_level: str | None = None
|
|
106
|
+
label: str | None = None
|
|
107
|
+
active_leaf_id: str | None = target_leaf_id
|
|
108
|
+
|
|
109
|
+
for entry in path:
|
|
110
|
+
if isinstance(entry, MessageEntry):
|
|
111
|
+
items.append((entry.id, entry.message))
|
|
112
|
+
elif isinstance(entry, CompactionEntry):
|
|
113
|
+
items = _apply_compaction(items, entry)
|
|
114
|
+
elif isinstance(entry, ModelChangeEntry):
|
|
115
|
+
model = entry.model
|
|
116
|
+
if entry.provider is not None:
|
|
117
|
+
provider = entry.provider
|
|
118
|
+
elif isinstance(entry, ThinkingLevelChangeEntry):
|
|
119
|
+
thinking_level = entry.thinking_level
|
|
120
|
+
elif isinstance(entry, LabelEntry):
|
|
121
|
+
label = entry.label
|
|
122
|
+
elif isinstance(entry, LeafEntry):
|
|
123
|
+
active_leaf_id = entry.leaf_id
|
|
124
|
+
|
|
125
|
+
return cls(
|
|
126
|
+
messages=tuple(msg for _, msg in items),
|
|
127
|
+
model=model,
|
|
128
|
+
provider=provider,
|
|
129
|
+
thinking_level=thinking_level,
|
|
130
|
+
label=label,
|
|
131
|
+
active_leaf_id=active_leaf_id,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
__all__ = [
|
|
136
|
+
"SessionState",
|
|
137
|
+
]
|