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/runtime/types.py ADDED
@@ -0,0 +1,303 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from tau.agent.service import Agent
10
+ from tau.agent.types import AgentConfig
11
+ from tau.builtins.tools import TOOLS
12
+ from tau.engine.service import Engine, EngineOptions
13
+ from tau.extensions.api import _RuntimeRef, LoadExtensionsResult
14
+ from tau.extensions.events import EventBus
15
+ from tau.extensions.loader import ExtensionLoader
16
+ from tau.extensions.runtime import ExtensionRuntime
17
+ from tau.hooks.service import Hooks
18
+ from tau.inference.api.text.service import TextLLM as LLM
19
+ from tau.agent.prompt.builder import build_prompt
20
+ from tau.agent.prompt.types import PromptOptions
21
+ from tau.session.manager import SessionManager
22
+ from tau.settings.manager import SettingsManager
23
+ from tau.settings.paths import get_config_dir, get_extensions_dir
24
+ from tau.tool.types import Tool
25
+ from tau.tool.registry import ToolRegistry
26
+
27
+
28
+ def _is_project_package(settings_manager: SettingsManager, name: str) -> bool:
29
+ """Return True if the package is project-scoped (in project settings)."""
30
+ project_pkgs = settings_manager.get_packages(local=True)
31
+ return any(p.name == name for p in project_pkgs)
32
+
33
+
34
+ class RuntimeConfig(BaseModel):
35
+ """Immutable configuration snapshot passed to RuntimeContext.create()."""
36
+ model_config = {'arbitrary_types_allowed': True}
37
+
38
+ cwd: Path
39
+ config_dir: Path | None = None
40
+
41
+ # LLM
42
+ model_id: str | None = None
43
+ provider: str | None = None
44
+
45
+ # Session
46
+ session_file: Path | None = None
47
+ persist_session: bool = True
48
+ resume: bool = False
49
+
50
+ # Run mode
51
+ mode: str = "interactive"
52
+
53
+ # Tools & prompt
54
+ tools: list[Tool] = Field(default_factory=list)
55
+ system_prompt: str = ""
56
+ disable_context_files: bool = False
57
+
58
+ # Trust
59
+ project_trusted: bool | None = None # None = auto-detect from trust store
60
+
61
+
62
+ class RuntimeContext:
63
+ """
64
+ Constructs and owns all dependencies for one Agent session.
65
+
66
+ Usage:
67
+ ctx = await RuntimeContext.create(config)
68
+ agent = ctx.agent
69
+ await agent.invoke("hello")
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ agent: Agent,
75
+ llm: LLM,
76
+ engine: Engine,
77
+ session_manager: SessionManager,
78
+ settings_manager: SettingsManager | None = None,
79
+ hooks: Hooks | None = None,
80
+ ext_runtime: ExtensionRuntime | None = None,
81
+ tool_registry: ToolRegistry | None = None,
82
+ ) -> None:
83
+ self.agent = agent
84
+ self.llm = llm
85
+ self.engine = engine
86
+ self.session_manager = session_manager
87
+ self.settings_manager = settings_manager
88
+ self.hooks: Hooks = hooks or agent.hooks
89
+ self.ext_runtime: ExtensionRuntime | None = ext_runtime
90
+ self.tool_registry: ToolRegistry = tool_registry or ToolRegistry()
91
+
92
+ @classmethod
93
+ async def create(
94
+ cls,
95
+ config: RuntimeConfig,
96
+ settings_manager: SettingsManager | None = None,
97
+ hooks: Hooks | None = None,
98
+ ext_runtime: ExtensionRuntime | None = None,
99
+ ) -> RuntimeContext:
100
+ """Bootstrap every dependency from config and return a fully wired context."""
101
+ cwd = config.cwd.resolve()
102
+ config_dir = (config.config_dir or get_config_dir()).resolve()
103
+
104
+ # Determine project trust status (needed for context file loading)
105
+ project_trusted: bool
106
+ if config.project_trusted is not None:
107
+ project_trusted = config.project_trusted
108
+ else:
109
+ # Will be determined below based on trust settings
110
+ project_trusted = False
111
+
112
+ # ── Settings ──────────────────────────────────────────────────────────
113
+ if settings_manager is None:
114
+ from tau.trust.manager import has_project_trust_inputs, trust_store
115
+ if not has_project_trust_inputs(cwd):
116
+ project_trusted = True
117
+ elif config.project_trusted is not None:
118
+ project_trusted = config.project_trusted
119
+ else:
120
+ # Load global settings first to read project_trust policy
121
+ _global_sm = SettingsManager.create(cwd, config_dir=config_dir, project_trusted=False)
122
+ policy = _global_sm.get_project_trust()
123
+ if policy == "always":
124
+ project_trusted = True
125
+ elif policy == "never":
126
+ project_trusted = False
127
+ else:
128
+ stored = trust_store.get(cwd)
129
+ project_trusted = stored if stored is not None else False
130
+ settings_manager = SettingsManager.create(cwd, config_dir=config_dir, project_trusted=project_trusted)
131
+
132
+ # ── LLM ───────────────────────────────────────────────────────────────
133
+ _DEFAULT_MODEL = "claude-sonnet-4-6"
134
+ model_id = config.model_id or settings_manager.get_model() or _DEFAULT_MODEL
135
+ if config.model_id is not None:
136
+ provider = config.provider
137
+ else:
138
+ provider = config.provider or settings_manager.get_provider()
139
+ llm = LLM(model_id=model_id, provider=provider)
140
+ from datetime import timedelta
141
+ llm.api.options.timeout = timedelta(milliseconds=settings_manager.get_http_idle_timeout_ms())
142
+ if settings_manager.is_retry_enabled():
143
+ llm.api.options.max_retries = settings_manager.get_retry_max_retries()
144
+ llm.api.options.retry_base_delay_ms = settings_manager.get_retry_base_delay_ms()
145
+ else:
146
+ llm.api.options.max_retries = 0
147
+ if llm.model.thinking:
148
+ llm.api.options.thinking_level = settings_manager.get_thinking_level() or llm.model.default_thinking_level
149
+
150
+ # ── Session manager ───────────────────────────────────────────────────
151
+ if config.resume and not config.session_file and config.persist_session:
152
+ session_manager = SessionManager.continue_recent(cwd)
153
+ else:
154
+ session_manager = SessionManager(
155
+ cwd=cwd,
156
+ session_file=config.session_file,
157
+ persist=config.persist_session,
158
+ )
159
+
160
+ # ── Shared hook bus ───────────────────────────────────────────────────
161
+ hooks = hooks or Hooks()
162
+
163
+ # ── Extensions ────────────────────────────────────────────────────────
164
+ # Only load on first session; on session switch the caller passes ext_runtime.
165
+ base_tools: list[Tool] = list(TOOLS) + list(config.tools)
166
+
167
+ if ext_runtime is None:
168
+ from tau.settings.paths import get_builtins_dir
169
+ from tau.hooks.runtime import RuntimeStartEvent
170
+ builtins_ext_dir = get_builtins_dir() / "extensions"
171
+ runtime_ref = _RuntimeRef()
172
+
173
+ # Earliest lifecycle signal — hooks bus exists, nothing loaded yet.
174
+ # Reaches core/manual subscribers only (no extensions registered yet);
175
+ # opens the runtime_start → runtime_ready → runtime_stop bracket.
176
+ await hooks.emit(RuntimeStartEvent())
177
+
178
+ if settings_manager.is_extensions_enabled():
179
+ project_ext_dir = get_extensions_dir(cwd)
180
+ global_ext_dir = get_extensions_dir()
181
+ entries = settings_manager.get_extension_list()
182
+ disabled_stems = {Path(e.path).stem for e in entries if not e.enabled}
183
+ entry_configs = {
184
+ Path(e.path).stem: (e.settings or {})
185
+ for e in entries if e.enabled
186
+ }
187
+ extra_entries = [e for e in entries if e.enabled]
188
+ extra_sources: dict[str, str] = {}
189
+
190
+ # Discover extension files contributed by installed packages
191
+ from tau.packages.manager import PackageManager
192
+ from tau.settings.paths import get_packages_venv
193
+ from tau.settings.types import ExtensionEntry as _ExtEntry
194
+ pkg_entries = settings_manager.get_all_packages()
195
+ if pkg_entries:
196
+ for _scope_local in (False, True):
197
+ _venv_dir = get_packages_venv(cwd if _scope_local else None)
198
+ _pkg_mgr = PackageManager(_venv_dir)
199
+ _site = _pkg_mgr.site_packages()
200
+ if _site and str(_site) not in sys.path:
201
+ sys.path.insert(0, str(_site))
202
+ for pkg in pkg_entries:
203
+ if not pkg.enabled:
204
+ continue
205
+ _venv_dir = get_packages_venv(
206
+ cwd if _is_project_package(settings_manager, pkg.name) else None
207
+ )
208
+ _pkg_mgr = PackageManager(_venv_dir)
209
+ for ext_file in _pkg_mgr.find_extension_files(pkg.name, pkg.installed_path):
210
+ extra_entries.append(_ExtEntry(path=str(ext_file), name=pkg.name))
211
+ extra_sources[str(ext_file)] = "package"
212
+ loader = ExtensionLoader(
213
+ builtins_dir=builtins_ext_dir,
214
+ project_dir=project_ext_dir,
215
+ global_dir=global_ext_dir,
216
+ extra_entries=extra_entries,
217
+ extra_sources=extra_sources,
218
+ disabled_stems=disabled_stems,
219
+ entry_configs=entry_configs,
220
+ llm=llm,
221
+ settings=settings_manager,
222
+ cwd=cwd,
223
+ runtime_ref=runtime_ref,
224
+ events=EventBus(),
225
+ )
226
+ else:
227
+ # Extensions disabled — load builtins only.
228
+ loader = ExtensionLoader(
229
+ builtins_dir=builtins_ext_dir,
230
+ llm=llm,
231
+ settings=settings_manager,
232
+ cwd=cwd,
233
+ runtime_ref=runtime_ref,
234
+ events=EventBus(),
235
+ )
236
+ load_result = await loader.load()
237
+ ext_runtime = ExtensionRuntime(load_result, hooks, runtime_ref)
238
+
239
+ assert ext_runtime is not None
240
+ # Collect tools and prompt appends contributed by extensions
241
+ extra_appends = ext_runtime.get_prompt_appends()
242
+
243
+ # ── Tool registry ─────────────────────────────────────────────────────
244
+ tool_registry = ToolRegistry()
245
+ for tool in base_tools:
246
+ source = "builtin" if tool in list(TOOLS) else "runtime"
247
+ tool_registry.register(tool, source=source)
248
+ for tool in ext_runtime.get_tools():
249
+ tool_registry.register(tool, source="extension")
250
+
251
+ all_tools: list[Tool] = tool_registry.list()
252
+
253
+ # ── Engine ────────────────────────────────────────────────────────────
254
+ engine = Engine(
255
+ cwd=cwd,
256
+ llm=llm,
257
+ tools=all_tools,
258
+ options=EngineOptions(),
259
+ hooks=hooks,
260
+ settings=settings_manager,
261
+ )
262
+
263
+ # ── Skills ────────────────────────────────────────────────────────────
264
+ from tau.skills.registry import skill_registry
265
+ skill_registry.load_external(cwd=cwd)
266
+ skills = skill_registry.list()
267
+
268
+ # ── System prompt ─────────────────────────────────────────────────────
269
+ system_prompt = config.system_prompt or build_prompt(PromptOptions(
270
+ cwd=cwd,
271
+ tools=all_tools,
272
+ extra_appends=extra_appends,
273
+ skills=skills,
274
+ disable_context_files=config.disable_context_files,
275
+ project_trusted=project_trusted,
276
+ ))
277
+
278
+ # ── Agent config ──────────────────────────────────────────────────────
279
+ agent_config = AgentConfig(
280
+ cwd=cwd,
281
+ system_prompt=system_prompt,
282
+ model=llm.model,
283
+ context_window=llm.model.context_window or 200_000,
284
+ )
285
+
286
+ # ── Agent ─────────────────────────────────────────────────────────────
287
+ agent = Agent(
288
+ engine=engine,
289
+ session_manager=session_manager,
290
+ config=agent_config,
291
+ hooks=hooks,
292
+ )
293
+
294
+ return cls(
295
+ agent=agent,
296
+ llm=llm,
297
+ engine=engine,
298
+ session_manager=session_manager,
299
+ settings_manager=settings_manager,
300
+ hooks=hooks,
301
+ ext_runtime=ext_runtime,
302
+ tool_registry=tool_registry,
303
+ )
@@ -0,0 +1,312 @@
1
+ """
2
+ Branch summarization for tree navigation.
3
+
4
+ When navigating to a different point in the session tree, this generates
5
+ a summary of the branch being left so context isn't lost.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ if TYPE_CHECKING:
13
+ from tau.inference.api.text.service import TextLLM
14
+ from tau.session.manager import SessionManager
15
+ from tau.session.types import SessionEntry
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Types
20
+ # ---------------------------------------------------------------------------
21
+
22
+ @dataclass
23
+ class BranchSummaryResult:
24
+ summary: str | None = None
25
+ read_files: list[str] = field(default_factory=list)
26
+ modified_files: list[str] = field(default_factory=list)
27
+ aborted: bool = False
28
+ error: str | None = None
29
+
30
+
31
+ @dataclass
32
+ class BranchSummaryDetails:
33
+ """Stored in BranchSummaryEntry.details for cumulative file tracking."""
34
+ read_files: list[str] = field(default_factory=list)
35
+ modified_files: list[str] = field(default_factory=list)
36
+
37
+
38
+ @dataclass
39
+ class FileOperations:
40
+ read: set[str] = field(default_factory=set)
41
+ written: set[str] = field(default_factory=set)
42
+ edited: set[str] = field(default_factory=set)
43
+
44
+
45
+ @dataclass
46
+ class BranchPreparation:
47
+ messages: list
48
+ file_ops: FileOperations
49
+ total_tokens: int
50
+
51
+
52
+ @dataclass
53
+ class CollectEntriesResult:
54
+ entries: list[SessionEntry]
55
+ common_ancestor_id: str | None
56
+
57
+
58
+ # ---------------------------------------------------------------------------
59
+ # Entry collection
60
+ # ---------------------------------------------------------------------------
61
+
62
+ def collect_entries_for_branch_summary(
63
+ session: SessionManager,
64
+ old_leaf_id: str | None,
65
+ target_id: str,
66
+ ) -> CollectEntriesResult:
67
+ """Collect entries that should be summarized when navigating from old_leaf_id to target_id."""
68
+ if not old_leaf_id:
69
+ return CollectEntriesResult(entries=[], common_ancestor_id=None)
70
+
71
+ old_path = {e.id for e in session.get_branch(old_leaf_id)}
72
+ target_path = session.get_branch(target_id)
73
+
74
+ common_ancestor_id: str | None = None
75
+ for entry in reversed(target_path):
76
+ if entry.id in old_path:
77
+ common_ancestor_id = entry.id
78
+ break
79
+
80
+ entries: list[SessionEntry] = []
81
+ current: str | None = old_leaf_id
82
+ while current and current != common_ancestor_id:
83
+ entry = session.get_entry(current)
84
+ if not entry:
85
+ break
86
+ entries.append(entry)
87
+ current = entry.parent_id
88
+
89
+ entries.reverse()
90
+ return CollectEntriesResult(entries=entries, common_ancestor_id=common_ancestor_id)
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # Entry → message conversion
95
+ # ---------------------------------------------------------------------------
96
+
97
+ def _get_message_from_entry(entry: Any) -> Any | None:
98
+ from tau.session.types import (
99
+ MessageEntry, CustomMessageEntry, BranchSummaryEntry, CompactionEntry,
100
+ ThinkingLevelChangeEntry, ModelChangeEntry, CustomInfoEntry,
101
+ LabelEntry, LeafEntry, SessionInfoEntry,
102
+ )
103
+ from tau.message.types import CustomMessage, BranchSummaryMessage, CompactionSummaryMessage
104
+
105
+ if isinstance(entry, MessageEntry):
106
+ from tau.message.types import ToolMessage
107
+ if isinstance(entry.message, ToolMessage):
108
+ return None
109
+ return entry.message
110
+ if isinstance(entry, CustomMessageEntry):
111
+ return CustomMessage.from_session(entry=entry)
112
+ if isinstance(entry, BranchSummaryEntry):
113
+ return BranchSummaryMessage(
114
+ summary=entry.summary,
115
+ from_id=entry.from_id,
116
+ timestamp=entry.timestamp,
117
+ )
118
+ if isinstance(entry, CompactionEntry):
119
+ return CompactionSummaryMessage(
120
+ summary=entry.summary,
121
+ tokens_before=entry.tokens_before,
122
+ timestamp=entry.timestamp,
123
+ )
124
+ return None
125
+
126
+
127
+ def _extract_file_ops_from_message(message: Any, file_ops: FileOperations) -> None:
128
+ from tau.message.types import AssistantMessage, ToolCallContent
129
+
130
+ if not isinstance(message, AssistantMessage):
131
+ return
132
+ for c in message.contents:
133
+ if not isinstance(c, ToolCallContent):
134
+ continue
135
+ path = c.args.get("path") if isinstance(c.args, dict) else None
136
+ if not isinstance(path, str):
137
+ continue
138
+ if c.name == "read":
139
+ file_ops.read.add(path)
140
+ elif c.name == "write":
141
+ file_ops.written.add(path)
142
+ elif c.name == "edit":
143
+ file_ops.edited.add(path)
144
+
145
+
146
+ # ---------------------------------------------------------------------------
147
+ # Preparation
148
+ # ---------------------------------------------------------------------------
149
+
150
+ def prepare_branch_entries(entries: list[Any], token_budget: int = 0) -> BranchPreparation:
151
+ """
152
+ Walk entries from newest to oldest, adding messages until the token budget is hit.
153
+ File ops are collected from ALL entries regardless of token budget.
154
+ """
155
+ from tau.session.compaction import estimate_tokens
156
+ from tau.session.types import BranchSummaryEntry
157
+
158
+ messages: list = []
159
+ file_ops = FileOperations()
160
+ total_tokens = 0
161
+
162
+ # First pass: collect file ops from existing branch summaries
163
+ for entry in entries:
164
+ if isinstance(entry, BranchSummaryEntry) and not entry.from_hook and entry.details:
165
+ details = entry.details
166
+ if isinstance(details, dict):
167
+ for f in details.get("read_files", []):
168
+ file_ops.read.add(f)
169
+ for f in details.get("modified_files", []):
170
+ file_ops.edited.add(f)
171
+
172
+ # Second pass: walk newest→oldest, collect messages within budget
173
+ for entry in reversed(entries):
174
+ message = _get_message_from_entry(entry)
175
+ if not message:
176
+ continue
177
+
178
+ _extract_file_ops_from_message(message, file_ops)
179
+ tokens = estimate_tokens(message)
180
+
181
+ if token_budget > 0 and total_tokens + tokens > token_budget:
182
+ from tau.session.types import CompactionEntry
183
+ if isinstance(entry, (CompactionEntry, BranchSummaryEntry)):
184
+ if total_tokens < token_budget * 0.9:
185
+ messages.insert(0, message)
186
+ total_tokens += tokens
187
+ break
188
+
189
+ messages.insert(0, message)
190
+ total_tokens += tokens
191
+
192
+ return BranchPreparation(messages=messages, file_ops=file_ops, total_tokens=total_tokens)
193
+
194
+
195
+ # ---------------------------------------------------------------------------
196
+ # File list formatting
197
+ # ---------------------------------------------------------------------------
198
+
199
+ def _compute_file_lists(file_ops: FileOperations) -> tuple[list[str], list[str]]:
200
+ modified = file_ops.edited | file_ops.written
201
+ read_only = sorted(f for f in file_ops.read if f not in modified)
202
+ modified_files = sorted(modified)
203
+ return read_only, modified_files
204
+
205
+
206
+ def _format_file_operations(read_files: list[str], modified_files: list[str]) -> str:
207
+ sections: list[str] = []
208
+ if read_files:
209
+ sections.append(f"<read-files>\n{chr(10).join(read_files)}\n</read-files>")
210
+ if modified_files:
211
+ sections.append(f"<modified-files>\n{chr(10).join(modified_files)}\n</modified-files>")
212
+ if not sections:
213
+ return ""
214
+ return "\n\n" + "\n\n".join(sections)
215
+
216
+
217
+ # ---------------------------------------------------------------------------
218
+ # Prompts
219
+ # ---------------------------------------------------------------------------
220
+
221
+ BRANCH_SUMMARY_PREAMBLE = (
222
+ "The user explored a different conversation branch before returning here.\n"
223
+ "Summary of that exploration:\n\n"
224
+ )
225
+
226
+ BRANCH_SUMMARY_PROMPT = """\
227
+ Create a structured summary of this conversation branch for context when returning later.
228
+
229
+ Use this EXACT format:
230
+
231
+ ## Goal
232
+ [What was the user trying to accomplish in this branch?]
233
+
234
+ ## Constraints & Preferences
235
+ - [Any constraints, preferences, or requirements mentioned]
236
+ - [Or "(none)" if none were mentioned]
237
+
238
+ ## Progress
239
+ ### Done
240
+ - [x] [Completed tasks/changes]
241
+
242
+ ### In Progress
243
+ - [ ] [Work that was started but not finished]
244
+
245
+ ### Blocked
246
+ - [Issues preventing progress, if any]
247
+
248
+ ## Key Decisions
249
+ - **[Decision]**: [Brief rationale]
250
+
251
+ ## Next Steps
252
+ 1. [What should happen next to continue this work]
253
+
254
+ Keep each section concise. Preserve exact file paths, function names, and error messages."""
255
+
256
+
257
+ # ---------------------------------------------------------------------------
258
+ # Summary generation
259
+ # ---------------------------------------------------------------------------
260
+
261
+ async def generate_branch_summary(
262
+ entries: list[Any],
263
+ llm: TextLLM,
264
+ context_window: int = 128_000,
265
+ reserve_tokens: int = 16_384,
266
+ custom_instructions: str | None = None,
267
+ replace_instructions: bool = False,
268
+ ) -> BranchSummaryResult:
269
+ """Generate a summary of abandoned branch entries."""
270
+ from tau.session.compaction import serialize_conversation, SUMMARIZATION_SYSTEM_PROMPT
271
+ from tau.message.types import UserMessage
272
+ from tau.inference.types import TextEndEvent, TextDeltaEvent, LLMContext
273
+
274
+ token_budget = context_window - reserve_tokens
275
+ prep = prepare_branch_entries(entries, token_budget)
276
+
277
+ if not prep.messages:
278
+ return BranchSummaryResult(summary="No content to summarize")
279
+
280
+ conversation_text = serialize_conversation(prep.messages)
281
+
282
+ if replace_instructions and custom_instructions:
283
+ instructions = custom_instructions
284
+ elif custom_instructions:
285
+ instructions = f"{BRANCH_SUMMARY_PROMPT}\n\nAdditional focus: {custom_instructions}"
286
+ else:
287
+ instructions = BRANCH_SUMMARY_PROMPT
288
+
289
+ prompt_text = f"<conversation>\n{conversation_text}\n</conversation>\n\n{instructions}"
290
+
291
+ context = LLMContext(
292
+ messages=[UserMessage.from_text(prompt_text)],
293
+ system_prompt=SUMMARIZATION_SYSTEM_PROMPT,
294
+ )
295
+ events = await llm.invoke(context)
296
+
297
+ text_end = next((e for e in events if isinstance(e, TextEndEvent)), None)
298
+ if text_end:
299
+ raw = text_end.text.content
300
+ else:
301
+ raw = "".join(e.text.content for e in events if isinstance(e, TextDeltaEvent))
302
+
303
+ summary = BRANCH_SUMMARY_PREAMBLE + raw
304
+
305
+ read_files, modified_files = _compute_file_lists(prep.file_ops)
306
+ summary += _format_file_operations(read_files, modified_files)
307
+
308
+ return BranchSummaryResult(
309
+ summary=summary or "No summary generated",
310
+ read_files=read_files,
311
+ modified_files=modified_files,
312
+ )