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.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- 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
|