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/rpc/mode.py ADDED
@@ -0,0 +1,783 @@
1
+ """
2
+ RPC mode — JSON-lines stdin → stdout protocol.
3
+
4
+ Each line on stdin is a JSON object with a ``type`` field and an optional ``id``.
5
+ Each line on stdout is a JSON object (event or response).
6
+
7
+ Protocol matches the reference implementation (rpc-types.ts).
8
+ Commands are dispatched via :func:`run_rpc_mode`.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import dataclasses
14
+ import json
15
+ import sys
16
+ from typing import TYPE_CHECKING, Any
17
+
18
+ if TYPE_CHECKING:
19
+ from tau.runtime.service import Runtime
20
+
21
+
22
+ # ---------------------------------------------------------------------------
23
+ # Output helpers
24
+ # ---------------------------------------------------------------------------
25
+
26
+ def _write(obj: dict) -> None:
27
+ """Write a JSON line to stdout immediately."""
28
+ sys.stdout.write(json.dumps(obj) + "\n")
29
+ sys.stdout.flush()
30
+
31
+
32
+ def _serialize_event(event: object) -> dict:
33
+ if dataclasses.is_dataclass(event) and not isinstance(event, type):
34
+ return dataclasses.asdict(event)
35
+ return {"type": type(event).__name__}
36
+
37
+
38
+ # ---------------------------------------------------------------------------
39
+ # Extension UI context for RPC
40
+ # ---------------------------------------------------------------------------
41
+
42
+ class RpcExtensionUIContext:
43
+ """
44
+ Implements the extension UI API for RPC mode.
45
+
46
+ Dialog methods (select, confirm, input, editor) emit an ``extension_ui_request``
47
+ on stdout and block until the client sends back an ``extension_ui_response``.
48
+ Fire-and-forget methods (notify, setStatus, setWidget, setTitle, set_editor_text)
49
+ emit without waiting for a reply.
50
+ """
51
+
52
+ def __init__(self, pending: dict[str, asyncio.Future]) -> None:
53
+ self._pending = pending
54
+ self._next_id = 0
55
+
56
+ def _new_req_id(self) -> str:
57
+ self._next_id += 1
58
+ return f"ui_{self._next_id}"
59
+
60
+ async def _dialog(self, payload: dict) -> Any:
61
+ """Emit a dialog request and wait for the client response."""
62
+ req_id = self._new_req_id()
63
+ fut: asyncio.Future = asyncio.get_event_loop().create_future()
64
+ self._pending[req_id] = fut
65
+ _write({"type": "extension_ui_request", "id": req_id, **payload})
66
+ try:
67
+ return await fut
68
+ finally:
69
+ self._pending.pop(req_id, None)
70
+
71
+ def _fire(self, payload: dict) -> None:
72
+ """Emit a fire-and-forget notification (no client response expected)."""
73
+ req_id = self._new_req_id()
74
+ _write({"type": "extension_ui_request", "id": req_id, **payload})
75
+
76
+ async def select(self, title: str, options: list[str]) -> str | None:
77
+ return await self._dialog({"method": "select", "title": title, "options": options})
78
+
79
+ async def confirm(self, title: str, message: str = "") -> bool:
80
+ result = await self._dialog({"method": "confirm", "title": title, "message": message})
81
+ if isinstance(result, dict):
82
+ if result.get("cancelled"):
83
+ return False
84
+ return bool(result.get("confirmed", False))
85
+ return bool(result)
86
+
87
+ async def input(self, title: str, placeholder: str = "") -> str | None:
88
+ return await self._dialog({"method": "input", "title": title, "placeholder": placeholder})
89
+
90
+ async def editor(self, title: str, prefill: str = "") -> str | None:
91
+ return await self._dialog({"method": "editor", "title": title, "prefill": prefill})
92
+
93
+ def notify(self, message: str, notify_type: str = "info") -> None:
94
+ self._fire({"method": "notify", "message": message, "notifyType": notify_type})
95
+
96
+ def set_status(self, status_key: str, status_text: str | None) -> None:
97
+ self._fire({"method": "setStatus", "statusKey": status_key, "statusText": status_text})
98
+
99
+ def set_widget(self, widget_key: str, widget_lines: list[str] | None, placement: str = "aboveEditor") -> None:
100
+ self._fire({"method": "setWidget", "widgetKey": widget_key, "widgetLines": widget_lines, "widgetPlacement": placement})
101
+
102
+ def set_title(self, title: str) -> None:
103
+ self._fire({"method": "setTitle", "title": title})
104
+
105
+ def set_editor_text(self, text: str) -> None:
106
+ self._fire({"method": "set_editor_text", "text": text})
107
+
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # Command dispatcher
111
+ # ---------------------------------------------------------------------------
112
+
113
+ async def _handle_command(cmd: dict, runtime: "Runtime", ui_pending: dict[str, asyncio.Future]) -> None:
114
+ """Dispatch one RPC command. Writes a response line when done."""
115
+ cmd_type = cmd.get("type", "")
116
+ cmd_id = cmd.get("id")
117
+
118
+ def _ok(data: dict | None = None) -> None:
119
+ resp: dict = {"type": "response", "command": cmd_type, "success": True}
120
+ if cmd_id is not None:
121
+ resp["id"] = cmd_id
122
+ if data is not None:
123
+ resp["data"] = data
124
+ _write(resp)
125
+
126
+ def _err(message: str) -> None:
127
+ resp: dict = {"type": "response", "command": cmd_type, "success": False, "error": message}
128
+ if cmd_id is not None:
129
+ resp["id"] = cmd_id
130
+ _write(resp)
131
+
132
+ try:
133
+ match cmd_type:
134
+
135
+ # ── Prompting ────────────────────────────────────────────────────
136
+
137
+ case "prompt":
138
+ text = cmd.get("message", "")
139
+ if not text:
140
+ _err("'message' is required")
141
+ return
142
+ streaming_behavior = cmd.get("streamingBehavior")
143
+ agent = runtime.agent
144
+ is_streaming = agent is not None and getattr(agent, "_running", False)
145
+
146
+ if is_streaming and streaming_behavior is None:
147
+ _err("Agent is streaming; specify streamingBehavior: 'steer' or 'followUp'")
148
+ return
149
+
150
+ if is_streaming and streaming_behavior == "steer":
151
+ from tau.message.types import UserMessage, TextContent
152
+ msg = UserMessage(contents=[TextContent(content=text)])
153
+ await agent._engine.steer(msg) # type: ignore[union-attr]
154
+ elif is_streaming and streaming_behavior == "followUp":
155
+ from tau.message.types import UserMessage, TextContent
156
+ msg = UserMessage(contents=[TextContent(content=text)])
157
+ await agent._engine.follow_up(msg) # type: ignore[union-attr]
158
+ else:
159
+ await runtime.invoke(text)
160
+ _ok()
161
+
162
+ case "steer":
163
+ text = cmd.get("message", "")
164
+ if not text:
165
+ _err("'message' is required")
166
+ return
167
+ agent = runtime.agent
168
+ if agent is None:
169
+ _err("No active agent")
170
+ return
171
+ from tau.message.types import UserMessage, TextContent
172
+ msg = UserMessage(contents=[TextContent(content=text)])
173
+ await agent._engine.steer(msg)
174
+ _ok()
175
+
176
+ case "follow_up":
177
+ text = cmd.get("message", "")
178
+ if not text:
179
+ _err("'message' is required")
180
+ return
181
+ agent = runtime.agent
182
+ if agent is None:
183
+ _err("No active agent")
184
+ return
185
+ from tau.message.types import UserMessage, TextContent
186
+ msg = UserMessage(contents=[TextContent(content=text)])
187
+ await agent._engine.follow_up(msg)
188
+ _ok()
189
+
190
+ case "abort":
191
+ agent = runtime.agent
192
+ if agent is not None:
193
+ cancel_fn = getattr(agent, "cancel", None) or getattr(agent, "abort", None)
194
+ if callable(cancel_fn):
195
+ cancel_fn()
196
+ _ok()
197
+
198
+ case "new_session":
199
+ cancelled = False
200
+ try:
201
+ await runtime.new_session()
202
+ except Exception:
203
+ cancelled = True
204
+ _ok({"cancelled": cancelled})
205
+
206
+ # ── State ────────────────────────────────────────────────────────
207
+
208
+ case "get_state":
209
+ agent = runtime.agent
210
+ is_streaming = agent is not None and getattr(agent, "_running", False)
211
+ sm = runtime.session_manager
212
+
213
+ llm = agent._engine.llm if agent is not None else None
214
+ model_info = None
215
+ if llm is not None:
216
+ model = getattr(llm, "model", None)
217
+ if model is not None:
218
+ model_info = {
219
+ "id": getattr(model, "id", ""),
220
+ "provider": getattr(model, "provider", ""),
221
+ }
222
+
223
+ thinking_level = None
224
+ if llm is not None:
225
+ opts = getattr(getattr(llm, "api", None), "options", None)
226
+ if opts is not None:
227
+ tl = getattr(opts, "thinking_level", None)
228
+ if tl is not None:
229
+ thinking_level = getattr(tl, "value", str(tl))
230
+
231
+ session_id = getattr(sm, "session_id", None) if sm is not None else None
232
+ session_file = str(getattr(sm, "session_file", "") or "") if sm is not None else None
233
+
234
+ msg_count = 0
235
+ if sm is not None:
236
+ from tau.session.types import MessageEntry
237
+ msg_count = sum(1 for e in sm.get_branch() if isinstance(e, MessageEntry))
238
+
239
+ auto_compact = True
240
+ if agent is not None:
241
+ compaction_cfg = getattr(getattr(agent, "_config", None), "compaction", None)
242
+ if compaction_cfg is not None:
243
+ auto_compact = bool(getattr(compaction_cfg, "enabled", True))
244
+
245
+ _ok({
246
+ "model": model_info,
247
+ "thinkingLevel": thinking_level,
248
+ "isStreaming": is_streaming,
249
+ "isCompacting": False,
250
+ "sessionFile": session_file,
251
+ "sessionId": session_id,
252
+ "autoCompactionEnabled": auto_compact,
253
+ "messageCount": msg_count,
254
+ "pendingMessageCount": 0,
255
+ })
256
+
257
+ # ── Model ────────────────────────────────────────────────────────
258
+
259
+ case "set_model":
260
+ model_id = cmd.get("modelId", "") or cmd.get("model_id", "")
261
+ provider = cmd.get("provider")
262
+ if not model_id:
263
+ _err("'modelId' is required")
264
+ return
265
+ await runtime.set_model(model_id, provider)
266
+ agent = runtime.agent
267
+ model_info = None
268
+ if agent is not None:
269
+ llm = agent._engine.llm
270
+ model = getattr(llm, "model", None)
271
+ if model is not None:
272
+ model_info = {"id": getattr(model, "id", ""), "provider": getattr(model, "provider", "")}
273
+ _ok(model_info)
274
+
275
+ case "cycle_model":
276
+ # Cycle to the next available model
277
+ agent = runtime.agent
278
+ new_model_info = None
279
+ if agent is not None:
280
+ try:
281
+ from tau.inference.api.text.service import TextLLM
282
+ llm = agent._engine.llm
283
+ current_id = getattr(getattr(llm, "model", None), "id", None)
284
+ all_models = TextLLM.list_available()
285
+ if all_models and current_id:
286
+ ids = [getattr(m, "id", None) for m in all_models]
287
+ try:
288
+ idx = ids.index(current_id)
289
+ next_model = all_models[(idx + 1) % len(all_models)]
290
+ next_id = getattr(next_model, "id", "")
291
+ next_provider = getattr(next_model, "provider", None)
292
+ await runtime.set_model(next_id, next_provider)
293
+ new_model_info = {"id": next_id, "provider": next_provider or ""}
294
+ except ValueError:
295
+ pass
296
+ except Exception:
297
+ pass
298
+ _ok({"model": new_model_info} if new_model_info else None)
299
+
300
+ case "get_available_models":
301
+ models: list[dict] = []
302
+ try:
303
+ from tau.inference.api.text.service import TextLLM
304
+ for m in TextLLM.list_available():
305
+ models.append({
306
+ "id": getattr(m, "id", str(m)),
307
+ "provider": getattr(m, "provider", ""),
308
+ "name": getattr(m, "name", "") or getattr(m, "id", ""),
309
+ "contextWindow": getattr(m, "context_length", None),
310
+ })
311
+ except Exception:
312
+ pass
313
+ _ok({"models": models})
314
+
315
+ # ── Thinking ─────────────────────────────────────────────────────
316
+
317
+ case "set_thinking_level":
318
+ level = cmd.get("level", "")
319
+ agent = runtime.agent
320
+ if agent is not None:
321
+ try:
322
+ from tau.inference.types import ThinkingLevel
323
+ tl = ThinkingLevel(level)
324
+ llm = agent._engine.llm
325
+ set_fn = getattr(llm, "set_thinking_level", None)
326
+ if callable(set_fn):
327
+ set_fn(tl)
328
+ except Exception as exc:
329
+ _err(str(exc))
330
+ return
331
+ _ok()
332
+
333
+ case "cycle_thinking_level":
334
+ agent = runtime.agent
335
+ new_level = None
336
+ if agent is not None:
337
+ try:
338
+ from tau.inference.types import ThinkingLevel
339
+ llm = agent._engine.llm
340
+ opts = getattr(getattr(llm, "api", None), "options", None)
341
+ if opts is not None:
342
+ levels = list(ThinkingLevel)
343
+ cur = getattr(opts, "thinking_level", ThinkingLevel.Off)
344
+ try:
345
+ idx = levels.index(cur)
346
+ next_tl = levels[(idx + 1) % len(levels)]
347
+ except ValueError:
348
+ next_tl = levels[0]
349
+ set_fn = getattr(llm, "set_thinking_level", None)
350
+ if callable(set_fn):
351
+ set_fn(next_tl)
352
+ new_level = getattr(next_tl, "value", str(next_tl))
353
+ except Exception:
354
+ pass
355
+ _ok({"level": new_level} if new_level is not None else None)
356
+
357
+ # ── Queue modes ──────────────────────────────────────────────────
358
+
359
+ case "set_steering_mode":
360
+ mode = cmd.get("mode", "one-at-a-time")
361
+ # Accept both "one-at-a-time" and "one_at_a_time" (internal)
362
+ py_mode = mode.replace("-", "_")
363
+ from tau.engine.types import SteeringMode
364
+ agent = runtime.agent
365
+ if agent is not None:
366
+ engine = agent._engine
367
+ queue = getattr(engine, "steering_queue", None)
368
+ if queue is not None and hasattr(queue, "mode"):
369
+ try:
370
+ queue.mode = SteeringMode(py_mode)
371
+ except ValueError:
372
+ pass
373
+ _ok()
374
+
375
+ case "set_follow_up_mode":
376
+ mode = cmd.get("mode", "one-at-a-time")
377
+ py_mode = mode.replace("-", "_")
378
+ from tau.engine.types import FollowupMode
379
+ agent = runtime.agent
380
+ if agent is not None:
381
+ engine = agent._engine
382
+ queue = getattr(engine, "follow_up_queue", None)
383
+ if queue is not None and hasattr(queue, "mode"):
384
+ try:
385
+ queue.mode = FollowupMode(py_mode)
386
+ except ValueError:
387
+ pass
388
+ _ok()
389
+
390
+ # ── Compaction ───────────────────────────────────────────────────
391
+
392
+ case "compact":
393
+ instructions = cmd.get("customInstructions")
394
+ agent = runtime.agent
395
+ result_data: dict | None = None
396
+ if agent is not None:
397
+ compact_fn = getattr(agent, "compact", None)
398
+ if callable(compact_fn):
399
+ import inspect
400
+ result = compact_fn(custom_instructions=instructions)
401
+ if inspect.isawaitable(result):
402
+ compaction_result = await result
403
+ if compaction_result is not None:
404
+ result_data = {
405
+ "summary": getattr(compaction_result, "summary", ""),
406
+ "firstKeptEntryId": getattr(compaction_result, "first_kept_entry_id", None),
407
+ "tokensBefore": getattr(compaction_result, "tokens_before", None),
408
+ }
409
+ _ok(result_data)
410
+
411
+ case "set_auto_compaction":
412
+ enabled = bool(cmd.get("enabled", True))
413
+ agent = runtime.agent
414
+ if agent is not None:
415
+ compaction_cfg = getattr(getattr(agent, "_config", None), "compaction", None)
416
+ if compaction_cfg is not None:
417
+ compaction_cfg.enabled = enabled
418
+ _ok()
419
+
420
+ # ── Retry ────────────────────────────────────────────────────────
421
+
422
+ case "set_auto_retry":
423
+ enabled = bool(cmd.get("enabled", True))
424
+ settings = runtime.settings_manager
425
+ if settings is not None:
426
+ set_fn = getattr(settings, "set_retry_enabled", None)
427
+ if callable(set_fn):
428
+ set_fn(enabled)
429
+ _ok()
430
+
431
+ case "abort_retry":
432
+ # Abort any in-progress retry delay
433
+ agent = runtime.agent
434
+ if agent is not None:
435
+ abort_fn = getattr(agent, "abort_retry", None)
436
+ if callable(abort_fn):
437
+ abort_fn()
438
+ _ok()
439
+
440
+ # ── Terminal ─────────────────────────────────────────────────────────
441
+
442
+ case "terminal":
443
+ terminal_cmd = cmd.get("command", "")
444
+ exclude = bool(cmd.get("excludeFromContext", cmd.get("exclude_from_context", False)))
445
+ if not terminal_cmd:
446
+ _err("'command' is required")
447
+ return
448
+ await runtime.execute_terminal(terminal_cmd, exclude=exclude)
449
+ _ok()
450
+
451
+ case "abort_terminal":
452
+ # Abort a running terminal subprocess if possible
453
+ agent = runtime.agent
454
+ if agent is not None:
455
+ abort_fn = getattr(agent, "abort_terminal", None)
456
+ if callable(abort_fn):
457
+ abort_fn()
458
+ _ok()
459
+
460
+ # ── Session ──────────────────────────────────────────────────────
461
+
462
+ case "get_session_stats":
463
+ sm = runtime.session_manager
464
+ if sm is None:
465
+ _ok({"sessionId": None, "totalMessages": 0, "cwd": None})
466
+ return
467
+ entries = sm.get_branch()
468
+ from tau.session.types import MessageEntry
469
+ from tau.message.types import UserMessage, AssistantMessage
470
+ user_count = 0
471
+ asst_count = 0
472
+ for e in entries:
473
+ if not isinstance(e, MessageEntry):
474
+ continue
475
+ if isinstance(e.message, UserMessage):
476
+ user_count += 1
477
+ elif isinstance(e.message, AssistantMessage):
478
+ asst_count += 1
479
+ agent = runtime.agent
480
+ context_usage = None
481
+ if agent is not None:
482
+ engine = getattr(agent, "_engine", None)
483
+ usage = getattr(engine, "context_usage", None) if engine else None
484
+ if usage is not None:
485
+ tokens = getattr(usage, "tokens", None)
486
+ window = getattr(usage, "context_window", None) or 0
487
+ percent = (tokens / window * 100) if (tokens and window) else None
488
+ context_usage = {"tokens": tokens, "contextWindow": window, "percent": percent}
489
+ _ok({
490
+ "sessionFile": str(getattr(sm, "session_file", "") or ""),
491
+ "sessionId": getattr(sm, "session_id", None),
492
+ "userMessages": user_count,
493
+ "assistantMessages": asst_count,
494
+ "totalMessages": user_count + asst_count,
495
+ "cwd": str(sm.cwd),
496
+ "contextUsage": context_usage,
497
+ })
498
+
499
+ case "export_html":
500
+ # HTML export is not implemented; return a not-supported error
501
+ _err("export_html is not supported in this build")
502
+
503
+ case "switch_session":
504
+ path = cmd.get("sessionPath", "") or cmd.get("path", "")
505
+ if not path:
506
+ _err("'sessionPath' is required")
507
+ return
508
+ from pathlib import Path as _Path
509
+ cancelled = False
510
+ try:
511
+ await runtime.resume_session(_Path(path))
512
+ except Exception as exc:
513
+ _err(str(exc))
514
+ return
515
+ _ok({"cancelled": cancelled})
516
+
517
+ case "fork":
518
+ entry_id = cmd.get("entryId", "") or cmd.get("entry_id", "")
519
+ position = cmd.get("position", "at")
520
+ if not entry_id:
521
+ _err("'entryId' is required")
522
+ return
523
+ cancelled = False
524
+ fork_text = ""
525
+ try:
526
+ # Read the original prompt text before forking
527
+ sm = runtime.session_manager
528
+ if sm is not None:
529
+ from tau.session.types import MessageEntry
530
+ from tau.message.types import UserMessage, TextContent
531
+ for e in sm.get_branch():
532
+ if isinstance(e, MessageEntry) and e.id == entry_id and isinstance(e.message, UserMessage):
533
+ for c in e.message.contents:
534
+ if isinstance(c, TextContent):
535
+ fork_text += c.content
536
+ break
537
+ await runtime.fork_session(entry_id, position=position)
538
+ except Exception as exc:
539
+ _err(str(exc))
540
+ return
541
+ _ok({"text": fork_text, "cancelled": cancelled})
542
+
543
+ case "clone":
544
+ sm = runtime.session_manager
545
+ if sm is None:
546
+ _err("No active session")
547
+ return
548
+ cancelled = False
549
+ leaf_id = getattr(sm, "leaf_id", None)
550
+ try:
551
+ if leaf_id:
552
+ await runtime.fork_session(leaf_id, position="at")
553
+ except Exception as exc:
554
+ _err(str(exc))
555
+ return
556
+ _ok({"cancelled": cancelled})
557
+
558
+ case "get_fork_messages":
559
+ sm = runtime.session_manager
560
+ if sm is None:
561
+ _ok({"messages": []})
562
+ return
563
+ from tau.session.types import MessageEntry
564
+ from tau.message.types import UserMessage, TextContent
565
+ fork_messages = []
566
+ for e in sm.get_branch():
567
+ if not isinstance(e, MessageEntry) or not isinstance(e.message, UserMessage):
568
+ continue
569
+ parts = []
570
+ for c in e.message.contents:
571
+ if isinstance(c, TextContent):
572
+ parts.append(c.content)
573
+ fork_messages.append({"entryId": e.id, "text": "".join(parts)})
574
+ _ok({"messages": fork_messages})
575
+
576
+ case "get_last_assistant_text":
577
+ sm = runtime.session_manager
578
+ text = ""
579
+ if sm is not None:
580
+ from tau.session.types import MessageEntry
581
+ from tau.message.types import AssistantMessage, TextContent
582
+ for entry in reversed(sm.get_branch()):
583
+ if isinstance(entry, MessageEntry) and isinstance(entry.message, AssistantMessage):
584
+ for c in entry.message.contents:
585
+ if isinstance(c, TextContent):
586
+ text += c.content
587
+ break
588
+ _ok({"text": text or None})
589
+
590
+ case "set_session_name":
591
+ name = cmd.get("name", "")
592
+ sm = runtime.session_manager
593
+ if sm is not None:
594
+ set_name_fn = getattr(sm, "set_name", None)
595
+ if callable(set_name_fn):
596
+ set_name_fn(name)
597
+ _ok()
598
+
599
+ # ── Messages ─────────────────────────────────────────────────────
600
+
601
+ case "get_messages":
602
+ sm = runtime.session_manager
603
+ if sm is None:
604
+ _ok({"messages": []})
605
+ return
606
+ from tau.session.types import MessageEntry
607
+ messages = []
608
+ for entry in sm.get_branch():
609
+ if not isinstance(entry, MessageEntry):
610
+ continue
611
+ msg = entry.message
612
+ role = getattr(msg, "role", None)
613
+ if role is None:
614
+ continue
615
+ role_val = role.value if hasattr(role, "value") else str(role)
616
+ parts: list[str] = []
617
+ for c in getattr(msg, "contents", []):
618
+ content_str = getattr(c, "content", None)
619
+ if isinstance(content_str, str):
620
+ parts.append(content_str)
621
+ messages.append({"role": role_val, "text": "".join(parts)})
622
+ _ok({"messages": messages})
623
+
624
+ # ── Commands ─────────────────────────────────────────────────────
625
+
626
+ case "get_commands":
627
+ cmds = []
628
+ for info in runtime.commands.list():
629
+ cmds.append({
630
+ "name": info.name,
631
+ "description": info.description,
632
+ "source": "extension",
633
+ })
634
+ # Also include prompt templates and skills
635
+ try:
636
+ from tau.prompts.registry import prompt_registry
637
+ for tmpl in prompt_registry.list():
638
+ cmds.append({"name": tmpl.name, "description": tmpl.description, "source": "prompt"})
639
+ except Exception:
640
+ pass
641
+ try:
642
+ from tau.skills.registry import skill_registry
643
+ for skill in skill_registry.list():
644
+ cmds.append({"name": f"skill:{skill.name}", "description": skill.description or "", "source": "skill"})
645
+ except Exception:
646
+ pass
647
+ _ok({"commands": cmds})
648
+
649
+ # ── Extension UI response (client → tau) ──────────────────────────
650
+
651
+ case "extension_ui_response":
652
+ req_id = cmd.get("id")
653
+ if req_id and req_id in ui_pending:
654
+ fut = ui_pending.pop(req_id)
655
+ if not fut.done():
656
+ if cmd.get("cancelled"):
657
+ fut.set_result(None)
658
+ elif "confirmed" in cmd:
659
+ fut.set_result({"confirmed": cmd["confirmed"]})
660
+ else:
661
+ fut.set_result(cmd.get("value"))
662
+
663
+ case _:
664
+ _err(f"Unknown command type: '{cmd_type}'")
665
+
666
+ except Exception as exc:
667
+ _err(str(exc))
668
+
669
+
670
+ # ---------------------------------------------------------------------------
671
+ # Main entry point
672
+ # ---------------------------------------------------------------------------
673
+
674
+ async def run_rpc_mode(runtime: "Runtime") -> None:
675
+ """Run the RPC mode loop — reads JSON lines from stdin, writes to stdout."""
676
+
677
+ # Pending extension UI futures keyed by request id
678
+ ui_pending: dict[str, asyncio.Future] = {}
679
+
680
+ # ── Subscribe to agent events and stream them out ────────────────────────
681
+
682
+ hook_names = [
683
+ "agent_start", "agent_end",
684
+ "turn_start", "turn_end",
685
+ "message_start", "message_update", "message_end",
686
+ "tool_execution_start", "tool_execution_update", "tool_execution_end",
687
+ "agent_error",
688
+ "compaction_start", "compaction_end",
689
+ "queue_update",
690
+ "settled",
691
+ ]
692
+
693
+ async def on_event(event: object) -> None:
694
+ _write(_serialize_event(event))
695
+
696
+ hooks = runtime.hooks
697
+ unsubs = [hooks.register(name, on_event) for name in hook_names if True]
698
+
699
+ # ── Signal handling (SIGTERM / SIGHUP) ──────────────────────────────────
700
+ loop = asyncio.get_event_loop()
701
+ shutdown_event = asyncio.Event()
702
+
703
+ def _on_signal() -> None:
704
+ agent = runtime.agent
705
+ if agent is not None:
706
+ cancel_fn = getattr(agent, "cancel", None) or getattr(agent, "abort", None)
707
+ if callable(cancel_fn):
708
+ cancel_fn()
709
+ shutdown_event.set()
710
+
711
+ import signal as _signal
712
+ try:
713
+ loop.add_signal_handler(_signal.SIGTERM, _on_signal)
714
+ loop.add_signal_handler(_signal.SIGHUP, _on_signal)
715
+ except (NotImplementedError, OSError):
716
+ pass # Windows
717
+
718
+ # ── Announce ready ───────────────────────────────────────────────────────
719
+ sm = runtime.session_manager
720
+ _write({
721
+ "type": "ready",
722
+ "sessionId": getattr(sm, "session_id", None) if sm is not None else None,
723
+ "cwd": str(sm.cwd) if sm is not None else None,
724
+ })
725
+
726
+ # ── Stdin reader ─────────────────────────────────────────────────────────
727
+ reader = asyncio.StreamReader()
728
+ protocol = asyncio.StreamReaderProtocol(reader)
729
+ try:
730
+ await loop.connect_read_pipe(lambda: protocol, sys.stdin)
731
+ except Exception:
732
+ # Fallback for environments that don't support connect_read_pipe
733
+ async def _stdin_loop() -> None:
734
+ import concurrent.futures
735
+ executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
736
+ while not shutdown_event.is_set():
737
+ try:
738
+ raw = await loop.run_in_executor(executor, sys.stdin.readline)
739
+ except Exception:
740
+ break
741
+ if not raw:
742
+ shutdown_event.set()
743
+ break
744
+ line = raw.rstrip("\r\n")
745
+ if not line:
746
+ continue
747
+ try:
748
+ obj = json.loads(line)
749
+ except json.JSONDecodeError as exc:
750
+ _write({"type": "response", "command": "parse", "success": False, "error": f"Failed to parse command: {exc}"})
751
+ continue
752
+ asyncio.ensure_future(_handle_command(obj, runtime, ui_pending))
753
+
754
+ await _stdin_loop()
755
+ for unsub in unsubs:
756
+ unsub()
757
+ return
758
+
759
+ async def _read_loop() -> None:
760
+ while not shutdown_event.is_set():
761
+ try:
762
+ raw = await reader.readline()
763
+ except Exception:
764
+ break
765
+ if not raw:
766
+ shutdown_event.set()
767
+ break
768
+ line = raw.decode(errors="replace").rstrip("\r\n")
769
+ if not line:
770
+ continue
771
+ try:
772
+ obj = json.loads(line)
773
+ except json.JSONDecodeError as exc:
774
+ _write({"type": "response", "command": "parse", "success": False, "error": f"Failed to parse command: {exc}"})
775
+ continue
776
+ asyncio.ensure_future(_handle_command(obj, runtime, ui_pending))
777
+
778
+ read_task = asyncio.ensure_future(_read_loop())
779
+ await shutdown_event.wait()
780
+ read_task.cancel()
781
+
782
+ for unsub in unsubs:
783
+ unsub()