tau-coding-agent 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 (283) hide show
  1. tau/__init__.py +0 -0
  2. tau/agent/__init__.py +11 -0
  3. tau/agent/prompt/__init__.py +10 -0
  4. tau/agent/prompt/builder.py +302 -0
  5. tau/agent/prompt/types.py +33 -0
  6. tau/agent/service.py +369 -0
  7. tau/agent/types.py +61 -0
  8. tau/auth/manager.py +247 -0
  9. tau/auth/storage.py +82 -0
  10. tau/auth/types.py +41 -0
  11. tau/builtins/__init__.py +4 -0
  12. tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
  13. tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
  14. tau/builtins/commands/__init__.py +41 -0
  15. tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
  16. tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
  17. tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
  18. tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
  19. tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
  20. tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
  21. tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
  22. tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
  23. tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
  24. tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
  25. tau/builtins/commands/clear.py +16 -0
  26. tau/builtins/commands/compact.py +28 -0
  27. tau/builtins/commands/reload.py +27 -0
  28. tau/builtins/commands/session.py +19 -0
  29. tau/builtins/extensions/footer/__init__.py +76 -0
  30. tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
  31. tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
  32. tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
  33. tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
  34. tau/builtins/extensions/footer/git.py +26 -0
  35. tau/builtins/extensions/footer/model.py +69 -0
  36. tau/builtins/extensions/footer/utils.py +44 -0
  37. tau/builtins/extensions/header/__init__.py +18 -0
  38. tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
  39. tau/builtins/models/__init__.py +0 -0
  40. tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
  41. tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
  42. tau/builtins/models/audio.py +43 -0
  43. tau/builtins/models/image.py +43 -0
  44. tau/builtins/models/text.py +482 -0
  45. tau/builtins/models/video.py +40 -0
  46. tau/builtins/prompts/commit.md +7 -0
  47. tau/builtins/prompts/docs.md +7 -0
  48. tau/builtins/prompts/explain.md +7 -0
  49. tau/builtins/prompts/fix.md +7 -0
  50. tau/builtins/prompts/refactor.md +7 -0
  51. tau/builtins/prompts/review.md +7 -0
  52. tau/builtins/prompts/test.md +7 -0
  53. tau/builtins/providers/__init__.py +0 -0
  54. tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
  55. tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
  56. tau/builtins/providers/audio.py +10 -0
  57. tau/builtins/providers/image.py +9 -0
  58. tau/builtins/providers/text.py +33 -0
  59. tau/builtins/providers/video.py +6 -0
  60. tau/builtins/skills/code-review/SKILL.md +4 -0
  61. tau/builtins/skills/debug/SKILL.md +4 -0
  62. tau/builtins/skills/git-commit/SKILL.md +4 -0
  63. tau/builtins/themes/dark.yaml +1 -0
  64. tau/builtins/themes/light.yaml +46 -0
  65. tau/builtins/tools/__init__.py +73 -0
  66. tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
  67. tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  68. tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
  69. tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
  70. tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
  71. tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
  72. tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
  73. tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
  74. tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
  75. tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
  76. tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
  77. tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
  78. tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
  79. tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
  80. tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
  81. tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
  82. tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
  83. tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
  84. tau/builtins/tools/edit.py +215 -0
  85. tau/builtins/tools/glob.py +112 -0
  86. tau/builtins/tools/grep.py +146 -0
  87. tau/builtins/tools/ls.py +135 -0
  88. tau/builtins/tools/read.py +122 -0
  89. tau/builtins/tools/terminal.py +150 -0
  90. tau/builtins/tools/write.py +105 -0
  91. tau/commands/__init__.py +10 -0
  92. tau/commands/registry.py +71 -0
  93. tau/commands/types.py +33 -0
  94. tau/console/__init__.py +0 -0
  95. tau/console/cli.py +266 -0
  96. tau/console/commands/__init__.py +0 -0
  97. tau/console/commands/auth.py +193 -0
  98. tau/console/commands/packages.py +104 -0
  99. tau/console/commands/update.py +76 -0
  100. tau/core/__init__.py +0 -0
  101. tau/core/registry.py +102 -0
  102. tau/engine/__init__.py +47 -0
  103. tau/engine/service.py +768 -0
  104. tau/engine/types.py +163 -0
  105. tau/extensions/__init__.py +28 -0
  106. tau/extensions/api.py +928 -0
  107. tau/extensions/context.py +462 -0
  108. tau/extensions/events.py +70 -0
  109. tau/extensions/loader.py +386 -0
  110. tau/extensions/runtime.py +184 -0
  111. tau/extensions/settings.py +137 -0
  112. tau/hooks/__init__.py +112 -0
  113. tau/hooks/engine.py +237 -0
  114. tau/hooks/inference.py +21 -0
  115. tau/hooks/runtime.py +126 -0
  116. tau/hooks/service.py +121 -0
  117. tau/hooks/session.py +117 -0
  118. tau/hooks/tui.py +61 -0
  119. tau/hooks/types.py +72 -0
  120. tau/inference/__init__.py +180 -0
  121. tau/inference/api/__init__.py +0 -0
  122. tau/inference/api/audio/__init__.py +0 -0
  123. tau/inference/api/audio/base.py +29 -0
  124. tau/inference/api/audio/builtins.py +15 -0
  125. tau/inference/api/audio/elevenlabs_audio.py +183 -0
  126. tau/inference/api/audio/gemini_audio.py +95 -0
  127. tau/inference/api/audio/openai_audio.py +159 -0
  128. tau/inference/api/audio/registry.py +15 -0
  129. tau/inference/api/audio/sarvam_audio.py +163 -0
  130. tau/inference/api/audio/service.py +103 -0
  131. tau/inference/api/audio/utils.py +47 -0
  132. tau/inference/api/image/__init__.py +0 -0
  133. tau/inference/api/image/base.py +17 -0
  134. tau/inference/api/image/builtins.py +8 -0
  135. tau/inference/api/image/gemini_image.py +77 -0
  136. tau/inference/api/image/openai_image.py +103 -0
  137. tau/inference/api/image/openrouter.py +144 -0
  138. tau/inference/api/image/registry.py +15 -0
  139. tau/inference/api/image/service.py +71 -0
  140. tau/inference/api/registry.py +82 -0
  141. tau/inference/api/text/__init__.py +0 -0
  142. tau/inference/api/text/anthropic_claude_code.py +222 -0
  143. tau/inference/api/text/anthropic_messages.py +196 -0
  144. tau/inference/api/text/base.py +40 -0
  145. tau/inference/api/text/builtins.py +19 -0
  146. tau/inference/api/text/gemini_generate.py +234 -0
  147. tau/inference/api/text/github_copilot_chat.py +172 -0
  148. tau/inference/api/text/google_antigravity.py +522 -0
  149. tau/inference/api/text/mistral_chat.py +284 -0
  150. tau/inference/api/text/ollama_chat.py +200 -0
  151. tau/inference/api/text/openai_codex_responses.py +497 -0
  152. tau/inference/api/text/openai_completions.py +227 -0
  153. tau/inference/api/text/openai_responses.py +235 -0
  154. tau/inference/api/text/registry.py +50 -0
  155. tau/inference/api/text/service.py +297 -0
  156. tau/inference/api/text/types.py +7 -0
  157. tau/inference/api/text/utils.py +228 -0
  158. tau/inference/api/video/__init__.py +0 -0
  159. tau/inference/api/video/base.py +26 -0
  160. tau/inference/api/video/builtins.py +7 -0
  161. tau/inference/api/video/fal_video.py +119 -0
  162. tau/inference/api/video/openrouter_video.py +142 -0
  163. tau/inference/api/video/registry.py +15 -0
  164. tau/inference/api/video/service.py +72 -0
  165. tau/inference/model/__init__.py +0 -0
  166. tau/inference/model/registry.py +102 -0
  167. tau/inference/model/types.py +65 -0
  168. tau/inference/provider/__init__.py +0 -0
  169. tau/inference/provider/oauth/__init__.py +35 -0
  170. tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
  171. tau/inference/provider/oauth/github_copilot.py +333 -0
  172. tau/inference/provider/oauth/google_antigravity.py +258 -0
  173. tau/inference/provider/oauth/openai_codex.py +309 -0
  174. tau/inference/provider/oauth/pkce.py +14 -0
  175. tau/inference/provider/oauth/types.py +46 -0
  176. tau/inference/provider/oauth/utils.py +154 -0
  177. tau/inference/provider/registry.py +141 -0
  178. tau/inference/provider/types.py +114 -0
  179. tau/inference/types.py +549 -0
  180. tau/inference/utils.py +219 -0
  181. tau/message/__init__.py +0 -0
  182. tau/message/types.py +482 -0
  183. tau/message/utils.py +178 -0
  184. tau/packages/__init__.py +11 -0
  185. tau/packages/manager.py +190 -0
  186. tau/packages/types.py +20 -0
  187. tau/packages/utils.py +67 -0
  188. tau/prompts/expand.py +58 -0
  189. tau/prompts/loader.py +69 -0
  190. tau/prompts/registry.py +45 -0
  191. tau/prompts/types.py +24 -0
  192. tau/rpc/__init__.py +8 -0
  193. tau/rpc/mode.py +783 -0
  194. tau/rpc/types.py +252 -0
  195. tau/runtime/service.py +759 -0
  196. tau/runtime/types.py +303 -0
  197. tau/session/branch_summarization.py +312 -0
  198. tau/session/compaction.py +646 -0
  199. tau/session/manager.py +652 -0
  200. tau/session/types.py +188 -0
  201. tau/session/utils.py +233 -0
  202. tau/settings/manager.py +1077 -0
  203. tau/settings/paths.py +150 -0
  204. tau/settings/storage.py +63 -0
  205. tau/settings/types.py +173 -0
  206. tau/settings/utils.py +25 -0
  207. tau/skills/loader.py +91 -0
  208. tau/skills/registry.py +70 -0
  209. tau/skills/types.py +25 -0
  210. tau/themes/loader.py +238 -0
  211. tau/themes/registry.py +108 -0
  212. tau/themes/types.py +19 -0
  213. tau/tool/__init__.py +3 -0
  214. tau/tool/registry.py +117 -0
  215. tau/tool/render.py +21 -0
  216. tau/tool/types.py +244 -0
  217. tau/trust/__init__.py +13 -0
  218. tau/trust/manager.py +80 -0
  219. tau/trust/types.py +14 -0
  220. tau/trust/utils.py +72 -0
  221. tau/tui/__init__.py +54 -0
  222. tau/tui/agent_hooks.py +346 -0
  223. tau/tui/ansi.py +330 -0
  224. tau/tui/app.py +540 -0
  225. tau/tui/autocomplete.py +33 -0
  226. tau/tui/capabilities.py +119 -0
  227. tau/tui/commands/__init__.py +3 -0
  228. tau/tui/commands/appearance.py +498 -0
  229. tau/tui/commands/auth.py +232 -0
  230. tau/tui/commands/context.py +38 -0
  231. tau/tui/commands/misc.py +82 -0
  232. tau/tui/commands/model.py +118 -0
  233. tau/tui/commands/session.py +464 -0
  234. tau/tui/component.py +268 -0
  235. tau/tui/components/__init__.py +0 -0
  236. tau/tui/components/autocomplete_manager.py +267 -0
  237. tau/tui/components/autocomplete_picker.py +143 -0
  238. tau/tui/components/box.py +90 -0
  239. tau/tui/components/command_palette.py +144 -0
  240. tau/tui/components/dynamic_border.py +19 -0
  241. tau/tui/components/file_picker.py +233 -0
  242. tau/tui/components/image.py +181 -0
  243. tau/tui/components/inline_selector.py +71 -0
  244. tau/tui/components/layout.py +1194 -0
  245. tau/tui/components/message_list.py +692 -0
  246. tau/tui/components/modal.py +97 -0
  247. tau/tui/components/model_palette.py +204 -0
  248. tau/tui/components/picker_overlay.py +174 -0
  249. tau/tui/components/prompt_overlay.py +236 -0
  250. tau/tui/components/resume_modal.py +372 -0
  251. tau/tui/components/select_list.py +222 -0
  252. tau/tui/components/settings_modal.py +274 -0
  253. tau/tui/components/settings_schema.py +203 -0
  254. tau/tui/components/spinner.py +119 -0
  255. tau/tui/components/text_input.py +396 -0
  256. tau/tui/components/text_prompt.py +82 -0
  257. tau/tui/components/tree_select_list.py +580 -0
  258. tau/tui/components/trust_screen.py +97 -0
  259. tau/tui/diff.py +114 -0
  260. tau/tui/fuzzy.py +99 -0
  261. tau/tui/input.py +496 -0
  262. tau/tui/input_handler.py +716 -0
  263. tau/tui/keybindings.py +87 -0
  264. tau/tui/markdown.py +286 -0
  265. tau/tui/message_renderers.py +31 -0
  266. tau/tui/overlay.py +326 -0
  267. tau/tui/renderer.py +378 -0
  268. tau/tui/terminal.py +499 -0
  269. tau/tui/theme.py +148 -0
  270. tau/tui/tui.py +544 -0
  271. tau/tui/ui_context.py +768 -0
  272. tau/tui/utils.py +20 -0
  273. tau/utils/__init__.py +0 -0
  274. tau/utils/http_proxy.py +221 -0
  275. tau/utils/image_processing.py +172 -0
  276. tau/utils/secrets.py +59 -0
  277. tau/utils/version_check.py +60 -0
  278. tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
  279. tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
  280. tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
  281. tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
  282. tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  283. tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
tau/session/types.py ADDED
@@ -0,0 +1,188 @@
1
+ from __future__ import annotations
2
+ import uuid
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from enum import Enum
6
+ from dataclasses import dataclass
7
+ from typing import Any, Literal, Annotated, TYPE_CHECKING, List
8
+
9
+ from pydantic import BaseModel, Field, ConfigDict
10
+ from tau.inference.types import ThinkingLevel
11
+
12
+ from tau.message.types import AgentMessage, ImageContent, TextContent
13
+
14
+ if TYPE_CHECKING:
15
+ pass
16
+
17
+
18
+ def generate_timestamp() -> float:
19
+ return datetime.now().timestamp()
20
+
21
+
22
+ def _generate_id() -> str:
23
+ return str(uuid.uuid4())[:8]
24
+
25
+
26
+ SESSION_VERSION = 3
27
+
28
+
29
+ class SessionType(str, Enum):
30
+ SESSION_HEADER = "session"
31
+ SESSION_MESSAGE = "message"
32
+ THINKING_LEVEL_CHANGE = "thinking_level_change"
33
+ MODEL_CHANGE = "model_change"
34
+ LABEL = "label"
35
+ CUSTOM_INFO = "custom"
36
+ SESSION_INFO = "session_info"
37
+ CUSTOM_MESSAGE = "custom_message"
38
+ LEAF = "leaf"
39
+ COMPACTION = "compaction"
40
+ BRANCH_SUMMARY = "branch_summary"
41
+
42
+
43
+ class BaseSessionEntry(BaseModel):
44
+ model_config = ConfigDict(arbitrary_types_allowed=True)
45
+ id: str = Field(default_factory=_generate_id)
46
+ timestamp: float = Field(default_factory=generate_timestamp)
47
+ parent_id: str | None = None
48
+
49
+
50
+ class SessionHeader(BaseModel):
51
+ model_config = ConfigDict(arbitrary_types_allowed=True)
52
+ type: Literal[SessionType.SESSION_HEADER] = SessionType.SESSION_HEADER
53
+ version: int = SESSION_VERSION
54
+ id: str = Field(default_factory=_generate_id)
55
+ timestamp: float = Field(default_factory=generate_timestamp)
56
+ cwd: Path
57
+ parent_session: Path | None = None
58
+
59
+
60
+ class SessionInfoEntry(BaseSessionEntry):
61
+ type: Literal[SessionType.SESSION_INFO] = SessionType.SESSION_INFO
62
+ name: str | None = None
63
+
64
+
65
+ class MessageAttachment(BaseModel):
66
+ path: str
67
+ mime_type: str | None = None
68
+
69
+
70
+ class MessageMeta(BaseModel):
71
+ attachments: list[MessageAttachment] | None = None
72
+
73
+
74
+ class MessageEntry(BaseSessionEntry):
75
+ type: Literal[SessionType.SESSION_MESSAGE] = SessionType.SESSION_MESSAGE
76
+ message: "AgentMessage"
77
+ meta: MessageMeta | None = None
78
+
79
+
80
+ class ThinkingLevelChangeEntry(BaseSessionEntry):
81
+ type: Literal[SessionType.THINKING_LEVEL_CHANGE] = SessionType.THINKING_LEVEL_CHANGE
82
+ thinking_level: ThinkingLevel
83
+
84
+
85
+ class ModelChangeEntry(BaseSessionEntry):
86
+ type: Literal[SessionType.MODEL_CHANGE] = SessionType.MODEL_CHANGE
87
+ model_id: str
88
+ provider_id: str
89
+
90
+
91
+ class LabelEntry(BaseSessionEntry):
92
+ type: Literal[SessionType.LABEL] = SessionType.LABEL
93
+ label: str | None = None
94
+ target_id: str
95
+
96
+
97
+ class LeafEntry(BaseSessionEntry):
98
+ type: Literal[SessionType.LEAF] = SessionType.LEAF
99
+ target_id: str | None = None
100
+
101
+
102
+ class CustomInfoEntry(BaseSessionEntry):
103
+ type: Literal[SessionType.CUSTOM_INFO] = SessionType.CUSTOM_INFO
104
+ custom_type: str
105
+ data: Any | None = None
106
+
107
+
108
+ class CustomMessageEntry(BaseSessionEntry):
109
+ type: Literal[SessionType.CUSTOM_MESSAGE] = SessionType.CUSTOM_MESSAGE
110
+ custom_type: str
111
+ content: List["TextContent | ImageContent"]
112
+ display: bool = True
113
+ details: Any | None = None
114
+
115
+
116
+ class CompactionEntry(BaseSessionEntry):
117
+ type: Literal[SessionType.COMPACTION] = SessionType.COMPACTION
118
+ summary: str
119
+ first_kept_entry_id: str
120
+ tokens_before: int
121
+ details: dict[str, Any] | None = None
122
+
123
+
124
+ class BranchSummaryEntry(BaseSessionEntry):
125
+ type: Literal[SessionType.BRANCH_SUMMARY] = SessionType.BRANCH_SUMMARY
126
+ from_id: str
127
+ summary: str
128
+ details: dict[str, Any] | None = None
129
+ from_hook: bool = False
130
+ label: str | None = None
131
+
132
+
133
+ SessionEntries = (
134
+ SessionInfoEntry
135
+ | MessageEntry
136
+ | ThinkingLevelChangeEntry
137
+ | ModelChangeEntry
138
+ | LabelEntry
139
+ | LeafEntry
140
+ | CustomInfoEntry
141
+ | CustomMessageEntry
142
+ | CompactionEntry
143
+ | BranchSummaryEntry
144
+ )
145
+
146
+ SessionEntry = Annotated[
147
+ SessionEntries,
148
+ Field(discriminator="type")
149
+ ]
150
+
151
+ SessionFileEntry = Annotated[
152
+ SessionHeader | SessionEntries,
153
+ Field(discriminator="type")
154
+ ]
155
+
156
+
157
+ class SessionTreeNode(BaseModel):
158
+ model_config = ConfigDict(arbitrary_types_allowed=True)
159
+ entry: SessionEntry
160
+ children: list[SessionTreeNode] = Field(default_factory=list)
161
+ label: str | None = None
162
+ label_timestamp: float | None = None
163
+
164
+
165
+ class SessionContext(BaseModel):
166
+ model_config = ConfigDict(arbitrary_types_allowed=True)
167
+ messages: list["AgentMessage"]
168
+ thinking_level: ThinkingLevel
169
+ model_id: str | None = None
170
+ provider_id: str | None = None
171
+
172
+
173
+ class SessionInfo(BaseModel):
174
+ model_config = ConfigDict(arbitrary_types_allowed=True)
175
+ path: Path
176
+ id: str
177
+ cwd: Path
178
+ name: str | None = None
179
+ parent_session: Path | None = None
180
+ created: datetime
181
+ modified: datetime
182
+ message_count: int
183
+
184
+
185
+ @dataclass
186
+ class SessionOptions:
187
+ id: str | None = None
188
+ parent_session: str | None = None
tau/session/utils.py ADDED
@@ -0,0 +1,233 @@
1
+ import re
2
+ import uuid
3
+ from uuid_extensions import uuid7str as _uuid7str
4
+ from typing import Any, Callable
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from pydantic import TypeAdapter, ValidationError
8
+
9
+ from tau.session.types import (
10
+ SessionEntry, SessionHeader, SessionInfo, MessageEntry, SessionFileEntry, SessionType
11
+ )
12
+ from tau.message.types import AgentMessage, LLMMessage, Role, TextContent, ImageContent
13
+ from tau.settings.paths import get_sessions_dir
14
+
15
+ def create_session_id() -> str:
16
+ """Create a new session ID using UUIDv7."""
17
+ return _uuid7str()
18
+
19
+ def generate_id(by_id: Any) -> str:
20
+ """
21
+ Generate a unique short ID (8 hex chars, collision-checked).
22
+
23
+ Args:
24
+ by_id: A container (like a set or dict) that supports the 'in' operator
25
+ to check for existing IDs.
26
+ """
27
+ for _ in range(100):
28
+ new_id = str(uuid.uuid4())[:8]
29
+ if new_id not in by_id:
30
+ return new_id
31
+
32
+ # Fallback to full UUID if somehow we have collisions
33
+ return str(uuid.uuid4())
34
+
35
+ def generate_timestamp() -> float:
36
+ """Generate a Unix timestamp for the current moment."""
37
+ now = datetime.now()
38
+ return now.timestamp()
39
+
40
+ def get_default_session_dir(cwd: str | Path, sessions_dir: Path | None = None) -> Path:
41
+ """Return the per-project session directory under ~/.tau/sessions/<encoded-cwd>/."""
42
+ base = sessions_dir if sessions_dir is not None else get_sessions_dir()
43
+ resolved = str(Path(cwd).resolve())
44
+ # Encode the absolute path into a safe directory name: --home-user-project--
45
+ safe = "--" + re.sub(r"^[/\\]", "", resolved).replace("/", "-").replace("\\", "-").replace(":", "-") + "--"
46
+ session_dir = base / safe
47
+ session_dir.mkdir(parents=True, exist_ok=True)
48
+ return session_dir
49
+
50
+ def read_session_file(session_file: Path) -> list[SessionFileEntry]:
51
+ """Load and parse a session file, returning a list of entries."""
52
+ if not session_file.exists():
53
+ return []
54
+
55
+ adapter = TypeAdapter(SessionFileEntry)
56
+
57
+ content = session_file.read_text(encoding="utf-8")
58
+ entries: list[SessionFileEntry] = []
59
+
60
+ for line in content.splitlines():
61
+ if not line.strip():
62
+ continue
63
+ try:
64
+ entry = adapter.validate_json(line)
65
+ entries.append(entry)
66
+ except Exception:
67
+ continue
68
+
69
+ if len(entries) == 0:
70
+ return []
71
+
72
+ header = entries[0]
73
+
74
+ if header.type != SessionType.SESSION_HEADER:
75
+ return []
76
+
77
+ return entries
78
+
79
+ def is_valid_session_file(session_file: Path | str) -> bool:
80
+ """Check if a file is a valid session file by validating its header."""
81
+ try:
82
+ path = Path(session_file)
83
+ if not path.exists():
84
+ return False
85
+
86
+ with path.open("r", encoding="utf-8") as file:
87
+ first_line = file.readline().strip()
88
+
89
+ if not first_line:
90
+ return False
91
+
92
+ SessionHeader.model_validate_json(first_line)
93
+ return True
94
+ except (OSError, ValidationError, ValueError):
95
+ return False
96
+
97
+ def find_most_recent_session(session_dir: Path | str) -> Path | None:
98
+ """Find the most recently modified session file in a directory."""
99
+ session_dir = Path(session_dir)
100
+ if not session_dir.is_dir():
101
+ return None
102
+
103
+ candidate_sessions = [p for p in session_dir.glob("*.jsonl") if is_valid_session_file(p)]
104
+
105
+ if not candidate_sessions:
106
+ return None
107
+
108
+ most_recent = max(candidate_sessions, key=lambda x: x.stat().st_mtime)
109
+ return most_recent
110
+
111
+ def is_message_with_contents(message: AgentMessage) -> bool:
112
+ """Check if a message is an LLM message with user or assistant role and content."""
113
+ if not isinstance(message, LLMMessage):
114
+ return False
115
+ if message.role not in (Role.USER, Role.ASSISTANT):
116
+ return False
117
+ return any(isinstance(c, (TextContent, ImageContent)) for c in message.contents)
118
+
119
+ def get_last_activity_time(entries: list[SessionEntry]) -> float | None:
120
+ """Extract the most recent message timestamp from a list of session entries."""
121
+ last_activity_time = None
122
+
123
+ for entry in entries:
124
+ if not isinstance(entry, MessageEntry):
125
+ continue
126
+
127
+ if not is_message_with_contents(entry.message):
128
+ continue
129
+
130
+ message_timestamp = getattr(entry.message, "timestamp", None)
131
+ if message_timestamp is None:
132
+ timestamp = entry.timestamp
133
+ elif isinstance(message_timestamp, (int, float)):
134
+ timestamp = float(message_timestamp)
135
+ else:
136
+ timestamp = float(message_timestamp.timestamp())
137
+
138
+ last_activity_time = max(last_activity_time or 0.0, timestamp)
139
+
140
+ return last_activity_time
141
+
142
+ def get_session_modified_date(entries: list[SessionEntry], header: SessionHeader | None = None) -> datetime:
143
+ """Get the modified timestamp of a session, based on last activity or header creation time."""
144
+ if last_activity_time := get_last_activity_time(entries=entries):
145
+ return datetime.fromtimestamp(last_activity_time)
146
+
147
+ header = header or entries[0]
148
+ return datetime.fromtimestamp(header.timestamp)
149
+
150
+ def build_session_info(file: Path) -> SessionInfo | None:
151
+ """Parse a session file and extract metadata into a SessionInfo object."""
152
+ content = file.read_text(encoding="utf-8")
153
+
154
+ file_entries: list[SessionFileEntry] = []
155
+ lines = content.strip().splitlines()
156
+ adapter = TypeAdapter(SessionFileEntry)
157
+
158
+ for line in lines:
159
+ if not line.strip():
160
+ continue
161
+ try:
162
+ file_entries.append(adapter.validate_json(line))
163
+ except Exception:
164
+ pass
165
+
166
+ if len(file_entries) == 0:
167
+ return None
168
+
169
+ header: SessionHeader | None = None
170
+ entries: list[SessionEntry] = []
171
+ message_count = 0
172
+ for entry in file_entries:
173
+ if isinstance(entry, SessionHeader):
174
+ header = entry
175
+ else:
176
+ entries.append(entry)
177
+ if isinstance(entry, MessageEntry):
178
+ message_count += 1
179
+
180
+ if header is None:
181
+ return None
182
+
183
+ cwd = header.cwd
184
+ parent_session = header.parent_session
185
+ created = datetime.fromtimestamp(header.timestamp)
186
+ modified = get_session_modified_date(entries, header)
187
+
188
+ return SessionInfo(
189
+ path=file,
190
+ id=header.id,
191
+ cwd=cwd,
192
+ parent_session=parent_session,
193
+ created=created,
194
+ modified=modified,
195
+ message_count=message_count
196
+ )
197
+
198
+ def list_sessions_from_dir(
199
+ dir_path: Path | str,
200
+ on_progress: Callable[[int, int], None] | None = None,
201
+ progress_offset: int = 0,
202
+ progress_total: int | None = None
203
+ ) -> list[SessionInfo]:
204
+ """
205
+ Read all .jsonl session files in a directory and return a list of SessionInfo objects.
206
+ Optionally reports progress through the on_progress callback.
207
+ """
208
+ sessions: list[SessionInfo] = []
209
+ dir_path = Path(dir_path)
210
+
211
+ if not dir_path.exists() or not dir_path.is_dir():
212
+ return sessions
213
+
214
+ try:
215
+ files = list(dir_path.glob("*.jsonl"))
216
+ total = progress_total if progress_total is not None else len(files)
217
+ loaded = 0
218
+
219
+ # We process files sequentially since Python I/O blocking is usually fine here,
220
+ # but could be updated to use ThreadPoolExecutor if concurrency is strictly needed.
221
+ for file in files:
222
+ info = build_session_info(file)
223
+ loaded += 1
224
+ if on_progress:
225
+ on_progress(progress_offset + loaded, total)
226
+
227
+ if info is not None:
228
+ sessions.append(info)
229
+
230
+ except Exception:
231
+ pass # Return what we have on error, or an empty list if early
232
+
233
+ return sessions