tau-coding-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tau/__init__.py +0 -0
- tau/agent/__init__.py +11 -0
- tau/agent/prompt/__init__.py +10 -0
- tau/agent/prompt/builder.py +302 -0
- tau/agent/prompt/types.py +33 -0
- tau/agent/service.py +369 -0
- tau/agent/types.py +61 -0
- tau/auth/manager.py +247 -0
- tau/auth/storage.py +82 -0
- tau/auth/types.py +41 -0
- tau/builtins/__init__.py +4 -0
- tau/builtins/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__init__.py +41 -0
- tau/builtins/commands/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/clear.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/compact.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/reload.cpython-314.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-313.pyc +0 -0
- tau/builtins/commands/__pycache__/session.cpython-314.pyc +0 -0
- tau/builtins/commands/clear.py +16 -0
- tau/builtins/commands/compact.py +28 -0
- tau/builtins/commands/reload.py +27 -0
- tau/builtins/commands/session.py +19 -0
- tau/builtins/extensions/footer/__init__.py +76 -0
- tau/builtins/extensions/footer/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/git.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/model.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/__pycache__/utils.cpython-313.pyc +0 -0
- tau/builtins/extensions/footer/git.py +26 -0
- tau/builtins/extensions/footer/model.py +69 -0
- tau/builtins/extensions/footer/utils.py +44 -0
- tau/builtins/extensions/header/__init__.py +18 -0
- tau/builtins/extensions/header/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__init__.py +0 -0
- tau/builtins/models/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/models/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/models/audio.py +43 -0
- tau/builtins/models/image.py +43 -0
- tau/builtins/models/text.py +482 -0
- tau/builtins/models/video.py +40 -0
- tau/builtins/prompts/commit.md +7 -0
- tau/builtins/prompts/docs.md +7 -0
- tau/builtins/prompts/explain.md +7 -0
- tau/builtins/prompts/fix.md +7 -0
- tau/builtins/prompts/refactor.md +7 -0
- tau/builtins/prompts/review.md +7 -0
- tau/builtins/prompts/test.md +7 -0
- tau/builtins/providers/__init__.py +0 -0
- tau/builtins/providers/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/providers/__pycache__/text.cpython-313.pyc +0 -0
- tau/builtins/providers/audio.py +10 -0
- tau/builtins/providers/image.py +9 -0
- tau/builtins/providers/text.py +33 -0
- tau/builtins/providers/video.py +6 -0
- tau/builtins/skills/code-review/SKILL.md +4 -0
- tau/builtins/skills/debug/SKILL.md +4 -0
- tau/builtins/skills/git-commit/SKILL.md +4 -0
- tau/builtins/themes/dark.yaml +1 -0
- tau/builtins/themes/light.yaml +46 -0
- tau/builtins/tools/__init__.py +73 -0
- tau/builtins/tools/__pycache__/__init__.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/bash.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/edit.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/glob.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/grep.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/ls.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/read.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/terminal.cpython-314.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-313.pyc +0 -0
- tau/builtins/tools/__pycache__/write.cpython-314.pyc +0 -0
- tau/builtins/tools/edit.py +215 -0
- tau/builtins/tools/glob.py +112 -0
- tau/builtins/tools/grep.py +146 -0
- tau/builtins/tools/ls.py +135 -0
- tau/builtins/tools/read.py +122 -0
- tau/builtins/tools/terminal.py +150 -0
- tau/builtins/tools/write.py +105 -0
- tau/commands/__init__.py +10 -0
- tau/commands/registry.py +71 -0
- tau/commands/types.py +33 -0
- tau/console/__init__.py +0 -0
- tau/console/cli.py +266 -0
- tau/console/commands/__init__.py +0 -0
- tau/console/commands/auth.py +193 -0
- tau/console/commands/packages.py +104 -0
- tau/console/commands/update.py +76 -0
- tau/core/__init__.py +0 -0
- tau/core/registry.py +102 -0
- tau/engine/__init__.py +47 -0
- tau/engine/service.py +768 -0
- tau/engine/types.py +163 -0
- tau/extensions/__init__.py +28 -0
- tau/extensions/api.py +928 -0
- tau/extensions/context.py +462 -0
- tau/extensions/events.py +70 -0
- tau/extensions/loader.py +386 -0
- tau/extensions/runtime.py +184 -0
- tau/extensions/settings.py +137 -0
- tau/hooks/__init__.py +112 -0
- tau/hooks/engine.py +237 -0
- tau/hooks/inference.py +21 -0
- tau/hooks/runtime.py +126 -0
- tau/hooks/service.py +121 -0
- tau/hooks/session.py +117 -0
- tau/hooks/tui.py +61 -0
- tau/hooks/types.py +72 -0
- tau/inference/__init__.py +180 -0
- tau/inference/api/__init__.py +0 -0
- tau/inference/api/audio/__init__.py +0 -0
- tau/inference/api/audio/base.py +29 -0
- tau/inference/api/audio/builtins.py +15 -0
- tau/inference/api/audio/elevenlabs_audio.py +183 -0
- tau/inference/api/audio/gemini_audio.py +95 -0
- tau/inference/api/audio/openai_audio.py +159 -0
- tau/inference/api/audio/registry.py +15 -0
- tau/inference/api/audio/sarvam_audio.py +163 -0
- tau/inference/api/audio/service.py +103 -0
- tau/inference/api/audio/utils.py +47 -0
- tau/inference/api/image/__init__.py +0 -0
- tau/inference/api/image/base.py +17 -0
- tau/inference/api/image/builtins.py +8 -0
- tau/inference/api/image/gemini_image.py +77 -0
- tau/inference/api/image/openai_image.py +103 -0
- tau/inference/api/image/openrouter.py +144 -0
- tau/inference/api/image/registry.py +15 -0
- tau/inference/api/image/service.py +71 -0
- tau/inference/api/registry.py +82 -0
- tau/inference/api/text/__init__.py +0 -0
- tau/inference/api/text/anthropic_claude_code.py +222 -0
- tau/inference/api/text/anthropic_messages.py +196 -0
- tau/inference/api/text/base.py +40 -0
- tau/inference/api/text/builtins.py +19 -0
- tau/inference/api/text/gemini_generate.py +234 -0
- tau/inference/api/text/github_copilot_chat.py +172 -0
- tau/inference/api/text/google_antigravity.py +522 -0
- tau/inference/api/text/mistral_chat.py +284 -0
- tau/inference/api/text/ollama_chat.py +200 -0
- tau/inference/api/text/openai_codex_responses.py +497 -0
- tau/inference/api/text/openai_completions.py +227 -0
- tau/inference/api/text/openai_responses.py +235 -0
- tau/inference/api/text/registry.py +50 -0
- tau/inference/api/text/service.py +297 -0
- tau/inference/api/text/types.py +7 -0
- tau/inference/api/text/utils.py +228 -0
- tau/inference/api/video/__init__.py +0 -0
- tau/inference/api/video/base.py +26 -0
- tau/inference/api/video/builtins.py +7 -0
- tau/inference/api/video/fal_video.py +119 -0
- tau/inference/api/video/openrouter_video.py +142 -0
- tau/inference/api/video/registry.py +15 -0
- tau/inference/api/video/service.py +72 -0
- tau/inference/model/__init__.py +0 -0
- tau/inference/model/registry.py +102 -0
- tau/inference/model/types.py +65 -0
- tau/inference/provider/__init__.py +0 -0
- tau/inference/provider/oauth/__init__.py +35 -0
- tau/inference/provider/oauth/anthropic_claude_code.py +286 -0
- tau/inference/provider/oauth/github_copilot.py +333 -0
- tau/inference/provider/oauth/google_antigravity.py +258 -0
- tau/inference/provider/oauth/openai_codex.py +309 -0
- tau/inference/provider/oauth/pkce.py +14 -0
- tau/inference/provider/oauth/types.py +46 -0
- tau/inference/provider/oauth/utils.py +154 -0
- tau/inference/provider/registry.py +141 -0
- tau/inference/provider/types.py +114 -0
- tau/inference/types.py +549 -0
- tau/inference/utils.py +219 -0
- tau/message/__init__.py +0 -0
- tau/message/types.py +482 -0
- tau/message/utils.py +178 -0
- tau/packages/__init__.py +11 -0
- tau/packages/manager.py +190 -0
- tau/packages/types.py +20 -0
- tau/packages/utils.py +67 -0
- tau/prompts/expand.py +58 -0
- tau/prompts/loader.py +69 -0
- tau/prompts/registry.py +45 -0
- tau/prompts/types.py +24 -0
- tau/rpc/__init__.py +8 -0
- tau/rpc/mode.py +783 -0
- tau/rpc/types.py +252 -0
- tau/runtime/service.py +759 -0
- tau/runtime/types.py +303 -0
- tau/session/branch_summarization.py +312 -0
- tau/session/compaction.py +646 -0
- tau/session/manager.py +652 -0
- tau/session/types.py +188 -0
- tau/session/utils.py +233 -0
- tau/settings/manager.py +1077 -0
- tau/settings/paths.py +150 -0
- tau/settings/storage.py +63 -0
- tau/settings/types.py +173 -0
- tau/settings/utils.py +25 -0
- tau/skills/loader.py +91 -0
- tau/skills/registry.py +70 -0
- tau/skills/types.py +25 -0
- tau/themes/loader.py +238 -0
- tau/themes/registry.py +108 -0
- tau/themes/types.py +19 -0
- tau/tool/__init__.py +3 -0
- tau/tool/registry.py +117 -0
- tau/tool/render.py +21 -0
- tau/tool/types.py +244 -0
- tau/trust/__init__.py +13 -0
- tau/trust/manager.py +80 -0
- tau/trust/types.py +14 -0
- tau/trust/utils.py +72 -0
- tau/tui/__init__.py +54 -0
- tau/tui/agent_hooks.py +346 -0
- tau/tui/ansi.py +330 -0
- tau/tui/app.py +540 -0
- tau/tui/autocomplete.py +33 -0
- tau/tui/capabilities.py +119 -0
- tau/tui/commands/__init__.py +3 -0
- tau/tui/commands/appearance.py +498 -0
- tau/tui/commands/auth.py +232 -0
- tau/tui/commands/context.py +38 -0
- tau/tui/commands/misc.py +82 -0
- tau/tui/commands/model.py +118 -0
- tau/tui/commands/session.py +464 -0
- tau/tui/component.py +268 -0
- tau/tui/components/__init__.py +0 -0
- tau/tui/components/autocomplete_manager.py +267 -0
- tau/tui/components/autocomplete_picker.py +143 -0
- tau/tui/components/box.py +90 -0
- tau/tui/components/command_palette.py +144 -0
- tau/tui/components/dynamic_border.py +19 -0
- tau/tui/components/file_picker.py +233 -0
- tau/tui/components/image.py +181 -0
- tau/tui/components/inline_selector.py +71 -0
- tau/tui/components/layout.py +1194 -0
- tau/tui/components/message_list.py +692 -0
- tau/tui/components/modal.py +97 -0
- tau/tui/components/model_palette.py +204 -0
- tau/tui/components/picker_overlay.py +174 -0
- tau/tui/components/prompt_overlay.py +236 -0
- tau/tui/components/resume_modal.py +372 -0
- tau/tui/components/select_list.py +222 -0
- tau/tui/components/settings_modal.py +274 -0
- tau/tui/components/settings_schema.py +203 -0
- tau/tui/components/spinner.py +119 -0
- tau/tui/components/text_input.py +396 -0
- tau/tui/components/text_prompt.py +82 -0
- tau/tui/components/tree_select_list.py +580 -0
- tau/tui/components/trust_screen.py +97 -0
- tau/tui/diff.py +114 -0
- tau/tui/fuzzy.py +99 -0
- tau/tui/input.py +496 -0
- tau/tui/input_handler.py +716 -0
- tau/tui/keybindings.py +87 -0
- tau/tui/markdown.py +286 -0
- tau/tui/message_renderers.py +31 -0
- tau/tui/overlay.py +326 -0
- tau/tui/renderer.py +378 -0
- tau/tui/terminal.py +499 -0
- tau/tui/theme.py +148 -0
- tau/tui/tui.py +544 -0
- tau/tui/ui_context.py +768 -0
- tau/tui/utils.py +20 -0
- tau/utils/__init__.py +0 -0
- tau/utils/http_proxy.py +221 -0
- tau/utils/image_processing.py +172 -0
- tau/utils/secrets.py +59 -0
- tau/utils/version_check.py +60 -0
- tau_coding_agent-0.1.0.dist-info/METADATA +177 -0
- tau_coding_agent-0.1.0.dist-info/RECORD +283 -0
- tau_coding_agent-0.1.0.dist-info/WHEEL +5 -0
- tau_coding_agent-0.1.0.dist-info/entry_points.txt +2 -0
- tau_coding_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
- tau_coding_agent-0.1.0.dist-info/top_level.txt +1 -0
tau/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()
|