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.
@@ -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()