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/service.py ADDED
@@ -0,0 +1,759 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING
6
+
7
+ from tau.runtime.types import RuntimeConfig, RuntimeContext
8
+ from tau.agent.service import Agent
9
+ from tau.agent.types import PromptOptions
10
+ from tau.commands.registry import CommandRegistry
11
+ from tau.commands.types import ParsedCommand
12
+ from tau.hooks.session import (
13
+ SessionStartEvent,
14
+ SessionStartReason,
15
+ SessionShutdownEvent,
16
+ SessionShutdownReason,
17
+ SessionBeforeSwitchEvent,
18
+ SessionBeforeSwitchReason,
19
+ SessionBeforeSwitchResult,
20
+ SessionBeforeForkEvent,
21
+ SessionBeforeForkResult,
22
+ SessionBeforeTreeEvent,
23
+ SessionBeforeTreeResult,
24
+ TreePreparation,
25
+ SessionTreeEvent,
26
+ )
27
+ from tau.hooks.runtime import InputEvent, InputEventResult, RuntimeReadyEvent, RuntimeStopEvent
28
+
29
+ if TYPE_CHECKING:
30
+ from tau.tui.components.layout import Layout
31
+
32
+
33
+ class Runtime:
34
+ """
35
+ Orchestrates the session lifecycle: creation, switching, and forking
36
+ on top of Agent and RuntimeContext.
37
+
38
+ Usage:
39
+ runtime = await Runtime.create(config)
40
+ await runtime.invoke("explain this code")
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ context: RuntimeContext,
46
+ config: RuntimeConfig,
47
+ ) -> None:
48
+ self._context = context
49
+ self._config = config
50
+ self.commands = CommandRegistry(runtime=self)
51
+ self._layout: Layout | None = None
52
+ self._stopped: bool = False
53
+ self.version_check_task: asyncio.Task[str | None] | None = None
54
+ if context.agent is not None:
55
+ context.agent._runtime = self
56
+ # Bind runtime ref so (event, ctx) handlers resolve live state
57
+ if context.ext_runtime is not None:
58
+ context.ext_runtime.runtime_ref.runtime = self
59
+ # Register extension commands into the command registry
60
+ if context.ext_runtime is not None:
61
+ for cmd in context.ext_runtime.get_commands():
62
+ self.commands.register(cmd)
63
+
64
+ # -------------------------------------------------------------------------
65
+ # Factory
66
+ # -------------------------------------------------------------------------
67
+
68
+ @classmethod
69
+ async def create(cls, config: RuntimeConfig) -> Runtime:
70
+ """Create a fully initialised Runtime from config and fire the session_start event."""
71
+ context = await RuntimeContext.create(config)
72
+ runtime = cls(context=context, config=config)
73
+ runtime._start_version_check()
74
+ await runtime._emit_session_start(SessionStartReason.Startup)
75
+ # Runtime is now fully wired (engine, agent, tools, extensions) and the
76
+ # session has started, but no mode-specific loop (TUI/print/rpc) has begun.
77
+ # Extensions can hook `runtime_ready` to start background work from here.
78
+ await context.hooks.emit(RuntimeReadyEvent())
79
+ return runtime
80
+
81
+ def _start_version_check(self) -> None:
82
+ from tau.settings.paths import get_app_version
83
+ from tau.utils.version_check import check_for_new_version
84
+
85
+ self.version_check_task = asyncio.ensure_future(check_for_new_version(get_app_version()))
86
+
87
+ # -------------------------------------------------------------------------
88
+ # Public properties
89
+ # -------------------------------------------------------------------------
90
+
91
+ @property
92
+ def agent(self) -> Agent | None:
93
+ """Get the current agent instance."""
94
+ return self._context.agent
95
+
96
+ @property
97
+ def hooks(self):
98
+ """Get the hooks dispatcher."""
99
+ return self._context.hooks
100
+
101
+ @property
102
+ def session_manager(self):
103
+ """Get the session manager."""
104
+ return self._context.session_manager
105
+
106
+ @property
107
+ def settings_manager(self):
108
+ """Get the settings manager."""
109
+ return self._context.settings_manager
110
+
111
+ @property
112
+ def extension_runtime(self):
113
+ """Get the extension runtime."""
114
+ return self._context.ext_runtime
115
+
116
+ @property
117
+ def extension_shortcuts(self):
118
+ """Get all registered extension keyboard shortcuts."""
119
+ if self._context.ext_runtime is not None:
120
+ return self._context.ext_runtime.get_shortcuts()
121
+ return []
122
+
123
+ def set_layout(self, layout: Layout) -> None:
124
+ """Set the TUI layout, making it available to internal services."""
125
+ self._layout = layout
126
+
127
+ def notify(self, message: str) -> None:
128
+ """Post a system status note to the active TUI, if attached."""
129
+ if self._layout is None:
130
+ return
131
+ import time
132
+ from tau.message.types import CustomMessage, LinesContent
133
+
134
+ msg = CustomMessage(
135
+ custom_type="system",
136
+ timestamp=time.time(),
137
+ contents=[LinesContent(lines=[message, ""])],
138
+ )
139
+ self._layout.add_message(msg)
140
+
141
+ # -------------------------------------------------------------------------
142
+ # Core input entry point
143
+ # -------------------------------------------------------------------------
144
+
145
+ async def user_input(self, text: str, options: PromptOptions | None = None) -> None:
146
+ """Accept raw user text. ! runs a shell command; / goes to CommandRegistry; everything else to the agent."""
147
+ match text.strip():
148
+ case "":
149
+ return
150
+ case t if t.startswith("!!"):
151
+ await self.execute_terminal(t[2:].strip(), exclude=True)
152
+ case t if t.startswith("!"):
153
+ await self.execute_terminal(t[1:].strip())
154
+ case t if t.startswith("/skill:"):
155
+ skill_part = t[7:].strip().split(None, 1)
156
+ skill_name = skill_part[0].lower() if skill_part else ""
157
+ skill_args = skill_part[1] if len(skill_part) > 1 else ""
158
+ from tau.skills.registry import skill_registry
159
+
160
+ skill = skill_registry.get(skill_name)
161
+ if skill is not None:
162
+ expanded = (
163
+ f'<skill name="{skill.name}" location="{skill.file_path}">\n'
164
+ f"References are relative to {skill.base_dir}.\n\n"
165
+ f"{skill.content}\n</skill>"
166
+ )
167
+ if skill_args:
168
+ expanded += f"\n\n{skill_args}"
169
+ await self.invoke(expanded, options)
170
+ case t if t.startswith("/"):
171
+ parts = t[1:].strip().split()
172
+ name, args = parts[0].lower(), parts[1:]
173
+ cmd = ParsedCommand(name=name, args=args, raw=t)
174
+ dispatched = await self.commands.dispatch(cmd)
175
+ if not dispatched:
176
+ from tau.prompts.registry import prompt_registry
177
+
178
+ expanded = prompt_registry.expand(name, " ".join(args))
179
+ if expanded is not None:
180
+ await self.invoke(expanded, options)
181
+ case t:
182
+ await self.invoke(t, options)
183
+
184
+ async def set_model(self, model_id: str, provider: str | None = None) -> None:
185
+ """Swap the active model. Only safe to call when the agent is idle."""
186
+ from tau.inference.api.text.service import TextLLM
187
+ from tau.hooks.tui import ModelSelectEvent
188
+
189
+ agent = self._context.agent
190
+ if agent is None:
191
+ return
192
+ old_model = agent._engine.llm.model
193
+ new_llm = TextLLM(model_id=model_id, provider=provider)
194
+ if new_llm.model.thinking:
195
+ new_llm.api.options.thinking_level = new_llm.model.default_thinking_level
196
+ agent._engine.set_llm(new_llm)
197
+ agent._context_window = new_llm.model.context_window or 128_000
198
+ await self._context.hooks.emit(
199
+ ModelSelectEvent(
200
+ model=new_llm.model,
201
+ previous_model=old_model,
202
+ source="set",
203
+ )
204
+ )
205
+ session = self._context.session_manager
206
+ if session is not None:
207
+ session.append_model_change(model_id, new_llm.provider_id)
208
+
209
+ sm = self._context.settings_manager
210
+ if sm is not None:
211
+ if provider:
212
+ sm.set_model_and_provider(provider, model_id)
213
+ else:
214
+ sm.set_model(model_id)
215
+
216
+ async def execute_terminal(self, cmd: str, exclude: bool = False) -> None:
217
+ """Run a shell command, stream output chunks, persist to session, and emit events."""
218
+ import asyncio
219
+ from asyncio.subprocess import PIPE, STDOUT
220
+ from tau.message.types import TerminalExecutionMessage
221
+ from tau.hooks.types import (
222
+ TerminalExecutionEvent,
223
+ TerminalOutputEvent,
224
+ UserTerminalEvent,
225
+ UserTerminalResult,
226
+ )
227
+
228
+ exit_code: int | None = None
229
+ cancelled = False
230
+ cwd = str(self._context.session_manager.cwd)
231
+
232
+ # Let extensions intercept before the shell runs
233
+ terminal_results = await self._context.hooks.emit(
234
+ UserTerminalEvent(command=cmd, private=exclude, cwd=cwd)
235
+ )
236
+ for r in terminal_results:
237
+ if isinstance(r, UserTerminalResult) and r.handled:
238
+ msg = TerminalExecutionMessage(
239
+ command=cmd, output=r.output, exit_code=r.exit_code, exclude=exclude
240
+ )
241
+ sm = self._context.session_manager
242
+ if sm is not None:
243
+ sm.append_message(msg)
244
+ await self._context.hooks.emit(TerminalExecutionEvent(message=msg, streaming=False))
245
+ return
246
+
247
+ msg = TerminalExecutionMessage(command=cmd, output="", exclude=exclude)
248
+
249
+ await self._context.hooks.emit(TerminalExecutionEvent(message=msg, streaming=True))
250
+
251
+ try:
252
+ proc = await asyncio.create_subprocess_shell(
253
+ cmd.strip(),
254
+ stdout=PIPE,
255
+ stderr=STDOUT,
256
+ cwd=cwd,
257
+ )
258
+ if proc.stdout is not None:
259
+ async for line in proc.stdout:
260
+ msg.output += line.decode(errors="replace")
261
+ await self._context.hooks.emit(TerminalOutputEvent(message=msg))
262
+ await proc.wait()
263
+ exit_code = proc.returncode
264
+ except Exception as exc:
265
+ msg.output += f"error: {exc}"
266
+ cancelled = True
267
+
268
+ msg.output = msg.output.rstrip()
269
+ msg.exit_code = exit_code
270
+ msg.cancelled = cancelled
271
+
272
+ sm = self._context.session_manager
273
+ if sm is not None:
274
+ sm.append_message(msg)
275
+
276
+ await self._context.hooks.emit(TerminalExecutionEvent(message=msg, streaming=False))
277
+
278
+ async def invoke(self, text: str, options: PromptOptions | None = None) -> None:
279
+ """Forward a plain prompt to the current session."""
280
+ if self._context.agent is None:
281
+ raise RuntimeError("No active session available.")
282
+ results = await self._context.hooks.emit(InputEvent(text=text))
283
+ for r in results:
284
+ if isinstance(r, InputEventResult) and r.action == "transform" and r.text is not None:
285
+ text = r.text
286
+ break
287
+ await self._context.agent.invoke(text, options)
288
+
289
+ async def reload_extensions(self):
290
+ """Re-discover and reload extensions, skills, prompts, and settings.
291
+
292
+ Applies all changes to the live engine and rebuilds the system prompt
293
+ immediately — no new session required.
294
+ """
295
+ from pathlib import Path
296
+ from tau.agent.prompt.builder import build_prompt
297
+ from tau.agent.prompt.types import PromptOptions
298
+ from tau.extensions.api import _RuntimeRef, LoadExtensionsResult
299
+ from tau.extensions.events import EventBus
300
+ from tau.extensions.loader import ExtensionLoader
301
+ from tau.extensions.runtime import ExtensionRuntime
302
+ from tau.settings.paths import get_extensions_dir
303
+ from tau.skills.registry import skill_registry
304
+ from tau.prompts.registry import prompt_registry
305
+
306
+ sm = self._context.settings_manager
307
+ if sm is None:
308
+ return LoadExtensionsResult()
309
+
310
+ # ── Settings ─────────────────────────────────────────────────────────
311
+ await sm.reload()
312
+
313
+ cwd = self._context.session_manager.cwd
314
+
315
+ # ── Resource discovery hook ──────────────────────────────────────────
316
+ from tau.hooks.types import ResourcesDiscoverEvent, ResourcesDiscoverResult
317
+
318
+ discover_results = await self._context.hooks.emit(ResourcesDiscoverEvent(cwd=str(cwd)))
319
+ extra_skill_paths: list[str] = []
320
+ extra_prompt_paths: list[str] = []
321
+ extra_theme_paths: list[str] = []
322
+ for r in discover_results:
323
+ if isinstance(r, ResourcesDiscoverResult):
324
+ extra_skill_paths.extend(r.skill_paths)
325
+ extra_prompt_paths.extend(r.prompt_paths)
326
+ extra_theme_paths.extend(r.theme_paths)
327
+
328
+ # ── Skills ───────────────────────────────────────────────────────────
329
+ skill_registry.reload(cwd=cwd, extra_paths=extra_skill_paths or None) # type: ignore[arg-type]
330
+
331
+ # ── Prompts ──────────────────────────────────────────────────────────
332
+ prompt_registry.reload(cwd=cwd, extra_paths=extra_prompt_paths or None) # type: ignore[arg-type]
333
+
334
+ # extra_theme_paths: load each directory into the theme registry
335
+ if extra_theme_paths:
336
+ from tau.themes.registry import theme_registry
337
+ import logging as _logging
338
+
339
+ _log = _logging.getLogger(__name__)
340
+ for tp in extra_theme_paths:
341
+ try:
342
+ from pathlib import Path as _Path
343
+
344
+ theme_registry.load_external(cwd=_Path(tp))
345
+ except Exception as _e:
346
+ _log.warning("resources_discover: failed to load theme path %r: %s", tp, _e)
347
+
348
+ old = self._context.ext_runtime
349
+ if old is not None:
350
+ for ext in old._extensions:
351
+ await self._emit_to_extension(ext, "extension_unload")
352
+ old.unsubscribe()
353
+
354
+ entries = sm.get_extension_list()
355
+ disabled_stems = {Path(e.path).stem for e in entries if not e.enabled}
356
+ entry_configs = {Path(e.path).stem: (e.settings or {}) for e in entries if e.enabled}
357
+ extra_entries = [e for e in entries if e.enabled]
358
+ runtime_ref = old.runtime_ref if old is not None else _RuntimeRef()
359
+
360
+ loader = ExtensionLoader(
361
+ project_dir=get_extensions_dir(cwd),
362
+ global_dir=get_extensions_dir(),
363
+ extra_entries=extra_entries,
364
+ disabled_stems=disabled_stems,
365
+ entry_configs=entry_configs,
366
+ llm=self._context.llm,
367
+ settings=sm,
368
+ cwd=cwd,
369
+ runtime_ref=runtime_ref,
370
+ events=EventBus(),
371
+ )
372
+ load_result = await loader.load()
373
+ new_ext = ExtensionRuntime(load_result, self._context.hooks, runtime_ref)
374
+ new_ext.runtime_ref.runtime = self
375
+ self._context.ext_runtime = new_ext
376
+
377
+ for cmd in new_ext.get_commands():
378
+ self.commands.register(cmd)
379
+
380
+ # ── Sync tools via registry then push to engine ───────────────────────
381
+ engine = self._context.engine
382
+ agent = self._context.agent
383
+ if engine is not None:
384
+ registry = self._context.tool_registry
385
+ registry.replace_source("extension", new_ext.get_tools())
386
+ registry.sync_to_engine(engine, layout=getattr(self, "_layout", None))
387
+
388
+ if agent is not None:
389
+ extra_appends = new_ext.get_prompt_appends()
390
+ skills = skill_registry.list()
391
+ agent._system_prompt = build_prompt(
392
+ PromptOptions(
393
+ cwd=cwd,
394
+ tools=registry.list(),
395
+ extra_appends=extra_appends,
396
+ skills=skills,
397
+ disable_context_files=self._config.disable_context_files,
398
+ project_trusted=self._config.project_trusted,
399
+ )
400
+ )
401
+
402
+ for ext in new_ext._extensions:
403
+ await self._emit_to_extension(ext, "extension_reloaded")
404
+
405
+ return load_result
406
+
407
+ async def reload_extension(self, ext_path: str):
408
+ """Reload a single extension by its loaded module path, applying live.
409
+
410
+ Re-reads settings, re-runs only this extension's ``register`` with fresh
411
+ config, and swaps its tools/commands/prompt in place — other extensions
412
+ keep their existing state and are *not* re-run (so their resources and
413
+ side effects are untouched). Falls back to a full reload if the target
414
+ can't be resolved.
415
+ """
416
+ from pathlib import Path
417
+ from tau.agent.prompt.builder import build_prompt
418
+ from tau.agent.prompt.types import PromptOptions
419
+ from tau.extensions.api import LoadExtensionsResult
420
+ from tau.extensions.events import EventBus
421
+ from tau.extensions.loader import ExtensionLoader
422
+ from tau.extensions.runtime import ExtensionRuntime
423
+ from tau.settings.paths import get_extensions_dir
424
+ from tau.skills.registry import skill_registry
425
+
426
+ sm = self._context.settings_manager
427
+ if sm is None:
428
+ return LoadExtensionsResult()
429
+
430
+ await sm.reload()
431
+
432
+ old = self._context.ext_runtime
433
+ if old is None:
434
+ return await self.reload_extensions()
435
+ target = next((e for e in old._extensions if e.path == ext_path), None)
436
+ if target is None:
437
+ # Unknown target — fall back to the all-extensions reload.
438
+ return await self.reload_extensions()
439
+
440
+ cwd = self._context.session_manager.cwd
441
+ entries = sm.get_extension_list()
442
+ entry_configs = {Path(e.path).stem: (e.settings or {}) for e in entries if e.enabled}
443
+ p = Path(ext_path)
444
+ stem = p.parent.name if p.name == "__init__.py" else p.stem
445
+ config = entry_configs.get(stem, {})
446
+
447
+ runtime_ref = old.runtime_ref
448
+ loader = ExtensionLoader(
449
+ project_dir=get_extensions_dir(cwd),
450
+ global_dir=get_extensions_dir(),
451
+ llm=self._context.llm,
452
+ settings=sm,
453
+ cwd=cwd,
454
+ runtime_ref=runtime_ref,
455
+ events=EventBus(),
456
+ )
457
+ # Populate the per-subdir caches (deps + manifest settings schema) for
458
+ # this extension so _load_one re-attaches its auto-generated panel.
459
+ loader._subdir_entries(p.parent)
460
+ new_ext, errs = await loader._load_one(p, config, source=target.source)
461
+ if new_ext is None:
462
+ # Keep the old extension on failure; surface load errors.
463
+ return LoadExtensionsResult(extensions=old._extensions, errors=errs)
464
+
465
+ # Let the outgoing extension release any resources it holds (subprocesses,
466
+ # background tasks, sockets) before it is replaced — reload does not do
467
+ # this automatically, so stateful extensions must handle `extension_unload`.
468
+ await self._emit_to_extension(target, "extension_unload")
469
+
470
+ new_list = [new_ext if e is target else e for e in old._extensions]
471
+ old.unsubscribe()
472
+ new_runtime = ExtensionRuntime(
473
+ LoadExtensionsResult(extensions=new_list, errors=errs),
474
+ self._context.hooks,
475
+ runtime_ref,
476
+ )
477
+ new_runtime.runtime_ref.runtime = self
478
+ self._context.ext_runtime = new_runtime
479
+
480
+ # ── Commands: drop the target's old commands, register its new set ────
481
+ for name in target.commands:
482
+ self.commands.unregister(name)
483
+ for cmd in new_ext.commands.values():
484
+ self.commands.register(cmd)
485
+
486
+ # ── Tools + prompt ────────────────────────────────────────────────────
487
+ engine = self._context.engine
488
+ agent = self._context.agent
489
+ if engine is not None:
490
+ registry = self._context.tool_registry
491
+ registry.replace_source("extension", new_runtime.get_tools())
492
+ registry.sync_to_engine(engine, layout=getattr(self, "_layout", None))
493
+
494
+ if agent is not None:
495
+ agent._system_prompt = build_prompt(
496
+ PromptOptions(
497
+ cwd=cwd,
498
+ tools=registry.list(),
499
+ extra_appends=new_runtime.get_prompt_appends(),
500
+ skills=skill_registry.list(),
501
+ disable_context_files=self._config.disable_context_files,
502
+ project_trusted=self._config.project_trusted,
503
+ )
504
+ )
505
+
506
+ # Give the freshly-loaded extension a chance to re-establish runtime state
507
+ # (e.g. warm up language servers) now that the runtime is already wired —
508
+ # `runtime_ready` only fires once at startup, not on reload.
509
+ await self._emit_to_extension(new_ext, "extension_reloaded")
510
+
511
+ return LoadExtensionsResult(extensions=new_list, errors=errs)
512
+
513
+ async def _emit_to_extension(self, ext, event_type: str) -> None:
514
+ """Dispatch a lifecycle event directly to a single extension's handlers.
515
+
516
+ Used for reload-only events (``extension_unload`` / ``extension_reloaded``)
517
+ that must reach exactly one extension rather than every handler on the bus.
518
+ Handler exceptions are swallowed so one bad handler can't block the reload.
519
+ """
520
+ import inspect
521
+ from types import SimpleNamespace
522
+ from tau.extensions.context import ExtensionContext
523
+
524
+ handlers = ext.handlers.get(event_type, [])
525
+ if not handlers:
526
+ return
527
+ ctx = ExtensionContext.from_runtime(self)
528
+ event = SimpleNamespace(type=event_type)
529
+ for handler in handlers:
530
+ try:
531
+ result = handler(event, ctx)
532
+ if inspect.isawaitable(result):
533
+ await result
534
+ except Exception:
535
+ # Don't let one failed handler abort the reload, but never fail
536
+ # silently — a botched dispose (e.g. servers not reaped) must be
537
+ # visible rather than leaking resources unnoticed.
538
+ import logging
539
+ logging.getLogger(__name__).exception(
540
+ "extension %s handler for %r raised", ext.path, event_type
541
+ )
542
+
543
+ # -------------------------------------------------------------------------
544
+ # Session lifecycle
545
+ # -------------------------------------------------------------------------
546
+
547
+ async def new_session(self, *, with_session=None) -> None:
548
+ """Shut down the current session and start a fresh one."""
549
+ await self._emit_session_shutdown(SessionShutdownReason.New)
550
+ self._config = self._config.model_copy(update={"session_file": None})
551
+ self._context = await RuntimeContext.create(
552
+ self._config,
553
+ settings_manager=self.settings_manager,
554
+ hooks=self.hooks,
555
+ ext_runtime=self.extension_runtime,
556
+ )
557
+ self._reinit_after_context_create()
558
+ await self._run_with_session(with_session)
559
+ await self._emit_session_start(SessionStartReason.New)
560
+
561
+ async def resume_session(self, session_file: Path, *, with_session=None) -> None:
562
+ """Shut down the current session and resume an existing one from a file."""
563
+ session_file = Path(session_file).resolve()
564
+
565
+ before_results = await self._context.hooks.emit(
566
+ SessionBeforeSwitchEvent(
567
+ reason=SessionBeforeSwitchReason.Resume, target_session_file=str(session_file)
568
+ )
569
+ )
570
+ for r in before_results:
571
+ if isinstance(r, SessionBeforeSwitchResult) and r.cancel:
572
+ return
573
+
574
+ await self._emit_session_shutdown(SessionShutdownReason.Resume)
575
+ self._config = self._config.model_copy(update={"session_file": session_file})
576
+ self._context = await RuntimeContext.create(
577
+ self._config,
578
+ settings_manager=self.settings_manager,
579
+ hooks=self.hooks,
580
+ ext_runtime=self.extension_runtime,
581
+ )
582
+ self._reinit_after_context_create()
583
+ await self._run_with_session(with_session)
584
+ await self._emit_session_start(SessionStartReason.Resume)
585
+
586
+ async def navigate_tree(
587
+ self,
588
+ target_id: str,
589
+ *,
590
+ summarize: bool = False,
591
+ custom_instructions: str | None = None,
592
+ replace_instructions: bool = False,
593
+ label: str | None = None,
594
+ ) -> bool:
595
+ """Navigate the session tree to target_id, optionally generating a branch summary.
596
+
597
+ Returns False if cancelled by an extension handler, True otherwise.
598
+ """
599
+ sm = self._context.session_manager
600
+ if target_id not in sm.by_id:
601
+ raise KeyError(f"Entry '{target_id}' not found in session.")
602
+
603
+ old_leaf_id = sm.get_leaf_id()
604
+ if target_id == old_leaf_id:
605
+ return True # already there
606
+
607
+ # Collect entries between the old leaf and the common ancestor
608
+ from tau.session.branch_summarization import collect_entries_for_branch_summary
609
+
610
+ collect_result = collect_entries_for_branch_summary(sm, old_leaf_id, target_id)
611
+ entries_to_summarize = collect_result.entries
612
+ common_ancestor_id = collect_result.common_ancestor_id
613
+
614
+ # Build preparation for the before_tree hook
615
+ preparation = TreePreparation(
616
+ target_id=target_id,
617
+ old_leaf_id=old_leaf_id,
618
+ common_ancestor_id=common_ancestor_id,
619
+ custom_instructions=custom_instructions,
620
+ replace_instructions=replace_instructions,
621
+ label=label,
622
+ )
623
+
624
+ # Let extensions inspect / mutate / cancel
625
+ results = await self._context.hooks.emit(SessionBeforeTreeEvent(preparation=preparation))
626
+ for r in results:
627
+ if isinstance(r, SessionBeforeTreeResult):
628
+ if r.cancel:
629
+ return False
630
+ if r.custom_instructions is not None:
631
+ custom_instructions = r.custom_instructions
632
+ if r.replace_instructions is not None:
633
+ replace_instructions = r.replace_instructions
634
+ if r.label is not None:
635
+ label = r.label
636
+
637
+ # Generate branch summary if requested
638
+ if summarize and entries_to_summarize:
639
+ sm_settings = self._context.settings_manager
640
+ reserve_tokens = (
641
+ sm_settings.get_branch_summary_reserve_tokens()
642
+ if sm_settings is not None
643
+ else 16_384
644
+ )
645
+ from tau.session.branch_summarization import generate_branch_summary
646
+
647
+ llm = self._context.llm
648
+ result = await generate_branch_summary(
649
+ entries_to_summarize,
650
+ llm,
651
+ reserve_tokens=reserve_tokens,
652
+ custom_instructions=custom_instructions,
653
+ replace_instructions=replace_instructions,
654
+ )
655
+ if result.summary and not result.aborted:
656
+ sm.append_branch_summary(
657
+ from_id=old_leaf_id or "",
658
+ summary=result.summary,
659
+ label=label,
660
+ details={
661
+ "read_files": result.read_files,
662
+ "modified_files": result.modified_files,
663
+ },
664
+ )
665
+
666
+ sm.branch(target_id)
667
+ await self._context.hooks.emit(
668
+ SessionTreeEvent(new_leaf_id=target_id, old_leaf_id=old_leaf_id)
669
+ )
670
+ await self._emit_session_start(SessionStartReason.Fork)
671
+ return True
672
+
673
+ async def fork_session(
674
+ self,
675
+ from_entry_id: str,
676
+ *,
677
+ position: str = "at",
678
+ with_session=None,
679
+ ) -> None:
680
+ """Branch the session tree at the given entry and start a new leaf."""
681
+ sm = self._context.session_manager
682
+ if from_entry_id not in sm.by_id:
683
+ raise KeyError(f"Entry '{from_entry_id}' not found in session.")
684
+
685
+ before_results = await self._context.hooks.emit(
686
+ SessionBeforeForkEvent(entry_id=from_entry_id, position=position) # type: ignore[arg-type]
687
+ )
688
+ for r in before_results:
689
+ if isinstance(r, SessionBeforeForkResult) and r.cancel:
690
+ return
691
+
692
+ sm.branch(from_entry_id)
693
+ await self._run_with_session(with_session)
694
+ await self._emit_session_start(SessionStartReason.Fork)
695
+
696
+ async def clone_session(self) -> None:
697
+ """Duplicate the current branch into a new session file and switch to it."""
698
+ sm = self._context.session_manager
699
+ leaf_id = sm.get_leaf_id()
700
+ if leaf_id is None:
701
+ raise ValueError("No active leaf to clone from.")
702
+
703
+ await self._emit_session_shutdown(SessionShutdownReason.Clone)
704
+ sm.create_branched_session(leaf_id)
705
+ self._reinit_after_context_create()
706
+ await self._emit_session_start(SessionStartReason.Clone)
707
+
708
+ def _reinit_after_context_create(self) -> None:
709
+ if self._context.agent is not None:
710
+ self._context.agent._runtime = self
711
+ # Keep the runtime ref pointing at this Runtime instance
712
+ if self._context.ext_runtime is not None:
713
+ self._context.ext_runtime.runtime_ref.runtime = self
714
+
715
+ async def _run_with_session(self, with_session) -> None:
716
+ """Call the with_session(ctx) callback if provided, with a fresh context."""
717
+ if with_session is None:
718
+ return
719
+ import inspect
720
+ from tau.extensions.context import ExtensionContext
721
+
722
+ ctx = ExtensionContext.from_runtime(self)
723
+ try:
724
+ result = with_session(ctx)
725
+ if inspect.isawaitable(result):
726
+ await result
727
+ except Exception:
728
+ import logging
729
+
730
+ logging.getLogger(__name__).exception("with_session callback raised")
731
+
732
+ # -------------------------------------------------------------------------
733
+ # Shutdown
734
+ # -------------------------------------------------------------------------
735
+
736
+ def shutdown(self) -> None:
737
+ pass
738
+
739
+ async def ashutdown(self) -> None:
740
+ """Tear down the runtime once the mode-specific loop has exited.
741
+
742
+ Emits `runtime_stop` (symmetric to the `runtime_ready` emitted in
743
+ `create`) so extensions can run terminal cleanup that must happen on quit
744
+ regardless of mode. Idempotent — guarded so a double call is a no-op.
745
+ """
746
+ if self._stopped:
747
+ return
748
+ self._stopped = True
749
+ await self._context.hooks.emit(RuntimeStopEvent())
750
+
751
+ # -------------------------------------------------------------------------
752
+ # Event helpers
753
+ # -------------------------------------------------------------------------
754
+
755
+ async def _emit_session_start(self, reason: SessionStartReason) -> None:
756
+ await self._context.hooks.emit(SessionStartEvent(reason=reason))
757
+
758
+ async def _emit_session_shutdown(self, reason: SessionShutdownReason) -> None:
759
+ await self._context.hooks.emit(SessionShutdownEvent(reason=reason))