tau-core 0.2.0__tar.gz
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_core-0.2.0/.gitignore +25 -0
- tau_core-0.2.0/LICENSE +21 -0
- tau_core-0.2.0/PKG-INFO +615 -0
- tau_core-0.2.0/README.md +588 -0
- tau_core-0.2.0/pyproject.toml +54 -0
- tau_core-0.2.0/src/tau_core/__init__.py +153 -0
- tau_core-0.2.0/src/tau_core/__main__.py +6 -0
- tau_core-0.2.0/src/tau_core/_version.py +10 -0
- tau_core-0.2.0/src/tau_core/agent.py +783 -0
- tau_core-0.2.0/src/tau_core/approval.py +513 -0
- tau_core-0.2.0/src/tau_core/builder.py +869 -0
- tau_core-0.2.0/src/tau_core/builtin_tools.py +37 -0
- tau_core-0.2.0/src/tau_core/channels/__init__.py +1 -0
- tau_core-0.2.0/src/tau_core/channels/terminal.py +533 -0
- tau_core-0.2.0/src/tau_core/cli.py +128 -0
- tau_core-0.2.0/src/tau_core/commands.py +89 -0
- tau_core-0.2.0/src/tau_core/compaction.py +201 -0
- tau_core-0.2.0/src/tau_core/config.py +479 -0
- tau_core-0.2.0/src/tau_core/events.py +93 -0
- tau_core-0.2.0/src/tau_core/hub/__init__.py +54 -0
- tau_core-0.2.0/src/tau_core/hub/cli.py +374 -0
- tau_core-0.2.0/src/tau_core/hub/control.py +238 -0
- tau_core-0.2.0/src/tau_core/hub/daemon.py +286 -0
- tau_core-0.2.0/src/tau_core/hub/launchd.py +164 -0
- tau_core-0.2.0/src/tau_core/hub/lock.py +101 -0
- tau_core-0.2.0/src/tau_core/hub/state.py +227 -0
- tau_core-0.2.0/src/tau_core/hub/templates/com.tau.hub.plist +48 -0
- tau_core-0.2.0/src/tau_core/hub/templates/tau-hub.service +30 -0
- tau_core-0.2.0/src/tau_core/hub/units.py +100 -0
- tau_core-0.2.0/src/tau_core/i18n.py +366 -0
- tau_core-0.2.0/src/tau_core/logs.py +88 -0
- tau_core-0.2.0/src/tau_core/models.py +265 -0
- tau_core-0.2.0/src/tau_core/persona.py +355 -0
- tau_core-0.2.0/src/tau_core/plugins.py +105 -0
- tau_core-0.2.0/src/tau_core/registry.py +385 -0
- tau_core-0.2.0/src/tau_core/sessions/__init__.py +52 -0
- tau_core-0.2.0/src/tau_core/sessions/base.py +77 -0
- tau_core-0.2.0/src/tau_core/sessions/file.py +400 -0
- tau_core-0.2.0/src/tau_core/sessions/memory.py +180 -0
- tau_core-0.2.0/src/tau_core/status.py +233 -0
- tau_core-0.2.0/src/tau_core/templates/dotenv.example +22 -0
- tau_core-0.2.0/src/tau_core/templates/models.toml +14 -0
- tau_core-0.2.0/src/tau_core/templates/persona.md +46 -0
- tau_core-0.2.0/src/tau_core/templates/tau.toml +64 -0
- tau_core-0.2.0/src/tau_core/templates/user.example.md +13 -0
- tau_core-0.2.0/src/tau_core/testing.py +258 -0
- tau_core-0.2.0/src/tau_core/tiers.py +159 -0
- tau_core-0.2.0/tests/conftest.py +49 -0
- tau_core-0.2.0/tests/test_agent.py +439 -0
- tau_core-0.2.0/tests/test_approval.py +633 -0
- tau_core-0.2.0/tests/test_builder.py +490 -0
- tau_core-0.2.0/tests/test_cli.py +219 -0
- tau_core-0.2.0/tests/test_compaction.py +196 -0
- tau_core-0.2.0/tests/test_config.py +318 -0
- tau_core-0.2.0/tests/test_hook_order.py +244 -0
- tau_core-0.2.0/tests/test_hub_control.py +146 -0
- tau_core-0.2.0/tests/test_hub_daemon.py +445 -0
- tau_core-0.2.0/tests/test_hub_lock.py +125 -0
- tau_core-0.2.0/tests/test_hub_service.py +355 -0
- tau_core-0.2.0/tests/test_hub_state.py +219 -0
- tau_core-0.2.0/tests/test_i18n.py +168 -0
- tau_core-0.2.0/tests/test_logs.py +58 -0
- tau_core-0.2.0/tests/test_loop_guard.py +201 -0
- tau_core-0.2.0/tests/test_models.py +220 -0
- tau_core-0.2.0/tests/test_persona.py +598 -0
- tau_core-0.2.0/tests/test_plugins.py +56 -0
- tau_core-0.2.0/tests/test_registry.py +259 -0
- tau_core-0.2.0/tests/test_repo_rules.py +79 -0
- tau_core-0.2.0/tests/test_resume.py +341 -0
- tau_core-0.2.0/tests/test_sessions.py +383 -0
- tau_core-0.2.0/tests/test_status.py +200 -0
- tau_core-0.2.0/tests/test_stream_contract.py +675 -0
- tau_core-0.2.0/tests/test_terminal_commands.py +376 -0
- tau_core-0.2.0/tests/test_tiers.py +524 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
.env
|
|
3
|
+
.env.*
|
|
4
|
+
!.env.example
|
|
5
|
+
*.local.env
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
|
|
9
|
+
# Python / uv
|
|
10
|
+
.venv/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.ruff_cache/
|
|
13
|
+
dist/
|
|
14
|
+
build/
|
|
15
|
+
*.egg-info/
|
|
16
|
+
|
|
17
|
+
# Personal facts about the user stay local (persona/user.example.md is the tracked template)
|
|
18
|
+
persona/user.md
|
|
19
|
+
|
|
20
|
+
# Tools tau wrote itself, waiting for approval
|
|
21
|
+
tools/_pending/*
|
|
22
|
+
!tools/_pending/.gitkeep
|
|
23
|
+
|
|
24
|
+
# MkDocs build output
|
|
25
|
+
site/
|
tau_core-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 fport
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
tau_core-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tau-core
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: tau's agent core: build_agent, persona, tiers, roles, sessions and the tau CLI.
|
|
5
|
+
Project-URL: Homepage, https://github.com/fport/tau
|
|
6
|
+
Project-URL: Documentation, https://docs.tau.getporti.com
|
|
7
|
+
Project-URL: Issues, https://github.com/fport/tau/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/fport/tau/releases
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,assistant,cli,home-automation,llm,robot,strands,tau
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Home Automation
|
|
21
|
+
Requires-Python: >=3.12
|
|
22
|
+
Requires-Dist: click>=8.1
|
|
23
|
+
Requires-Dist: python-dotenv>=1.0
|
|
24
|
+
Requires-Dist: rich>=13
|
|
25
|
+
Requires-Dist: strands-agents[anthropic]<1.58,>=1.57
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# tau-core
|
|
29
|
+
|
|
30
|
+
The agent core of tau, a personal "smart body" agent platform: `build_agent`, the persona,
|
|
31
|
+
permission tiers, model roles, session stores and the `tau` command line.
|
|
32
|
+
It is built on [Strands Agents](https://strandsagents.com/) and is published on its own as
|
|
33
|
+
`tau-core` (import name `tau_core`).
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pip install tau-core # or, inside the tau repo: uv sync --all-packages --group dev
|
|
37
|
+
tau --help # chat, hub, components, doctor, init, version, plus plugin commands (tui, ...)
|
|
38
|
+
tau init # create ~/.tau/tau.toml and the persona files from the templates
|
|
39
|
+
tau doctor # check config, credentials, sessions, persona, .env, the hub
|
|
40
|
+
tau chat # talk to tau in the terminal
|
|
41
|
+
tau --lang tr chat # same, with Turkish UI chrome
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import asyncio
|
|
46
|
+
from tau_core import build_agent
|
|
47
|
+
|
|
48
|
+
agent = build_agent("home") # config from tau.toml + .env, model from the "brain" role
|
|
49
|
+
print(asyncio.run(agent.ask("Merhaba!")))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`build_agent` makes no hub assumptions: no network of its own, no launchd, no globals. With
|
|
53
|
+
`tau_core.testing.FakeModel` it runs fully offline, which is how the test suite runs.
|
|
54
|
+
|
|
55
|
+
## Configuration
|
|
56
|
+
|
|
57
|
+
`tau.toml` (under `TAU_HOME`) maps each model role to a provider and model id; code only ever
|
|
58
|
+
asks for a role (`brain`, `fast`, `local`). Credentials and roots come from the environment or
|
|
59
|
+
`TAU_HOME/.env` (never overriding variables that are already set): `TAU_HOME` (config root,
|
|
60
|
+
default: the current directory if it has a `tau.toml`, else `~/.tau`), `TAU_DATA_DIR` (mutable
|
|
61
|
+
state, default `~/.local/share/tau`), `TAU_PROFILE` (`home` | `travel` | `sport`, default
|
|
62
|
+
`home`), `TAU_ROLE` (`hub`|`node`), `TAU_HUB_PORT`, `TAU_LANG` (see below). Built-in providers:
|
|
63
|
+
`bedrock`, `anthropic` (with an optional per-role `fallback`) and `fake`; a `tau.providers`
|
|
64
|
+
component adds more (see [Components](#components)). A missing credential stops `tau chat` with
|
|
65
|
+
a message naming the variables; `tau doctor` reports it without starting anything.
|
|
66
|
+
|
|
67
|
+
```toml
|
|
68
|
+
[models.roles.brain]
|
|
69
|
+
provider = "bedrock"
|
|
70
|
+
model_id = "..."
|
|
71
|
+
max_tokens = 4096
|
|
72
|
+
cache = { strategy = "auto", ttl = "1h" } # optional: Strands CacheConfig (bedrock, anthropic)
|
|
73
|
+
|
|
74
|
+
[agent]
|
|
75
|
+
max_turns = 12 # model calls per user turn (Limits(turns=...))
|
|
76
|
+
context = "sliding" # sliding | summarize: the model's window on a long session
|
|
77
|
+
window_size = 60 # sliding: messages kept; summarize: recent messages never summarized
|
|
78
|
+
max_total_tokens = 200000 # optional per-turn token budget (Limits(total_tokens=...))
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`context` picks Strands' `SlidingWindowConversationManager` or `SummarizingConversationManager`;
|
|
82
|
+
the session store still keeps every message, only what the model sees is windowed. A session
|
|
83
|
+
remembers the manager it was recorded with (Strands refuses to restore it under the other one),
|
|
84
|
+
so after changing `context` an earlier session fails to open with `Session <id> was recorded
|
|
85
|
+
with a different [agent] context setting; set it back to sliding or start a new session`
|
|
86
|
+
(`SessionContextMismatch`, a `ConfigError`) in `tau chat --session`/`--resume`, `tau tui
|
|
87
|
+
--resume`, `/resume` and the TUI's picker and sidebar alike; the current session is kept. The SDK's
|
|
88
|
+
`context_manager = "auto"`/`"agentic"` presets are rejected with a `ConfigError`: they register
|
|
89
|
+
untiered SDK tools that the tier gate would refuse. `cache` accepts the `CacheConfig` fields
|
|
90
|
+
(`strategy`, `ttl`, `system_prompt_ttl`, `tools_ttl`); the fake provider ignores it.
|
|
91
|
+
|
|
92
|
+
## UI language and commands
|
|
93
|
+
|
|
94
|
+
Everything a person reads in a channel's chrome (commands, notices, the approval prompt, tier
|
|
95
|
+
labels, the TUI's titles and key hints) comes from one message table, `tau_core.i18n`, so the
|
|
96
|
+
terminal and the TUI never drift. The language is per installation:
|
|
97
|
+
|
|
98
|
+
```toml
|
|
99
|
+
[ui]
|
|
100
|
+
language = "en" # en | tr
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`TAU_LANG` overrides the file, `tau --lang tr ...` overrides both (it is exported as `TAU_LANG`,
|
|
104
|
+
like `--profile`), and `/lang en|tr` switches it for one run. `Config.ui.language` is what a
|
|
105
|
+
channel reads. What the *model* reads (system-prompt scaffolding, tool descriptions, the gate's
|
|
106
|
+
refusal texts) is fixed English; errors raised before a `Config` exists (`ConfigError`,
|
|
107
|
+
`MissingCredentials`, hub messages, `tau hub status`) are plain English too. The persona itself
|
|
108
|
+
is Turkish by default and answers in the language the user writes.
|
|
109
|
+
|
|
110
|
+
Slash commands are shared by `tau chat` and `tau tui` (`tau_core.commands`); the Turkish aliases
|
|
111
|
+
are accepted forever but never shown in hints:
|
|
112
|
+
|
|
113
|
+
| command | aliases | meaning |
|
|
114
|
+
|---|---|---|
|
|
115
|
+
| `/help` | `/yardım`, `/yardim`, `/?` | list the commands with one-line help |
|
|
116
|
+
| `/tools` | `/araçlar`, `/araclar` | registered tools with tier and description |
|
|
117
|
+
| `/sessions` | `/oturumlar` | the 10 most recent earlier sessions, numbered |
|
|
118
|
+
| `/resume [n\|id]` | `/devam` | resume the most recent earlier session, the n-th of the last listing, or an id |
|
|
119
|
+
| `/new` | `/yeni` | start a fresh session |
|
|
120
|
+
| `/session` | `/oturum` | current session id, title, message count, store |
|
|
121
|
+
| `/compact [focus]` | `/özet`, `/ozet` | summarize this session into a new one and continue there; the old one is kept (see [Compaction](#compaction)) |
|
|
122
|
+
| `/clear` | `/temizle`, `/cls` | clear the screen; the session continues, nothing is deleted (`--resume` shows the history again) |
|
|
123
|
+
| `/status` | `/durum` | profile, session, model, context, tools, UI language, versions, hub |
|
|
124
|
+
| `/model [role]` | `/rol` | list the model roles, or switch this session to one (remembered by `/resume`) |
|
|
125
|
+
| `/lang en\|tr` | `/dil` | switch the UI language for this run |
|
|
126
|
+
| `/quit` | `/çık`, `/cik`, `/exit` | leave (`/exit` is an alias, `/quit` is the name the hints show) |
|
|
127
|
+
|
|
128
|
+
Matching is `str.casefold()` on the typed name (`/ARAÇLAR` works). In code:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from tau_core import LANGUAGES, SLASH_COMMANDS, command_hint, resolve_command, t
|
|
132
|
+
|
|
133
|
+
resolve_command("/Devam 2") # ("resume", "2"); None for "/x" or plain text
|
|
134
|
+
command_hint() # "/help, /tools, /sessions, /resume, /new, ..."
|
|
135
|
+
t("session.not_found", "tr", session="x") # "Oturum bulunamadı: x."
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`t(key, language, **kwargs)` formats with `str.format`, raises `KeyError` for an unknown key and
|
|
139
|
+
falls back to English for a language that lacks the key. Plugins add their own keys with
|
|
140
|
+
`tau_core.i18n.register_messages({"mine.hello": {"en": "...", "tr": "..."}})`.
|
|
141
|
+
|
|
142
|
+
## Persona
|
|
143
|
+
|
|
144
|
+
The system prompt is rebuilt before every model call (a Strands `BeforeModelCallEvent` hook), so
|
|
145
|
+
an edit to a persona file applies on the next model call without a restart, even inside a tool
|
|
146
|
+
loop. Its layout is fixed:
|
|
147
|
+
|
|
148
|
+
```text
|
|
149
|
+
<persona/persona.md>
|
|
150
|
+
|
|
151
|
+
## User
|
|
152
|
+
<persona/user.md, or: No user file (persona/user.md). Address the user formally.>
|
|
153
|
+
|
|
154
|
+
## Now
|
|
155
|
+
- Date and time: 29.09.2026 15:30, Tuesday (Europe/Istanbul)
|
|
156
|
+
- Profile: home
|
|
157
|
+
- Host role: hub
|
|
158
|
+
- Live nodes: unknown
|
|
159
|
+
- Budget today: unknown
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The scaffolding (headings, labels, `unknown`) is English because the model reads it; the
|
|
163
|
+
persona decides the reply language. The model receives the prompt as three content blocks
|
|
164
|
+
(`PersonaLoader.build_system_content`): the persona plus `## User` (stable), a `cachePoint`,
|
|
165
|
+
then `## Now` (changes every minute), so a provider that caches system prompts reuses the
|
|
166
|
+
stable part; `build_system_prompt` returns the same text as one string for tests and logging.
|
|
167
|
+
|
|
168
|
+
- `persona.md` is tau's character: Jarvis-style, Turkish by default and mirroring the language
|
|
169
|
+
the user writes in, polite, dry humor, at most two sentences for notifications and voice, no
|
|
170
|
+
jokes in approval requests, never inventing facts about the user. It is tracked, so it holds
|
|
171
|
+
nothing personal.
|
|
172
|
+
- `user.md` holds the facts about the user and how to address them. It is gitignored; start
|
|
173
|
+
from the made-up template with `cp persona/user.example.md persona/user.md`. Without it (or
|
|
174
|
+
when it is empty) tau addresses the user formally ("siz") and knows nothing about them.
|
|
175
|
+
- A file is re-read only when its `(st_mtime_ns, st_size)` changes. HTML comments
|
|
176
|
+
(`<!-- ... -->`) are notes for humans and never reach the model.
|
|
177
|
+
- A missing, empty or unreadable `persona.md` makes `build_agent` raise `ConfigError`. If the
|
|
178
|
+
file breaks later (deleted, truncated mid-save, not UTF-8), the last good version stays in
|
|
179
|
+
use until it is fixed; each such problem is logged once.
|
|
180
|
+
- Time is always Europe/Istanbul. A value no layer provides yet reads `unknown` (an empty
|
|
181
|
+
node list reads `none`), and the persona tells the model never to guess it.
|
|
182
|
+
|
|
183
|
+
Later layers fill the dynamic block through context sources: zero-argument callables that
|
|
184
|
+
return partial overrides (`nodes`, `budget`), merged in order. They run before every model
|
|
185
|
+
call, so they must return a cached value quickly. Unknown keys are ignored, `None` means
|
|
186
|
+
unknown, and a source that raises is skipped and logged once. Without an explicit
|
|
187
|
+
`context_sources` list, `build_agent` uses the enabled `tau.context` components (see
|
|
188
|
+
[Components](#components)); an explicit list, even an empty one, replaces them.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
agent = build_agent(
|
|
192
|
+
context_sources=[
|
|
193
|
+
lambda: {"nodes": ["mac", "arm"]}, # live nodes (L5)
|
|
194
|
+
lambda: {"budget": "fast 0,12 $ / 1 $"}, # today's spend (L2)
|
|
195
|
+
],
|
|
196
|
+
)
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Tiers and approval
|
|
200
|
+
|
|
201
|
+
Every tool declares a tier; a tool without one cannot be registered.
|
|
202
|
+
|
|
203
|
+
| Tier | Meaning | What happens |
|
|
204
|
+
|---|---|---|
|
|
205
|
+
| 0 `Tier.READ` | reads, no side effects | runs freely |
|
|
206
|
+
| 1 `Tier.DIGITAL_WRITE` | changes digital state (files, notes, messages) | runs and is logged |
|
|
207
|
+
| 2 `Tier.PHYSICAL` | acts on the physical world, or activates self-written code | waits for an approval |
|
|
208
|
+
|
|
209
|
+
```python
|
|
210
|
+
from tau_core import Tier, tau_tool
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@tau_tool(tier=Tier.PHYSICAL, effect="Switches the desk lamp on or off.")
|
|
214
|
+
def desk_lamp(state: str) -> str:
|
|
215
|
+
"""Switches the desk lamp on or off.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
state: "on" or "off".
|
|
219
|
+
"""
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
`effect` is the sentence approval prompts show (default: the tool description); write
|
|
223
|
+
descriptions and effects in English, the model reads them. A plain
|
|
224
|
+
Strands `@tool` has no tier: `ToolCatalog.add` raises `UntieredToolError`, `build_agent` refuses
|
|
225
|
+
it when passed in `tools=` and skips it with a warning when a plugin ships it. `tau_tool`
|
|
226
|
+
records the tier and effect in the tool spec (`tool_spec["annotations"]["tau"] = {"tier",
|
|
227
|
+
"effect"}`), so a `@tau_tool` method on a class keeps its tier when accessed through an
|
|
228
|
+
instance (Strands builds a new tool object for bound methods); `tier_of`/`effect_of` read the
|
|
229
|
+
annotation first and the older `tau_tier`/`tau_effect` attributes second.
|
|
230
|
+
|
|
231
|
+
Every call passes `TierGate` (`build_agent` wires it in through `TierHook`, registered at
|
|
232
|
+
`HookOrder.SDK_LAST` so the `tool_use` the gate sees is the one Strands runs, whatever other
|
|
233
|
+
hooks, interventions or plugins rewrite before it):
|
|
234
|
+
|
|
235
|
+
- **Unknown tool** (not in the catalog, looked up live): refused with
|
|
236
|
+
`Tool '{tool}' is not registered; it was not run.` and an `error` agent event.
|
|
237
|
+
- **Tier 0 and 1**: allowed.
|
|
238
|
+
- **Tier 2**: allowed when an exemption matches, otherwise the channel's `Approver` decides.
|
|
239
|
+
Anything but `Decision.APPROVED` (a rejection, a timeout, an approver that raises or answers
|
|
240
|
+
something invalid) refuses the call, and the model reads
|
|
241
|
+
`The user did not approve this action: {tool}. Do not perform it; tell the user briefly.` as
|
|
242
|
+
the tool result. Prompts are asked one at a time.
|
|
243
|
+
- **Every call of every tier** emits a `tool_call` agent event (`tool`, `tier`, `arguments`,
|
|
244
|
+
`status`, `result_summary`, `approval`, `duration_ms`, `tool_use_id`); every tier-2 question
|
|
245
|
+
emits an `approval` event. The log gets tier 0 at `DEBUG`, tiers 1 and 2 at `INFO` (with
|
|
246
|
+
arguments, result summary and approval; `tau chat` writes `INFO` and up to
|
|
247
|
+
`<data_dir>/logs/tau.log`), unknown tools at `WARNING`.
|
|
248
|
+
|
|
249
|
+
A channel (TUI, Telegram, TauBar) implements `Approver` and passes it to `build_agent`:
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
from tau_core import ApprovalRequest, Decision, build_agent
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ButtonApprover:
|
|
256
|
+
name = "telegram"
|
|
257
|
+
timeout_seconds = 600 # optional: stop waiting after 10 minutes and record TIMEOUT
|
|
258
|
+
|
|
259
|
+
async def decide(self, request: ApprovalRequest) -> Decision:
|
|
260
|
+
# show request.tool, request.arguments and request.effect; await the user's button
|
|
261
|
+
...
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
agent = build_agent(approver=ButtonApprover(), channel="telegram")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
- Without an approver, `AutoRejectApprover` rejects every tier-2 call. No approver approves on
|
|
268
|
+
its own; tests script decisions with `tau_core.testing.ScriptedApprover([Decision.APPROVED])`
|
|
269
|
+
and use `UnansweredApprover` for timeouts.
|
|
270
|
+
- `TerminalApprover` (`tau chat`) prints the tool, its arguments as JSON and the effect in the
|
|
271
|
+
UI language (`format_approval_prompt(request, language)`), and approves only on `y`, `yes`,
|
|
272
|
+
`e` or `evet` whatever the language; anything else, Ctrl-D included, rejects. It has no
|
|
273
|
+
timeout: a blocked `input()` cannot be cancelled.
|
|
274
|
+
- **Timeouts:** `TierGate(timeout_seconds=...)`, else the approver's own `timeout_seconds`, else
|
|
275
|
+
none. When it runs out the question is cancelled and the decision is `timeout`; silence never
|
|
276
|
+
approves.
|
|
277
|
+
- **Exemptions** let registered routines (#037) skip per-call approval:
|
|
278
|
+
`build_agent(exemptions=[fn])` or `agent.gate.add_exemption(fn)` (returns an undo function).
|
|
279
|
+
`fn(tool_name, arguments)` sees a copy of the arguments and must return `True`; it is asked
|
|
280
|
+
only for tier-2 calls to known tools, one that raises does not exempt, and the call is still
|
|
281
|
+
recorded with `approval: "exempt"`.
|
|
282
|
+
|
|
283
|
+
`TierGate.check()` and `record()` do not need Strands, so the reflex engine and the MCP server
|
|
284
|
+
can use the same policy.
|
|
285
|
+
|
|
286
|
+
**Extra components.** `build_agent(hooks=[...], plugins=[...])` passes Strands `HookProvider`s
|
|
287
|
+
and `Plugin`s through to the `Agent`. The gate keeps the last word: `TierHook` runs at
|
|
288
|
+
`HookOrder.SDK_LAST`, no `BeforeToolCallEvent` callback may run after it (`build_agent`
|
|
289
|
+
checks the registry and raises `ConfigError` for a hook registered above `SDK_LAST`, since the
|
|
290
|
+
SDK accepts any order), and a tool a plugin registers behind the catalog's back is refused as
|
|
291
|
+
unknown (fail closed); a component that wants its tools to run ships them through `tau.tools`
|
|
292
|
+
with a tier. A hook added to the SDK agent *after* build cannot be refused; the gate then
|
|
293
|
+
records a refused call that ran, or an approved call whose arguments changed, as `error`
|
|
294
|
+
with an `error` event (`where: tier_gate`). When any registered tool is tier 2, `build_agent` uses the SDK's
|
|
295
|
+
`SequentialToolExecutor`, so two approved physical calls in one model turn never run at the
|
|
296
|
+
same time. Every span carries `trace_attributes` `tau.session_id`, `tau.channel`,
|
|
297
|
+
`tau.profile`.
|
|
298
|
+
|
|
299
|
+
## Turns and stop reasons
|
|
300
|
+
|
|
301
|
+
`TauAgent.stream(prompt)` runs one user turn and yields `TurnEvent`s; `ask(prompt)` returns
|
|
302
|
+
only the text. Calls are serialized with an `asyncio.Lock` (created per event loop), so two
|
|
303
|
+
channels on one agent queue up instead of overlapping.
|
|
304
|
+
|
|
305
|
+
| event | meaning |
|
|
306
|
+
|---|---|
|
|
307
|
+
| `TextDelta(text)` | a piece of the reply |
|
|
308
|
+
| `ReasoningDelta(text)` | a piece of the model's extended thinking (SDK `reasoningText`) |
|
|
309
|
+
| `ToolUseStarted(tool_use_id, tool, tier)` | the model asked for a tool (`tier` is `None` for an unknown tool) |
|
|
310
|
+
| `ToolProgress(tool_use_id, tool, text)` | progress from a streaming tool (SDK `tool_stream_event`) |
|
|
311
|
+
| `ToolFinished(tool_use_id, tool, status, summary, approval)` | from the `tool_call` event the gate recorded |
|
|
312
|
+
| `Throttled(delay_seconds)` | the provider throttled the call; the SDK retries after the delay |
|
|
313
|
+
| `Notice(text)` | something the user should read, in the UI language |
|
|
314
|
+
| `TurnEnd(stop_reason)` | always the last event |
|
|
315
|
+
|
|
316
|
+
`to_turn_event(event)` is the pure mapping from the SDK's dict events (`data`,
|
|
317
|
+
`current_tool_use`, `reasoningText`, `tool_stream_event`, `event_loop_throttled_delay`,
|
|
318
|
+
`force_stop`); the terminal prints `Throttled` as a dim line and ignores reasoning and progress
|
|
319
|
+
for now. `TurnEnd.stop_reason` (`tau_core.STOP_REASONS`):
|
|
320
|
+
|
|
321
|
+
| stop reason | what happened | notice / event |
|
|
322
|
+
|---|---|---|
|
|
323
|
+
| `end_turn` | the model finished | – |
|
|
324
|
+
| `limit_turns` | `[agent] max_turns` model calls were used; the last call's tools still ran | `notice.turn_limit`, `turn_limit` event |
|
|
325
|
+
| `limit_total_tokens` | `[agent] max_total_tokens` was reached | `notice.total_tokens`, `turn_limit` event |
|
|
326
|
+
| `max_tokens` | the reply hit the provider's output limit; the partial reply is kept | `notice.max_tokens`, `notice` event |
|
|
327
|
+
| `cancelled` | `TauAgent.cancel()` stopped the turn at the SDK's next safe point; a cancel while no turn runs is ignored | `notice.cancelled`, `notice` event |
|
|
328
|
+
| `interrupt` | a hook kept interrupting after `max_turns` refusals; tau cleared the SDK's interrupt state | `notice.interrupt_refused`, `error` event |
|
|
329
|
+
| `error` | the model call failed (`notice.model_error`), the context window overflowed (`notice.overflow`) | `error` event with `where`: `model`, `context` or `interrupt` |
|
|
330
|
+
| `busy` | another invocation held the SDK's concurrency guard; nothing ran | `notice.busy`, no event |
|
|
331
|
+
|
|
332
|
+
tau never appends to the history by hand. When a turn leaves the history *dangling* - on a
|
|
333
|
+
`user` message (a failed model call, the loop guard or token budget after a tool result, a
|
|
334
|
+
cancel around tool execution, a session restored after a crash) or on an `assistant` message
|
|
335
|
+
with a `toolUse` and no result (the hub died while an approval was pending, or an interrupt
|
|
336
|
+
was given up) - the **next** `stream()` sends a two-message prompt to the SDK: an `assistant`
|
|
337
|
+
message with the last notice in parentheses (or "The previous turn did not finish." for a
|
|
338
|
+
crash-restored session), then the new `user` message. Both go through the SDK's own append
|
|
339
|
+
path, so they get tracking ids and reach the session store like every other message; for a
|
|
340
|
+
dangling `toolUse` the SDK first inserts its own "Tool was interrupted." result, so roles keep
|
|
341
|
+
alternating. The pending notice survives a `busy` turn (nothing was appended) and a window
|
|
342
|
+
trim (the check is by identity of the last message, not by length). A cancel during model
|
|
343
|
+
streaming needs no repair: the SDK stores its own "Cancelled by user" placeholder as the
|
|
344
|
+
reply. A hook that raises a Strands interrupt (`event.interrupt(...)`) gets
|
|
345
|
+
`{"tau": "rejected"}` as the answer inside the same turn, once per interrupt up to
|
|
346
|
+
`max_turns` refusals, with the turn's remaining `Limits` budget; the user sees
|
|
347
|
+
`notice.interrupt_refused`. If that does not clear it, or a session comes back from the store
|
|
348
|
+
in interrupt state, tau leaves the SDK's interrupt state itself and repairs on the next turn,
|
|
349
|
+
so the agent is never left stuck (interrupt-based approvals are issue 068).
|
|
350
|
+
|
|
351
|
+
## Sessions
|
|
352
|
+
|
|
353
|
+
Every conversation is a session: its messages, its agent events (tool calls, approval
|
|
354
|
+
decisions, turn limits, errors) and its metadata (channel, profile, title = the first user
|
|
355
|
+
message, cut to 60 characters). Callers only use `SessionStore`, a Strands
|
|
356
|
+
`SessionRepository` plus `list_sessions`, `append_event`/`read_events`, `write_meta`/`read_meta`
|
|
357
|
+
and `event_sink`. `[sessions].backend` in `tau.toml` picks the backend and
|
|
358
|
+
`open_session_store(config)` builds it:
|
|
359
|
+
|
|
360
|
+
| Backend | Where | Notes |
|
|
361
|
+
|----------|----------------------------------------|-----------------------------------------|
|
|
362
|
+
| `memory` | the process | tests and throwaway runs |
|
|
363
|
+
| `file` | `TAU_DATA_DIR/sessions/<session id>/` | the default in the repo's `tau.toml` |
|
|
364
|
+
| `cloud` | D1 through the tau-cloud Worker API | issue 018; the hub holds no D1 secrets |
|
|
365
|
+
|
|
366
|
+
The `file` layout, one directory per session (ids look like `20260929-153012-3f9a`, UTC):
|
|
367
|
+
|
|
368
|
+
```
|
|
369
|
+
session.json Strands session record; updated_at orders /sessions
|
|
370
|
+
meta.json channel, profile, title
|
|
371
|
+
events.jsonl one agent event per line, append-only
|
|
372
|
+
agents/tau/agent.json Strands agent state
|
|
373
|
+
agents/tau/messages/000000.json one file per message, in order
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
JSON files are replaced atomically (temp file, fsync, rename), directories are `0700` and
|
|
377
|
+
files `0600`, symlinks and ids that are not plain names are refused. **Nothing is ever
|
|
378
|
+
deleted**: the store has no delete method and never overwrites a message; retention is "keep
|
|
379
|
+
everything".
|
|
380
|
+
|
|
381
|
+
In `tau chat`:
|
|
382
|
+
|
|
383
|
+
- `/sessions` lists the last 10 earlier sessions that have messages
|
|
384
|
+
(`n. id date channel title (N messages)`).
|
|
385
|
+
- `/resume` resumes the most recent earlier session and says which one; `/resume <n>` resumes
|
|
386
|
+
the n-th of the last `/sessions` listing (without a listing, of the one `/sessions` would
|
|
387
|
+
print now); `/resume <id>` resumes that session. `tau chat --resume [ID]` does the same at
|
|
388
|
+
start-up.
|
|
389
|
+
- `/new` starts a fresh session; `/session` shows the current id, title, message count and
|
|
390
|
+
backend; `/clear` erases the screen (an ANSI clear, then the ready banner again) and nothing
|
|
391
|
+
else: the session and the agent go on, and `--resume` shows the history again.
|
|
392
|
+
- `/status` prints the profile, the session (id, title, stored message count, role), the model
|
|
393
|
+
(role → provider, whether the role's `fallback` is in use, the model id; never a key), the
|
|
394
|
+
context mode and window with the in-memory message count, the tools (registered, how many
|
|
395
|
+
need approval), the UI language, the installed `tau-*` versions and whether a hub answers on
|
|
396
|
+
`http://127.0.0.1:<control_port>/health` (the `tau doctor` probe, one second, information
|
|
397
|
+
only).
|
|
398
|
+
- `/model` lists the roles of `tau.toml` with their providers and marks the current one;
|
|
399
|
+
`/model fast` rebuilds the agent on the **same** session with that role: the new agent is
|
|
400
|
+
built first (`build_agent(role=..., session_id=<current>)`, which restores the stored history
|
|
401
|
+
and takes over a pending repair notice), and only then is the previous one closed, so a
|
|
402
|
+
failed build changes nothing. Every session records the `role` it last ran on in its
|
|
403
|
+
metadata (`build_agent` writes it next to `channel` and `profile`), so `/resume` and
|
|
404
|
+
`tau chat --resume` come back on that role, also for sessions opened by `/new` and
|
|
405
|
+
`/compact`. `tau chat --role ROLE` pins the role for the run and wins over the stored one
|
|
406
|
+
(and is recorded in turn); an unknown role, missing credentials or an unavailable provider
|
|
407
|
+
leave the current agent unchanged and print why. `TauAgent.role` is the role an agent was
|
|
408
|
+
built for.
|
|
409
|
+
|
|
410
|
+
Resuming rebuilds the agent on the stored session (`build_agent(session_id=...,
|
|
411
|
+
session_store=...)`), so Strands restores `agent.messages`; the model sees the last
|
|
412
|
+
`[agent] window_size` messages (or a summary plus the recent ones), the store keeps them all.
|
|
413
|
+
A new backend implements the nine `SessionRepository` methods plus the five extras and arrives
|
|
414
|
+
as a `tau.sessions` entry point or through `register_session_backend(name, factory)` (see
|
|
415
|
+
[Components](#components)); `SESSION_BACKENDS` is the built-in seed.
|
|
416
|
+
|
|
417
|
+
### Compaction
|
|
418
|
+
|
|
419
|
+
`/compact [focus]` is how a long conversation goes on without its whole history, while every
|
|
420
|
+
session stays on record:
|
|
421
|
+
|
|
422
|
+
1. `tau_core.compaction.summarize(agent, focus=...)` runs a throwaway Strands `Agent` on the
|
|
423
|
+
agent's **own model** over a deep copy of `agent.messages` (no tools, no hooks, no session
|
|
424
|
+
manager, so the tier gate is not involved and the live agent is untouched), with an English
|
|
425
|
+
system prompt that asks for a continuation summary: facts about the user, decisions, open
|
|
426
|
+
tasks, tool results that still matter, and the language the user writes in; `focus` is
|
|
427
|
+
appended as `Focus on: ...`. An empty history, a failed model call or an empty reply raise
|
|
428
|
+
`CompactionError` and nothing changes.
|
|
429
|
+
2. `seed_messages(summary, previous_session_id)` is the two-message history the new session
|
|
430
|
+
starts with: a user message `Summary of the previous conversation (session <id>):` plus the
|
|
431
|
+
summary, and the assistant's `Understood. I will continue from this summary.`.
|
|
432
|
+
3. `build_agent(seed_messages=..., session_id=None)` opens the new session with that history.
|
|
433
|
+
The seed goes in as the Strands `Agent(messages=...)`; the SDK's `RepositorySessionManager`
|
|
434
|
+
writes every initial message to the store when it creates the agent record, so the seed is
|
|
435
|
+
part of the session and survives `--resume` (a test proves the file-store round trip).
|
|
436
|
+
4. The new session's metadata gets `title` (the old title, or its first user line) and
|
|
437
|
+
`compacted_from = <old id>`; the old session gets `compacted_to = <new id>` and keeps every
|
|
438
|
+
message. `/sessions` lists both.
|
|
439
|
+
|
|
440
|
+
The channel then prints `Compacted N messages into a summary. New session <id>; the previous
|
|
441
|
+
session <old id> is kept.` and the summary itself. `N` is what was summarized: the messages in
|
|
442
|
+
memory, which the conversation window may have trimmed below the stored count.
|
|
443
|
+
|
|
444
|
+
## Hub
|
|
445
|
+
|
|
446
|
+
`tau hub run` runs tau as a long-lived process in the hub role. Only one hub runs per machine
|
|
447
|
+
(`flock` on `<TAU_DATA_DIR>/hub/hub.lock`; a second start exits 1 with `tau hub is already
|
|
448
|
+
running (pid N)…`). While it runs it refreshes `hub/state.json` every `[hub].heartbeat_seconds`; SIGTERM
|
|
449
|
+
or SIGINT stops it cleanly and writes `stopped_at`. A start that finds a `state.json` without
|
|
450
|
+
`stopped_at` records a crash report (`crash_reports.jsonl`, `last_crash.json`); the reason comes
|
|
451
|
+
from `crash.json` when the dying hub could write one, else `unexpected exit (no clean shutdown)`.
|
|
452
|
+
|
|
453
|
+
```sh
|
|
454
|
+
tau hub run # foreground; Ctrl-C or `tau hub stop` stops it
|
|
455
|
+
tau hub status # running or not, heartbeat, last crash, launchd state (--json for scripts)
|
|
456
|
+
tau hub stop # SIGTERM to the running hub; waits until it is gone
|
|
457
|
+
tau hub install # macOS: launchd user agent, restarted after a crash or kill
|
|
458
|
+
tau hub uninstall
|
|
459
|
+
tau hub systemd-unit # prints the systemd user unit (RPi5 hub)
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
The control API listens on `127.0.0.1:<control_port>` only (default 7877, `TAU_HUB_PORT`
|
|
463
|
+
overrides) and speaks JSON: `GET /health` → `{"ok": true, "pid", "uptime_s"}`, `GET /status` →
|
|
464
|
+
`role`, `profile`, `version`, `started_at`, `heartbeat_at`, `uptime_s`, `last_crash`,
|
|
465
|
+
`approvals_pending`, `nodes`. Requests with a non-loopback `Host` header get 403.
|
|
466
|
+
|
|
467
|
+
Extending the hub: a `HubService` has a `name`, `async start(hub)` and `async stop()`; services
|
|
468
|
+
start in order after the crash check and stop in reverse. `tau hub run` starts the control API
|
|
469
|
+
first and then every enabled `tau.hub_services` component (see [Components](#components)): the
|
|
470
|
+
entry point is a `factory(config) -> HubService`, the service is *optional* (one that fails to
|
|
471
|
+
build or to start is logged and skipped, the hub runs on), and inside `start(hub)` it can call
|
|
472
|
+
`hub.add_status_source(fn)` to add keys to `/status` and
|
|
473
|
+
`hub.find_service("control-api").add_route(method, path, handler)` to add routes (handlers run
|
|
474
|
+
in the server thread; reach the event loop with `asyncio.run_coroutine_threadsafe(..., hub.loop)`).
|
|
475
|
+
Routes are additive only: `add_route` raises `ValueError` for a `(method, path)` that exists, so
|
|
476
|
+
no service can replace `GET /health`, `GET /status` or another service's route (the service
|
|
477
|
+
then fails to start and is skipped). `find_service` returns only services that have *started*:
|
|
478
|
+
`None` for one that failed to start, has not started yet or after the hub stopped, so a service
|
|
479
|
+
never wires into a half-initialised component. A service's own settings live in
|
|
480
|
+
`[component.<name>]` and are read with `config.component(name)`.
|
|
481
|
+
|
|
482
|
+
```python
|
|
483
|
+
class TelegramRelay: # in another distribution
|
|
484
|
+
name = "telegram"
|
|
485
|
+
|
|
486
|
+
def __init__(self, config):
|
|
487
|
+
self.settings = config.component("telegram")
|
|
488
|
+
|
|
489
|
+
async def start(self, hub):
|
|
490
|
+
hub.add_status_source(lambda: {"telegram": {"connected": True}})
|
|
491
|
+
api = hub.find_service("control-api")
|
|
492
|
+
api.add_route("POST", "/telegram/send", self.send)
|
|
493
|
+
|
|
494
|
+
async def stop(self): ...
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def make_relay(config): # [project.entry-points."tau.hub_services"] telegram = "pkg:make_relay"
|
|
498
|
+
return TelegramRelay(config)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
launchd is driven through the `Launchctl`
|
|
502
|
+
protocol; tests pass `FakeLaunchctl` to the CLI as `obj={"launchctl": fake}`. The launchd plist
|
|
503
|
+
and systemd unit are package templates; `infra/services/` in the tau repo has rendered examples
|
|
504
|
+
and the install notes.
|
|
505
|
+
|
|
506
|
+
## Components
|
|
507
|
+
|
|
508
|
+
Every pluggable part of tau is a *component*: it reaches tau through a built-in seed
|
|
509
|
+
(`PROVIDERS`, `SESSION_BACKENDS`), an entry point in a `tau.*` group declared by any installed
|
|
510
|
+
distribution, or an in-process registration (`register_provider`, `register_session_backend`),
|
|
511
|
+
and `tau.toml` decides which discovered components are on (ADR 0005). `tau_core.registry` loads
|
|
512
|
+
them all with one rule: an entry point that fails to load, or loads the wrong kind of object,
|
|
513
|
+
is logged with its distribution name and skipped; it never breaks the CLI.
|
|
514
|
+
|
|
515
|
+
| group | entry point value | picked by |
|
|
516
|
+
|---|---|---|
|
|
517
|
+
| `tau.tools` | an `AgentTool` with a tier, a list of them, or a zero-argument factory | `build_agent` (all, minus `tools.disabled`) |
|
|
518
|
+
| `tau.commands` | a `click.Command`, mounted as `tau <name>` | the `tau` command |
|
|
519
|
+
| `tau.providers` | `factory(RoleConfig, env) -> Model`; optional `required_env = [["VAR", ...], ...]` on the factory (credential alternatives) | a role's `provider` in `tau.toml` |
|
|
520
|
+
| `tau.sessions` | `factory(Config) -> SessionStore` | `[sessions].backend` |
|
|
521
|
+
| `tau.context` | `factory(Config) -> ContextSource` (a callable returning `{"nodes": ..., "budget": ...}`) | `build_agent` when no `context_sources` are passed |
|
|
522
|
+
| `tau.hub_services` | `factory(Config) -> HubService` | `tau hub run`, after the control API |
|
|
523
|
+
|
|
524
|
+
`tau.channels` stays reserved for hub-hosted channels. A plugin package declares what it ships:
|
|
525
|
+
|
|
526
|
+
```toml
|
|
527
|
+
[project.entry-points."tau.tools"]
|
|
528
|
+
mine = "my_package:TOOLS" # tools carry a tier (tau_tool); untiered ones are skipped
|
|
529
|
+
|
|
530
|
+
[project.entry-points."tau.commands"]
|
|
531
|
+
mine = "my_package.cli:command" # `tau mine`
|
|
532
|
+
|
|
533
|
+
[project.entry-points."tau.providers"]
|
|
534
|
+
ollama = "my_package.models:make_model" # make_model(role_cfg, env) -> Model; make_model.required_env = []
|
|
535
|
+
|
|
536
|
+
[project.entry-points."tau.sessions"]
|
|
537
|
+
cloud = "my_package.sessions:make_store" # make_store(config) -> SessionStore
|
|
538
|
+
|
|
539
|
+
[project.entry-points."tau.context"]
|
|
540
|
+
nodes = "my_package.mesh:make_source" # make_source(config) -> callable returning {"nodes": [...]}
|
|
541
|
+
|
|
542
|
+
[project.entry-points."tau.hub_services"]
|
|
543
|
+
telegram = "my_package.relay:make_relay" # make_relay(config) -> HubService
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
A seed name wins over an entry point with the same name (an installed package cannot replace
|
|
547
|
+
`bedrock` silently); `register_provider` / `register_session_backend` replace anything, since
|
|
548
|
+
they are explicit code. `Config.load` validates provider and backend *names* without loading a
|
|
549
|
+
single entry point, so `tau version` never imports a provider SDK.
|
|
550
|
+
|
|
551
|
+
Switch discovered components on or off in `tau.toml` without uninstalling them:
|
|
552
|
+
|
|
553
|
+
```toml
|
|
554
|
+
[components]
|
|
555
|
+
context = [] # enabled tau.context sources; empty = every discovered one
|
|
556
|
+
[components.tools]
|
|
557
|
+
disabled = ["demo_physical_action", "dist:tau-example-tool"] # tool names or dist:<distribution>
|
|
558
|
+
[components.hub]
|
|
559
|
+
services = [] # enabled tau.hub_services; empty = every discovered one
|
|
560
|
+
|
|
561
|
+
[component.telegram] # a component's own settings: config.component("telegram")
|
|
562
|
+
chat_id_env = "TELEGRAM_CHAT_ID"
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
`tools.disabled` can only remove tools; it never adds one, gives one a tier or changes a tier,
|
|
566
|
+
and an explicit `build_agent(tools=[...])` bypasses it. Any other top-level table in `tau.toml`
|
|
567
|
+
is logged once and ignored, never a failure. Nothing in `[components]` can replace the tier
|
|
568
|
+
gate, the approver or the catalog: a tool that reaches the Strands agent behind the catalog's
|
|
569
|
+
back (through a `Plugin`, for instance) is refused as unknown.
|
|
570
|
+
|
|
571
|
+
Three commands show, check and set up an installation (English, `--json` where useful):
|
|
572
|
+
|
|
573
|
+
```sh
|
|
574
|
+
tau components # every component in every group: distribution, version, enabled/disabled, load error
|
|
575
|
+
tau doctor # config, per-role provider + credentials (no client built), session store, persona
|
|
576
|
+
# files, .env names vs .env.example, component load errors, [components] names that
|
|
577
|
+
# no installed component provides, control API reachable; exit 1 when a check fails
|
|
578
|
+
# ("warn" for a planned provider such as ollama and for the unknown names)
|
|
579
|
+
tau init # tau.toml from the shipped template (provider: bedrock > anthropic > fake, by the
|
|
580
|
+
# credentials present in the environment or <home>/.env), persona/persona.md,
|
|
581
|
+
# user.example.md, user.md (from the example unless it exists) and .env.example;
|
|
582
|
+
# asks the UI language unless --language or --yes
|
|
583
|
+
tau init --home ~/.tau --provider anthropic --language tr --profile home --yes # non-interactive
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
`tau init` never overwrites an existing `tau.toml` or `persona.md` without `--force` and never
|
|
587
|
+
overwrites `user.md` or `.env.example` (the latter is the list of names `tau doctor` checks
|
|
588
|
+
`.env` against; add your own names there); `--profile` appends `TAU_PROFILE` to `<home>/.env`
|
|
589
|
+
when it is not set there. The shipped templates equal the repository's `tau.toml`,
|
|
590
|
+
`persona/persona.md` and `persona/user.example.md` (a test keeps them identical); the
|
|
591
|
+
`.env.example` template is the repository's file without its L0 remote-access section. A
|
|
592
|
+
`tau.providers` entry point that is declared but does not load is reported as `Model provider
|
|
593
|
+
'x' from <distribution> failed to load: ...` by `tau doctor`, `tau chat` and `tau components`
|
|
594
|
+
alike; only a name no distribution declares is an `Unknown model provider`.
|
|
595
|
+
|
|
596
|
+
## Logging
|
|
597
|
+
|
|
598
|
+
`configure_logging(logs_dir, verbose=False)` sends `WARNING`s (or `INFO` with `verbose`) to
|
|
599
|
+
stderr as one-line messages and, when `logs_dir` is given, `INFO` and up with tracebacks to
|
|
600
|
+
`<logs_dir>/tau.log` (`tau chat` and `tau hub run` use `config.logs_dir`); it returns the log
|
|
601
|
+
file path. `reset_logging()` removes those handlers again. Both are part of the public API so a
|
|
602
|
+
channel in another package (the TUI, Telegram) writes to the same file; a full-screen UI that
|
|
603
|
+
owns the terminal passes `console=False` to keep stderr silent while the file log still
|
|
604
|
+
receives every record.
|
|
605
|
+
|
|
606
|
+
## Development
|
|
607
|
+
|
|
608
|
+
```sh
|
|
609
|
+
uv sync --all-packages --group dev
|
|
610
|
+
uv run pytest -q
|
|
611
|
+
uv run ruff check . && uv run ruff format .
|
|
612
|
+
uv build --package tau-core
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
License: MIT.
|