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.
Files changed (141) hide show
  1. package/README.md +318 -0
  2. package/package.json +45 -0
  3. package/pyproject.toml +50 -0
  4. package/src/my_agent_core/__init__.py +123 -0
  5. package/src/my_agent_core/agent.py +441 -0
  6. package/src/my_agent_core/background.py +121 -0
  7. package/src/my_agent_core/context.py +505 -0
  8. package/src/my_agent_core/events.py +153 -0
  9. package/src/my_agent_core/extensions/__init__.py +9 -0
  10. package/src/my_agent_core/extensions/core.py +197 -0
  11. package/src/my_agent_core/hooks.py +130 -0
  12. package/src/my_agent_core/loop.py +709 -0
  13. package/src/my_agent_core/main.py +134 -0
  14. package/src/my_agent_core/memory.py +241 -0
  15. package/src/my_agent_core/message_queue.py +110 -0
  16. package/src/my_agent_core/plugins.py +212 -0
  17. package/src/my_agent_core/registry.py +186 -0
  18. package/src/my_agent_core/session/__init__.py +79 -0
  19. package/src/my_agent_core/session/entries.py +197 -0
  20. package/src/my_agent_core/session/jsonl.py +60 -0
  21. package/src/my_agent_core/session/memory.py +137 -0
  22. package/src/my_agent_core/session/session.py +400 -0
  23. package/src/my_agent_core/session/storage.py +245 -0
  24. package/src/my_agent_core/session/store.py +131 -0
  25. package/src/my_agent_core/session/tree.py +86 -0
  26. package/src/my_agent_core/skills.py +149 -0
  27. package/src/my_agent_core/subagent_tasks.py +170 -0
  28. package/src/my_agent_core/subagents.py +148 -0
  29. package/src/my_agent_core/task_store.py +248 -0
  30. package/src/my_agent_core/tool_history.py +189 -0
  31. package/src/my_agent_core/tools/__init__.py +5 -0
  32. package/src/my_agent_core/tools/builtin/__init__.py +5 -0
  33. package/src/my_agent_core/tools/builtin/task.py +30 -0
  34. package/src/my_agent_core/tools/builtin/task_tools.py +215 -0
  35. package/src/my_agent_core/tools/core.py +239 -0
  36. package/src/my_agent_llm/__init__.py +45 -0
  37. package/src/my_agent_llm/auth/__init__.py +46 -0
  38. package/src/my_agent_llm/auth/antigravity.py +209 -0
  39. package/src/my_agent_llm/auth/manager.py +259 -0
  40. package/src/my_agent_llm/auth/quota.py +56 -0
  41. package/src/my_agent_llm/auth/schema.py +94 -0
  42. package/src/my_agent_llm/client.py +116 -0
  43. package/src/my_agent_llm/config.py +17 -0
  44. package/src/my_agent_llm/events.py +84 -0
  45. package/src/my_agent_llm/models.py +195 -0
  46. package/src/my_agent_llm/providers/__init__.py +4 -0
  47. package/src/my_agent_llm/providers/_base.py +94 -0
  48. package/src/my_agent_llm/providers/anthropic.py +298 -0
  49. package/src/my_agent_llm/providers/antigravity.py +480 -0
  50. package/src/my_agent_llm/providers/deepseek.py +196 -0
  51. package/src/my_agent_llm/providers/openai.py +364 -0
  52. package/src/my_agent_llm/providers/registry.py +16 -0
  53. package/src/my_agent_llm/stream.py +218 -0
  54. package/src/my_coding_agent/__init__.py +66 -0
  55. package/src/my_coding_agent/agent.py +208 -0
  56. package/src/my_coding_agent/cli.py +78 -0
  57. package/src/my_coding_agent/file_reference.py +80 -0
  58. package/src/my_coding_agent/macro.py +408 -0
  59. package/src/my_coding_agent/mcp.py +243 -0
  60. package/src/my_coding_agent/mutation_queue.py +37 -0
  61. package/src/my_coding_agent/paths.py +119 -0
  62. package/src/my_coding_agent/permissions.py +84 -0
  63. package/src/my_coding_agent/prompt.py +54 -0
  64. package/src/my_coding_agent/rpc_server.py +2817 -0
  65. package/src/my_coding_agent/settings.py +126 -0
  66. package/src/my_coding_agent/tools/__init__.py +55 -0
  67. package/src/my_coding_agent/tools/base.py +58 -0
  68. package/src/my_coding_agent/tools/bash.py +206 -0
  69. package/src/my_coding_agent/tools/edit.py +226 -0
  70. package/src/my_coding_agent/tools/find.py +118 -0
  71. package/src/my_coding_agent/tools/grep.py +177 -0
  72. package/src/my_coding_agent/tools/ls.py +112 -0
  73. package/src/my_coding_agent/tools/read.py +113 -0
  74. package/src/my_coding_agent/tools/write.py +72 -0
  75. package/tui/README.md +27 -0
  76. package/tui/bin/my-agent.js +98 -0
  77. package/tui/dist/app.d.ts +41 -0
  78. package/tui/dist/app.js +110 -0
  79. package/tui/dist/bridge/event-translator.d.ts +92 -0
  80. package/tui/dist/bridge/event-translator.js +216 -0
  81. package/tui/dist/bridge/kernel-bridge.d.ts +48 -0
  82. package/tui/dist/bridge/kernel-bridge.js +132 -0
  83. package/tui/dist/client.d.ts +63 -0
  84. package/tui/dist/client.js +239 -0
  85. package/tui/dist/components/assistant-message.d.ts +19 -0
  86. package/tui/dist/components/assistant-message.js +90 -0
  87. package/tui/dist/components/compaction-summary-message.d.ts +19 -0
  88. package/tui/dist/components/compaction-summary-message.js +46 -0
  89. package/tui/dist/components/custom-editor.d.ts +18 -0
  90. package/tui/dist/components/custom-editor.js +56 -0
  91. package/tui/dist/components/dynamic-border.d.ts +9 -0
  92. package/tui/dist/components/dynamic-border.js +14 -0
  93. package/tui/dist/components/footer.d.ts +39 -0
  94. package/tui/dist/components/footer.js +199 -0
  95. package/tui/dist/components/header.d.ts +4 -0
  96. package/tui/dist/components/header.js +21 -0
  97. package/tui/dist/components/keys.d.ts +5 -0
  98. package/tui/dist/components/keys.js +12 -0
  99. package/tui/dist/components/login-selector.d.ts +26 -0
  100. package/tui/dist/components/login-selector.js +181 -0
  101. package/tui/dist/components/logout-selector.d.ts +19 -0
  102. package/tui/dist/components/logout-selector.js +88 -0
  103. package/tui/dist/components/model-selector.d.ts +40 -0
  104. package/tui/dist/components/model-selector.js +268 -0
  105. package/tui/dist/components/session-selector.d.ts +54 -0
  106. package/tui/dist/components/session-selector.js +393 -0
  107. package/tui/dist/components/settings-selector.d.ts +24 -0
  108. package/tui/dist/components/settings-selector.js +146 -0
  109. package/tui/dist/components/status-indicator.d.ts +25 -0
  110. package/tui/dist/components/status-indicator.js +60 -0
  111. package/tui/dist/components/theme-selector.d.ts +14 -0
  112. package/tui/dist/components/theme-selector.js +77 -0
  113. package/tui/dist/components/thinking-selector.d.ts +21 -0
  114. package/tui/dist/components/thinking-selector.js +128 -0
  115. package/tui/dist/components/tool-execution.d.ts +31 -0
  116. package/tui/dist/components/tool-execution.js +206 -0
  117. package/tui/dist/components/tree-selector.d.ts +40 -0
  118. package/tui/dist/components/tree-selector.js +173 -0
  119. package/tui/dist/components/user-message-selector.d.ts +21 -0
  120. package/tui/dist/components/user-message-selector.js +103 -0
  121. package/tui/dist/components/user-message.d.ts +5 -0
  122. package/tui/dist/components/user-message.js +15 -0
  123. package/tui/dist/index.d.ts +11 -0
  124. package/tui/dist/index.js +11 -0
  125. package/tui/dist/interactive/chat-viewport.d.ts +19 -0
  126. package/tui/dist/interactive/chat-viewport.js +41 -0
  127. package/tui/dist/interactive/components.d.ts +1 -0
  128. package/tui/dist/interactive/components.js +1 -0
  129. package/tui/dist/interactive/interactive-mode.d.ts +89 -0
  130. package/tui/dist/interactive/interactive-mode.js +1625 -0
  131. package/tui/dist/interactive/theme.d.ts +1 -0
  132. package/tui/dist/interactive/theme.js +1 -0
  133. package/tui/dist/interactive/tui-renderer.d.ts +8 -0
  134. package/tui/dist/interactive/tui-renderer.js +10 -0
  135. package/tui/dist/protocol.d.ts +78 -0
  136. package/tui/dist/protocol.js +1 -0
  137. package/tui/dist/theme/dark.json +54 -0
  138. package/tui/dist/theme/light.json +71 -0
  139. package/tui/dist/theme/theme.d.ts +20 -0
  140. package/tui/dist/theme/theme.js +86 -0
  141. 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
+ ]