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