ms-tau-sdk 2.0.11.dev52__tar.gz → 2.0.12.dev53__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.11.dev52 → ms_tau_sdk-2.0.12.dev53}/CHANGELOG.md +33 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/PKG-INFO +1 -1
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/pyproject.toml +1 -1
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +5 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +8 -3
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/a2a.py +156 -22
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/manager.py +18 -2
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/settings.py +20 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/tools/mcp_applications.py +7 -25
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/tools/mcp_connection.py +46 -9
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/.gitignore +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/README.md +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/app.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/config.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/state.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/agent_skills/tau_security_and_access/SKILL.md +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/chat.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/conversations.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/dependencies.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/health.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/inspection.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/local_chat.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/models.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/request_identity.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/api/sessions.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/app.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/application.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/backend/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/backend/assertions.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/backend/auth.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/backend/client.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/backend/local.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/backend/mcp.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/backend/models.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/backend/routes.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/cli.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/errors.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/logging.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/providers/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/providers/definitions.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/providers/factory.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/resources/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/resources/loader.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/events.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/extensions.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/failures.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/observability.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/provenance.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/requester.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/session.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/runtime/task_context.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/sessions/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/sessions/storage.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/skills.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/tools/__init__.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/tools/skill_read.py +0 -0
- {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/tools/task_control.py +0 -0
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.12 — 2026-10-11
|
|
4
|
+
|
|
5
|
+
- **A direct Message that runs out of time gets a typed answer
|
|
6
|
+
([#94](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/94)).** A `message:send` answered
|
|
7
|
+
with a Message, or JSON-RPC `message/send`, whose turn reached
|
|
8
|
+
`MAINSEQUENCE_TAU_TURN_TIMEOUT_SECONDS` got a plain-text HTTP 500 "Internal Server Error". It now
|
|
9
|
+
gets HTTP 504 `application/a2a+json` with status `DEADLINE_EXCEEDED` and an `ErrorInfo` with
|
|
10
|
+
reason `MESSAGE_RESPONSE_LIMIT_EXCEEDED`, domain `mainsequence.ai` and `metadata.limitSeconds`,
|
|
11
|
+
the limit that expired. JSON-RPC returns the same `ErrorInfo` in a JSON-RPC error (`-32000`).
|
|
12
|
+
The turn is stopped as before; its work is neither rolled back nor replayed. Use Task mode for
|
|
13
|
+
longer work.
|
|
14
|
+
- **A direct Message has the platform's 90-second limit.** The platform gives a direct A2A
|
|
15
|
+
Message one strict limit, 90 seconds, and passes it to the runtime it hosts in
|
|
16
|
+
`MAINSEQUENCE_A2A_MESSAGE_LIMIT_SECONDS`, a platform-set variable that users do not set. The
|
|
17
|
+
runtime enforces it for every caller, from when it receives the request and over the whole
|
|
18
|
+
request, including output-contract repair turns: the turn is stopped and the request gets the
|
|
19
|
+
504 above with the platform's limit. A shorter `MAINSEQUENCE_TAU_TURN_TIMEOUT_SECONDS` still ends
|
|
20
|
+
the turn first; no setting extends the platform's limit. A Task-mode `message:send` with
|
|
21
|
+
`returnImmediately: false` waits at most until the same limit and returns the Task, which keeps
|
|
22
|
+
running. Streams, chat, caller delivery, Task dispatch and Task turns are unchanged. Without the
|
|
23
|
+
variable, as in local mode (which refuses it), a direct Message has only the turn limit.
|
|
24
|
+
- **Platform MCP tools wait for the limit the platform declares
|
|
25
|
+
([#93](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/93), ADR 0022 amendment).** Every
|
|
26
|
+
Main Sequence MCP tool call waited `MAINSEQUENCE_TAU_BACKEND_READ_TIMEOUT_SECONDS` (60 seconds by
|
|
27
|
+
default), so an `a2a.send_message` in Message mode whose target answered after 60 seconds was
|
|
28
|
+
abandoned by the calling Agent while the platform still waited and completed the send. A
|
|
29
|
+
platform tool whose `_meta` declares its limit under `mainsequence.ai/timeout-seconds/v1`, the
|
|
30
|
+
key application tools use, is now waited for that long. A tool that declares nothing, or an
|
|
31
|
+
invalid value, keeps the session default; an invalid value is logged as
|
|
32
|
+
`runtime.mcp.invalid_timeout`. `MAINSEQUENCE_TAU_BACKEND_READ_TIMEOUT_SECONDS` now covers only
|
|
33
|
+
ordinary backend reads and platform tools without a declared limit: it has no effect on a Message
|
|
34
|
+
or any platform limit and can only shorten a wait.
|
|
35
|
+
|
|
3
36
|
## 2.0.11 — 2026-10-11
|
|
4
37
|
|
|
5
38
|
- **Every MCP connection honors per-call response deadlines
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: ms-tau-sdk
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.12.dev53
|
|
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
|
|
@@ -54,6 +54,11 @@ For a deployed Agent, use the projected `a2a.send_message` MCP operation. The pl
|
|
|
54
54
|
resolves the target `AgentSession`, waits for its runtime to become ready, and dispatches the
|
|
55
55
|
message. For asynchronous work, use the returned Task handle with `a2a.wait_task`.
|
|
56
56
|
|
|
57
|
+
The platform gives a direct Message (`response_kind: "message"`) 90 seconds, and no setting
|
|
58
|
+
extends that limit. A target that has not answered by then stops its turn, and the send fails
|
|
59
|
+
because of the limit. Send work that may take longer with `response_kind: "task"`, and do not
|
|
60
|
+
resend a Message that ran out of time: the work it already did is not rolled back.
|
|
61
|
+
|
|
57
62
|
For local Tau development, start `ms-tau` and wait for its local `/ready` result. The public A2A
|
|
58
63
|
REST and JSON-RPC surfaces support Message and Task execution against the workspace-local SQLite
|
|
59
64
|
store, including streaming, list/get/cancel/subscribe, and continuation. An A2A protocol Task does
|
|
@@ -35,9 +35,9 @@ The SDK looks for the CLI in `MAINSEQUENCE_CLI`, then beside the Python interpre
|
|
|
35
35
|
CLI. Do not add token lines to `.env`. A pair found there still works, but it is deprecated and
|
|
36
36
|
startup logs a warning. `mainsequence refresh-token` run in that directory removes the lines.
|
|
37
37
|
|
|
38
|
-
Do not set `APP_NAME`, `FASTAPI_PUBLIC_BASE_URL`, `MAINSEQUENCE_CALLER_AUTH_MODE=assertion`,
|
|
39
|
-
`MAINSEQUENCE_CALLER_ASSERTION_*` variables
|
|
40
|
-
local mode refuses to start with them.
|
|
38
|
+
Do not set `APP_NAME`, `FASTAPI_PUBLIC_BASE_URL`, `MAINSEQUENCE_CALLER_AUTH_MODE=assertion`, the
|
|
39
|
+
`MAINSEQUENCE_CALLER_ASSERTION_*` variables or `MAINSEQUENCE_A2A_MESSAGE_LIMIT_SECONDS`. The
|
|
40
|
+
platform sets them when it hosts a runtime, and local mode refuses to start with them.
|
|
41
41
|
|
|
42
42
|
Set `MAINSEQUENCE_ENDPOINT` only for a non-default platform endpoint. The CLI session must be for
|
|
43
43
|
the same endpoint. Local mode binds to `127.0.0.1:8787` by default. Treat an explicit public bind
|
|
@@ -118,6 +118,11 @@ To request a Task from `message:send`, send the response-kind extension header
|
|
|
118
118
|
response. `message:stream` is always Task-oriented and does not accept `responseKind` or
|
|
119
119
|
`returnImmediately`.
|
|
120
120
|
|
|
121
|
+
Local mode has no platform Message limit: a direct Message ends only at
|
|
122
|
+
`MAINSEQUENCE_TAU_TURN_TIMEOUT_SECONDS`. A hosted Agent answers a direct Message within the
|
|
123
|
+
platform's 90 seconds or with HTTP 504 `DEADLINE_EXCEEDED`, so design work that may take longer as
|
|
124
|
+
a Task.
|
|
125
|
+
|
|
121
126
|
Task terminality is derived only from its A2A state. `completed`, `failed`, `canceled`, and
|
|
122
127
|
`rejected` are terminal; `input_required` and `auth_required` are resumable. A normal execution
|
|
123
128
|
failure produces a durable failed Task and a standard responder `status.message`. A
|
|
@@ -382,6 +382,72 @@ def _unsupported_operation_json_rpc_error(
|
|
|
382
382
|
}
|
|
383
383
|
|
|
384
384
|
|
|
385
|
+
class _MessageLimitExceeded(Exception):
|
|
386
|
+
"""A Message-mode request reached its time limit before the Agent answered."""
|
|
387
|
+
|
|
388
|
+
def __init__(self, limit_seconds: float) -> None:
|
|
389
|
+
super().__init__(f"The Message time limit of {limit_seconds} seconds expired")
|
|
390
|
+
self.limit_seconds = limit_seconds
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _message_limit_exceeded(context_id: str, limit_seconds: float) -> _MessageLimitExceeded:
|
|
394
|
+
logger.warning(
|
|
395
|
+
"a2a.message.limit_exceeded",
|
|
396
|
+
message="A Message-mode request reached its time limit before the Agent answered",
|
|
397
|
+
context_id=context_id,
|
|
398
|
+
limit_seconds=limit_seconds,
|
|
399
|
+
)
|
|
400
|
+
return _MessageLimitExceeded(limit_seconds)
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _seconds_text(value: float) -> str:
|
|
404
|
+
return str(int(value)) if float(value).is_integer() else str(value)
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def _message_limit_text(limit_seconds: float) -> str:
|
|
408
|
+
return (
|
|
409
|
+
f"The Agent did not answer the Message within {_seconds_text(limit_seconds)} seconds. "
|
|
410
|
+
"Use Task mode for longer work."
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _message_limit_error_info(limit_seconds: float) -> dict[str, Any]:
|
|
415
|
+
return {
|
|
416
|
+
"@type": "type.googleapis.com/google.rpc.ErrorInfo",
|
|
417
|
+
"reason": "MESSAGE_RESPONSE_LIMIT_EXCEEDED",
|
|
418
|
+
"domain": "mainsequence.ai",
|
|
419
|
+
# ErrorInfo metadata values are strings.
|
|
420
|
+
"metadata": {"limitSeconds": _seconds_text(limit_seconds)},
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def _message_limit_rest_error(limit_seconds: float) -> JSONResponse:
|
|
425
|
+
return JSONResponse(
|
|
426
|
+
status_code=504,
|
|
427
|
+
media_type="application/a2a+json",
|
|
428
|
+
content={
|
|
429
|
+
"error": {
|
|
430
|
+
"code": 504,
|
|
431
|
+
"status": "DEADLINE_EXCEEDED",
|
|
432
|
+
"message": _message_limit_text(limit_seconds),
|
|
433
|
+
"details": [_message_limit_error_info(limit_seconds)],
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _message_limit_json_rpc_error(request_id: object, limit_seconds: float) -> dict[str, Any]:
|
|
440
|
+
return {
|
|
441
|
+
"jsonrpc": "2.0",
|
|
442
|
+
"id": request_id,
|
|
443
|
+
"error": {
|
|
444
|
+
"code": -32000,
|
|
445
|
+
"message": _message_limit_text(limit_seconds),
|
|
446
|
+
"data": [_message_limit_error_info(limit_seconds)],
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
385
451
|
def _without_push_notification_capability(
|
|
386
452
|
agent_card: dict[str, Any] | None,
|
|
387
453
|
) -> dict[str, Any] | None:
|
|
@@ -744,6 +810,7 @@ async def _collect_turn(
|
|
|
744
810
|
provenance: TurnProvenance,
|
|
745
811
|
turn_uid: str | None = None,
|
|
746
812
|
caller_assertion: CallerAssertion | None = None,
|
|
813
|
+
deadline: float | None = None,
|
|
747
814
|
) -> str:
|
|
748
815
|
accumulator = _TurnAccumulator(max_output_bytes=max_output_bytes)
|
|
749
816
|
prompt_options: dict[str, Any] = {"provenance": provenance}
|
|
@@ -751,6 +818,8 @@ async def _collect_turn(
|
|
|
751
818
|
prompt_options["turn_uid"] = turn_uid
|
|
752
819
|
if caller_assertion is not None:
|
|
753
820
|
prompt_options["caller_assertion"] = caller_assertion
|
|
821
|
+
if deadline is not None:
|
|
822
|
+
prompt_options["deadline"] = deadline
|
|
754
823
|
async for event in manager.prompt(context_id, prompt, **prompt_options):
|
|
755
824
|
accumulator.consume(event)
|
|
756
825
|
context = active_task_execution()
|
|
@@ -779,6 +848,7 @@ async def _collect_validated_turn(
|
|
|
779
848
|
provenance: TurnProvenance,
|
|
780
849
|
turn_uid: str | None = None,
|
|
781
850
|
caller_assertion: CallerAssertion | None = None,
|
|
851
|
+
deadline: float | None = None,
|
|
782
852
|
) -> str:
|
|
783
853
|
text = await _collect_turn(
|
|
784
854
|
manager,
|
|
@@ -788,6 +858,7 @@ async def _collect_validated_turn(
|
|
|
788
858
|
provenance=provenance,
|
|
789
859
|
turn_uid=turn_uid,
|
|
790
860
|
caller_assertion=caller_assertion,
|
|
861
|
+
deadline=deadline,
|
|
791
862
|
)
|
|
792
863
|
if not contract.enabled:
|
|
793
864
|
return text
|
|
@@ -818,6 +889,7 @@ async def _collect_validated_turn(
|
|
|
818
889
|
max_output_bytes=max_output_bytes,
|
|
819
890
|
provenance=provenance,
|
|
820
891
|
caller_assertion=caller_assertion,
|
|
892
|
+
deadline=deadline,
|
|
821
893
|
)
|
|
822
894
|
raise AssertionError("strict JSON repair loop did not terminate")
|
|
823
895
|
|
|
@@ -948,6 +1020,16 @@ async def _wait_for_task_return_state(
|
|
|
948
1020
|
return current
|
|
949
1021
|
|
|
950
1022
|
|
|
1023
|
+
def _task_wait_seconds(config: TauSDKSettings, deadline: float | None) -> float:
|
|
1024
|
+
"""How long a request waits for its Task: the configured wait, ended by the platform's
|
|
1025
|
+
Message limit when the platform sets one. Either way the Task keeps running."""
|
|
1026
|
+
|
|
1027
|
+
if deadline is None:
|
|
1028
|
+
return config.a2a_task_wait_timeout_seconds
|
|
1029
|
+
remaining = deadline - asyncio.get_running_loop().time()
|
|
1030
|
+
return min(config.a2a_task_wait_timeout_seconds, remaining)
|
|
1031
|
+
|
|
1032
|
+
|
|
951
1033
|
@dataclass(frozen=True, slots=True)
|
|
952
1034
|
class _ClaimedTask:
|
|
953
1035
|
attempt_uid: str
|
|
@@ -1621,6 +1703,7 @@ async def _execute_message(
|
|
|
1621
1703
|
max_output_bytes: int,
|
|
1622
1704
|
provenance: TurnProvenance,
|
|
1623
1705
|
caller_assertion: CallerAssertion | None = None,
|
|
1706
|
+
deadline: float | None = None,
|
|
1624
1707
|
) -> dict[str, Any]:
|
|
1625
1708
|
text = await _collect_validated_turn(
|
|
1626
1709
|
manager,
|
|
@@ -1630,6 +1713,7 @@ async def _execute_message(
|
|
|
1630
1713
|
max_output_bytes=max_output_bytes,
|
|
1631
1714
|
provenance=provenance,
|
|
1632
1715
|
caller_assertion=caller_assertion,
|
|
1716
|
+
deadline=deadline,
|
|
1633
1717
|
)
|
|
1634
1718
|
message = _agent_message(
|
|
1635
1719
|
context_id=context_id,
|
|
@@ -2288,6 +2372,32 @@ async def message_send(
|
|
|
2288
2372
|
Header(alias="A2A-Extensions"),
|
|
2289
2373
|
] = None,
|
|
2290
2374
|
) -> dict[str, Any]:
|
|
2375
|
+
try:
|
|
2376
|
+
return await _message_send(body, client, manager, config, request, a2a_extensions)
|
|
2377
|
+
except _MessageLimitExceeded as error:
|
|
2378
|
+
return _message_limit_rest_error(error.limit_seconds) # type: ignore[return-value]
|
|
2379
|
+
|
|
2380
|
+
|
|
2381
|
+
async def _message_send(
|
|
2382
|
+
body: dict[str, Any],
|
|
2383
|
+
client: MainSequenceClient,
|
|
2384
|
+
manager: SessionRuntimeManager,
|
|
2385
|
+
config: TauSDKSettings,
|
|
2386
|
+
request: Request,
|
|
2387
|
+
a2a_extensions: str | None,
|
|
2388
|
+
) -> dict[str, Any]:
|
|
2389
|
+
"""Answer one ``message:send``, for REST and JSON-RPC alike.
|
|
2390
|
+
|
|
2391
|
+
When the platform sets a Message limit, it runs from the moment the request arrives. It
|
|
2392
|
+
bounds a Message-mode request as a whole, including output-contract repair turns, and it ends
|
|
2393
|
+
the wait of a Task-mode request that does not return immediately. A Message-mode request that
|
|
2394
|
+
reaches its limit, or its turn's own limit, raises ``_MessageLimitExceeded``, which each
|
|
2395
|
+
transport answers in its own form.
|
|
2396
|
+
"""
|
|
2397
|
+
platform_limit = config.a2a_message_limit_seconds
|
|
2398
|
+
deadline = (
|
|
2399
|
+
None if platform_limit is None else asyncio.get_running_loop().time() + platform_limit
|
|
2400
|
+
)
|
|
2291
2401
|
try:
|
|
2292
2402
|
provenance = _a2a_turn_provenance(config, request)
|
|
2293
2403
|
except CallerIdentityError as error:
|
|
@@ -2362,7 +2472,7 @@ async def message_send(
|
|
|
2362
2472
|
client,
|
|
2363
2473
|
task,
|
|
2364
2474
|
poll_interval_seconds=config.a2a_task_event_poll_interval_seconds,
|
|
2365
|
-
wait_timeout_seconds=config
|
|
2475
|
+
wait_timeout_seconds=_task_wait_seconds(config, deadline),
|
|
2366
2476
|
)
|
|
2367
2477
|
task = await _task_with_history(client, task, history_length=history_length)
|
|
2368
2478
|
return {"task": _task_payload(task, history_length=history_length)}
|
|
@@ -2397,7 +2507,7 @@ async def message_send(
|
|
|
2397
2507
|
client,
|
|
2398
2508
|
task,
|
|
2399
2509
|
poll_interval_seconds=config.a2a_task_event_poll_interval_seconds,
|
|
2400
|
-
wait_timeout_seconds=config
|
|
2510
|
+
wait_timeout_seconds=_task_wait_seconds(config, deadline),
|
|
2401
2511
|
)
|
|
2402
2512
|
task = await _task_with_history(client, task, history_length=history_length)
|
|
2403
2513
|
return {"task": _task_payload(task, history_length=history_length)}
|
|
@@ -2422,31 +2532,52 @@ async def message_send(
|
|
|
2422
2532
|
client,
|
|
2423
2533
|
task,
|
|
2424
2534
|
poll_interval_seconds=config.a2a_task_event_poll_interval_seconds,
|
|
2425
|
-
wait_timeout_seconds=config
|
|
2535
|
+
wait_timeout_seconds=_task_wait_seconds(config, deadline),
|
|
2426
2536
|
)
|
|
2427
2537
|
task = await _task_with_history(client, task, history_length=history_length)
|
|
2428
2538
|
return {"task": _task_payload(task, history_length=history_length)}
|
|
2429
2539
|
request_message_id = str(message["messageId"])
|
|
2540
|
+
context_id = str(message["contextId"])
|
|
2430
2541
|
if config.local_mode:
|
|
2431
2542
|
replay = await client.begin_local_conversation_message(message_to_protocol(message))
|
|
2432
2543
|
if replay is not None:
|
|
2433
2544
|
return {"message": replay}
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2545
|
+
if (
|
|
2546
|
+
platform_limit is not None
|
|
2547
|
+
and deadline is not None
|
|
2548
|
+
and asyncio.get_running_loop().time() >= deadline
|
|
2549
|
+
):
|
|
2550
|
+
# The request used up the platform's limit before its turn could start, so none starts.
|
|
2551
|
+
raise _message_limit_exceeded(context_id, platform_limit)
|
|
2552
|
+
request_limit = asyncio.timeout_at(deadline)
|
|
2553
|
+
try:
|
|
2554
|
+
async with request_limit:
|
|
2555
|
+
result = await _execute_message(
|
|
2556
|
+
manager,
|
|
2557
|
+
context_id=context_id,
|
|
2558
|
+
prompt=prompt,
|
|
2559
|
+
output_contract=output_contract,
|
|
2560
|
+
max_output_bytes=config.max_turn_output_bytes,
|
|
2561
|
+
provenance=provenance,
|
|
2562
|
+
caller_assertion=request_caller_assertion(request),
|
|
2563
|
+
deadline=deadline,
|
|
2564
|
+
)
|
|
2565
|
+
except TimeoutError as error:
|
|
2566
|
+
# The platform's limit for the request or the turn's own limit expired, and the turn was
|
|
2567
|
+
# stopped. The caller learns which limit.
|
|
2568
|
+
raise _message_limit_exceeded(
|
|
2569
|
+
context_id,
|
|
2570
|
+
platform_limit
|
|
2571
|
+
if platform_limit is not None and request_limit.expired()
|
|
2572
|
+
else config.turn_timeout_seconds,
|
|
2573
|
+
) from error
|
|
2443
2574
|
if config.local_mode:
|
|
2444
2575
|
result = await client.complete_local_conversation_message(
|
|
2445
|
-
|
|
2576
|
+
context_id,
|
|
2446
2577
|
request_message_id,
|
|
2447
2578
|
result,
|
|
2448
2579
|
)
|
|
2449
|
-
manager.mark_response_delivered(
|
|
2580
|
+
manager.mark_response_delivered(context_id)
|
|
2450
2581
|
return {"message": result}
|
|
2451
2582
|
|
|
2452
2583
|
|
|
@@ -2703,14 +2834,17 @@ async def json_rpc(
|
|
|
2703
2834
|
|
|
2704
2835
|
try:
|
|
2705
2836
|
if method in {"SendMessage", "message/send"}:
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2837
|
+
try:
|
|
2838
|
+
result = await _message_send(
|
|
2839
|
+
params,
|
|
2840
|
+
client,
|
|
2841
|
+
manager,
|
|
2842
|
+
config,
|
|
2843
|
+
request,
|
|
2844
|
+
a2a_extensions,
|
|
2845
|
+
)
|
|
2846
|
+
except _MessageLimitExceeded as error:
|
|
2847
|
+
return _message_limit_json_rpc_error(request_id, error.limit_seconds)
|
|
2714
2848
|
elif method in {"SendStreamingMessage", "message/stream"}:
|
|
2715
2849
|
_response_kind(
|
|
2716
2850
|
params,
|
|
@@ -1325,6 +1325,7 @@ class SessionRuntimeManager:
|
|
|
1325
1325
|
turn_uid: str | None = None,
|
|
1326
1326
|
caller_assertion: CallerAssertion | None = None,
|
|
1327
1327
|
caller_delivery: CallerDelivery | None = None,
|
|
1328
|
+
deadline: float | None = None,
|
|
1328
1329
|
) -> AsyncIterator[TauRuntimeEvent]:
|
|
1329
1330
|
"""Run one turn in a session.
|
|
1330
1331
|
|
|
@@ -1332,6 +1333,9 @@ class SessionRuntimeManager:
|
|
|
1332
1333
|
request that starts the turn. It is presented only when the turn is marked active.
|
|
1333
1334
|
``caller_delivery`` is, instead, the caller delivery that a turn the platform starts
|
|
1334
1335
|
resumes the session for. It is named only when the turn is marked active.
|
|
1336
|
+
``deadline`` is the event-loop time by which the request that waits for the turn must be
|
|
1337
|
+
answered. The turn then ends at that deadline or at its own time limit, whichever comes
|
|
1338
|
+
first, and either one stops it the same way.
|
|
1335
1339
|
"""
|
|
1336
1340
|
|
|
1337
1341
|
agent_uid = await self._session_agent_uid(session_uid)
|
|
@@ -1351,6 +1355,7 @@ class SessionRuntimeManager:
|
|
|
1351
1355
|
platform_event=platform_event,
|
|
1352
1356
|
caller_assertion=caller_assertion,
|
|
1353
1357
|
caller_delivery=caller_delivery,
|
|
1358
|
+
deadline=deadline,
|
|
1354
1359
|
):
|
|
1355
1360
|
yield event
|
|
1356
1361
|
|
|
@@ -1375,6 +1380,7 @@ class SessionRuntimeManager:
|
|
|
1375
1380
|
platform_event: PlatformEvent | None = None,
|
|
1376
1381
|
caller_assertion: CallerAssertion | None = None,
|
|
1377
1382
|
caller_delivery: CallerDelivery | None = None,
|
|
1383
|
+
deadline: float | None = None,
|
|
1378
1384
|
) -> AsyncIterator[TauRuntimeEvent]:
|
|
1379
1385
|
live_turn = self._open_live_turn(session_uid, content, provenance)
|
|
1380
1386
|
try:
|
|
@@ -1385,6 +1391,7 @@ class SessionRuntimeManager:
|
|
|
1385
1391
|
platform_event=platform_event,
|
|
1386
1392
|
caller_assertion=caller_assertion,
|
|
1387
1393
|
caller_delivery=caller_delivery,
|
|
1394
|
+
deadline=deadline,
|
|
1388
1395
|
):
|
|
1389
1396
|
if live_turn is not None:
|
|
1390
1397
|
live_turn.observe(event)
|
|
@@ -1518,6 +1525,7 @@ class SessionRuntimeManager:
|
|
|
1518
1525
|
platform_event: PlatformEvent | None = None,
|
|
1519
1526
|
caller_assertion: CallerAssertion | None = None,
|
|
1520
1527
|
caller_delivery: CallerDelivery | None = None,
|
|
1528
|
+
deadline: float | None = None,
|
|
1521
1529
|
) -> AsyncIterator[TauRuntimeEvent]:
|
|
1522
1530
|
logger.info(
|
|
1523
1531
|
"agent.run.accepted",
|
|
@@ -1612,10 +1620,12 @@ class SessionRuntimeManager:
|
|
|
1612
1620
|
model=runtime.model,
|
|
1613
1621
|
is_streaming=True,
|
|
1614
1622
|
)
|
|
1623
|
+
limit_at = asyncio.get_running_loop().time() + self.settings.turn_timeout_seconds
|
|
1624
|
+
turn_limit = asyncio.timeout_at(limit_at if deadline is None else min(limit_at, deadline))
|
|
1615
1625
|
requester_token = bind_turn_requester(requester_binding)
|
|
1616
1626
|
try:
|
|
1617
1627
|
try:
|
|
1618
|
-
async with
|
|
1628
|
+
async with turn_limit:
|
|
1619
1629
|
async for event in runtime.prompt(
|
|
1620
1630
|
content,
|
|
1621
1631
|
provenance=provenance,
|
|
@@ -1640,7 +1650,13 @@ class SessionRuntimeManager:
|
|
|
1640
1650
|
runtime.cancel(force=True)
|
|
1641
1651
|
raise
|
|
1642
1652
|
except asyncio.CancelledError:
|
|
1643
|
-
if
|
|
1653
|
+
if turn_limit.expired():
|
|
1654
|
+
# The request's own timer for the same deadline cancelled the turn in the same
|
|
1655
|
+
# step, so the turn's limit could not report itself. It still expired.
|
|
1656
|
+
terminal_status = "timed_out"
|
|
1657
|
+
error_type = "TimeoutError"
|
|
1658
|
+
runtime.cancel(force=True)
|
|
1659
|
+
elif not output_complete:
|
|
1644
1660
|
terminal_status = "cancelled"
|
|
1645
1661
|
error_type = "CancelledError"
|
|
1646
1662
|
raise
|
|
@@ -29,6 +29,7 @@ CALLER_ASSERTION_JWKS_URL_ENV = "MAINSEQUENCE_CALLER_ASSERTION_JWKS_URL"
|
|
|
29
29
|
RESOURCE_RELEASE_UID_ENV = "APP_NAME"
|
|
30
30
|
FASTAPI_PUBLIC_BASE_URL_ENV = "FASTAPI_PUBLIC_BASE_URL"
|
|
31
31
|
ORGANIZATION_ENVIRONMENT_UID_ENV = "MAINSEQUENCE_ORGANIZATION_ENVIRONMENT_UID"
|
|
32
|
+
A2A_MESSAGE_LIMIT_ENV = "MAINSEQUENCE_A2A_MESSAGE_LIMIT_SECONDS"
|
|
32
33
|
HOSTED_SIGNAL_ENVS = (
|
|
33
34
|
RESOURCE_RELEASE_UID_ENV,
|
|
34
35
|
FASTAPI_PUBLIC_BASE_URL_ENV,
|
|
@@ -138,6 +139,14 @@ class TauSDKSettings(BaseSettings):
|
|
|
138
139
|
default=None,
|
|
139
140
|
validation_alias=ORGANIZATION_ENVIRONMENT_UID_ENV,
|
|
140
141
|
)
|
|
142
|
+
# The platform's limit for a direct A2A Message, which holds its request open until the Agent
|
|
143
|
+
# answers. The platform sets it when it hosts the runtime; unset means no platform limit.
|
|
144
|
+
a2a_message_limit_seconds: float | None = Field(
|
|
145
|
+
default=None,
|
|
146
|
+
gt=0,
|
|
147
|
+
allow_inf_nan=False,
|
|
148
|
+
validation_alias=A2A_MESSAGE_LIMIT_ENV,
|
|
149
|
+
)
|
|
141
150
|
local_mode: bool = Field(default=False, validation_alias="TAU_LOCAL_MODE")
|
|
142
151
|
exclude_base_tools: bool = Field(default=False, validation_alias="TAU_EXCLUDE_BASE_TOOLS")
|
|
143
152
|
exclude_mainsequence_mcp: bool = Field(
|
|
@@ -404,6 +413,12 @@ class TauSDKSettings(BaseSettings):
|
|
|
404
413
|
# name a mode exactly.
|
|
405
414
|
return None if value == "" else value
|
|
406
415
|
|
|
416
|
+
@field_validator("a2a_message_limit_seconds", mode="before")
|
|
417
|
+
@classmethod
|
|
418
|
+
def ignore_empty_message_limit(cls, value: object) -> object:
|
|
419
|
+
# Only an empty value counts as unset. Any other value must be a number of seconds.
|
|
420
|
+
return None if value == "" else value
|
|
421
|
+
|
|
407
422
|
@field_validator("local_state_root", "state_root")
|
|
408
423
|
@classmethod
|
|
409
424
|
def normalize_state_roots(cls, value: Path) -> Path:
|
|
@@ -427,6 +442,11 @@ class TauSDKSettings(BaseSettings):
|
|
|
427
442
|
f"{CALLER_AUTH_MODE_ENV}=assertion and {', '.join(HOSTED_SIGNAL_ENVS)}: "
|
|
428
443
|
"the platform sets them only when it hosts the runtime"
|
|
429
444
|
)
|
|
445
|
+
if self.a2a_message_limit_seconds is not None:
|
|
446
|
+
raise ValueError(
|
|
447
|
+
"TAU_LOCAL_MODE has no platform Message limit. Unset "
|
|
448
|
+
f"{A2A_MESSAGE_LIMIT_ENV}: the platform sets it only when it hosts the runtime"
|
|
449
|
+
)
|
|
430
450
|
if self.host == "0.0.0.0" and "host" not in self.model_fields_set:
|
|
431
451
|
self.host = "127.0.0.1"
|
|
432
452
|
elif self.auth_mode != "runtime_credential":
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/tools/mcp_applications.py
RENAMED
|
@@ -17,7 +17,6 @@ opening the session is tried once more when it fails before the tool call is sen
|
|
|
17
17
|
from __future__ import annotations
|
|
18
18
|
|
|
19
19
|
import json
|
|
20
|
-
import math
|
|
21
20
|
import re
|
|
22
21
|
from collections.abc import AsyncGenerator, Mapping, Sequence
|
|
23
22
|
from typing import Protocol
|
|
@@ -49,16 +48,14 @@ from ms_tau_sdk.runtime.requester import (
|
|
|
49
48
|
_TurnApplicationAccess,
|
|
50
49
|
)
|
|
51
50
|
from ms_tau_sdk.settings import TauSDKSettings
|
|
52
|
-
from ms_tau_sdk.tools.mcp_connection import
|
|
51
|
+
from ms_tau_sdk.tools.mcp_connection import TIMEOUT_META_KEY as TIMEOUT_META_KEY
|
|
52
|
+
from ms_tau_sdk.tools.mcp_connection import declared_timeout, mcp_tool_result
|
|
53
53
|
|
|
54
54
|
logger = structlog.get_logger(__name__)
|
|
55
55
|
|
|
56
56
|
_APPLICATION_NAME = re.compile(r"^[a-z][a-z0-9_]{0,39}$")
|
|
57
57
|
_RESERVED_NAMES = frozenset({"mainsequence"})
|
|
58
58
|
|
|
59
|
-
TIMEOUT_META_KEY = "mainsequence.ai/timeout-seconds/v1"
|
|
60
|
-
"""The ``_meta`` key under which an application tool declares how long it may run, in seconds."""
|
|
61
|
-
|
|
62
59
|
|
|
63
60
|
class ApplicationConnector(Protocol):
|
|
64
61
|
async def __call__(
|
|
@@ -204,25 +201,6 @@ def _retryable(error: BaseException) -> bool:
|
|
|
204
201
|
return isinstance(error, httpx.TransportError) or _timed_out(error)
|
|
205
202
|
|
|
206
203
|
|
|
207
|
-
def _declared_timeout(name: str, tool: types.Tool) -> float | None:
|
|
208
|
-
value = (tool.meta or {}).get(TIMEOUT_META_KEY)
|
|
209
|
-
if value is None:
|
|
210
|
-
return None
|
|
211
|
-
if (
|
|
212
|
-
isinstance(value, bool)
|
|
213
|
-
or not isinstance(value, int | float)
|
|
214
|
-
or not math.isfinite(value)
|
|
215
|
-
or value <= 0
|
|
216
|
-
):
|
|
217
|
-
logger.warning(
|
|
218
|
-
"runtime.mcp_application.invalid_timeout",
|
|
219
|
-
application=name,
|
|
220
|
-
mcp_tool=tool.name,
|
|
221
|
-
)
|
|
222
|
-
return None
|
|
223
|
-
return float(value)
|
|
224
|
-
|
|
225
|
-
|
|
226
204
|
def _error(name: str, text: str, **details: JSONValue) -> AgentToolResult:
|
|
227
205
|
return AgentToolResult(
|
|
228
206
|
content=[TextContent(text=text)],
|
|
@@ -339,7 +317,11 @@ def _application_tools(
|
|
|
339
317
|
try:
|
|
340
318
|
declared = next(
|
|
341
319
|
(
|
|
342
|
-
|
|
320
|
+
declared_timeout(
|
|
321
|
+
tool,
|
|
322
|
+
invalid_event="runtime.mcp_application.invalid_timeout",
|
|
323
|
+
application=name,
|
|
324
|
+
)
|
|
343
325
|
for tool in client.tools
|
|
344
326
|
if tool.name == tool_name
|
|
345
327
|
),
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import math
|
|
6
7
|
import re
|
|
7
8
|
from collections.abc import Callable, Mapping
|
|
8
9
|
from copy import deepcopy
|
|
9
10
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Literal, Protocol, cast
|
|
11
|
+
from typing import Any, Literal, Protocol, cast
|
|
11
12
|
|
|
13
|
+
import structlog
|
|
12
14
|
from mcp import types
|
|
13
15
|
from tau_agent.messages import ImageContent, TextContent
|
|
14
16
|
from tau_agent.tools import (
|
|
@@ -19,8 +21,39 @@ from tau_agent.tools import (
|
|
|
19
21
|
)
|
|
20
22
|
from tau_agent.types import JSONValue
|
|
21
23
|
|
|
24
|
+
logger = structlog.get_logger(__name__)
|
|
25
|
+
|
|
22
26
|
_INVALID_TOOL_NAME = re.compile(r"[^A-Za-z0-9_-]")
|
|
23
27
|
|
|
28
|
+
TIMEOUT_META_KEY = "mainsequence.ai/timeout-seconds/v1"
|
|
29
|
+
"""The ``_meta`` key under which an MCP tool declares how long it may run, in seconds."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def declared_timeout(
|
|
33
|
+
tool: types.Tool,
|
|
34
|
+
*,
|
|
35
|
+
invalid_event: str,
|
|
36
|
+
**log_fields: JSONValue,
|
|
37
|
+
) -> float | None:
|
|
38
|
+
"""Return the time limit ``tool`` declares under ``TIMEOUT_META_KEY``, in seconds.
|
|
39
|
+
|
|
40
|
+
None when it declares none. A declaration that is not a finite number greater than zero
|
|
41
|
+
counts as none, and is logged as ``invalid_event`` with the tool's name and ``log_fields``.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
value = (tool.meta or {}).get(TIMEOUT_META_KEY)
|
|
45
|
+
if value is None:
|
|
46
|
+
return None
|
|
47
|
+
if (
|
|
48
|
+
isinstance(value, bool)
|
|
49
|
+
or not isinstance(value, int | float)
|
|
50
|
+
or not math.isfinite(value)
|
|
51
|
+
or value <= 0
|
|
52
|
+
):
|
|
53
|
+
logger.warning(invalid_event, mcp_tool=tool.name, **log_fields)
|
|
54
|
+
return None
|
|
55
|
+
return float(value)
|
|
56
|
+
|
|
24
57
|
|
|
25
58
|
class MCPConnection(Protocol):
|
|
26
59
|
"""The part of an MCP connection client that the adapter uses."""
|
|
@@ -34,6 +67,7 @@ class MCPConnection(Protocol):
|
|
|
34
67
|
arguments: dict[str, object],
|
|
35
68
|
*,
|
|
36
69
|
meta: dict[str, object] | None = None,
|
|
70
|
+
timeout_seconds: float | None = None,
|
|
37
71
|
) -> types.CallToolResult: ...
|
|
38
72
|
|
|
39
73
|
async def read_resource(self, uri: str) -> types.ReadResourceResult: ...
|
|
@@ -129,6 +163,11 @@ def _create_tool(
|
|
|
129
163
|
and bool(getattr(annotations, "idempotentHint", False))
|
|
130
164
|
else "sequential"
|
|
131
165
|
)
|
|
166
|
+
# A tool that declares its time limit is waited for that long; any other tool gets the
|
|
167
|
+
# connection's default wait.
|
|
168
|
+
timeout_seconds = declared_timeout(
|
|
169
|
+
tool, invalid_event="runtime.mcp.invalid_timeout", connection=policy.prefix
|
|
170
|
+
)
|
|
132
171
|
|
|
133
172
|
async def execute(
|
|
134
173
|
tool_call_id: str,
|
|
@@ -145,14 +184,12 @@ def _create_tool(
|
|
|
145
184
|
if policy.check_arguments is not None:
|
|
146
185
|
policy.check_arguments(canonical_name, arguments)
|
|
147
186
|
private_meta = policy.private_meta(tool) if policy.private_meta is not None else None
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
meta=dict(private_meta),
|
|
155
|
-
)
|
|
187
|
+
call_options: dict[str, Any] = {}
|
|
188
|
+
if private_meta is not None:
|
|
189
|
+
call_options["meta"] = dict(private_meta)
|
|
190
|
+
if timeout_seconds is not None:
|
|
191
|
+
call_options["timeout_seconds"] = timeout_seconds
|
|
192
|
+
result = await client.call_tool(canonical_name, dict(arguments), **call_options)
|
|
156
193
|
return mcp_tool_result(
|
|
157
194
|
canonical_name=canonical_name,
|
|
158
195
|
result=result,
|
|
File without changes
|
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/__init__.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/app.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/cli.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/config.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/env_file.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/logs.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/proxy.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/packages/tau-board/src/ms_tau_board/state.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
|
|
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.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/assistant_ui.py
RENAMED
|
File without changes
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/protocols/chat_history.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
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/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
|
{ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.12.dev53}/src/ms_tau_sdk/tools/mainsequence_mcp.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|