ms-tau-sdk 2.0.9.dev48__tar.gz → 2.0.9.dev49__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.
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/CHANGELOG.md +18 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/PKG-INFO +1 -1
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/pyproject.toml +1 -1
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +10 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/backend/mcp.py +32 -10
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/manager.py +2 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/settings.py +6 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/tools/mcp_applications.py +163 -28
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/.gitignore +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/README.md +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/app.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/config.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/state.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/agent_skills/tau_security_and_access/SKILL.md +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/a2a.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/chat.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/conversations.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/dependencies.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/health.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/inspection.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/local_chat.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/models.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/request_identity.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/api/sessions.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/app.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/application.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/backend/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/backend/assertions.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/backend/auth.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/backend/client.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/backend/local.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/backend/models.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/backend/routes.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/cli.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/errors.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/logging.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/protocols/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/providers/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/providers/definitions.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/providers/factory.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/resources/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/resources/loader.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/events.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/extensions.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/failures.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/observability.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/provenance.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/requester.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/session.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/task_context.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/sessions/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/sessions/storage.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/skills.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/tools/__init__.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/tools/mcp_connection.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/tools/skill_read.py +0 -0
- {ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/tools/task_control.py +0 -0
|
@@ -9,6 +9,24 @@
|
|
|
9
9
|
the session loads, or, before the session has loaded, the one its Agent card names (one extra
|
|
10
10
|
request, only then). Local turns carry the workspace's local Agent,
|
|
11
11
|
`local-agent-<workspace digest>`.
|
|
12
|
+
- **Application tool calls get their own time limits, and failures say what happened
|
|
13
|
+
([#84](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/84), ADR 0022).**
|
|
14
|
+
- A call that met an error status from the application or its gateway, such as 403, 502 or 503,
|
|
15
|
+
waited until the turn's 15-minute limit. It now ends at once with that status. The Main
|
|
16
|
+
Sequence MCP connection had the same defect and is fixed too.
|
|
17
|
+
- Each application tool declares how long it may run in its MCP `_meta` under
|
|
18
|
+
`mainsequence.ai/timeout-seconds/v1`, for example
|
|
19
|
+
`@mcp.tool(meta={"mainsequence.ai/timeout-seconds/v1": 300})`. A tool that declares nothing
|
|
20
|
+
gets the new `TAU_MCP_TOOL_TIMEOUT_SECONDS` (default `60`).
|
|
21
|
+
`MAINSEQUENCE_TAU_BACKEND_READ_TIMEOUT_SECONDS` no longer limits the call itself. To find the
|
|
22
|
+
limit, `<name>__call_tool` now reads the application's tool list before each call.
|
|
23
|
+
- The model reads what failed, for example "The orders tool rebuild_report did not answer
|
|
24
|
+
within 300 seconds." or "The orders application answered with a server error (502).", instead
|
|
25
|
+
of "could not be reached" for every failure. Details and the
|
|
26
|
+
`runtime.mcp_application.failed` log event carry `failure`, `status` and `timeout_seconds`.
|
|
27
|
+
- Opening the application's session is tried once more when it fails on the connection, a
|
|
28
|
+
time-out or a server error. A call that was sent is never sent again.
|
|
29
|
+
- See [Application tool time limits](docs/guides/application-tool-time-limits.md).
|
|
12
30
|
|
|
13
31
|
## 2.0.8 — 2026-10-08
|
|
14
32
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ms-tau-sdk
|
|
3
|
-
Version: 2.0.9.
|
|
3
|
+
Version: 2.0.9.dev49
|
|
4
4
|
Summary: Workspace-bound Tau application primitives for Main Sequence projects
|
|
5
5
|
Project-URL: Changelog, https://github.com/mainsequence-sdk/ms-tau-sdk/blob/development/CHANGELOG.md
|
|
6
6
|
Project-URL: Documentation, https://github.com/mainsequence-sdk/ms-tau-sdk/tree/development/docs
|
|
@@ -81,6 +81,16 @@ in the system prompt because a tool named `read` exists. Extensions must not reg
|
|
|
81
81
|
this mode.
|
|
82
82
|
Excluding coding tools does not sandbox extension Python code.
|
|
83
83
|
|
|
84
|
+
## Application tool time limits
|
|
85
|
+
|
|
86
|
+
A declared application's tool sets how long it may run, in seconds, in its MCP `_meta` under
|
|
87
|
+
`mainsequence.ai/timeout-seconds/v1`; with the MCP Python SDK that is
|
|
88
|
+
`@mcp.tool(meta={"mainsequence.ai/timeout-seconds/v1": 300})` in the application. Set the limit on
|
|
89
|
+
the application's tool, not in the Agent. A tool that declares nothing gets
|
|
90
|
+
`TAU_MCP_TOOL_TIMEOUT_SECONDS` (default `60`); set it like the tool composition settings above, in
|
|
91
|
+
`harness_agent.spec.env_vars`, the process environment, or
|
|
92
|
+
`TauSDKSettings(mcp_tool_timeout_seconds=...)`. No call outlives its turn.
|
|
93
|
+
|
|
84
94
|
## Invariants extensions must not replace
|
|
85
95
|
|
|
86
96
|
Extensions may change effective agent capabilities, but they must not replace or bypass:
|
|
@@ -7,7 +7,7 @@ import contextvars
|
|
|
7
7
|
from collections.abc import AsyncGenerator
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from datetime import timedelta
|
|
10
|
-
from typing import Literal, Self
|
|
10
|
+
from typing import Any, Literal, Self
|
|
11
11
|
|
|
12
12
|
import httpx
|
|
13
13
|
from mcp import ClientSession, types
|
|
@@ -52,6 +52,7 @@ class _CallToolCommand:
|
|
|
52
52
|
name: str
|
|
53
53
|
arguments: dict[str, object]
|
|
54
54
|
meta: dict[str, object] | None
|
|
55
|
+
timeout_seconds: float | None
|
|
55
56
|
log_context: dict[str, object]
|
|
56
57
|
result: asyncio.Future[types.CallToolResult]
|
|
57
58
|
|
|
@@ -99,10 +100,8 @@ class MCPConnectionClient:
|
|
|
99
100
|
timeout: httpx.Timeout,
|
|
100
101
|
read_timeout_seconds: float,
|
|
101
102
|
read_concurrency: int,
|
|
102
|
-
read_catalog: bool = True,
|
|
103
103
|
) -> None:
|
|
104
104
|
self.name = name
|
|
105
|
-
self._read_catalog = read_catalog
|
|
106
105
|
self.display_name = display_name
|
|
107
106
|
self.url = url
|
|
108
107
|
self._http_auth = auth
|
|
@@ -113,6 +112,9 @@ class MCPConnectionClient:
|
|
|
113
112
|
self._owner_task: asyncio.Task[None] | None = None
|
|
114
113
|
self._ready: asyncio.Future[None] | None = None
|
|
115
114
|
self._failure: Exception | None = None
|
|
115
|
+
# The results of the calls already handed to the session, so that a failed connection
|
|
116
|
+
# ends them as well as the calls still queued.
|
|
117
|
+
self._in_flight: set[asyncio.Future[Any]] = set()
|
|
116
118
|
self.tools: tuple[types.Tool, ...] = ()
|
|
117
119
|
self.resources: tuple[types.Resource, ...] = ()
|
|
118
120
|
self._parallel_tool_names: frozenset[str] = frozenset()
|
|
@@ -198,8 +200,6 @@ class MCPConnectionClient:
|
|
|
198
200
|
session: ClientSession,
|
|
199
201
|
initialized: types.InitializeResult | None,
|
|
200
202
|
) -> None:
|
|
201
|
-
if not self._read_catalog:
|
|
202
|
-
return
|
|
203
203
|
# Read only what the server offers; a server without resources answers "not found".
|
|
204
204
|
capabilities = initialized.capabilities if initialized is not None else None
|
|
205
205
|
offers_tools = capabilities is None or capabilities.tools is not None
|
|
@@ -219,14 +219,23 @@ class MCPConnectionClient:
|
|
|
219
219
|
read_slots = asyncio.Semaphore(self._read_concurrency)
|
|
220
220
|
|
|
221
221
|
async def execute(command: _CallToolCommand | _ReadResourceCommand) -> None:
|
|
222
|
+
self._in_flight.add(command.result)
|
|
222
223
|
try:
|
|
223
224
|
with bound_contextvars(**command.log_context):
|
|
224
225
|
if isinstance(command, _CallToolCommand):
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
if command.timeout_seconds is None:
|
|
227
|
+
tool_result = await session.call_tool(
|
|
228
|
+
command.name,
|
|
229
|
+
command.arguments,
|
|
230
|
+
meta=command.meta,
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
tool_result = await session.call_tool(
|
|
234
|
+
command.name,
|
|
235
|
+
command.arguments,
|
|
236
|
+
read_timeout_seconds=timedelta(seconds=command.timeout_seconds),
|
|
237
|
+
meta=command.meta,
|
|
238
|
+
)
|
|
230
239
|
if not command.result.done():
|
|
231
240
|
command.result.set_result(tool_result)
|
|
232
241
|
else:
|
|
@@ -236,6 +245,11 @@ class MCPConnectionClient:
|
|
|
236
245
|
except Exception as error:
|
|
237
246
|
if not command.result.done():
|
|
238
247
|
command.result.set_exception(error)
|
|
248
|
+
finally:
|
|
249
|
+
# A transport failure cancels the session's work before its cause is known; the
|
|
250
|
+
# owner task then ends this call with that cause.
|
|
251
|
+
if command.result.done():
|
|
252
|
+
self._in_flight.discard(command.result)
|
|
239
253
|
|
|
240
254
|
async def execute_read(
|
|
241
255
|
command: _CallToolCommand | _ReadResourceCommand,
|
|
@@ -271,6 +285,10 @@ class MCPConnectionClient:
|
|
|
271
285
|
await asyncio.gather(*tuple(active_reads), return_exceptions=True)
|
|
272
286
|
|
|
273
287
|
def _fail_pending_commands(self, error: Exception) -> None:
|
|
288
|
+
for result in self._in_flight:
|
|
289
|
+
if not result.done():
|
|
290
|
+
result.set_exception(error)
|
|
291
|
+
self._in_flight.clear()
|
|
274
292
|
if self._commands is None:
|
|
275
293
|
return
|
|
276
294
|
while not self._commands.empty():
|
|
@@ -293,7 +311,10 @@ class MCPConnectionClient:
|
|
|
293
311
|
arguments: dict[str, object],
|
|
294
312
|
*,
|
|
295
313
|
meta: dict[str, object] | None = None,
|
|
314
|
+
timeout_seconds: float | None = None,
|
|
296
315
|
) -> types.CallToolResult:
|
|
316
|
+
"""Call one tool; ``timeout_seconds`` replaces the session's wait for its answer."""
|
|
317
|
+
|
|
297
318
|
commands = self._require_commands()
|
|
298
319
|
result: asyncio.Future[types.CallToolResult] = asyncio.get_running_loop().create_future()
|
|
299
320
|
commands.put_nowait(
|
|
@@ -301,6 +322,7 @@ class MCPConnectionClient:
|
|
|
301
322
|
name=name,
|
|
302
323
|
arguments=arguments,
|
|
303
324
|
meta=meta,
|
|
325
|
+
timeout_seconds=timeout_seconds,
|
|
304
326
|
log_context=dict(get_contextvars()),
|
|
305
327
|
result=result,
|
|
306
328
|
)
|
|
@@ -1152,6 +1152,8 @@ class SessionRuntimeManager:
|
|
|
1152
1152
|
create_mcp_application_tools(
|
|
1153
1153
|
bootstrap.mcp_applications,
|
|
1154
1154
|
connect=application_connector(self.settings),
|
|
1155
|
+
tool_timeout_seconds=self.settings.mcp_tool_timeout_seconds,
|
|
1156
|
+
open_timeout_seconds=self.settings.backend_read_timeout_seconds,
|
|
1155
1157
|
)
|
|
1156
1158
|
)
|
|
1157
1159
|
tools.extend(create_task_control_tools())
|
|
@@ -242,6 +242,12 @@ class TauSDKSettings(BaseSettings):
|
|
|
242
242
|
ge=1,
|
|
243
243
|
validation_alias="MAINSEQUENCE_TAU_MCP_READ_CONCURRENCY",
|
|
244
244
|
)
|
|
245
|
+
# How long an application tool may run when its tool declares no time limit (ADR 0022).
|
|
246
|
+
mcp_tool_timeout_seconds: float = Field(
|
|
247
|
+
default=60.0,
|
|
248
|
+
gt=0,
|
|
249
|
+
validation_alias="TAU_MCP_TOOL_TIMEOUT_SECONDS",
|
|
250
|
+
)
|
|
245
251
|
backend_max_response_bytes: int = Field(
|
|
246
252
|
default=10 * 1024 * 1024,
|
|
247
253
|
gt=0,
|
|
@@ -8,17 +8,22 @@ session for the call, and closes it afterwards. The token carries the delegation
|
|
|
8
8
|
turn serves, when it serves one, and is the Agent's own otherwise; the application decides what the
|
|
9
9
|
call may do. No session, token or catalog is shared between turns or people, and nothing is read
|
|
10
10
|
when the session loads, because no turn is running then.
|
|
11
|
+
|
|
12
|
+
Each tool declares how long it may run in its ``_meta`` (ADR 0022); a tool that declares nothing
|
|
13
|
+
gets ``TAU_MCP_TOOL_TIMEOUT_SECONDS``. A failure reaches the model in plain words, and
|
|
14
|
+
opening the session is tried once more when it fails before the tool call is sent.
|
|
11
15
|
"""
|
|
12
16
|
|
|
13
17
|
from __future__ import annotations
|
|
14
18
|
|
|
15
19
|
import json
|
|
20
|
+
import math
|
|
16
21
|
import re
|
|
17
22
|
from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping, Sequence
|
|
18
23
|
|
|
19
24
|
import httpx
|
|
20
25
|
import structlog
|
|
21
|
-
from mcp import types
|
|
26
|
+
from mcp import McpError, types
|
|
22
27
|
from tau_agent.messages import TextContent
|
|
23
28
|
from tau_agent.tools import (
|
|
24
29
|
AgentTool,
|
|
@@ -44,7 +49,10 @@ logger = structlog.get_logger(__name__)
|
|
|
44
49
|
_APPLICATION_NAME = re.compile(r"^[a-z][a-z0-9_]{0,39}$")
|
|
45
50
|
_RESERVED_NAMES = frozenset({"mainsequence"})
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
TIMEOUT_META_KEY = "mainsequence.ai/timeout-seconds/v1"
|
|
53
|
+
"""The ``_meta`` key under which an application tool declares how long it may run, in seconds."""
|
|
54
|
+
|
|
55
|
+
type ApplicationConnector = Callable[[MCPApplication], Awaitable[MCPConnectionClient]]
|
|
48
56
|
|
|
49
57
|
|
|
50
58
|
class _ApplicationTokenAuth(httpx.Auth):
|
|
@@ -69,33 +77,101 @@ class _ApplicationTokenAuth(httpx.Auth):
|
|
|
69
77
|
def application_connector(settings: TauSDKSettings) -> ApplicationConnector:
|
|
70
78
|
"""Open an MCP session to one application for the current turn."""
|
|
71
79
|
|
|
72
|
-
async def connect(application: MCPApplication
|
|
80
|
+
async def connect(application: MCPApplication) -> MCPConnectionClient:
|
|
73
81
|
access = await _turn_application_access(application.resource_release_uid)
|
|
74
82
|
return await MCPConnectionClient(
|
|
75
83
|
name=application.name,
|
|
76
84
|
display_name=application.name,
|
|
77
85
|
url=f"{str(access.rpc_url).rstrip('/')}/mcp",
|
|
78
86
|
auth=_ApplicationTokenAuth(access),
|
|
87
|
+
# The session's wait bounds each request: the backend read timeout while the session
|
|
88
|
+
# opens, the tool's own limit for the call. The HTTP read wait never cuts a call short.
|
|
79
89
|
timeout=httpx.Timeout(
|
|
80
90
|
connect=settings.backend_connect_timeout_seconds,
|
|
81
|
-
read=settings.
|
|
91
|
+
read=settings.turn_timeout_seconds,
|
|
82
92
|
write=settings.backend_write_timeout_seconds,
|
|
83
93
|
pool=settings.backend_pool_timeout_seconds,
|
|
84
94
|
),
|
|
85
95
|
read_timeout_seconds=settings.backend_read_timeout_seconds,
|
|
86
96
|
read_concurrency=settings.mcp_read_concurrency,
|
|
87
|
-
read_catalog=read_catalog,
|
|
88
97
|
).open()
|
|
89
98
|
|
|
90
99
|
return connect
|
|
91
100
|
|
|
92
101
|
|
|
93
|
-
def
|
|
102
|
+
def _timed_out(error: BaseException) -> bool:
|
|
103
|
+
if isinstance(error, McpError):
|
|
104
|
+
return error.error.code == httpx.codes.REQUEST_TIMEOUT
|
|
105
|
+
return isinstance(error, httpx.TimeoutException) and not isinstance(error, httpx.ConnectTimeout)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _failure(
|
|
109
|
+
name: str,
|
|
110
|
+
error: BaseException,
|
|
111
|
+
*,
|
|
112
|
+
tool: str | None,
|
|
113
|
+
timeout_seconds: float,
|
|
114
|
+
) -> tuple[str, dict[str, JSONValue]]:
|
|
115
|
+
"""What the model reads about a failure, and its details; never the error's own text."""
|
|
116
|
+
|
|
94
117
|
if isinstance(error, RequesterBindingError):
|
|
95
|
-
return "Your access for this request ended."
|
|
96
|
-
if isinstance(error, BackendError)
|
|
97
|
-
|
|
98
|
-
|
|
118
|
+
return "Your access for this request ended.", {"failure": "access_ended"}
|
|
119
|
+
if isinstance(error, BackendError):
|
|
120
|
+
if error.backend_status in {403, 404}:
|
|
121
|
+
return (
|
|
122
|
+
f"The {name} application is not available for this call.",
|
|
123
|
+
{"failure": "not_available"},
|
|
124
|
+
)
|
|
125
|
+
return (
|
|
126
|
+
f"Main Sequence could not provide access to the {name} application.",
|
|
127
|
+
{"failure": "access_unavailable"},
|
|
128
|
+
)
|
|
129
|
+
if _timed_out(error):
|
|
130
|
+
subject = f"The {name} tool {tool}" if tool is not None else f"The {name} application"
|
|
131
|
+
return (
|
|
132
|
+
f"{subject} did not answer within {timeout_seconds:g} seconds.",
|
|
133
|
+
{"failure": "timeout", "timeout_seconds": timeout_seconds},
|
|
134
|
+
)
|
|
135
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
136
|
+
status = error.response.status_code
|
|
137
|
+
if status >= 500:
|
|
138
|
+
text = f"The {name} application answered with a server error ({status})."
|
|
139
|
+
return text, {"failure": "server_error", "status": status}
|
|
140
|
+
if status in {401, 403}:
|
|
141
|
+
text = f"The {name} application refused this call ({status})."
|
|
142
|
+
return text, {"failure": "refused", "status": status}
|
|
143
|
+
text = f"The {name} application answered with an error ({status})."
|
|
144
|
+
return text, {"failure": "error_status", "status": status}
|
|
145
|
+
if isinstance(error, httpx.ConnectError | httpx.ConnectTimeout):
|
|
146
|
+
return f"The {name} application could not be reached.", {"failure": "unreachable"}
|
|
147
|
+
return f"The {name} application could not complete this call.", {"failure": "failed"}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _retryable(error: BaseException) -> bool:
|
|
151
|
+
"""Whether opening the session failed in a way that a second attempt may not repeat."""
|
|
152
|
+
|
|
153
|
+
if isinstance(error, httpx.HTTPStatusError):
|
|
154
|
+
return error.response.status_code >= 500
|
|
155
|
+
return isinstance(error, httpx.TransportError) or _timed_out(error)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _declared_timeout(name: str, tool: types.Tool) -> float | None:
|
|
159
|
+
value = (tool.meta or {}).get(TIMEOUT_META_KEY)
|
|
160
|
+
if value is None:
|
|
161
|
+
return None
|
|
162
|
+
if (
|
|
163
|
+
isinstance(value, bool)
|
|
164
|
+
or not isinstance(value, int | float)
|
|
165
|
+
or not math.isfinite(value)
|
|
166
|
+
or value <= 0
|
|
167
|
+
):
|
|
168
|
+
logger.warning(
|
|
169
|
+
"runtime.mcp_application.invalid_timeout",
|
|
170
|
+
application=name,
|
|
171
|
+
mcp_tool=tool.name,
|
|
172
|
+
)
|
|
173
|
+
return None
|
|
174
|
+
return float(value)
|
|
99
175
|
|
|
100
176
|
|
|
101
177
|
def _error(name: str, text: str, **details: JSONValue) -> AgentToolResult:
|
|
@@ -118,9 +194,53 @@ def _application_tools(
|
|
|
118
194
|
application: MCPApplication,
|
|
119
195
|
*,
|
|
120
196
|
connect: ApplicationConnector,
|
|
197
|
+
tool_timeout_seconds: float,
|
|
198
|
+
open_timeout_seconds: float,
|
|
121
199
|
) -> list[AgentTool]:
|
|
122
200
|
name = application.name
|
|
123
201
|
|
|
202
|
+
async def open_session(signal: ToolCancellationToken | None) -> MCPConnectionClient:
|
|
203
|
+
# Nothing has reached a tool while the session opens, so one more attempt is safe.
|
|
204
|
+
try:
|
|
205
|
+
return await connect(application)
|
|
206
|
+
except Exception as error:
|
|
207
|
+
if not _retryable(error) or (signal is not None and signal.is_cancelled()):
|
|
208
|
+
raise
|
|
209
|
+
logger.info(
|
|
210
|
+
"runtime.mcp_application.retried",
|
|
211
|
+
application=name,
|
|
212
|
+
error_type=type(error).__name__,
|
|
213
|
+
)
|
|
214
|
+
return await connect(application)
|
|
215
|
+
|
|
216
|
+
def failed(
|
|
217
|
+
operation: str,
|
|
218
|
+
error: BaseException,
|
|
219
|
+
*,
|
|
220
|
+
mcp_tool: str | None = None,
|
|
221
|
+
call_timeout_seconds: float | None = None,
|
|
222
|
+
) -> AgentToolResult:
|
|
223
|
+
# A failure after the call was sent, which has a time limit by then, is the tool's;
|
|
224
|
+
# a failure while the session opens is the application's.
|
|
225
|
+
text, failure = _failure(
|
|
226
|
+
name,
|
|
227
|
+
error,
|
|
228
|
+
tool=mcp_tool if call_timeout_seconds is not None else None,
|
|
229
|
+
timeout_seconds=(
|
|
230
|
+
call_timeout_seconds if call_timeout_seconds is not None else open_timeout_seconds
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
if mcp_tool is not None:
|
|
234
|
+
failure["mcp_tool"] = mcp_tool
|
|
235
|
+
logger.warning(
|
|
236
|
+
"runtime.mcp_application.failed",
|
|
237
|
+
application=name,
|
|
238
|
+
operation=operation,
|
|
239
|
+
error_type=type(error).__name__,
|
|
240
|
+
**failure,
|
|
241
|
+
)
|
|
242
|
+
return _error(name, text, **failure)
|
|
243
|
+
|
|
124
244
|
async def list_tools(
|
|
125
245
|
tool_call_id: str,
|
|
126
246
|
arguments: Mapping[str, JSONValue],
|
|
@@ -131,19 +251,13 @@ def _application_tools(
|
|
|
131
251
|
if signal is not None and signal.is_cancelled():
|
|
132
252
|
return _error(name, f"Listing the {name} tools was cancelled.", cancelled=True)
|
|
133
253
|
try:
|
|
134
|
-
client = await
|
|
254
|
+
client = await open_session(signal)
|
|
135
255
|
try:
|
|
136
256
|
tools = client.tools
|
|
137
257
|
finally:
|
|
138
258
|
await client.aclose()
|
|
139
259
|
except Exception as error:
|
|
140
|
-
|
|
141
|
-
"runtime.mcp_application.failed",
|
|
142
|
-
application=name,
|
|
143
|
-
operation="list_tools",
|
|
144
|
-
error_type=type(error).__name__,
|
|
145
|
-
)
|
|
146
|
-
return _error(name, _failure(name, error))
|
|
260
|
+
return failed("list_tools", error)
|
|
147
261
|
entries = [_tool_entry(tool) for tool in tools]
|
|
148
262
|
return AgentToolResult(
|
|
149
263
|
content=[
|
|
@@ -170,20 +284,28 @@ def _application_tools(
|
|
|
170
284
|
return _error(name, "`arguments` must be an object.")
|
|
171
285
|
if signal is not None and signal.is_cancelled():
|
|
172
286
|
return _error(name, f"The {name} tool call was cancelled.", cancelled=True)
|
|
287
|
+
call_timeout_seconds: float | None = None
|
|
173
288
|
try:
|
|
174
|
-
client = await
|
|
289
|
+
client = await open_session(signal)
|
|
175
290
|
try:
|
|
176
|
-
|
|
291
|
+
declared = next(
|
|
292
|
+
(
|
|
293
|
+
_declared_timeout(name, tool)
|
|
294
|
+
for tool in client.tools
|
|
295
|
+
if tool.name == tool_name
|
|
296
|
+
),
|
|
297
|
+
None,
|
|
298
|
+
)
|
|
299
|
+
call_timeout_seconds = declared if declared is not None else tool_timeout_seconds
|
|
300
|
+
result = await client.call_tool(
|
|
301
|
+
tool_name, dict(tool_arguments), timeout_seconds=call_timeout_seconds
|
|
302
|
+
)
|
|
177
303
|
finally:
|
|
178
304
|
await client.aclose()
|
|
179
305
|
except Exception as error:
|
|
180
|
-
|
|
181
|
-
"
|
|
182
|
-
application=name,
|
|
183
|
-
operation="call_tool",
|
|
184
|
-
error_type=type(error).__name__,
|
|
306
|
+
return failed(
|
|
307
|
+
"call_tool", error, mcp_tool=tool_name, call_timeout_seconds=call_timeout_seconds
|
|
185
308
|
)
|
|
186
|
-
return _error(name, _failure(name, error), mcp_tool=tool_name)
|
|
187
309
|
projected = mcp_tool_result(canonical_name=tool_name, result=result, display_name=name)
|
|
188
310
|
details = dict(projected.details) if isinstance(projected.details, dict) else {}
|
|
189
311
|
details["application"] = name
|
|
@@ -230,8 +352,14 @@ def create_mcp_application_tools(
|
|
|
230
352
|
applications: Sequence[MCPApplication],
|
|
231
353
|
*,
|
|
232
354
|
connect: ApplicationConnector,
|
|
355
|
+
tool_timeout_seconds: float,
|
|
356
|
+
open_timeout_seconds: float,
|
|
233
357
|
) -> list[AgentTool]:
|
|
234
|
-
"""Two tools for each declared application, in declaration order.
|
|
358
|
+
"""Two tools for each declared application, in declaration order.
|
|
359
|
+
|
|
360
|
+
``tool_timeout_seconds`` bounds a call to a tool that declares no limit of its own;
|
|
361
|
+
``open_timeout_seconds`` is the wait for each request that opens the session.
|
|
362
|
+
"""
|
|
235
363
|
|
|
236
364
|
tools: list[AgentTool] = []
|
|
237
365
|
seen: set[str] = set()
|
|
@@ -242,5 +370,12 @@ def create_mcp_application_tools(
|
|
|
242
370
|
if name in seen:
|
|
243
371
|
raise ConfigurationError(f"Duplicate MCP application name: {name!r}")
|
|
244
372
|
seen.add(name)
|
|
245
|
-
tools.extend(
|
|
373
|
+
tools.extend(
|
|
374
|
+
_application_tools(
|
|
375
|
+
application,
|
|
376
|
+
connect=connect,
|
|
377
|
+
tool_timeout_seconds=tool_timeout_seconds,
|
|
378
|
+
open_timeout_seconds=open_timeout_seconds,
|
|
379
|
+
)
|
|
380
|
+
)
|
|
246
381
|
return tools
|
|
File without changes
|
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/__init__.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/app.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/cli.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/config.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/env_file.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/logs.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/proxy.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/state.py
RENAMED
|
File without changes
|
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/packages/tau-board/src/ms_tau_board/static/app.js
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ms_tau_sdk-2.0.9.dev48 → ms_tau_sdk-2.0.9.dev49}/src/ms_tau_sdk/runtime/deployment_health.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|