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
@@ -0,0 +1,462 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import TYPE_CHECKING, Awaitable, Callable, Literal
6
+
7
+ if TYPE_CHECKING:
8
+ from tau.runtime.service import Runtime
9
+ from tau.session.manager import SessionManager
10
+ from tau.session.types import SessionEntry
11
+ from tau.settings.manager import SettingsManager
12
+ from tau.tui.ui_context import UIContext
13
+
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # Session method option dataclasses
17
+ # ---------------------------------------------------------------------------
18
+
19
+ @dataclass
20
+ class NewSessionOptions:
21
+ """Options for ExtensionContext.new_session()."""
22
+ parent_session: str | None = None
23
+ with_session: Callable[[ExtensionContext], Awaitable[None] | None] | None = None
24
+
25
+
26
+ @dataclass
27
+ class ForkOptions:
28
+ """Options for ExtensionContext.fork()."""
29
+ position: Literal["before", "at"] = "at"
30
+ with_session: Callable[[ExtensionContext], Awaitable[None] | None] | None = None
31
+
32
+
33
+ @dataclass
34
+ class NavigateTreeOptions:
35
+ """Options for ExtensionContext.navigate_tree()."""
36
+ summarize: bool = False
37
+ custom_instructions: str | None = None
38
+ replace_instructions: bool = False
39
+ label: str | None = None
40
+
41
+
42
+ @dataclass
43
+ class SwitchSessionOptions:
44
+ """Options for ExtensionContext.switch_session()."""
45
+ with_session: Callable[[ExtensionContext], Awaitable[None] | None] | None = None
46
+
47
+
48
+ class ExtensionContext:
49
+ """
50
+ Runtime context passed to extension command handlers and event handlers.
51
+
52
+ Provides live session state rather than the static info available at
53
+ registration time (which lives on ExtensionAPI).
54
+
55
+ Handler signature:
56
+ async def my_cmd(ctx: ExtensionContext, args: list[str]) -> None:
57
+ print(ctx.cwd, ctx.model_id)
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ cwd: Path,
63
+ settings: SettingsManager | None,
64
+ model_id: str,
65
+ provider_id: str,
66
+ session_manager: SessionManager | None = None,
67
+ layout: object | None = None,
68
+ runtime: Runtime | None = None,
69
+ model_thinking: bool = False,
70
+ ) -> None:
71
+ self._cwd = cwd
72
+ self._settings = settings
73
+ self._model_id = model_id
74
+ self._provider_id = provider_id
75
+ self._session_manager = session_manager
76
+ self._layout = layout
77
+ self._runtime = runtime
78
+ self._model_thinking = model_thinking
79
+
80
+ @classmethod
81
+ def from_runtime(cls, runtime: Runtime) -> ExtensionContext:
82
+ """Construct a context snapshot from the live runtime."""
83
+ sm = runtime.session_manager
84
+ agent = runtime.agent
85
+ llm = agent._engine.llm if agent is not None else None
86
+ return cls(
87
+ cwd=sm.cwd if sm is not None else Path("."),
88
+ settings=runtime.settings_manager,
89
+ model_id=llm.model.id if llm is not None else "",
90
+ provider_id=llm.provider_id if llm is not None else "",
91
+ session_manager=sm,
92
+ layout=getattr(runtime, "_layout", None),
93
+ runtime=runtime,
94
+ model_thinking=bool(llm.model.thinking) if llm is not None else False,
95
+ )
96
+
97
+ @property
98
+ def cwd(self) -> Path:
99
+ """Current working directory."""
100
+ return self._cwd
101
+
102
+ @property
103
+ def settings(self) -> SettingsManager | None:
104
+ """Settings manager, or None if not available."""
105
+ return self._settings
106
+
107
+ @property
108
+ def model_id(self) -> str:
109
+ """Active model identifier."""
110
+ return self._model_id
111
+
112
+ @property
113
+ def provider_id(self) -> str:
114
+ """Active provider identifier."""
115
+ return self._provider_id
116
+
117
+ @property
118
+ def model_thinking(self) -> bool:
119
+ """Whether the active model supports extended thinking."""
120
+ return self._model_thinking
121
+
122
+ @property
123
+ def mode(self) -> str:
124
+ """Execution mode: ``'tui'`` when running inside an interactive TUI session, ``'headless'`` otherwise."""
125
+ return "tui" if self._layout is not None else "headless"
126
+
127
+ @property
128
+ def session_entries(self) -> list[SessionEntry]:
129
+ """All entries in the session file across every branch (the full tree).
130
+
131
+ Use this when you need to inspect the complete history regardless of
132
+ which branch the user is currently on. Most extensions should prefer
133
+ ``branch_entries`` instead.
134
+ """
135
+ if self._session_manager is None:
136
+ return []
137
+ return self._session_manager.get_entries()
138
+
139
+ @property
140
+ def branch_entries(self) -> list[SessionEntry]:
141
+ """Entries on the current branch only (root → current leaf, in order).
142
+
143
+ This is the correct view for restoring per-branch extension state — it
144
+ excludes entries that belong to abandoned branches, so you always see
145
+ the data that is actually in scope for the current conversation.
146
+
147
+ Example — restore saved state from the most recent custom entry::
148
+
149
+ for entry in reversed(ctx.branch_entries):
150
+ if isinstance(entry, CustomInfoEntry) and entry.custom_type == "my-ext":
151
+ restore(entry.data)
152
+ break
153
+ """
154
+ if self._session_manager is None:
155
+ return []
156
+ return self._session_manager.get_branch()
157
+
158
+ @property
159
+ def ui(self) -> "UIContext | None":
160
+ """TUI customization API. None when running outside a TUI session."""
161
+ if self._layout is None:
162
+ return None
163
+ from tau.tui.ui_context import UIContext
164
+ return UIContext(self._layout, settings=self._settings) # type: ignore[arg-type]
165
+
166
+ @property
167
+ def has_ui(self) -> bool:
168
+ """True when dialog-capable UI is available (TUI mode)."""
169
+ return self._layout is not None
170
+
171
+ # ── Agent state ───────────────────────────────────────────────────────────
172
+
173
+ def is_idle(self) -> bool:
174
+ """Return True when the agent is not currently streaming a response."""
175
+ if self._runtime is None:
176
+ return True
177
+ agent = getattr(self._runtime, "agent", None)
178
+ if agent is None:
179
+ return True
180
+ return not getattr(agent, "_running", False)
181
+
182
+ def abort(self) -> None:
183
+ """Abort the current agent operation (no-op if idle)."""
184
+ if self._runtime is None:
185
+ return
186
+ agent = getattr(self._runtime, "agent", None)
187
+ if agent is None:
188
+ return
189
+ cancel_fn = getattr(agent, "cancel", None)
190
+ if callable(cancel_fn):
191
+ cancel_fn()
192
+
193
+ def shutdown(self) -> None:
194
+ """Gracefully shut down tau and exit."""
195
+ import sys
196
+ sys.exit(0)
197
+
198
+ def get_context_usage(self) -> dict | None:
199
+ """Return current context usage info or None if unavailable.
200
+
201
+ Keys: ``tokens`` (int | None), ``context_window`` (int), ``percent`` (float | None).
202
+ """
203
+ if self._runtime is None:
204
+ return None
205
+ agent = getattr(self._runtime, "agent", None)
206
+ if agent is None:
207
+ return None
208
+ usage = agent.get_context_usage()
209
+ if usage is None:
210
+ return None
211
+ tokens = getattr(usage, "tokens", None)
212
+ window = getattr(usage, "context_window", None) or 0
213
+ percent = getattr(usage, "percent", None)
214
+ return {"tokens": tokens, "context_window": window, "percent": percent}
215
+
216
+ def compact(self, custom_instructions: str | None = None) -> None:
217
+ """Trigger context compaction without waiting for completion."""
218
+ import asyncio
219
+ if self._runtime is None:
220
+ return
221
+ agent = getattr(self._runtime, "agent", None)
222
+ if agent is None:
223
+ return
224
+ compact_fn = getattr(agent, "compact", None)
225
+ if callable(compact_fn):
226
+ import inspect
227
+ result = compact_fn(custom_instructions=custom_instructions)
228
+ if inspect.isawaitable(result):
229
+ asyncio.ensure_future(result) # type: ignore[arg-type]
230
+
231
+ def get_system_prompt(self) -> str:
232
+ """Return the current effective system prompt."""
233
+ if self._runtime is None:
234
+ return ""
235
+ agent = getattr(self._runtime, "agent", None)
236
+ if agent is None:
237
+ return ""
238
+ return getattr(agent, "_system_prompt", "") or ""
239
+
240
+ # ── Session control (command context) ─────────────────────────────────────
241
+
242
+ async def wait_for_idle(self) -> None:
243
+ """Suspend until the agent finishes its current turn."""
244
+ if self._runtime is None:
245
+ return
246
+ agent = self._runtime.agent
247
+ if agent is None:
248
+ return
249
+ await agent._engine.state.idle_event.wait()
250
+
251
+ async def new_session(self, options: NewSessionOptions | None = None) -> dict:
252
+ """Start a new session. Returns ``{"cancelled": bool}``.
253
+
254
+ ``options.with_session(ctx)`` runs inside the new session before the UI
255
+ transitions — use it to inject initial messages via ``ctx.send_user_message``.
256
+ """
257
+ if self._runtime is None:
258
+ return {"cancelled": True}
259
+ opts = options or NewSessionOptions()
260
+ await self._runtime.new_session(with_session=opts.with_session)
261
+ return {"cancelled": False}
262
+
263
+ async def fork(self, entry_id: str, options: ForkOptions | None = None) -> dict:
264
+ """Fork from a specific entry. Returns ``{"cancelled": bool}``."""
265
+ if self._runtime is None:
266
+ return {"cancelled": True}
267
+ opts = options or ForkOptions()
268
+ await self._runtime.fork_session(
269
+ entry_id, position=opts.position, with_session=opts.with_session
270
+ )
271
+ return {"cancelled": False}
272
+
273
+ async def navigate_tree(
274
+ self,
275
+ target_id: str,
276
+ *,
277
+ summarize: bool = False,
278
+ custom_instructions: str | None = None,
279
+ options: NavigateTreeOptions | None = None,
280
+ ) -> dict:
281
+ """Navigate the session tree. Returns ``{"cancelled": bool}``."""
282
+ if self._runtime is None:
283
+ return {"cancelled": True}
284
+ opts = options or NavigateTreeOptions(
285
+ summarize=summarize, custom_instructions=custom_instructions
286
+ )
287
+ ok = await self._runtime.navigate_tree(
288
+ target_id,
289
+ summarize=opts.summarize,
290
+ custom_instructions=opts.custom_instructions,
291
+ replace_instructions=opts.replace_instructions,
292
+ label=opts.label,
293
+ )
294
+ return {"cancelled": not ok}
295
+
296
+ async def switch_session(
297
+ self,
298
+ session_path: str,
299
+ options: SwitchSessionOptions | None = None,
300
+ ) -> dict:
301
+ """Switch to a different session file. Returns ``{"cancelled": bool}``."""
302
+ if self._runtime is None:
303
+ return {"cancelled": True}
304
+ opts = options or SwitchSessionOptions()
305
+ await self._runtime.resume_session(Path(session_path), with_session=opts.with_session)
306
+ return {"cancelled": False}
307
+
308
+ async def send_message(self, content: str) -> None:
309
+ """Append a plain text message to the current session as a user turn.
310
+
311
+ Useful inside ``with_session`` callbacks to seed a new session with
312
+ context before the agent processes its first real turn.
313
+ """
314
+ if self._runtime is None:
315
+ return
316
+ agent = getattr(self._runtime, "agent", None)
317
+ if agent is None:
318
+ return
319
+ engine = getattr(agent, "_engine", None)
320
+ if engine is None:
321
+ return
322
+ from tau.message.types import UserMessage, TextContent
323
+ msg = UserMessage(contents=[TextContent(content=content)])
324
+ await engine.steer(msg)
325
+
326
+ async def send_user_message(
327
+ self,
328
+ content: str,
329
+ deliver_as: Literal["steer", "follow_up"] = "steer",
330
+ ) -> None:
331
+ """Inject a user message into the active engine queue.
332
+
333
+ ``deliver_as='steer'`` inserts immediately (mid-turn interception).
334
+ ``deliver_as='follow_up'`` queues for after the current turn completes.
335
+ """
336
+ if self._runtime is None:
337
+ return
338
+ agent = getattr(self._runtime, "agent", None)
339
+ if agent is None:
340
+ return
341
+ engine = getattr(agent, "_engine", None)
342
+ if engine is None:
343
+ return
344
+ from tau.message.types import UserMessage, TextContent
345
+ msg = UserMessage(contents=[TextContent(content=content)])
346
+ if deliver_as == "follow_up":
347
+ await engine.follow_up(msg)
348
+ else:
349
+ await engine.steer(msg)
350
+
351
+ async def is_project_trusted(self) -> bool | None:
352
+ """Return the trust status of the current project directory.
353
+
354
+ Checks in order:
355
+ 1. The ``project_trust`` hook (extensions can override).
356
+ 2. The settings manager's cached trust state (set at startup or by the prompt).
357
+ 3. Returns ``None`` if undecided.
358
+ """
359
+ if self._runtime is None:
360
+ return None
361
+ sm_session = getattr(self._runtime, "session_manager", None)
362
+ cwd = str(sm_session.cwd) if sm_session is not None else ""
363
+
364
+ from tau.hooks.types import ProjectTrustEvent, ProjectTrustResult
365
+ results = await self._runtime.hooks.emit(ProjectTrustEvent(project_dir=cwd))
366
+ for r in results:
367
+ if isinstance(r, ProjectTrustResult) and r.trusted is not None:
368
+ return r.trusted
369
+
370
+ # Fall back to the settings manager's runtime trust state
371
+ sm = getattr(self._runtime, "settings_manager", None)
372
+ if sm is not None:
373
+ return sm.is_project_trusted()
374
+ return None
375
+
376
+ def set_project_trusted(self, trusted: bool, *, remember: bool = False) -> None:
377
+ """Set the project's trust state for this session.
378
+
379
+ ``remember=True`` persists the decision to ``~/.tau/trust.json``
380
+ so it survives future sessions.
381
+ """
382
+ sm = getattr(self._runtime, "settings_manager", None) if self._runtime else None
383
+ if sm is None:
384
+ return
385
+ sm.set_project_trusted(trusted)
386
+ if remember:
387
+ from tau.trust.manager import trust_store
388
+ sm_session = getattr(self._runtime, "session_manager", None)
389
+ cwd = sm_session.cwd if sm_session is not None else None
390
+ if cwd is not None:
391
+ trust_store.set(str(cwd), trusted)
392
+
393
+ async def reload(self) -> None:
394
+ """Reload extensions, skills, prompts, and settings."""
395
+ if self._runtime is None:
396
+ return
397
+ await self._runtime.reload_extensions()
398
+
399
+ # ── Context inspection helpers ─────────────────────────────────────────────
400
+
401
+ def has_pending_messages(self) -> bool:
402
+ """Return True if any steering or follow-up messages are queued.
403
+
404
+ Useful to avoid injecting a duplicate message when one is already
405
+ waiting to be processed by the engine.
406
+ """
407
+ if self._runtime is None:
408
+ return False
409
+ agent = getattr(self._runtime, "agent", None)
410
+ if agent is None:
411
+ return False
412
+ has_fn = getattr(agent, "has_pending_messages", None)
413
+ if callable(has_fn):
414
+ return bool(has_fn())
415
+ return False
416
+
417
+ @property
418
+ def signal(self) -> "object | None":
419
+ """The current abort signal (``asyncio.Event``) while the agent is streaming.
420
+
421
+ The event is *set* when the current operation has been aborted.
422
+ Returns ``None`` when the agent is idle or unavailable.
423
+ """
424
+ if self._runtime is None:
425
+ return None
426
+ agent = getattr(self._runtime, "agent", None)
427
+ if agent is None:
428
+ return None
429
+ return getattr(agent, "_signal", None)
430
+
431
+ def get_system_prompt_options(self) -> dict:
432
+ """Return metadata about how the active system prompt was assembled.
433
+
434
+ Keys:
435
+ - ``skills`` — list of skill names currently loaded
436
+ - ``prompts`` — list of prompt template names currently loaded
437
+ - ``tools`` — list of tool names registered with the engine
438
+ - ``system_prompt_length`` — character length of the built prompt
439
+ """
440
+ from tau.skills.registry import skill_registry
441
+ from tau.prompts.registry import prompt_registry
442
+
443
+ skill_names = [s.name for s in skill_registry.list()]
444
+ prompt_names = [p.name for p in prompt_registry.list()]
445
+
446
+ tool_names: list[str] = []
447
+ if self._runtime is not None:
448
+ agent = getattr(self._runtime, "agent", None)
449
+ if agent is not None:
450
+ engine = getattr(agent, "_engine", None)
451
+ if engine is not None:
452
+ tools = getattr(engine, "tools", None) or []
453
+ tool_names = [t.name for t in tools]
454
+
455
+ prompt_len = len(self.get_system_prompt())
456
+
457
+ return {
458
+ "skills": skill_names,
459
+ "prompts": prompt_names,
460
+ "tools": tool_names,
461
+ "system_prompt_length": prompt_len,
462
+ }
@@ -0,0 +1,70 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from collections import defaultdict
6
+ from typing import Any, Callable
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class EventBus:
12
+ """
13
+ Lightweight pub/sub bus for cross-extension communication.
14
+
15
+ Separate from the main hook bus — intended for extensions to send
16
+ signals to each other rather than to the core runtime.
17
+
18
+ Usage:
19
+ # Publisher
20
+ await tau.events.emit("my-ext:done", {"count": 42})
21
+
22
+ # Subscriber (in another extension or same one)
23
+ @tau.events.on("my-ext:done")
24
+ async def handler(data):
25
+ print(data["count"])
26
+ """
27
+
28
+ def __init__(self) -> None:
29
+ self._handlers: dict[str, list[Callable]] = defaultdict(list)
30
+
31
+ def on(self, event: str, handler: Callable | None = None) -> Any:
32
+ """Subscribe to a named event.
33
+
34
+ Direct call:
35
+ tau.events.on("my-ext:done", handler)
36
+
37
+ Decorator:
38
+ @tau.events.on("my-ext:done")
39
+ async def handler(data): ...
40
+ """
41
+ if handler is not None:
42
+ self._handlers[event].append(handler)
43
+ return lambda: self._handlers[event].remove(handler)
44
+
45
+ def decorator(fn: Callable) -> Callable:
46
+ """Register the function as an event handler."""
47
+ self._handlers[event].append(fn)
48
+ return fn
49
+
50
+ return decorator
51
+
52
+ async def emit(self, event: str, data: Any = None) -> list[Any]:
53
+ """Fire all handlers subscribed to *event*. Returns collected results."""
54
+ results: list[Any] = []
55
+ for handler in list(self._handlers.get(event, [])):
56
+ try:
57
+ result = handler(data)
58
+ if asyncio.iscoroutine(result):
59
+ result = await result
60
+ results.append(result)
61
+ except Exception:
62
+ logger.exception("EventBus handler %r raised on event %r", handler, event)
63
+ return results
64
+
65
+ def clear(self, event: str | None = None) -> None:
66
+ """Remove all handlers for one event, or all events if None."""
67
+ if event is None:
68
+ self._handlers.clear()
69
+ else:
70
+ self._handlers.pop(event, None)