ms-tau-sdk 2.0.11.dev52__tar.gz → 2.0.15.dev62__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.
Files changed (87) hide show
  1. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/CHANGELOG.md +76 -0
  2. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/PKG-INFO +7 -13
  3. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/README.md +5 -11
  4. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/pyproject.toml +2 -2
  5. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +5 -0
  6. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +8 -3
  7. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +18 -5
  8. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/agent_skills/tau_security_and_access/SKILL.md +7 -1
  9. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/a2a.py +156 -22
  10. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/backend/mcp.py +220 -76
  11. ms_tau_sdk-2.0.15.dev62/src/ms_tau_sdk/resources/loader.py +76 -0
  12. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/manager.py +18 -2
  13. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/settings.py +20 -0
  14. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/tools/mainsequence_mcp.py +1 -1
  15. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/tools/mcp_applications.py +288 -51
  16. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/tools/mcp_connection.py +60 -21
  17. ms_tau_sdk-2.0.11.dev52/src/ms_tau_sdk/resources/loader.py +0 -30
  18. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/.gitignore +0 -0
  19. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  20. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/app.py +0 -0
  21. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  22. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  23. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  24. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  25. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  26. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/state.py +0 -0
  27. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  28. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
  29. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
  30. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  31. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
  32. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/__init__.py +0 -0
  33. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
  34. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/__init__.py +0 -0
  35. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/chat.py +0 -0
  36. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/conversations.py +0 -0
  37. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/dependencies.py +0 -0
  38. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/health.py +0 -0
  39. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/inspection.py +0 -0
  40. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/local_chat.py +0 -0
  41. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/models.py +0 -0
  42. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/request_identity.py +0 -0
  43. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/api/sessions.py +0 -0
  44. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/app.py +0 -0
  45. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/application.py +0 -0
  46. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/backend/__init__.py +0 -0
  47. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/backend/assertions.py +0 -0
  48. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/backend/auth.py +0 -0
  49. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/backend/client.py +0 -0
  50. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/backend/local.py +0 -0
  51. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/backend/models.py +0 -0
  52. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/backend/routes.py +0 -0
  53. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/cli.py +0 -0
  54. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/errors.py +0 -0
  55. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/logging.py +0 -0
  56. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  57. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
  58. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  59. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
  60. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  61. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
  62. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  63. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/providers/__init__.py +0 -0
  64. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/providers/definitions.py +0 -0
  65. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/providers/factory.py +0 -0
  66. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
  67. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  68. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/resources/__init__.py +0 -0
  69. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  70. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  71. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  72. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/events.py +0 -0
  73. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  74. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/failures.py +0 -0
  75. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
  76. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/observability.py +0 -0
  77. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  78. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/requester.py +0 -0
  79. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/session.py +0 -0
  80. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  81. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/runtime/task_context.py +0 -0
  82. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  83. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/sessions/storage.py +0 -0
  84. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/skills.py +0 -0
  85. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/tools/__init__.py +0 -0
  86. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/tools/skill_read.py +0 -0
  87. {ms_tau_sdk-2.0.11.dev52 → ms_tau_sdk-2.0.15.dev62}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -1,5 +1,81 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ - **Tau reads skills nested in folders
6
+ ([#99](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/99)).** Tau read only the skill
7
+ folders directly in `.tau/skills` and `.agents/skills`, so a skill one folder deeper, such as
8
+ `.agents/skills/mcp_tools/<skill>/SKILL.md`, the folder an application publishes with the
9
+ mainsequence SDK's skills publisher, was skipped without a diagnostic. A folder there without a
10
+ `SKILL.md` now groups skills, at any depth, and its skills are read, listed in the system prompt,
11
+ reloaded and opened by the skill-scoped `read` like the others. This includes the managed
12
+ development skills in `.agents/skills/ms_tau_sdk` and `.agents/skills/mainsequence`. A skill
13
+ name found twice in the same folder keeps the first in path order, with Tau's duplicate diagnostic; a folder inside
14
+ a skill is not a skill, and a linked group folder is not followed.
15
+
16
+ ## 2.0.14 — 2026-10-13
17
+
18
+ - **An Agent reads the skills and resources its declared applications publish
19
+ ([#98](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/98), ADR 0023).** An application
20
+ that published skills through the MCP Skills extension, as the mainsequence SDK's skills
21
+ publisher does, reached its Agent only through `<name>__list_tools` and `<name>__call_tool`: the
22
+ Agent could not discover or read the skills. Each declared application now also gets
23
+ `<name>__list_resources`, which lists its skills, each with its files, and its other resources,
24
+ and `<name>__read_resource`, which reads one of them by URI. `<name>__list_tools` says when the
25
+ application publishes them. An application's session first asks MCP's `server/discover` on
26
+ protocol `2026-07-28`, where the extension is declared, and opens an application that does not
27
+ answer it with `initialize`, as before; the platform's own MCP is unchanged. Only what the
28
+ application lists is read, and a skill file is returned only when its bytes match the size and
29
+ SHA-256 digest of the skill's list. Every read names the application. Published skills never
30
+ become the project's skills and grant nothing; with the coding tools, a model that follows one
31
+ can run commands, which the [Security and access guide](docs/reference/security-model.md#calling-other-applications)
32
+ now states.
33
+
34
+ ## 2.0.13 — 2026-10-12
35
+
36
+ - **ms-tau-sdk installs the MCP Python SDK 2.x.** The `mcp` dependency moves from `mcp>=1.27,<2`
37
+ to `mcp>=2.3,<3`, which brings `httpx2`, the HTTP client MCP 2.x uses. A repository that runs a
38
+ Harness Agent and also serves an application MCP with `mainsequence[mcp]`, which requires MCP 2.x
39
+ for the MCP Skills extension, could not lock both packages; it now can. Agent behaviour does not
40
+ change: platform and application MCP calls keep their time limits, cancellation, the one token
41
+ renewal on 401, and the same failure texts and details, including the refused, server-error and
42
+ timeout statuses that MCP 2.x no longer reports by itself. A project that pins `mcp<2` itself,
43
+ or uses the MCP 1.x API in its own code (`FastMCP`, `McpError`, camelCase fields), must move to
44
+ MCP 2.x. The time-limits guide shows `MCPServer`, MCP 2.x's name for `FastMCP`.
45
+
46
+ ## 2.0.12 — 2026-10-11
47
+
48
+ - **A direct Message that runs out of time gets a typed answer
49
+ ([#94](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/94)).** A `message:send` answered
50
+ with a Message, or JSON-RPC `message/send`, whose turn reached
51
+ `MAINSEQUENCE_TAU_TURN_TIMEOUT_SECONDS` got a plain-text HTTP 500 "Internal Server Error". It now
52
+ gets HTTP 504 `application/a2a+json` with status `DEADLINE_EXCEEDED` and an `ErrorInfo` with
53
+ reason `MESSAGE_RESPONSE_LIMIT_EXCEEDED`, domain `mainsequence.ai` and `metadata.limitSeconds`,
54
+ the limit that expired. JSON-RPC returns the same `ErrorInfo` in a JSON-RPC error (`-32000`).
55
+ The turn is stopped as before; its work is neither rolled back nor replayed. Use Task mode for
56
+ longer work.
57
+ - **A direct Message has the platform's 90-second limit.** The platform gives a direct A2A
58
+ Message one strict limit, 90 seconds, and passes it to the runtime it hosts in
59
+ `MAINSEQUENCE_A2A_MESSAGE_LIMIT_SECONDS`, a platform-set variable that users do not set. The
60
+ runtime enforces it for every caller, from when it receives the request and over the whole
61
+ request, including output-contract repair turns: the turn is stopped and the request gets the
62
+ 504 above with the platform's limit. A shorter `MAINSEQUENCE_TAU_TURN_TIMEOUT_SECONDS` still ends
63
+ the turn first; no setting extends the platform's limit. A Task-mode `message:send` with
64
+ `returnImmediately: false` waits at most until the same limit and returns the Task, which keeps
65
+ running. Streams, chat, caller delivery, Task dispatch and Task turns are unchanged. Without the
66
+ variable, as in local mode (which refuses it), a direct Message has only the turn limit.
67
+ - **Platform MCP tools wait for the limit the platform declares
68
+ ([#93](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/93), ADR 0022 amendment).** Every
69
+ Main Sequence MCP tool call waited `MAINSEQUENCE_TAU_BACKEND_READ_TIMEOUT_SECONDS` (60 seconds by
70
+ default), so an `a2a.send_message` in Message mode whose target answered after 60 seconds was
71
+ abandoned by the calling Agent while the platform still waited and completed the send. A
72
+ platform tool whose `_meta` declares its limit under `mainsequence.ai/timeout-seconds/v1`, the
73
+ key application tools use, is now waited for that long. A tool that declares nothing, or an
74
+ invalid value, keeps the session default; an invalid value is logged as
75
+ `runtime.mcp.invalid_timeout`. `MAINSEQUENCE_TAU_BACKEND_READ_TIMEOUT_SECONDS` now covers only
76
+ ordinary backend reads and platform tools without a declared limit: it has no effect on a Message
77
+ or any platform limit and can only shorten a wait.
78
+
3
79
  ## 2.0.11 — 2026-10-11
4
80
 
5
81
  - **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.11.dev52
3
+ Version: 2.0.15.dev62
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
@@ -18,7 +18,7 @@ Requires-Dist: cryptography>=42
18
18
  Requires-Dist: fastapi>=0.116
19
19
  Requires-Dist: httpx>=0.28
20
20
  Requires-Dist: jsonschema<5,>=4.25
21
- Requires-Dist: mcp<2,>=1.27
21
+ Requires-Dist: mcp<3,>=2.3
22
22
  Requires-Dist: pydantic-settings>=2.10
23
23
  Requires-Dist: pydantic>=2.11
24
24
  Requires-Dist: pyjwt[crypto]<3,>=2.10.1
@@ -139,12 +139,6 @@ and call the platform or another platform application with `platform_client()`,
139
139
  person's delegation by default while the turn serves one; neither exposes a proof or a token. See
140
140
  the [public API](docs/reference/public-api.md#current_requester-and-platform_client).
141
141
 
142
- Job-hosted batch execution is an [accepted design](docs/adrs/0015-job-hosted-batch-execution.md)
143
- with implementation pending. It will run this SDK's configured Tau composition for one assignment
144
- inside a project Job, close it, and exit without starting Uvicorn. The Job continues to own its
145
- status and logs. See the [batch guide](docs/guides/job-hosted-batch-execution.md) for the target
146
- Python integration; the current release does not expose that batch API.
147
-
148
142
  ## Local development without platform sessions
149
143
 
150
144
  Local mode runs the same workspace Tau runtime without registering an Agent or AgentSession. The
@@ -305,8 +299,8 @@ Kubernetes manifest, runtime image, executor bundle, or container-publication pi
305
299
  consuming project owns its dependency lock, deployable artifact, system dependencies, project
306
300
  code, prompts, skills, hooks, and extensions.
307
301
 
308
- The project identity and migration are defined by
309
- [ADR 56](./docs/adrs/adr-56-main-sequence-tau-sdk-workspace-bound-library-deployment.md). See the
302
+ The project identity is defined by
303
+ [ADR 0001](./docs/adrs/0001-workspace-bound-python-sdk.md). See the
310
304
  [quickstart](./docs/getting-started/quickstart.md), [documentation index](./docs/README.md), and
311
305
  [release guide](./docs/reference/releasing.md) for the complete contracts.
312
306
 
@@ -315,12 +309,12 @@ The project identity and migration are defined by
315
309
  The repository uses Python 3.13 and `uv`:
316
310
 
317
311
  ```bash
318
- uv sync --frozen
312
+ uv sync --frozen --extra tau-board
319
313
  uv run pytest
320
- uv run ruff check .
321
- uv run mypy
322
314
  ```
323
315
 
316
+ [Testing and release gates](docs/reference/testing.md) lists every check CI runs.
317
+
324
318
  Releases are immutable, and a merge to `main` is the release. Merging `development` into `main`
325
319
  builds and verifies the wheel and source distribution of the version `pyproject.toml` declares,
326
320
  publishes them to PyPI through OIDC trusted publishing, and tags the merge—for example, `v1.0.0`.
@@ -106,12 +106,6 @@ and call the platform or another platform application with `platform_client()`,
106
106
  person's delegation by default while the turn serves one; neither exposes a proof or a token. See
107
107
  the [public API](docs/reference/public-api.md#current_requester-and-platform_client).
108
108
 
109
- Job-hosted batch execution is an [accepted design](docs/adrs/0015-job-hosted-batch-execution.md)
110
- with implementation pending. It will run this SDK's configured Tau composition for one assignment
111
- inside a project Job, close it, and exit without starting Uvicorn. The Job continues to own its
112
- status and logs. See the [batch guide](docs/guides/job-hosted-batch-execution.md) for the target
113
- Python integration; the current release does not expose that batch API.
114
-
115
109
  ## Local development without platform sessions
116
110
 
117
111
  Local mode runs the same workspace Tau runtime without registering an Agent or AgentSession. The
@@ -272,8 +266,8 @@ Kubernetes manifest, runtime image, executor bundle, or container-publication pi
272
266
  consuming project owns its dependency lock, deployable artifact, system dependencies, project
273
267
  code, prompts, skills, hooks, and extensions.
274
268
 
275
- The project identity and migration are defined by
276
- [ADR 56](./docs/adrs/adr-56-main-sequence-tau-sdk-workspace-bound-library-deployment.md). See the
269
+ The project identity is defined by
270
+ [ADR 0001](./docs/adrs/0001-workspace-bound-python-sdk.md). See the
277
271
  [quickstart](./docs/getting-started/quickstart.md), [documentation index](./docs/README.md), and
278
272
  [release guide](./docs/reference/releasing.md) for the complete contracts.
279
273
 
@@ -282,12 +276,12 @@ The project identity and migration are defined by
282
276
  The repository uses Python 3.13 and `uv`:
283
277
 
284
278
  ```bash
285
- uv sync --frozen
279
+ uv sync --frozen --extra tau-board
286
280
  uv run pytest
287
- uv run ruff check .
288
- uv run mypy
289
281
  ```
290
282
 
283
+ [Testing and release gates](docs/reference/testing.md) lists every check CI runs.
284
+
291
285
  Releases are immutable, and a merge to `main` is the release. Merging `development` into `main`
292
286
  builds and verifies the wheel and source distribution of the version `pyproject.toml` declares,
293
287
  publishes them to PyPI through OIDC trusted publishing, and tags the merge—for example, `v1.0.0`.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "2.0.11.dev52"
7
+ version = "2.0.15.dev62"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -22,7 +22,7 @@ dependencies = [
22
22
  "fastapi>=0.116",
23
23
  "httpx>=0.28",
24
24
  "jsonschema>=4.25,<5",
25
- "mcp>=1.27,<2",
25
+ "mcp>=2.3,<3",
26
26
  "pydantic>=2.11",
27
27
  "pydantic-settings>=2.10",
28
28
  "pyjwt[crypto]>=2.10.1,<3",
@@ -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`, or the
39
- `MAINSEQUENCE_CALLER_ASSERTION_*` variables. The platform sets them when it hosts a runtime, and
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
@@ -26,8 +26,13 @@ second SDK-specific prompt override or companion append file. Other resources fo
26
26
  version pinned by the installed SDK.
27
27
 
28
28
  `.agents/skills/ms_tau_sdk/` is different: it contains managed development instructions for coding
29
- agents working on the repository. It is not loaded as the runtime's `.tau` behavior and must not be
30
- used as application configuration.
29
+ agents working on the repository. It is not the runtime's `.tau` behavior and must not be used as
30
+ application configuration.
31
+
32
+ Tau reads skills in `.tau/skills/` and `.agents/skills/` at any depth: a folder without a
33
+ `SKILL.md` groups skills, such as `.agents/skills/mcp_tools/<skill>/SKILL.md`, the folder an
34
+ application can publish with the mainsequence SDK's skills publisher. The managed skills in
35
+ `.agents/skills/ms_tau_sdk/` are read too.
31
36
 
32
37
  ## Extension ownership
33
38
 
@@ -74,7 +79,7 @@ Task controls requires an actual Task context and is specified by ADR 0015.
74
79
  The A2A Task controls are `task_request_input` and `task_request_authorization`. They remain
75
80
  available in every mode for the agent's own Task; extensions must not register those names.
76
81
  Register project tools under `.tau/extensions/` and verify the effective catalog after loading.
77
- With base tools excluded, the skill-scoped `read` keeps `.tau/skills/` available: it opens a
82
+ With base tools excluded, the skill-scoped `read` keeps the project skills available: it opens a
78
83
  discovered skill's `SKILL.md` and the files in its directory, such as `references/*.md`, and
79
84
  rejects every other path, including symlinks that resolve outside the skill. Tau lists the skills
80
85
  in the system prompt because a tool named `read` exists. Extensions must not register `read` in
@@ -96,6 +101,16 @@ retry guidance for up to `TAU_MCP_APPLICATION_READY_TIMEOUT_SECONDS` (default `1
96
101
  turn time. This startup budget is separate from the tool's execution limit; an expired readiness
97
102
  wait reports `readiness_timeout`. Cancellation, ended delegation and terminal refusals stop the wait.
98
103
 
104
+ ## Skills that applications publish
105
+
106
+ A declared application can publish Agent Skills through the MCP Skills extension, as the
107
+ mainsequence SDK's skills publisher does. The Agent lists them with `<name>__list_resources` and
108
+ reads one file with `<name>__read_resource`; a skill file is returned only when its bytes match the
109
+ size and SHA-256 digest the skill lists. Published skills stay the application's: they are not
110
+ project skills under `.tau/skills/`, and fields such as `allowed-tools` grant nothing. With the
111
+ coding tools, a model that follows a published skill can run commands; set
112
+ `TAU_EXCLUDE_BASE_TOOLS=true` when that must not happen.
113
+
99
114
  ## Invariants extensions must not replace
100
115
 
101
116
  Extensions may change effective agent capabilities, but they must not replace or bypass:
@@ -167,8 +182,6 @@ Rules for tools that act for the requester:
167
182
  - A delegated call uses the person's ordinary permissions, including administrative ones; any other
168
183
  call uses the Agent's own. Each operation decides what it needs. Never infer write permission
169
184
  from view access.
170
- - This needs the platform's matching change, deployed in the same cutover; check the deployed
171
- platform and the installed SDK before depending on it.
172
185
  - Return to the model only business results, such as rows, numbers, and names. Never return the
173
186
  response object, its headers, a token, a proof, or a raw error body.
174
187
  - Keep nothing for another turn or another person. The delegation ends with the turn, and a task
@@ -178,7 +178,13 @@ What makes it safe:
178
178
 
179
179
  A delegated read returns anything the person can view in the Agent's Environment: what they
180
180
  created and what others shared with them, directly or through a team. This holds for Secrets and
181
- every other object. The platform enforces sharing; which of those objects a tool uses is part of
181
+ other shared resources. Ordinary Teams use explicit membership: the creator is added once and
182
+ can be removed; current Organization admins are protected writer members. Removing the admin
183
+ role keeps membership and writer access but ends protection. Only Organization admins manage
184
+ membership, and managing a workload that created a Team does not grant Team control. A Team's
185
+ members still receive the resources shared with it. The Full directory remains unchanged.
186
+ See the SDK Security and access guide's **Teams and membership** section for platform rollout.
187
+ The platform enforces sharing; which of those objects a tool uses is part of
182
188
  the tool's design. For Secrets, the naming convention states that design:
183
189
 
184
190
  - **A personal credential:** a name tied to the person, such as `QUOTES_API_KEY__<person uid>`.
@@ -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.a2a_task_wait_timeout_seconds,
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.a2a_task_wait_timeout_seconds,
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.a2a_task_wait_timeout_seconds,
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
- result = await _execute_message(
2435
- manager,
2436
- context_id=str(message["contextId"]),
2437
- prompt=prompt,
2438
- output_contract=output_contract,
2439
- max_output_bytes=config.max_turn_output_bytes,
2440
- provenance=provenance,
2441
- caller_assertion=request_caller_assertion(request),
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
- str(message["contextId"]),
2576
+ context_id,
2446
2577
  request_message_id,
2447
2578
  result,
2448
2579
  )
2449
- manager.mark_response_delivered(str(message["contextId"]))
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
- result = await message_send(
2707
- params,
2708
- client,
2709
- manager,
2710
- config,
2711
- request,
2712
- a2a_extensions,
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,