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/manager.py
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Callable, List
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from tau.session.types import (
|
|
7
|
+
SessionFileEntry, SessionHeader,
|
|
8
|
+
SessionEntry, LabelEntry, LeafEntry,
|
|
9
|
+
SessionOptions, SessionType, MessageEntry,
|
|
10
|
+
ThinkingLevelChangeEntry, SessionInfoEntry,
|
|
11
|
+
ModelChangeEntry, CustomInfoEntry,
|
|
12
|
+
CustomMessageEntry, CompactionEntry, BranchSummaryEntry,
|
|
13
|
+
SessionContext, SessionInfo,
|
|
14
|
+
SessionTreeNode, MessageMeta,
|
|
15
|
+
)
|
|
16
|
+
from tau.session.utils import (
|
|
17
|
+
create_session_id, generate_timestamp, generate_id, read_session_file,
|
|
18
|
+
is_valid_session_file, find_most_recent_session, is_message_with_contents,
|
|
19
|
+
get_last_activity_time, get_session_modified_date, build_session_info,
|
|
20
|
+
list_sessions_from_dir, get_default_session_dir,
|
|
21
|
+
)
|
|
22
|
+
from tau.settings.paths import get_sessions_dir
|
|
23
|
+
from tau.message.types import (
|
|
24
|
+
AgentMessage, AssistantMessage, CustomMessage,
|
|
25
|
+
ImageContent, TextContent, LLMMessage, Role,
|
|
26
|
+
)
|
|
27
|
+
from tau.inference.types import ThinkingLevel
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SessionManager:
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
cwd: str | Path,
|
|
34
|
+
session_dir: Path | None = None,
|
|
35
|
+
session_file: Path | None = None,
|
|
36
|
+
persist: bool = True,
|
|
37
|
+
):
|
|
38
|
+
self.session_id: str | None = None
|
|
39
|
+
self.cwd = Path(cwd).resolve()
|
|
40
|
+
self.persist = persist
|
|
41
|
+
self.session_dir = (
|
|
42
|
+
Path(session_dir).resolve() if session_dir
|
|
43
|
+
else get_default_session_dir(self.cwd)
|
|
44
|
+
)
|
|
45
|
+
self.session_file = session_file
|
|
46
|
+
self.by_id: dict[str, SessionEntry] = {}
|
|
47
|
+
self.labels_by_id: dict[str, str] = {}
|
|
48
|
+
self.label_timestamps_by_id: dict[str, float] = {}
|
|
49
|
+
self.leaf_id: str | None = None
|
|
50
|
+
self.entries: list[SessionFileEntry] = []
|
|
51
|
+
self.flushed: bool = False
|
|
52
|
+
|
|
53
|
+
if self.persist and not self.session_dir.exists():
|
|
54
|
+
self.session_dir.mkdir(parents=True, exist_ok=True)
|
|
55
|
+
|
|
56
|
+
if self.session_file:
|
|
57
|
+
self.set_session(self.session_file)
|
|
58
|
+
else:
|
|
59
|
+
self.new_session()
|
|
60
|
+
|
|
61
|
+
def set_session(self, session_file: Path):
|
|
62
|
+
"""Load or initialize a session from a file."""
|
|
63
|
+
self.session_file = session_file
|
|
64
|
+
if session_file.exists():
|
|
65
|
+
self.entries = read_session_file(session_file)
|
|
66
|
+
|
|
67
|
+
if not self.entries:
|
|
68
|
+
# File missing or invalid — start fresh in that file location
|
|
69
|
+
session_id = create_session_id()
|
|
70
|
+
header = SessionHeader(
|
|
71
|
+
id=session_id,
|
|
72
|
+
timestamp=generate_timestamp(),
|
|
73
|
+
cwd=self.cwd,
|
|
74
|
+
)
|
|
75
|
+
self.session_id = session_id
|
|
76
|
+
self.entries = [header]
|
|
77
|
+
self.by_id.clear()
|
|
78
|
+
self.labels_by_id.clear()
|
|
79
|
+
self.label_timestamps_by_id.clear()
|
|
80
|
+
self.leaf_id = None
|
|
81
|
+
self.flushed = False
|
|
82
|
+
if self.persist:
|
|
83
|
+
self._rewrite_file()
|
|
84
|
+
self.flushed = True
|
|
85
|
+
else:
|
|
86
|
+
for entry in self.entries:
|
|
87
|
+
if isinstance(entry, SessionHeader):
|
|
88
|
+
self.session_id = entry.id
|
|
89
|
+
break
|
|
90
|
+
self._build_index()
|
|
91
|
+
self.flushed = True
|
|
92
|
+
|
|
93
|
+
def new_session(self, options: SessionOptions | None = None):
|
|
94
|
+
"""Create a new session, optionally with parent session and custom ID."""
|
|
95
|
+
options = options or SessionOptions()
|
|
96
|
+
session_id = options.id or create_session_id()
|
|
97
|
+
parent_session = Path(options.parent_session).resolve() if options.parent_session else None
|
|
98
|
+
header = SessionHeader(
|
|
99
|
+
id=session_id,
|
|
100
|
+
timestamp=generate_timestamp(),
|
|
101
|
+
cwd=self.cwd,
|
|
102
|
+
parent_session=parent_session,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
self.session_id = session_id
|
|
106
|
+
self.entries = [header]
|
|
107
|
+
self.by_id.clear()
|
|
108
|
+
self.labels_by_id.clear()
|
|
109
|
+
self.label_timestamps_by_id.clear()
|
|
110
|
+
self.leaf_id = None
|
|
111
|
+
self.flushed = False
|
|
112
|
+
|
|
113
|
+
if self.persist:
|
|
114
|
+
file_timestamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S-%f")
|
|
115
|
+
self.session_file = (self.session_dir / f"{file_timestamp}_{session_id}.jsonl").resolve()
|
|
116
|
+
|
|
117
|
+
return self.session_file
|
|
118
|
+
|
|
119
|
+
def _rewrite_file(self):
|
|
120
|
+
"""Write all session entries to the session file."""
|
|
121
|
+
if not self.persist or not self.session_file:
|
|
122
|
+
return None
|
|
123
|
+
lines = [entry.model_dump_json(exclude_none=True) for entry in self.entries]
|
|
124
|
+
self.session_file.write_text("\n".join(lines), encoding="utf-8")
|
|
125
|
+
|
|
126
|
+
def _build_index(self):
|
|
127
|
+
"""Rebuild internal indices from loaded entries."""
|
|
128
|
+
self.by_id.clear()
|
|
129
|
+
self.labels_by_id.clear()
|
|
130
|
+
self.label_timestamps_by_id.clear()
|
|
131
|
+
self.leaf_id = None
|
|
132
|
+
|
|
133
|
+
for entry in self.entries:
|
|
134
|
+
if isinstance(entry, SessionHeader):
|
|
135
|
+
continue
|
|
136
|
+
self.by_id[entry.id] = entry
|
|
137
|
+
if isinstance(entry, LeafEntry):
|
|
138
|
+
# LeafEntry records a navigation point — target_id is the new leaf.
|
|
139
|
+
self.leaf_id = entry.target_id
|
|
140
|
+
else:
|
|
141
|
+
self.leaf_id = entry.id
|
|
142
|
+
if isinstance(entry, LabelEntry):
|
|
143
|
+
if entry.label:
|
|
144
|
+
self.labels_by_id[entry.target_id] = entry.label
|
|
145
|
+
self.label_timestamps_by_id[entry.target_id] = entry.timestamp
|
|
146
|
+
else:
|
|
147
|
+
self.labels_by_id.pop(entry.target_id, None)
|
|
148
|
+
self.label_timestamps_by_id.pop(entry.target_id, None)
|
|
149
|
+
|
|
150
|
+
def _persist(self, entry: SessionEntry):
|
|
151
|
+
"""Append an entry to the session file."""
|
|
152
|
+
if not self.persist or not self.session_file:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
has_assistant_message = any(
|
|
156
|
+
isinstance(e, MessageEntry) and isinstance(e.message, AssistantMessage)
|
|
157
|
+
for e in self.entries
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if not has_assistant_message:
|
|
161
|
+
self.flushed = False
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
with self.session_file.open("a", encoding="utf-8") as f:
|
|
165
|
+
if not self.flushed:
|
|
166
|
+
lines = [e.model_dump_json(exclude_none=True) + "\n" for e in self.entries]
|
|
167
|
+
f.writelines(lines)
|
|
168
|
+
self.flushed = True
|
|
169
|
+
else:
|
|
170
|
+
f.write(entry.model_dump_json(exclude_none=True) + "\n")
|
|
171
|
+
|
|
172
|
+
def _append_entry(self, entry: SessionEntry) -> str:
|
|
173
|
+
"""Add an entry to the session and persist it."""
|
|
174
|
+
self.entries.append(entry)
|
|
175
|
+
self.by_id[entry.id] = entry
|
|
176
|
+
self.leaf_id = entry.id
|
|
177
|
+
self._persist(entry)
|
|
178
|
+
return entry.id
|
|
179
|
+
|
|
180
|
+
def append_message(self, message: AgentMessage, meta: MessageMeta | None = None) -> str:
|
|
181
|
+
"""Add a message to the session."""
|
|
182
|
+
entry = MessageEntry(message=message, parent_id=self.leaf_id, meta=meta)
|
|
183
|
+
return self._append_entry(entry)
|
|
184
|
+
|
|
185
|
+
def remove_last_message(self, role: str | None = None) -> bool:
|
|
186
|
+
"""Remove the message entry at the current leaf, if it matches role.
|
|
187
|
+
|
|
188
|
+
Only ever touches the entry at the tip of the *current* branch — never
|
|
189
|
+
reaches into other branches — so this stays correct after navigating
|
|
190
|
+
the tree. Returns True if an entry was removed.
|
|
191
|
+
"""
|
|
192
|
+
entry = self.by_id.get(self.leaf_id) if self.leaf_id is not None else None
|
|
193
|
+
if not isinstance(entry, MessageEntry):
|
|
194
|
+
return False
|
|
195
|
+
if role is not None and getattr(entry.message, "role", None) != role:
|
|
196
|
+
return False
|
|
197
|
+
self.entries.remove(entry)
|
|
198
|
+
self.by_id.pop(entry.id, None)
|
|
199
|
+
self.leaf_id = entry.parent_id
|
|
200
|
+
if self.flushed:
|
|
201
|
+
self._rewrite_file()
|
|
202
|
+
return True
|
|
203
|
+
|
|
204
|
+
def append_thinking_level_change(self, thinking_level: ThinkingLevel) -> str:
|
|
205
|
+
"""Record a change in the thinking level setting."""
|
|
206
|
+
entry = ThinkingLevelChangeEntry(thinking_level=thinking_level, parent_id=self.leaf_id)
|
|
207
|
+
return self._append_entry(entry)
|
|
208
|
+
|
|
209
|
+
def append_model_change(self, model_id: str, provider_id: str) -> str:
|
|
210
|
+
"""Record a model or provider change."""
|
|
211
|
+
entry = ModelChangeEntry(model_id=model_id, provider_id=provider_id, parent_id=self.leaf_id)
|
|
212
|
+
return self._append_entry(entry)
|
|
213
|
+
|
|
214
|
+
def append_label_change(self, target_id: str, label: str | None = None) -> str:
|
|
215
|
+
"""Add or remove a label from an entry."""
|
|
216
|
+
entry = LabelEntry(target_id=target_id, label=label, parent_id=self.leaf_id)
|
|
217
|
+
if label:
|
|
218
|
+
self.labels_by_id[target_id] = label
|
|
219
|
+
self.label_timestamps_by_id[target_id] = entry.timestamp
|
|
220
|
+
else:
|
|
221
|
+
self.labels_by_id.pop(target_id, None)
|
|
222
|
+
self.label_timestamps_by_id.pop(target_id, None)
|
|
223
|
+
return self._append_entry(entry)
|
|
224
|
+
|
|
225
|
+
def append_custom_info(self, custom_type: str, data: Any | None = None) -> str:
|
|
226
|
+
"""Add custom metadata to the session."""
|
|
227
|
+
entry = CustomInfoEntry(custom_type=custom_type, data=data, parent_id=self.leaf_id)
|
|
228
|
+
return self._append_entry(entry)
|
|
229
|
+
|
|
230
|
+
def append_custom_message(
|
|
231
|
+
self,
|
|
232
|
+
custom_type: str,
|
|
233
|
+
content: Any,
|
|
234
|
+
display: bool = True,
|
|
235
|
+
details: Any | None = None,
|
|
236
|
+
) -> str:
|
|
237
|
+
"""Add a custom message to the session."""
|
|
238
|
+
entry = CustomMessageEntry(
|
|
239
|
+
custom_type=custom_type,
|
|
240
|
+
content=content,
|
|
241
|
+
display=display,
|
|
242
|
+
details=details,
|
|
243
|
+
parent_id=self.leaf_id,
|
|
244
|
+
)
|
|
245
|
+
return self._append_entry(entry)
|
|
246
|
+
|
|
247
|
+
def append_branch_summary(
|
|
248
|
+
self,
|
|
249
|
+
from_id: str,
|
|
250
|
+
summary: str,
|
|
251
|
+
details: dict | None = None,
|
|
252
|
+
from_hook: bool = False,
|
|
253
|
+
label: str | None = None,
|
|
254
|
+
) -> str:
|
|
255
|
+
"""Record a summary when abandoning a branch."""
|
|
256
|
+
entry = BranchSummaryEntry(
|
|
257
|
+
from_id=from_id,
|
|
258
|
+
summary=summary,
|
|
259
|
+
details=details,
|
|
260
|
+
from_hook=from_hook,
|
|
261
|
+
label=label,
|
|
262
|
+
parent_id=self.leaf_id,
|
|
263
|
+
)
|
|
264
|
+
return self._append_entry(entry)
|
|
265
|
+
|
|
266
|
+
def branch_with_summary(
|
|
267
|
+
self,
|
|
268
|
+
branch_from_id: str | None,
|
|
269
|
+
summary: str,
|
|
270
|
+
details: dict | None = None,
|
|
271
|
+
from_hook: bool = False,
|
|
272
|
+
) -> str:
|
|
273
|
+
"""Navigate to branch_from_id and append a branch_summary entry capturing the abandoned path."""
|
|
274
|
+
if branch_from_id is not None and branch_from_id not in self.by_id:
|
|
275
|
+
raise KeyError(f"Entry {branch_from_id} not found.")
|
|
276
|
+
self.leaf_id = branch_from_id
|
|
277
|
+
entry = BranchSummaryEntry(
|
|
278
|
+
from_id=branch_from_id or "root",
|
|
279
|
+
summary=summary,
|
|
280
|
+
details=details,
|
|
281
|
+
from_hook=from_hook,
|
|
282
|
+
parent_id=branch_from_id,
|
|
283
|
+
)
|
|
284
|
+
return self._append_entry(entry)
|
|
285
|
+
|
|
286
|
+
def append_compaction(
|
|
287
|
+
self,
|
|
288
|
+
summary: str,
|
|
289
|
+
first_kept_entry_id: str,
|
|
290
|
+
tokens_before: int,
|
|
291
|
+
details: dict | None = None,
|
|
292
|
+
) -> str:
|
|
293
|
+
"""Record a context compaction."""
|
|
294
|
+
entry = CompactionEntry(
|
|
295
|
+
summary=summary,
|
|
296
|
+
first_kept_entry_id=first_kept_entry_id,
|
|
297
|
+
tokens_before=tokens_before,
|
|
298
|
+
details=details,
|
|
299
|
+
parent_id=self.leaf_id,
|
|
300
|
+
)
|
|
301
|
+
return self._append_entry(entry)
|
|
302
|
+
|
|
303
|
+
def append_session_info(self, name: str) -> str:
|
|
304
|
+
"""Set the session name."""
|
|
305
|
+
entry = SessionInfoEntry(name=name, parent_id=self.leaf_id)
|
|
306
|
+
return self._append_entry(entry)
|
|
307
|
+
|
|
308
|
+
def get_session_name(self) -> str | None:
|
|
309
|
+
"""Return the most recent session name, or None if not set."""
|
|
310
|
+
for entry in reversed(self.entries):
|
|
311
|
+
if isinstance(entry, SessionInfoEntry) and entry.name and entry.name.strip():
|
|
312
|
+
return entry.name.strip()
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
def get_leaf_id(self) -> str | None:
|
|
316
|
+
"""Return the ID of the current leaf entry, or None if not set."""
|
|
317
|
+
return self.leaf_id
|
|
318
|
+
|
|
319
|
+
def get_leaf_entry(self) -> SessionEntry | None:
|
|
320
|
+
"""Return the current leaf entry, or None if not found."""
|
|
321
|
+
return self.by_id.get(self.leaf_id) if self.leaf_id else None
|
|
322
|
+
|
|
323
|
+
def get_entry(self, id: str) -> SessionEntry | None:
|
|
324
|
+
"""Retrieve an entry by ID, or None if not found."""
|
|
325
|
+
return self.by_id.get(id)
|
|
326
|
+
|
|
327
|
+
def get_children(self, parent_id: str) -> list[SessionEntry]:
|
|
328
|
+
"""Return all entries with the given parent_id, sorted by timestamp."""
|
|
329
|
+
return sorted(
|
|
330
|
+
[entry for entry in self.get_entries() if entry.parent_id == parent_id],
|
|
331
|
+
key=lambda entry: entry.timestamp,
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
def get_label(self, id: str) -> str | None:
|
|
335
|
+
"""Return the label for an entry, or None if not labeled."""
|
|
336
|
+
return self.labels_by_id.get(id)
|
|
337
|
+
|
|
338
|
+
def get_branch(self, from_id: str | None = None) -> list[SessionEntry]:
|
|
339
|
+
"""Return entries from root to the given id (or leaf_id), in root→leaf order."""
|
|
340
|
+
path: list[SessionEntry] = []
|
|
341
|
+
cursor = from_id or self.leaf_id
|
|
342
|
+
while cursor:
|
|
343
|
+
current_entry = self.by_id.get(cursor)
|
|
344
|
+
if not current_entry:
|
|
345
|
+
break
|
|
346
|
+
path.append(current_entry)
|
|
347
|
+
cursor = current_entry.parent_id
|
|
348
|
+
path.reverse()
|
|
349
|
+
return path
|
|
350
|
+
|
|
351
|
+
def build_session_context(self) -> SessionContext:
|
|
352
|
+
"""Build a context object from the current branch, including messages and settings."""
|
|
353
|
+
from tau.message.types import CompactionSummaryMessage
|
|
354
|
+
|
|
355
|
+
thinking_level: ThinkingLevel = ThinkingLevel.Off
|
|
356
|
+
model_id: str | None = None
|
|
357
|
+
provider_id: str | None = None
|
|
358
|
+
messages: list[AgentMessage] = []
|
|
359
|
+
|
|
360
|
+
entries = self.get_branch()
|
|
361
|
+
|
|
362
|
+
if not entries:
|
|
363
|
+
return SessionContext(
|
|
364
|
+
messages=messages,
|
|
365
|
+
thinking_level=thinking_level,
|
|
366
|
+
model_id=model_id,
|
|
367
|
+
provider_id=provider_id,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Scan all entries for model/thinking-level changes
|
|
371
|
+
for entry in entries:
|
|
372
|
+
match entry:
|
|
373
|
+
case ThinkingLevelChangeEntry():
|
|
374
|
+
thinking_level = entry.thinking_level
|
|
375
|
+
case ModelChangeEntry():
|
|
376
|
+
model_id = entry.model_id
|
|
377
|
+
provider_id = entry.provider_id
|
|
378
|
+
|
|
379
|
+
# Drop history before the most recent compaction
|
|
380
|
+
last_compaction: CompactionEntry | None = None
|
|
381
|
+
for entry in entries:
|
|
382
|
+
if isinstance(entry, CompactionEntry):
|
|
383
|
+
last_compaction = entry
|
|
384
|
+
|
|
385
|
+
if last_compaction is not None:
|
|
386
|
+
first_kept_idx = next(
|
|
387
|
+
(i for i, e in enumerate(entries) if e.id == last_compaction.first_kept_entry_id),
|
|
388
|
+
len(entries),
|
|
389
|
+
)
|
|
390
|
+
slice_entries = entries[first_kept_idx:]
|
|
391
|
+
else:
|
|
392
|
+
slice_entries = entries
|
|
393
|
+
|
|
394
|
+
for entry in slice_entries:
|
|
395
|
+
match entry:
|
|
396
|
+
case MessageEntry():
|
|
397
|
+
messages.append(entry.message)
|
|
398
|
+
case CustomMessageEntry():
|
|
399
|
+
messages.append(CustomMessage.from_session(entry=entry))
|
|
400
|
+
case BranchSummaryEntry():
|
|
401
|
+
from tau.message.types import BranchSummaryMessage
|
|
402
|
+
messages.append(BranchSummaryMessage(
|
|
403
|
+
summary=entry.summary,
|
|
404
|
+
from_id=entry.from_id,
|
|
405
|
+
timestamp=entry.timestamp,
|
|
406
|
+
))
|
|
407
|
+
case CompactionEntry():
|
|
408
|
+
messages.insert(0, CompactionSummaryMessage(
|
|
409
|
+
summary=entry.summary,
|
|
410
|
+
tokens_before=entry.tokens_before,
|
|
411
|
+
timestamp=entry.timestamp,
|
|
412
|
+
))
|
|
413
|
+
|
|
414
|
+
return SessionContext(
|
|
415
|
+
messages=messages,
|
|
416
|
+
thinking_level=thinking_level,
|
|
417
|
+
model_id=model_id,
|
|
418
|
+
provider_id=provider_id,
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
def get_header(self) -> SessionHeader | None:
|
|
422
|
+
"""Return the session header entry, or None if not found."""
|
|
423
|
+
for entry in self.entries:
|
|
424
|
+
if isinstance(entry, SessionHeader):
|
|
425
|
+
return entry
|
|
426
|
+
return None
|
|
427
|
+
|
|
428
|
+
def get_entries(self) -> list[SessionEntry]:
|
|
429
|
+
"""Return all non-header entries in the session."""
|
|
430
|
+
return [entry for entry in self.entries if not isinstance(entry, SessionHeader)]
|
|
431
|
+
|
|
432
|
+
def get_tree(self) -> list[SessionTreeNode]:
|
|
433
|
+
"""Build a hierarchical tree structure of all entries."""
|
|
434
|
+
node_map: dict[str, SessionTreeNode] = {}
|
|
435
|
+
roots: list[SessionTreeNode] = []
|
|
436
|
+
|
|
437
|
+
for entry in self.get_entries():
|
|
438
|
+
label = self.labels_by_id.get(entry.id)
|
|
439
|
+
label_timestamp = self.label_timestamps_by_id.get(entry.id)
|
|
440
|
+
node_map[entry.id] = SessionTreeNode(
|
|
441
|
+
entry=entry,
|
|
442
|
+
children=[],
|
|
443
|
+
label_timestamp=label_timestamp,
|
|
444
|
+
label=label,
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
for entry in self.get_entries():
|
|
448
|
+
node = node_map[entry.id]
|
|
449
|
+
if entry.parent_id is None or entry.parent_id == entry.id:
|
|
450
|
+
roots.append(node)
|
|
451
|
+
else:
|
|
452
|
+
parent_node = node_map.get(entry.parent_id)
|
|
453
|
+
if parent_node is None:
|
|
454
|
+
roots.append(node)
|
|
455
|
+
else:
|
|
456
|
+
parent_node.children.append(node)
|
|
457
|
+
|
|
458
|
+
stack = roots.copy()
|
|
459
|
+
while stack:
|
|
460
|
+
node = stack.pop()
|
|
461
|
+
node.children.sort(key=lambda child: child.entry.timestamp)
|
|
462
|
+
stack.extend(node.children)
|
|
463
|
+
|
|
464
|
+
roots.sort(key=lambda node: node.entry.timestamp)
|
|
465
|
+
return roots
|
|
466
|
+
|
|
467
|
+
def branch(self, from_id: str):
|
|
468
|
+
"""Navigate to a given entry and record the navigation point."""
|
|
469
|
+
if from_id not in self.by_id:
|
|
470
|
+
raise KeyError(f"Entry {from_id} not found.")
|
|
471
|
+
# Persist a LeafEntry so the navigation point survives restarts.
|
|
472
|
+
leaf_entry = LeafEntry(parent_id=self.leaf_id, target_id=from_id)
|
|
473
|
+
self.entries.append(leaf_entry)
|
|
474
|
+
self.by_id[leaf_entry.id] = leaf_entry
|
|
475
|
+
self._persist(leaf_entry)
|
|
476
|
+
self.leaf_id = from_id
|
|
477
|
+
|
|
478
|
+
def reset_leaf(self):
|
|
479
|
+
"""Clear the leaf pointer."""
|
|
480
|
+
self.leaf_id = None
|
|
481
|
+
|
|
482
|
+
def create_branched_session(self, leaf_id: str) -> Path | None:
|
|
483
|
+
"""Create a new session file forking from the given entry."""
|
|
484
|
+
previous_session_file = self.session_file
|
|
485
|
+
path = self.get_branch(leaf_id)
|
|
486
|
+
|
|
487
|
+
if not path:
|
|
488
|
+
raise ValueError(f"Entry {leaf_id} not found.")
|
|
489
|
+
|
|
490
|
+
path_without_labels = [entry for entry in path if not isinstance(entry, LabelEntry)]
|
|
491
|
+
|
|
492
|
+
session_id = create_session_id()
|
|
493
|
+
file_timestamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S-%f")
|
|
494
|
+
new_session_file = self.session_dir / f"{file_timestamp}_{session_id}.jsonl"
|
|
495
|
+
|
|
496
|
+
header = SessionHeader(
|
|
497
|
+
id=session_id,
|
|
498
|
+
timestamp=generate_timestamp(),
|
|
499
|
+
cwd=self.cwd,
|
|
500
|
+
parent_session=previous_session_file if self.persist else None,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
path_entry_ids = {entry.id for entry in path_without_labels}
|
|
504
|
+
labels_to_write: list[tuple[str, str, float]] = [
|
|
505
|
+
(target_id, label, self.label_timestamps_by_id[target_id])
|
|
506
|
+
for target_id, label in self.labels_by_id.items()
|
|
507
|
+
if target_id in path_entry_ids
|
|
508
|
+
]
|
|
509
|
+
|
|
510
|
+
label_entries: list[LabelEntry] = []
|
|
511
|
+
last_entry = path_without_labels[-1] if path_without_labels else None
|
|
512
|
+
parent_id = last_entry.id if last_entry else None
|
|
513
|
+
used_ids = set(path_entry_ids)
|
|
514
|
+
|
|
515
|
+
for (target_id, label, label_timestamp) in labels_to_write:
|
|
516
|
+
label_entry = LabelEntry(
|
|
517
|
+
id=generate_id(used_ids),
|
|
518
|
+
parent_id=parent_id,
|
|
519
|
+
timestamp=label_timestamp,
|
|
520
|
+
target_id=target_id,
|
|
521
|
+
label=label,
|
|
522
|
+
)
|
|
523
|
+
used_ids.add(label_entry.id)
|
|
524
|
+
label_entries.append(label_entry)
|
|
525
|
+
parent_id = label_entry.id
|
|
526
|
+
|
|
527
|
+
self.entries = [header, *path_without_labels, *label_entries]
|
|
528
|
+
self.session_id = session_id
|
|
529
|
+
self.session_file = new_session_file if self.persist else None
|
|
530
|
+
self._build_index()
|
|
531
|
+
|
|
532
|
+
has_assistant = any(
|
|
533
|
+
isinstance(entry, MessageEntry) and isinstance(entry.message, AssistantMessage)
|
|
534
|
+
for entry in self.entries
|
|
535
|
+
)
|
|
536
|
+
|
|
537
|
+
if self.persist:
|
|
538
|
+
if has_assistant:
|
|
539
|
+
self._rewrite_file()
|
|
540
|
+
self.flushed = True
|
|
541
|
+
else:
|
|
542
|
+
self.flushed = False
|
|
543
|
+
return new_session_file
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
@classmethod
|
|
547
|
+
def create(cls, cwd: Path | str, session_dir: Path | str | None = None) -> SessionManager:
|
|
548
|
+
"""Create a new SessionManager with a fresh session."""
|
|
549
|
+
cwd = Path(cwd).resolve()
|
|
550
|
+
session_dir = Path(session_dir).resolve() if session_dir else get_default_session_dir(cwd)
|
|
551
|
+
return SessionManager(cwd, session_dir)
|
|
552
|
+
|
|
553
|
+
@staticmethod
|
|
554
|
+
def open(
|
|
555
|
+
path: Path | str,
|
|
556
|
+
session_dir: Path | str | None = None,
|
|
557
|
+
cwd_override: Path | str | None = None,
|
|
558
|
+
) -> SessionManager:
|
|
559
|
+
"""Load an existing session from a file."""
|
|
560
|
+
path = Path(path).resolve()
|
|
561
|
+
entries = read_session_file(path)
|
|
562
|
+
header = next((e for e in entries if isinstance(e, SessionHeader)), None)
|
|
563
|
+
if header is None:
|
|
564
|
+
raise ValueError(f"No header found in session file: {path}")
|
|
565
|
+
cwd = Path(cwd_override).resolve() if cwd_override else Path(header.cwd).resolve()
|
|
566
|
+
session_dir = Path(session_dir).resolve() if session_dir else path.parent
|
|
567
|
+
return SessionManager(cwd, session_dir, path)
|
|
568
|
+
|
|
569
|
+
@staticmethod
|
|
570
|
+
def continue_recent(cwd: Path | str, session_dir: Path | str | None = None) -> SessionManager:
|
|
571
|
+
"""Load the most recent session, or create a new one if none exist."""
|
|
572
|
+
cwd = Path(cwd).resolve()
|
|
573
|
+
session_dir = Path(session_dir).resolve() if session_dir else get_default_session_dir(cwd)
|
|
574
|
+
most_recent = find_most_recent_session(session_dir)
|
|
575
|
+
if most_recent:
|
|
576
|
+
return SessionManager(cwd, session_dir, most_recent)
|
|
577
|
+
return SessionManager(cwd, session_dir)
|
|
578
|
+
|
|
579
|
+
@staticmethod
|
|
580
|
+
def in_memory(cwd: Path | None = None) -> SessionManager:
|
|
581
|
+
"""Create an in-memory session that is not persisted to disk."""
|
|
582
|
+
cwd = cwd or Path.cwd()
|
|
583
|
+
return SessionManager(cwd, None, None, False)
|
|
584
|
+
|
|
585
|
+
@staticmethod
|
|
586
|
+
def fork_from(
|
|
587
|
+
source: Path | str,
|
|
588
|
+
target_cwd: Path | str,
|
|
589
|
+
session_dir: Path | str | None = None,
|
|
590
|
+
) -> SessionManager:
|
|
591
|
+
"""Create a new session forking from an existing session file."""
|
|
592
|
+
source = Path(source).resolve()
|
|
593
|
+
target_cwd = Path(target_cwd).resolve()
|
|
594
|
+
source_entries = read_session_file(source)
|
|
595
|
+
|
|
596
|
+
if not source_entries:
|
|
597
|
+
raise ValueError(f"Cannot fork: source session file is empty or invalid: {source}")
|
|
598
|
+
if not isinstance(source_entries[0], SessionHeader):
|
|
599
|
+
raise ValueError(f"Cannot fork: source session has no header: {source}")
|
|
600
|
+
|
|
601
|
+
session_dir = Path(session_dir).resolve() if session_dir else get_default_session_dir(target_cwd)
|
|
602
|
+
session_dir.mkdir(parents=True, exist_ok=True)
|
|
603
|
+
|
|
604
|
+
new_session_id = create_session_id()
|
|
605
|
+
file_timestamp = datetime.now().strftime("%Y-%m-%dT%H-%M-%S-%f")
|
|
606
|
+
new_session_file = session_dir / f"{file_timestamp}_{new_session_id}.jsonl"
|
|
607
|
+
|
|
608
|
+
new_header = SessionHeader(
|
|
609
|
+
id=new_session_id,
|
|
610
|
+
timestamp=generate_timestamp(),
|
|
611
|
+
cwd=target_cwd,
|
|
612
|
+
parent_session=source,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
with new_session_file.open("w", encoding="utf-8") as f:
|
|
616
|
+
f.write(new_header.model_dump_json() + "\n")
|
|
617
|
+
for entry in source_entries:
|
|
618
|
+
if isinstance(entry, SessionHeader):
|
|
619
|
+
continue
|
|
620
|
+
f.write(entry.model_dump_json() + "\n")
|
|
621
|
+
|
|
622
|
+
return SessionManager(target_cwd, session_dir, new_session_file)
|
|
623
|
+
|
|
624
|
+
@staticmethod
|
|
625
|
+
def list(
|
|
626
|
+
cwd: Path | str,
|
|
627
|
+
session_dir: Path | str | None = None,
|
|
628
|
+
on_progress: Callable[[int, int], None] | None = None,
|
|
629
|
+
) -> list[SessionInfo]:
|
|
630
|
+
cwd = Path(cwd).resolve()
|
|
631
|
+
session_dir = Path(session_dir).resolve() if session_dir else get_default_session_dir(cwd)
|
|
632
|
+
sessions = list_sessions_from_dir(session_dir, on_progress=on_progress)
|
|
633
|
+
sessions.sort(key=lambda s: s.modified.timestamp(), reverse=True)
|
|
634
|
+
return sessions
|
|
635
|
+
|
|
636
|
+
@staticmethod
|
|
637
|
+
def list_all(on_progress: Callable[[int, int], None] | None = None) -> List[SessionInfo]:
|
|
638
|
+
sessions_dir = get_sessions_dir()
|
|
639
|
+
if not sessions_dir.exists():
|
|
640
|
+
return []
|
|
641
|
+
|
|
642
|
+
sessions: list[SessionInfo] = []
|
|
643
|
+
try:
|
|
644
|
+
for cwd_dir in sessions_dir.iterdir():
|
|
645
|
+
if cwd_dir.is_dir():
|
|
646
|
+
dir_sessions = list_sessions_from_dir(cwd_dir, on_progress=on_progress)
|
|
647
|
+
sessions.extend(dir_sessions)
|
|
648
|
+
except Exception:
|
|
649
|
+
pass
|
|
650
|
+
|
|
651
|
+
sessions.sort(key=lambda s: s.modified.timestamp(), reverse=True)
|
|
652
|
+
return sessions
|