tanglebrain 0.16.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.
- tanglebrain/__init__.py +23 -0
- tanglebrain/adapters/__init__.py +15 -0
- tanglebrain/adapters/api.py +65 -0
- tanglebrain/adapters/base.py +46 -0
- tanglebrain/adapters/cli.py +341 -0
- tanglebrain/adapters/openai_compat.py +197 -0
- tanglebrain/classifier.py +99 -0
- tanglebrain/cli.py +251 -0
- tanglebrain/config/pricing.yaml +14 -0
- tanglebrain/config/roster.yaml +129 -0
- tanglebrain/config/settings.yaml +39 -0
- tanglebrain/delegate.py +485 -0
- tanglebrain/gui/__init__.py +10 -0
- tanglebrain/gui/server.py +162 -0
- tanglebrain/gui/static/index.html +295 -0
- tanglebrain/gui/static/logo.png +0 -0
- tanglebrain/gui/views.py +180 -0
- tanglebrain/mcp_server.py +208 -0
- tanglebrain/measurement.py +548 -0
- tanglebrain/roster.py +415 -0
- tanglebrain/roster_edit.py +201 -0
- tanglebrain/router.py +232 -0
- tanglebrain/selector.py +117 -0
- tanglebrain/settings.py +132 -0
- tanglebrain-0.16.0.dist-info/METADATA +369 -0
- tanglebrain-0.16.0.dist-info/RECORD +30 -0
- tanglebrain-0.16.0.dist-info/WHEEL +5 -0
- tanglebrain-0.16.0.dist-info/entry_points.txt +4 -0
- tanglebrain-0.16.0.dist-info/licenses/LICENSE +21 -0
- tanglebrain-0.16.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""MCP server exposing TangleBrain's delegate tools to an orchestrator.
|
|
2
|
+
|
|
3
|
+
A stdio MCP server an orchestrator (e.g. claude / codex / gemini) registers so it can offload
|
|
4
|
+
sub-tasks to a configured backend mid-task — the mechanism that makes frontier-first decompose
|
|
5
|
+
actually offload work rather than running everything on the orchestrator itself. It exposes four
|
|
6
|
+
tools: ``delegate_local`` (free local default), ``delegate`` (route to a configured ``can_delegate``
|
|
7
|
+
target by id or by capability), ``delegate_many`` (fan several sub-tasks out concurrently), and
|
|
8
|
+
``delegate_targets`` (the configured target menu).
|
|
9
|
+
|
|
10
|
+
It is a **thin wrapper** over :mod:`tanglebrain.delegate` (which reuses the roster + selector +
|
|
11
|
+
adapters): the routing logic lives there, MCP plumbing lives here. The tools are **sync** — FastMCP
|
|
12
|
+
runs sync tools in a worker thread, so they can call the sync adapter directly without duplicating
|
|
13
|
+
the HTTP call as async.
|
|
14
|
+
|
|
15
|
+
The ``delegate`` tool's description enumerates the configured targets; it is built **once at server
|
|
16
|
+
startup** from the roster, so a roster edit is reflected on the next server launch (orchestrators
|
|
17
|
+
spawn the server per session). The live menu is always available via the ``delegate_targets`` tool.
|
|
18
|
+
|
|
19
|
+
Threat model: the server performs **no authentication** — any process that can reach its stdio is
|
|
20
|
+
trusted to delegate unlimited prompts to the local model. That matches the intended use (a local
|
|
21
|
+
orchestrator CLI launches it as a child); do not expose it beyond the launching CLI.
|
|
22
|
+
|
|
23
|
+
Requires the optional ``mcp`` dependency: ``pip install "tanglebrain[delegate]"``.
|
|
24
|
+
|
|
25
|
+
Run it directly for a manual smoke test::
|
|
26
|
+
|
|
27
|
+
tanglebrain-delegate # serves over stdio
|
|
28
|
+
|
|
29
|
+
or register it with an orchestrator CLI (flag shapes vary by version — see the README)::
|
|
30
|
+
|
|
31
|
+
claude mcp add tanglebrain-delegate -- tanglebrain-delegate
|
|
32
|
+
"""
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
|
|
37
|
+
from mcp.server.fastmcp import FastMCP
|
|
38
|
+
|
|
39
|
+
from tanglebrain.delegate import (
|
|
40
|
+
DEFAULT_DELEGATE_MAX_TOKENS,
|
|
41
|
+
NoDelegateFit,
|
|
42
|
+
_render_target_menu,
|
|
43
|
+
delegate_targets as _list_delegate_targets,
|
|
44
|
+
run_delegate,
|
|
45
|
+
run_delegate_many,
|
|
46
|
+
run_local_delegate,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
mcp = FastMCP("tanglebrain-delegate")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _delegate_tool_description() -> str:
|
|
53
|
+
"""Build the ``delegate`` tool description, enumerating the configured targets from the roster.
|
|
54
|
+
|
|
55
|
+
Best-effort: if the roster can't be loaded at startup the menu is replaced with a short note
|
|
56
|
+
(the server still starts; ``delegate_targets`` / ``delegate`` surface the real error on call).
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
The full tool-description string handed to ``@mcp.tool(description=...)``.
|
|
60
|
+
"""
|
|
61
|
+
header = (
|
|
62
|
+
"Delegate a self-contained sub-task to a CONFIGURED backend and return its text.\n\n"
|
|
63
|
+
"Two ways to choose where it goes (in precedence order):\n"
|
|
64
|
+
" - `target` = one of the configured target ids listed below (explicit; wins if both given).\n"
|
|
65
|
+
" - `task` = a capability tag (a `good_at` value, e.g. `code`, `summarization`); TangleBrain "
|
|
66
|
+
"picks the cheapest configured backend good_at that capability for you. If none fits, the "
|
|
67
|
+
"tool tells you to handle the sub-task yourself — you are the most capable backend here.\n"
|
|
68
|
+
"Omit both (or use the delegate_local tool) to use the free local model. Paid backends are "
|
|
69
|
+
"never auto-selected by `task` — reach one only by naming it explicitly as `target`.\n"
|
|
70
|
+
"Hand the result back for review rather than trusting it blind.\n\n"
|
|
71
|
+
"Configured delegate targets:\n"
|
|
72
|
+
)
|
|
73
|
+
try:
|
|
74
|
+
menu = _render_target_menu(_list_delegate_targets())
|
|
75
|
+
except Exception as exc: # roster unreadable at startup — keep the server usable
|
|
76
|
+
menu = f" (could not load the target menu: {exc}; call delegate_targets to retry)"
|
|
77
|
+
return header + menu
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@mcp.tool()
|
|
81
|
+
def delegate_local(prompt: str, max_tokens: int = DEFAULT_DELEGATE_MAX_TOKENS) -> str:
|
|
82
|
+
"""Delegate a self-contained sub-task to TangleBrain's free local model (gpt-oss-120b).
|
|
83
|
+
|
|
84
|
+
Use this whenever you (the orchestrator) would otherwise spend your own rate-limited
|
|
85
|
+
tokens on bulk work that doesn't need your full capability: code generation, refactoring,
|
|
86
|
+
drafting, extraction, transformation, summarization, boilerplate, test writing. It runs on
|
|
87
|
+
a local 120B model at **$0 marginal cost** and unlimited throughput, so offload freely and
|
|
88
|
+
keep your own budget for decomposition and review.
|
|
89
|
+
|
|
90
|
+
Hand the result back for review rather than trusting it blind — you decide whether to accept,
|
|
91
|
+
re-delegate with a tighter prompt, or do it yourself.
|
|
92
|
+
|
|
93
|
+
On failure (endpoint down, bad config, timeout) this raises and you see the error — there is
|
|
94
|
+
no transparent retry or model swap here; you decide what to do next.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
prompt: The self-contained sub-task to delegate. Give it everything it needs — the local
|
|
98
|
+
model has no access to your conversation context.
|
|
99
|
+
max_tokens: Completion token cap (default 2048 — the local model needs headroom for its
|
|
100
|
+
internal reasoning before emitting the final answer).
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
The local model's final response text.
|
|
104
|
+
"""
|
|
105
|
+
return run_local_delegate(prompt, max_tokens=max_tokens)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# NB: the description is evaluated at import time (decorator argument), so importing this module
|
|
109
|
+
# reads the roster once to build the target menu — intentional ("built once at server startup"),
|
|
110
|
+
# and guarded inside _delegate_tool_description so an unreadable roster can't crash import.
|
|
111
|
+
@mcp.tool(description=_delegate_tool_description())
|
|
112
|
+
def delegate(
|
|
113
|
+
prompt: str,
|
|
114
|
+
target: str | None = None,
|
|
115
|
+
task: str | None = None,
|
|
116
|
+
max_tokens: int = DEFAULT_DELEGATE_MAX_TOKENS,
|
|
117
|
+
) -> str:
|
|
118
|
+
"""Delegate a sub-task to a configured backend (see the tool description for the target menu).
|
|
119
|
+
|
|
120
|
+
Choose where the sub-task goes by precedence: explicit ``target`` id (wins if both are given) →
|
|
121
|
+
``task`` capability (TangleBrain picks the cheapest configured backend ``good_at`` it; paid
|
|
122
|
+
backends are never auto-selected) → the free local model when both are omitted. Targeting a paid
|
|
123
|
+
(``api``) backend by id stays gated behind the operator's billing flag — it raises if billing is
|
|
124
|
+
off rather than spending silently.
|
|
125
|
+
|
|
126
|
+
When ``task`` is given but no configured backend fits it, this does **not** error — it returns a
|
|
127
|
+
short instruction telling you (the orchestrator) to handle the sub-task yourself, since you are
|
|
128
|
+
the most capable backend available. Call ``delegate_targets`` for the live menu.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
prompt: The self-contained sub-task to delegate. Give it everything it needs — the target
|
|
132
|
+
backend has no access to your conversation context.
|
|
133
|
+
target: The id of a configured ``can_delegate`` backend (explicit; takes precedence over
|
|
134
|
+
``task``), or ``None``.
|
|
135
|
+
task: A capability tag (a ``good_at`` value) to route by fit when no ``target`` is given.
|
|
136
|
+
max_tokens: Completion token cap (default 2048 — a local reasoning model needs headroom for
|
|
137
|
+
its internal reasoning before emitting the final answer).
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The target backend's final response text, or — when ``task`` matches no configured backend —
|
|
141
|
+
a short instruction to handle the sub-task yourself.
|
|
142
|
+
"""
|
|
143
|
+
try:
|
|
144
|
+
return run_delegate(prompt, target=target, task=task, max_tokens=max_tokens)
|
|
145
|
+
except NoDelegateFit as exc:
|
|
146
|
+
return (
|
|
147
|
+
f"[tanglebrain] {exc}. Handle this sub-task yourself — you are the most capable "
|
|
148
|
+
"backend available here."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@mcp.tool()
|
|
153
|
+
def delegate_targets() -> str:
|
|
154
|
+
"""List the configured delegate targets (the live menu) as a JSON array.
|
|
155
|
+
|
|
156
|
+
Call this to see which backends you may delegate to and what each is good at, then pass a chosen
|
|
157
|
+
id as ``delegate``'s ``target``. Each element is
|
|
158
|
+
``{"id", "tier", "good_at", "cost", "kind"}``; the array is empty when no ``can_delegate`` target
|
|
159
|
+
is configured (only the default local model is then available, via ``delegate_local``). Emits no
|
|
160
|
+
credentials. Reads the roster live, so it reflects edits since server startup.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
A JSON-encoded array of target descriptors.
|
|
164
|
+
"""
|
|
165
|
+
return json.dumps(_list_delegate_targets())
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@mcp.tool()
|
|
169
|
+
def delegate_many(tasks: list[dict], max_concurrency: int | None = None) -> str:
|
|
170
|
+
"""Fan out several sub-tasks CONCURRENTLY and get all results back at once.
|
|
171
|
+
|
|
172
|
+
Use this instead of calling ``delegate`` one sub-task at a time when you have independent pieces
|
|
173
|
+
of work to offload in parallel — each runs at the same time and you collect them together. Each
|
|
174
|
+
item routes independently, so one batch can mix backends (send grunt to local, code to a sub).
|
|
175
|
+
|
|
176
|
+
Each item in ``tasks`` is a mapping ``{"prompt": str, "target"?: str, "task"?: str,
|
|
177
|
+
"max_tokens"?: int}`` — ``target``/``task`` mean the same as on the ``delegate`` tool (explicit id,
|
|
178
|
+
or capability; omit both for the free local model). Concurrency is bounded automatically (derived
|
|
179
|
+
from the host, or the operator's configured cap); pass ``max_concurrency`` to throttle a heavy
|
|
180
|
+
batch lower.
|
|
181
|
+
|
|
182
|
+
A failing sub-task never sinks the others. The result is a JSON array, **one entry per input task
|
|
183
|
+
in input order**, each ``{"index", "status", ...}``:
|
|
184
|
+
- ``{"index", "status": "ok", "text": ...}`` — the backend's output.
|
|
185
|
+
- ``{"index", "status": "no_fit", "message": ...}`` — no backend fit a ``task`` capability;
|
|
186
|
+
handle that one yourself.
|
|
187
|
+
- ``{"index", "status": "error", "error": ...}`` — that sub-task failed (e.g. bad target id,
|
|
188
|
+
backend down); the rest still ran.
|
|
189
|
+
|
|
190
|
+
This is dispatch + collect only — **you** synthesise the pieces back into one answer.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
tasks: The list of sub-task descriptors to fan out.
|
|
194
|
+
max_concurrency: Optional cap to lower (never exceed) the automatic concurrency limit.
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
A JSON-encoded array of per-task results, ordered by input index.
|
|
198
|
+
"""
|
|
199
|
+
return json.dumps(run_delegate_many(tasks, max_concurrency=max_concurrency))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main() -> None:
|
|
203
|
+
"""Console entry point: serve the delegate over stdio (``tanglebrain-delegate``)."""
|
|
204
|
+
mcp.run()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
if __name__ == "__main__":
|
|
208
|
+
main()
|