ms-tau-sdk 1.2.9.dev16__tar.gz → 1.2.10.dev18__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 (75) hide show
  1. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/CHANGELOG.md +23 -5
  2. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/PKG-INFO +26 -2
  3. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/README.md +25 -1
  4. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/app.py +5 -1
  5. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/state.py +174 -7
  6. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/static/app.js +87 -4
  7. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/static/board.css +4 -0
  8. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/static/index.html +12 -3
  9. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/pyproject.toml +1 -1
  10. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +20 -0
  11. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +36 -1
  12. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +5 -0
  13. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +39 -3
  14. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/api/a2a.py +520 -86
  15. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/app.py +20 -8
  16. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/backend/client.py +41 -6
  17. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/backend/local.py +827 -51
  18. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/backend/models.py +17 -2
  19. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/backend/routes.py +1 -0
  20. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/errors.py +7 -0
  21. ms_tau_sdk-1.2.10.dev18/src/ms_tau_sdk/protocols/a2a_failure.py +118 -0
  22. ms_tau_sdk-1.2.10.dev18/src/ms_tau_sdk/protocols/a2a_roles.py +78 -0
  23. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/manager.py +30 -2
  24. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/task_context.py +14 -1
  25. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/settings.py +28 -0
  26. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/tools/task_control.py +4 -4
  27. ms_tau_sdk-1.2.9.dev16/src/ms_tau_sdk/protocols/a2a_roles.py +0 -23
  28. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/.gitignore +0 -0
  29. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  30. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  31. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  32. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  33. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  34. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  35. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  36. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  37. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/__init__.py +0 -0
  38. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/api/__init__.py +0 -0
  39. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/api/chat.py +0 -0
  40. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/api/dependencies.py +0 -0
  41. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/api/health.py +0 -0
  42. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/api/inspection.py +0 -0
  43. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/api/models.py +0 -0
  44. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/api/sessions.py +0 -0
  45. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/application.py +0 -0
  46. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/backend/__init__.py +0 -0
  47. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/backend/auth.py +0 -0
  48. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/backend/mcp.py +0 -0
  49. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/cli.py +0 -0
  50. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/logging.py +0 -0
  51. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  52. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  53. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  54. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  55. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/providers/__init__.py +0 -0
  56. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/providers/definitions.py +0 -0
  57. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/providers/factory.py +0 -0
  58. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  59. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/resources/__init__.py +0 -0
  60. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/resources/loader.py +0 -0
  61. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  62. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  63. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  64. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/events.py +0 -0
  65. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  66. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/failures.py +0 -0
  67. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/observability.py +0 -0
  68. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  69. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/session.py +0 -0
  70. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  71. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  72. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/sessions/storage.py +0 -0
  73. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/skills.py +0 -0
  74. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/tools/__init__.py +0 -0
  75. {ms_tau_sdk-1.2.9.dev16 → ms_tau_sdk-1.2.10.dev18}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
@@ -1,10 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## 1.2.9 — 2026-09-25
4
-
5
- - Fixed managed A2A Task creation and continuation to send dictionary-valued message extensions
6
- to Django. Missing extensions now become `{}`; unsupported shapes return HTTP 400 before a
7
- backend Task mutation, instead of surfacing Django validation as HTTP 502. (Issue #40.)
3
+ ## 1.2.10 — 2026-09-26
4
+
5
+ - Restored the managed A2A Task creation and continuation adapter removed before the 1.2.9
6
+ release. It sends Django's required snake_case `message_id` and `reference_task_ids` fields
7
+ and omits empty `extensions`, which the backend defaults. Local Task messages retain the
8
+ complete A2A binding shape. A nonempty extension URI list on a managed Task returns HTTP 400
9
+ until the backend can persist it. A clean-wheel check now guards this contract. Fixes #42.
10
+
11
+ ## 1.2.9 — 2026-09-26
12
+
13
+ - Added bounded A2A Task Message history with hard-cut A2A v1 role projection, canonical durable
14
+ status Messages, exact idempotent settlement replay, and reference-only status events.
15
+ - Correlated every Task attempt with one immutable Tau turn and exact entry boundaries, including
16
+ local Task-owned lease fencing, committed/abandoned resolution, and late-write rejection.
17
+ - Rebuilt Tau Board Task detail around Overview, Conversation, Result, Execution, and Technical
18
+ evidence, with byte-exact streamed Artifact reconstruction and bounded secret-redacted execution
19
+ entries.
20
+ - Guaranteed A2A failure observability: standard terminal failed status events, conformant safe
21
+ status Messages, explicit terminalization-unknown transport errors, and bounded request waiting.
22
+ - Added SDK-owned local Task reconciliation. Unclaimed submitted Tasks resume after restart;
23
+ exhausted starts fail terminally, while stale working attempts with uncertain side effects fail
24
+ as ambiguous instead of being blindly executed twice. Health, structured logs, and Tau Board now
25
+ expose recovery and failure evidence.
8
26
  - Added Tau Board's Agent view for readable effective Agent Card inspection, source-grouped tool
9
27
  catalog inspection, project extension diagnostics, and bounded registered-entry source viewing.
10
28
  - Added an explicit local project-tool workbench with schema-generated inputs, authoritative
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 1.2.9.dev16
3
+ Version: 1.2.10.dev18
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
@@ -116,6 +116,12 @@ from ms_tau_sdk import create_app
116
116
  app = create_app()
117
117
  ```
118
118
 
119
+ Job-hosted batch execution is an [accepted design](docs/adrs/0015-job-hosted-batch-execution.md)
120
+ with implementation pending. It will run this SDK's configured Tau composition for one assignment
121
+ inside a project Job, close it, and exit without starting Uvicorn. The Job continues to own its
122
+ status and logs. See the [batch guide](docs/guides/job-hosted-batch-execution.md) for the target
123
+ Python integration; the current release does not expose that batch API.
124
+
119
125
  ## Local development without platform sessions
120
126
 
121
127
  Local mode runs the same workspace Tau runtime without registering an Agent or AgentSession. The
@@ -144,7 +150,25 @@ workspace default. Public A2A Message, Task, streaming, continuation, list/get/c
144
150
  subscription flows run without creating a platform AgentSession. Incoming local A2A calls do not
145
151
  need managed-gateway caller headers: supplied context IDs are mapped into the workspace-local
146
152
  session namespace and local provenance is recorded. Task records and event streams survive process
147
- restart.
153
+ restart. On startup TAU reschedules unclaimed `submitted` Tasks. A stale `working` Task is not
154
+ blindly executed again: if the previous process may have produced an external side effect, TAU
155
+ settles it as `failed` with `ambiguous_execution_outcome`.
156
+
157
+ Task terminality comes only from the standard A2A state. `completed`, `failed`, `canceled`, and
158
+ `rejected` are terminal; `input_required` and `auth_required` are resumable interruptions. A normal
159
+ provider, tool, validation, or execution failure is durably settled as `failed` with a safe A2A
160
+ status Message. If TAU cannot persist that transition, a live stream reports
161
+ `task_terminalization_unknown`; poll or subscribe to the Task while its recovery owner resolves the
162
+ durable state. `returnImmediately=false` waits for at most the configured Task wait period and does
163
+ not cancel a still-running Task when that transport wait expires.
164
+
165
+ Task responses expose bounded A2A Message history separately from Artifacts and execution data.
166
+ Use `configuration.historyLength` on Message send/stream and `historyLength` on Task get/list;
167
+ omission selects the SDK's bounded 100-Message tail, zero omits history without reading it, and a
168
+ positive value returns that latest tail oldest-to-newest. Public A2A roles are `ROLE_USER` and
169
+ `ROLE_AGENT`; Main Sequence requester/responder direction values remain internal to persistence.
170
+ Tau Board renders the conversation, final Artifacts, exact attempt/turn execution, and raw technical
171
+ events as distinct sections.
148
172
 
149
173
  Local mode also appends privacy-filtered, structured JSON Lines to
150
174
  `~/.tau/mainsequence/<workspace-hash>/logs/tau.jsonl`, with bounded rotation. Both the database
@@ -85,6 +85,12 @@ from ms_tau_sdk import create_app
85
85
  app = create_app()
86
86
  ```
87
87
 
88
+ Job-hosted batch execution is an [accepted design](docs/adrs/0015-job-hosted-batch-execution.md)
89
+ with implementation pending. It will run this SDK's configured Tau composition for one assignment
90
+ inside a project Job, close it, and exit without starting Uvicorn. The Job continues to own its
91
+ status and logs. See the [batch guide](docs/guides/job-hosted-batch-execution.md) for the target
92
+ Python integration; the current release does not expose that batch API.
93
+
88
94
  ## Local development without platform sessions
89
95
 
90
96
  Local mode runs the same workspace Tau runtime without registering an Agent or AgentSession. The
@@ -113,7 +119,25 @@ workspace default. Public A2A Message, Task, streaming, continuation, list/get/c
113
119
  subscription flows run without creating a platform AgentSession. Incoming local A2A calls do not
114
120
  need managed-gateway caller headers: supplied context IDs are mapped into the workspace-local
115
121
  session namespace and local provenance is recorded. Task records and event streams survive process
116
- restart.
122
+ restart. On startup TAU reschedules unclaimed `submitted` Tasks. A stale `working` Task is not
123
+ blindly executed again: if the previous process may have produced an external side effect, TAU
124
+ settles it as `failed` with `ambiguous_execution_outcome`.
125
+
126
+ Task terminality comes only from the standard A2A state. `completed`, `failed`, `canceled`, and
127
+ `rejected` are terminal; `input_required` and `auth_required` are resumable interruptions. A normal
128
+ provider, tool, validation, or execution failure is durably settled as `failed` with a safe A2A
129
+ status Message. If TAU cannot persist that transition, a live stream reports
130
+ `task_terminalization_unknown`; poll or subscribe to the Task while its recovery owner resolves the
131
+ durable state. `returnImmediately=false` waits for at most the configured Task wait period and does
132
+ not cancel a still-running Task when that transport wait expires.
133
+
134
+ Task responses expose bounded A2A Message history separately from Artifacts and execution data.
135
+ Use `configuration.historyLength` on Message send/stream and `historyLength` on Task get/list;
136
+ omission selects the SDK's bounded 100-Message tail, zero omits history without reading it, and a
137
+ positive value returns that latest tail oldest-to-newest. Public A2A roles are `ROLE_USER` and
138
+ `ROLE_AGENT`; Main Sequence requester/responder direction values remain internal to persistence.
139
+ Tau Board renders the conversation, final Artifacts, exact attempt/turn execution, and raw technical
140
+ events as distinct sections.
117
141
 
118
142
  Local mode also appends privacy-filtered, structured JSON Lines to
119
143
  `~/.tau/mainsequence/<workspace-hash>/logs/tau.jsonl`, with bounded rotation. Both the database
@@ -283,10 +283,14 @@ async def session_list(request: Request) -> Response:
283
283
 
284
284
  async def task_list(request: Request) -> Response:
285
285
  token, session, new = _session(request)
286
- directory = _directory(request, session, _selected(request, session))
286
+ profile = _selected(request, session)
287
+ directory = _directory(request, session, profile)
287
288
  result = await asyncio.to_thread(
288
289
  state.tasks, directory, session_uid=request.query_params.get("sessionUid", "")
289
290
  )
291
+ health = session.health.get(profile.name, {})
292
+ policy = health.get("a2a_task_policy")
293
+ result["policy"] = policy if isinstance(policy, dict) else {}
290
294
  return _cookie(JSONResponse(result), token, new)
291
295
 
292
296
 
@@ -2,12 +2,14 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import json
6
+ import re
5
7
  import sqlite3
6
8
  from contextlib import closing
7
9
  from pathlib import Path
8
10
  from typing import Any
9
11
 
10
- SCHEMA_VERSION = "2"
12
+ SCHEMA_VERSION = "4"
11
13
  TABLES = (
12
14
  "sessions",
13
15
  "entries",
@@ -27,6 +29,20 @@ PRIVATE_FIELDS = {
27
29
  "status_message_json",
28
30
  "metadata_json",
29
31
  }
32
+ EXECUTION_VALUE_MAX_BYTES = 64 * 1024
33
+ _SENSITIVE_ENTRY_KEYS = {
34
+ "api_key",
35
+ "authorization",
36
+ "client_secret",
37
+ "credential",
38
+ "credentials",
39
+ "password",
40
+ "prompt",
41
+ "secret",
42
+ "system_prompt",
43
+ "token",
44
+ }
45
+ _BEARER_PATTERN = re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]+")
30
46
 
31
47
 
32
48
  class StateError(ValueError):
@@ -167,7 +183,9 @@ def tasks(directory: Path, *, session_uid: str = "", limit: int = 100) -> dict[s
167
183
  params: tuple[object, ...] = (session_uid, limit) if session_uid else (limit,)
168
184
  rows = connection.execute(
169
185
  "SELECT uid, task_id, context_id, agent_session_uid, status, "
170
- "status_timestamp, cancellation_requested, created_at, updated_at "
186
+ "status_timestamp, cancellation_requested, failure_code, "
187
+ "failure_category, failure_retryable, correlation_id, recovery_owner, "
188
+ "recovery_count, terminal_at, created_at, updated_at "
171
189
  f"FROM a2a_tasks {where} ORDER BY created_at DESC LIMIT ?",
172
190
  params,
173
191
  ).fetchall()
@@ -190,6 +208,15 @@ def task_detail(directory: Path, task_id: str) -> dict[str, Any]:
190
208
  if task is None:
191
209
  raise StateError("Task not found")
192
210
  uid = task["uid"]
211
+ runtime_selection = connection.execute(
212
+ "SELECT provider, model, thinking FROM sessions WHERE uid = ?",
213
+ (task["context_id"],),
214
+ ).fetchone()
215
+ messages = connection.execute(
216
+ "SELECT sequence, message_id, role, message_json, created_at "
217
+ "FROM a2a_task_messages WHERE task_uid = ? ORDER BY sequence",
218
+ (uid,),
219
+ ).fetchall()
193
220
  events = connection.execute(
194
221
  "SELECT sequence, uid, event_type, status, message_uid, output_uid, "
195
222
  "payload_json, created_at FROM a2a_task_events "
@@ -197,20 +224,160 @@ def task_detail(directory: Path, task_id: str) -> dict[str, Any]:
197
224
  (uid,),
198
225
  ).fetchall()
199
226
  attempts = connection.execute(
200
- "SELECT uid, attempt_number, state, created_at, updated_at "
227
+ "SELECT uid, attempt_number, state, outcome_category, failure_code, "
228
+ "failure_detail, retryable, correlation_id, turn_uid, "
229
+ "entry_start_sequence, entry_end_sequence, turn_resolution, "
230
+ "created_at, updated_at "
201
231
  "FROM a2a_task_attempts WHERE task_uid = ? ORDER BY attempt_number",
202
232
  (uid,),
203
233
  ).fetchall()
204
234
  outputs = connection.execute(
205
- "SELECT artifact_id, revision, name, finalized, created_at, updated_at "
235
+ "SELECT artifact_id, revision, name, parts_json, metadata_json, "
236
+ "finalized, created_at, updated_at "
206
237
  "FROM a2a_task_outputs WHERE task_uid = ? ORDER BY created_at",
207
238
  (uid,),
208
239
  ).fetchall()
240
+ execution_entries = connection.execute(
241
+ """
242
+ SELECT DISTINCT entries.sequence, entries.entry_type,
243
+ entries.entry_json, entries.idempotency_key, entries.turn_uid
244
+ FROM entries
245
+ JOIN a2a_task_attempts AS attempts
246
+ ON attempts.turn_uid = entries.turn_uid
247
+ AND attempts.task_uid = ?
248
+ WHERE entries.session_uid = ?
249
+ AND attempts.turn_uid IS NOT NULL
250
+ AND (
251
+ attempts.entry_start_sequence IS NULL
252
+ OR entries.sequence >= attempts.entry_start_sequence
253
+ )
254
+ AND (
255
+ attempts.entry_end_sequence IS NULL
256
+ OR entries.sequence < attempts.entry_end_sequence
257
+ )
258
+ ORDER BY entries.sequence
259
+ """,
260
+ (uid, task["context_id"]),
261
+ ).fetchall()
209
262
  except sqlite3.Error as error:
210
263
  raise StateError(f"Cannot read local Task: {type(error).__name__}") from error
264
+ task_value = dict(task)
265
+ raw_status_message = task_value.pop("status_message_json", None)
266
+ try:
267
+ status_message = json.loads(raw_status_message) if raw_status_message else None
268
+ except (TypeError, json.JSONDecodeError):
269
+ status_message = None
270
+ task_value["status_message"] = status_message
271
+ task_value["runtime"] = dict(runtime_selection) if runtime_selection is not None else None
272
+ status_text = ""
273
+ if isinstance(status_message, dict):
274
+ parts = status_message.get("parts")
275
+ if isinstance(parts, list):
276
+ status_text = "\n".join(
277
+ str(part.get("text") or "")
278
+ for part in parts
279
+ if isinstance(part, dict) and part.get("text")
280
+ )
281
+ task_value["failure"] = {
282
+ "code": str(task_value.get("failure_code") or ""),
283
+ "category": str(task_value.get("failure_category") or ""),
284
+ "retryable": (
285
+ bool(task_value["failure_retryable"])
286
+ if task_value.get("failure_retryable") is not None
287
+ else None
288
+ ),
289
+ "message": status_text,
290
+ "correlationId": str(task_value.get("correlation_id") or ""),
291
+ }
292
+ message_values = []
293
+ for row in messages:
294
+ value = dict(row)
295
+ value["message"] = _decode_json(value.pop("message_json"), {})
296
+ message_values.append(value)
297
+ output_values = []
298
+ for row in outputs:
299
+ value = dict(row)
300
+ parts = _decode_json(value.pop("parts_json"), [])
301
+ metadata = _decode_json(value.pop("metadata_json"), {})
302
+ text = "".join(str(part.get("text") or "") for part in parts if isinstance(part, dict))
303
+ byte_size = 0
304
+ for part in parts:
305
+ if not isinstance(part, dict):
306
+ continue
307
+ if isinstance(part.get("text"), str):
308
+ byte_size += len(part["text"].encode("utf-8"))
309
+ elif "data" in part:
310
+ byte_size += len(
311
+ json.dumps(
312
+ part["data"],
313
+ ensure_ascii=False,
314
+ separators=(",", ":"),
315
+ sort_keys=True,
316
+ ).encode("utf-8")
317
+ )
318
+ value.update(
319
+ {
320
+ "parts": parts,
321
+ "metadata": metadata,
322
+ "text": text,
323
+ "byte_size": byte_size,
324
+ }
325
+ )
326
+ output_values.append(value)
327
+ event_values = []
328
+ for row in events:
329
+ value = dict(row)
330
+ value["payload"] = _decode_json(value.pop("payload_json"), {})
331
+ event_values.append(value)
332
+ entry_values = []
333
+ for row in execution_entries:
334
+ value = dict(row)
335
+ value["entry"] = _safe_execution_value(_decode_json(value.pop("entry_json"), {}))
336
+ entry_values.append(value)
211
337
  return {
212
- "task": dict(task),
213
- "events": [dict(row) for row in events],
338
+ "task": task_value,
339
+ "messages": message_values,
340
+ "events": event_values,
214
341
  "attempts": [dict(row) for row in attempts],
215
- "outputs": [dict(row) for row in outputs],
342
+ "outputs": output_values,
343
+ "executionEntries": entry_values,
344
+ }
345
+
346
+
347
+ def _decode_json(value: object, default: Any) -> Any:
348
+ if value is None or value == "":
349
+ return default
350
+ try:
351
+ return json.loads(str(value))
352
+ except (TypeError, json.JSONDecodeError):
353
+ return default
354
+
355
+
356
+ def _safe_execution_value(value: Any, *, key: str = "") -> Any:
357
+ """Bound and redact operational entry data before it reaches the browser."""
358
+
359
+ normalized = key.lower().replace("-", "_")
360
+ if normalized in _SENSITIVE_ENTRY_KEYS or normalized.endswith(
361
+ ("_credential", "_password", "_secret", "_token")
362
+ ):
363
+ return "[REDACTED]"
364
+ safe: Any
365
+ if isinstance(value, dict):
366
+ safe = {
367
+ str(child_key): _safe_execution_value(child, key=str(child_key))
368
+ for child_key, child in value.items()
369
+ }
370
+ elif isinstance(value, list):
371
+ safe = [_safe_execution_value(child) for child in value]
372
+ elif isinstance(value, str):
373
+ safe = _BEARER_PATTERN.sub("Bearer [REDACTED]", value)
374
+ else:
375
+ safe = value
376
+ encoded = json.dumps(safe, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
377
+ if len(encoded) <= EXECUTION_VALUE_MAX_BYTES:
378
+ return safe
379
+ return {
380
+ "truncated": True,
381
+ "originalBytes": len(encoded),
382
+ "message": "Execution entry exceeds the Tau Board inspection limit.",
216
383
  }
@@ -539,7 +539,8 @@ function displayTask(task) {
539
539
  $("a2a-context").value = task.contextId || "";
540
540
  const timing = taskTiming(task);
541
541
  const statusLabel = timing.completed ? "Completed" : (timing.terminal ? "Terminal status" : "Status recorded");
542
- $("task-output").textContent = `${task.id} · ${task.status?.state || "unknown"}\nCreated: ${displayTime(timing.created)}\n${statusLabel}: ${displayTime(timing.statusTime)}\n\n${taskText(task)}`;
542
+ const statusText = (task.status?.message?.parts || []).map((part) => part.text || "").filter(Boolean).join("\n");
543
+ $("task-output").textContent = `${task.id} · ${task.status?.state || "unknown"}\nCreated: ${displayTime(timing.created)}\n${statusLabel}: ${displayTime(timing.statusTime)}${statusText ? `\nReason: ${statusText}` : ""}\n\n${taskText(task)}`;
543
544
  }
544
545
  function handleA2AFrame(frame) {
545
546
  const value = frame.result || frame;
@@ -713,7 +714,12 @@ async function refreshTaskExplorer() {
713
714
  const result = await json(`/api/board/tasks?${query}`, { headers });
714
715
  const list = $("tasks-list"); clear(list);
715
716
  for (const task of result.tasks || []) {
716
- const button = node("button", `${task.task_id} · ${task.status}\n${task.context_id}`, "button is-light");
717
+ const threshold = task.status === "submitted" ? result.policy?.pending_timeout_seconds : result.policy?.stale_after_seconds;
718
+ const recorded = task.status === "submitted" ? task.created_at : task.status_timestamp;
719
+ const ageSeconds = recorded ? (Date.now() - new Date(recorded).valueOf()) / 1000 : 0;
720
+ const stale = ["submitted", "working"].includes(task.status) && threshold && ageSeconds > threshold;
721
+ const suffix = task.failure_code ? `\n${task.failure_code}` : (stale ? "\nRecovery overdue" : "");
722
+ const button = node("button", `${task.task_id} · ${task.status}\n${task.context_id}${suffix}`, stale ? "button is-warning" : "button is-light");
717
723
  button.type = "button";
718
724
  button.classList.toggle("is-selected", selectedTaskId === task.task_id);
719
725
  button.addEventListener("click", () => inspectTask(task.task_id).catch(report));
@@ -765,12 +771,89 @@ async function loadTaskLogs(reset = false) {
765
771
  renderTaskTimeline();
766
772
  }
767
773
 
774
+ function renderTaskConversation() {
775
+ const list = $("tasks-conversation"); clear(list);
776
+ for (const record of selectedTaskData?.messages || []) {
777
+ const message = record.message || {};
778
+ const card = node("article", "", "task-card");
779
+ const head = node("div", "", "task-card-head");
780
+ const role = message.role === "ROLE_REQUESTER" ? "Requester" : (message.role === "ROLE_RESPONDER" ? "Responder" : (message.role || "Message"));
781
+ head.append(node("strong", role), node("span", `#${record.sequence}`, "tag is-light"), node("span", displayTime(record.created_at), "mono"));
782
+ card.append(head);
783
+ for (const part of message.parts || []) {
784
+ if (typeof part.text === "string") card.append(node("pre", part.text));
785
+ else if (Object.hasOwn(part, "data")) card.append(node("pre", JSON.stringify(part.data, null, 2)));
786
+ else card.append(node("pre", JSON.stringify(part, null, 2)));
787
+ }
788
+ list.append(card);
789
+ }
790
+ if (!(selectedTaskData?.messages || []).length) list.append(node("p", "No durable Task Messages.", "help"));
791
+ }
792
+
793
+ function renderTaskResults() {
794
+ const list = $("tasks-result"); clear(list);
795
+ for (const artifact of selectedTaskData?.outputs || []) {
796
+ const card = node("article", "", "task-card");
797
+ const head = node("div", "", "task-card-head");
798
+ head.append(node("strong", artifact.name || artifact.artifact_id), node("span", `revision ${artifact.revision}`, "tag is-light"), node("span", artifact.finalized ? "Final" : "Open", artifact.finalized ? "tag is-success is-light" : "tag is-warning is-light"), node("span", `${artifact.byte_size || 0} bytes`, "tag is-light"));
799
+ card.append(head);
800
+ if (artifact.text) card.append(node("pre", artifact.text));
801
+ for (const part of artifact.parts || []) {
802
+ if (!Object.hasOwn(part, "data")) continue;
803
+ card.append(node("pre", JSON.stringify(part.data, null, 2)));
804
+ }
805
+ list.append(card);
806
+ }
807
+ if (!(selectedTaskData?.outputs || []).length) list.append(node("p", "No Task Artifacts.", "help"));
808
+ }
809
+
810
+ function executionLabel(entry) {
811
+ const raw = JSON.stringify(entry.entry || {}).toLowerCase();
812
+ if (raw.includes("tool_result") || raw.includes("toolresult")) return "Tool result";
813
+ if (raw.includes("tool_call") || raw.includes("toolcall")) return "Tool call";
814
+ if (raw.includes("reasoning") || raw.includes("thinking")) return "Recorded reasoning";
815
+ if (entry.entry_type === "message") return "Model message";
816
+ return entry.entry_type || "Tau entry";
817
+ }
818
+
819
+ function executionOutcome(entry) {
820
+ const value = entry.entry || {};
821
+ const raw = JSON.stringify(value).toLowerCase();
822
+ if (raw.includes('"is_error":true') || raw.includes('"error":true') || raw.includes('"status":"failed"')) return "Failed";
823
+ if (raw.includes('"status":"completed"') || raw.includes('"status":"success"') || raw.includes('"success":true')) return "Succeeded";
824
+ return "";
825
+ }
826
+
827
+ function renderTaskExecution() {
828
+ const list = $("tasks-execution"); clear(list);
829
+ const entries = selectedTaskData?.executionEntries || [];
830
+ for (const attempt of selectedTaskData?.attempts || []) {
831
+ const card = node("article", "", "task-card");
832
+ const head = node("div", "", "task-card-head");
833
+ head.append(node("strong", `Attempt ${attempt.attempt_number}`), node("span", attempt.state, "tag is-info is-light"), node("span", attempt.turn_resolution || "unresolved", attempt.turn_resolution === "committed" ? "tag is-success is-light" : "tag is-warning is-light"));
834
+ card.append(head);
835
+ detailLine(card, "Turn", attempt.turn_uid || "—");
836
+ detailLine(card, "Entry interval", attempt.entry_start_sequence == null ? "—" : `[${attempt.entry_start_sequence}, ${attempt.entry_end_sequence ?? "?"})`);
837
+ const attemptEntries = entries.filter((entry) => entry.turn_uid && entry.turn_uid === attempt.turn_uid && (attempt.entry_start_sequence == null || entry.sequence >= attempt.entry_start_sequence) && (attempt.entry_end_sequence == null || entry.sequence < attempt.entry_end_sequence));
838
+ for (const entry of attemptEntries) card.append(taskEventItem("Tau", `#${entry.sequence}`, executionLabel(entry), entry, executionOutcome(entry)));
839
+ if (!attemptEntries.length) card.append(node("p", "No retained entries in this turn interval.", "help"));
840
+ list.append(card);
841
+ }
842
+ if (!(selectedTaskData?.attempts || []).length) list.append(node("p", "No execution attempts.", "help"));
843
+ }
844
+
768
845
  async function inspectTask(taskId) {
769
846
  selectedTaskId = taskId;
770
847
  selectedTaskData = await json(`/api/board/tasks/${encodeURIComponent(taskId)}`, { headers: { "X-Tau-Board-Profile": $("tasks-profile").value } });
771
848
  const task = selectedTaskData.task;
772
849
  const detail = $("tasks-detail"); clear(detail);
773
- for (const [label, value] of [["Task ID", task.task_id], ["Session", task.context_id], ["Status", task.status], ["Created", task.created_at], ["Updated", task.updated_at], ["Attempts", selectedTaskData.attempts.length], ["Artifacts", selectedTaskData.outputs.length]]) detailLine(detail, label, value);
850
+ const failure = task.failure || {};
851
+ const runtime = task.runtime || {};
852
+ const duration = task.created_at && (task.terminal_at || task.status_timestamp) ? Math.max(0, new Date(task.terminal_at || task.status_timestamp).valueOf() - new Date(task.created_at).valueOf()) : null;
853
+ for (const [label, value] of [["Task ID", task.task_id], ["Session", task.context_id], ["Status", task.status], ["Created", displayTime(task.created_at)], ["Status time", displayTime(task.status_timestamp)], ["Terminal time", displayTime(task.terminal_at)], ["Duration", duration == null || Number.isNaN(duration) ? "—" : `${duration} ms`], ["Provider", runtime.provider || "—"], ["Model", runtime.model || "—"], ["Thinking", runtime.thinking || "—"], ["Failure", failure.message || "—"], ["Failure code", failure.code || "—"], ["Failure category", failure.category || "—"], ["Retry safe", failure.retryable == null ? "—" : String(failure.retryable)], ["Recovery owner", task.recovery_owner || "—"], ["Recovery attempts", task.recovery_count || 0], ["Correlation ID", failure.correlationId || "—"], ["Attempts", selectedTaskData.attempts.length], ["Messages", selectedTaskData.messages.length], ["Artifacts", selectedTaskData.outputs.length]]) detailLine(detail, label, value);
854
+ renderTaskConversation();
855
+ renderTaskResults();
856
+ renderTaskExecution();
774
857
  $("tasks-open-a2a").disabled = false;
775
858
  $("tasks-open-session").disabled = false;
776
859
  for (const button of $("tasks-list").querySelectorAll("button")) button.classList.toggle("is-selected", button.textContent.startsWith(`${taskId} ·`));
@@ -928,7 +1011,7 @@ async function start() {
928
1011
  bind("a2a-new-task", "click", () => { $("a2a-task-id").value = ""; $("a2a-kind").value = "task"; $("a2a-input").focus(); });
929
1012
  bind("refresh-tasks", "click", refreshTasks);
930
1013
  bind("tasks-refresh", "click", refreshTaskExplorer);
931
- bind("tasks-profile", "change", () => { selectedTaskId = ""; selectedTaskData = null; clear($("tasks-detail")); clear($("tasks-timeline")); return refreshTaskExplorer(); });
1014
+ bind("tasks-profile", "change", () => { selectedTaskId = ""; selectedTaskData = null; for (const id of ["tasks-detail", "tasks-conversation", "tasks-result", "tasks-execution", "tasks-timeline"]) clear($(id)); return refreshTaskExplorer(); });
932
1015
  bind("tasks-session", "change", refreshTaskExplorer);
933
1016
  bind("tasks-open-id", "click", () => inspectTask($("tasks-id").value.trim()));
934
1017
  bind("tasks-older", "click", () => loadTaskLogs());
@@ -40,6 +40,10 @@ td { max-width: 24rem; overflow-wrap: anywhere; font-size: .82rem; }
40
40
  .tool-field { margin-bottom: .8rem; }
41
41
  .extension-card { border-bottom: 1px solid var(--bulma-border-weak); padding: .55rem 0; font-size: .82rem; overflow-wrap: anywhere; }
42
42
  #tasks-detail { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 1rem; }
43
+ .task-section { display: grid; gap: .7rem; }
44
+ .task-card { border: 1px solid var(--bulma-border-weak); border-radius: .45rem; padding: .8rem; background: var(--bulma-scheme-main-bis); }
45
+ .task-card .task-card-head { display: flex; flex-wrap: wrap; gap: .45rem; align-items: center; margin-bottom: .55rem; }
46
+ .task-card pre { margin-top: .5rem; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 24rem; overflow: auto; }
43
47
  .settings-table td { vertical-align: middle; }
44
48
  .settings-table td:first-child { min-width: 15rem; }
45
49
  .settings-table td:last-child { min-width: 14rem; }
@@ -146,9 +146,18 @@
146
146
  <div class="column"><div class="box">
147
147
  <h2 class="title is-5">Task detail</h2>
148
148
  <div id="tasks-detail" class="detail-list">Select a Task to inspect its status and events.</div>
149
- <div class="buttons mt-4"><button class="button is-small" id="tasks-open-a2a" disabled>Open in A2A</button><button class="button is-small" id="tasks-open-session" disabled>Open session logs</button></div>
150
- <h3 class="title is-6 mt-5">Event timeline</h3>
151
- <p class="help">Task state events and related session logs are labelled separately. Raw event fields are available by expanding a row.</p>
149
+ <div class="buttons mt-4"><button class="button is-small" id="tasks-open-a2a" disabled>Open in A2A Console</button><button class="button is-small" id="tasks-open-session" disabled>Open session logs</button></div>
150
+ <h3 class="title is-6 mt-5">Conversation</h3>
151
+ <p class="help">Durable requester and responder Messages associated with this Task.</p>
152
+ <div id="tasks-conversation" class="task-section"></div>
153
+ <h3 class="title is-6 mt-5">Result</h3>
154
+ <p class="help">Final Artifacts. Streamed text fragments are joined without added separators.</p>
155
+ <div id="tasks-result" class="task-section"></div>
156
+ <h3 class="title is-6 mt-5">Execution</h3>
157
+ <p class="help">Exact attempt and Tau turn correlation. Internal evidence is operational data, not public Task history.</p>
158
+ <div id="tasks-execution" class="task-section"></div>
159
+ <h3 class="title is-6 mt-5">Technical</h3>
160
+ <p class="help">Task events and related session logs are labelled separately. Expand a row for raw evidence.</p>
152
161
  <div id="tasks-timeline" class="log-list"></div>
153
162
  <button class="button is-small mt-3 is-hidden" id="tasks-older">Load older logs</button>
154
163
  </div></div>
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "1.2.9.dev16"
7
+ version = "1.2.10.dev18"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -76,5 +76,25 @@ When an A2A operation fails, identify the boundary before changing code:
76
76
  5. Wire validation: request/response role, context, extension, or payload invalid.
77
77
  6. Task lifecycle: fence, lease, cancellation, event order, or retry semantics violated.
78
78
 
79
+ For Task lifecycle diagnosis, derive terminality from the Task state; never introduce a second
80
+ terminal flag. Normal execution errors must settle `failed` and streaming must emit that
81
+ authoritative terminal status update. `task_terminalization_unknown` means settlement durability
82
+ is unknown and belongs to the recovery owner. Local SQLite recovery is SDK-owned; managed dispatch
83
+ recovery is backend-owned. Do not retry a stale working attempt when project or MCP side effects may
84
+ have occurred unless a checkpoint or idempotency contract proves replay safe.
85
+
86
+ Treat Task Messages, Artifacts, Tau entries, Task events, and logs as separate ontologies. Use
87
+ `historyLength` only for the bounded durable Message tail. Omission means the SDK default of 100,
88
+ zero omits history without a tail read, and positive values return the latest bounded tail in
89
+ oldest-to-newest order. Public A2A v1 roles are `ROLE_USER` and `ROLE_AGENT`; persistence uses the
90
+ Main Sequence requester/responder direction values. Never expose internal Tau entries, reasoning,
91
+ tool traffic, prompts, or logs as public Task history.
92
+
93
+ Allocate the Tau turn UID before attempt start and pass that exact UID to the runtime prompt. A
94
+ Task attempt owns one turn and one half-open entry interval with a `committed` or `abandoned`
95
+ resolution. Do not correlate by timestamps. A status settlement is absent or one complete durable
96
+ responder Message; never send the removed ad hoc status-detail shape. Status events reference that
97
+ Message and consumers reload the Task snapshot.
98
+
79
99
  Change this SDK only for catalog projection, proof attachment, runtime execution, or transport
80
100
  translation defects. Platform contract changes belong to Django.
@@ -77,7 +77,9 @@ a platform AgentSession. Local mode supports:
77
77
  - subscription resumption with `afterSequence`;
78
78
  - continuation of a Task in `input_required` or `auth_required` by sending a new Message carrying
79
79
  that `taskId`; and
80
- - Task status, messages, artifacts, attempts, and events surviving process restart.
80
+ - Task status, messages, artifacts, attempts, and events surviving process restart, with the SDK
81
+ rescheduling safe unclaimed work and terminalizing stale uncertain work rather than replaying
82
+ possible side effects.
81
83
 
82
84
  To request a Task from `message:send`, send the response-kind extension header
83
85
  `A2A-Extensions: https://mainsequence.ai/a2a/extensions/response-kind/v1` together with
@@ -85,6 +87,33 @@ To request a Task from `message:send`, send the response-kind extension header
85
87
  response. `message:stream` is always Task-oriented and does not accept `responseKind` or
86
88
  `returnImmediately`.
87
89
 
90
+ Task terminality is derived only from its A2A state. `completed`, `failed`, `canceled`, and
91
+ `rejected` are terminal; `input_required` and `auth_required` are resumable. A normal execution
92
+ failure produces a durable failed Task and a standard responder `status.message`. A
93
+ `task_terminalization_unknown` stream error means the terminal write itself could not be proven;
94
+ retrieve or subscribe to the Task while recovery resolves it. Never treat an HTTP disconnect or
95
+ bounded wait expiry as Task cancellation.
96
+
97
+ Task responses include a bounded Message tail by default. Set `configuration.historyLength` on
98
+ Message send/stream or `historyLength` on Task get/list: zero omits history and performs no tail
99
+ read, while a positive value returns the latest Messages oldest-to-newest, capped at 100. History
100
+ contains requester and deliberately durable responder/status Messages only. Final output remains
101
+ under `artifacts`; Tau entries, tool activity, events, and logs never become public history. Public
102
+ roles are `ROLE_USER`/`ROLE_AGENT`; the local SQLite binding stores requester/responder directions.
103
+
104
+ Each local Task attempt reserves one immutable Tau turn. Its entries share that turn UID and remain
105
+ inside the recorded half-open sequence interval. Completed and interrupted settlement requires a
106
+ committed turn; failed, rejected, or canceled settlement can abandon a pending turn, after which
107
+ late append or commit is rejected. An unresolved Task-owned turn prevents lease replacement until
108
+ the local Task reconciler records its recovery outcome. Ordinary chat/direct-Message turns have no
109
+ Task owner and retain normal lease cleanup.
110
+
111
+ After restart, the local reconciler schedules an unclaimed `submitted` Task from its persisted
112
+ request. It does not blindly rerun a stale `working` Task: once the old lease and stale interval
113
+ expire, an attempt without replay-safe checkpoint evidence becomes failed with
114
+ `ambiguous_execution_outcome`. Inspect the failure code, attempt, recovery owner, and correlation ID
115
+ in Tau Board and the structured log.
116
+
88
117
  Incoming local A2A requests do not need managed-gateway `X-Caller-*` headers. TAU assigns
89
118
  workspace-local provenance and maps each supplied `contextId` into the same workspace-scoped local
90
119
  session namespace used by chat. A supplied `taskId` remains the public Task identifier. The local
@@ -123,6 +152,12 @@ tool still has the Tau process's real filesystem, network, environment, and cred
123
152
  there is no automatic rollback. A busy session, stale catalog, expired confirmation, non-project
124
153
  tool, oversized output, timeout, or cancellation must fail explicitly.
125
154
 
155
+ The Tasks tab is the primary inspection surface: Overview states the lifecycle, Conversation shows
156
+ durable Messages, Result shows complete Artifacts, Execution uses exact attempt/turn correlation,
157
+ and Technical retains mutation events and logs. **Open in A2A Console** is an action shortcut, not
158
+ the Task explanation. Many `output_updated` records represent revisions of one Artifact and must
159
+ not be interpreted as many agent responses.
160
+
126
161
  ## Outbound A2A through Main Sequence MCP
127
162
 
128
163
  Keep the projected `a2a.send_message` tool available. A local agent may send a Message or create a
@@ -59,6 +59,11 @@ Set them in `harness_agent.spec.env_vars` in a managed repository workflow, in t
59
59
  environment for local `ms-tau`, or through explicit `TauSDKSettings` fields in a Python host. They
60
60
  apply to all sessions in that process; neither Agent Card skills nor prompt text removes tools.
61
61
 
62
+ The accepted, pending job-hosted batch entry point uses these same two settings. It must not
63
+ start Main Sequence MCP or add coding tools when the corresponding source is excluded. The table
64
+ below describes the currently implemented service and local modes; batch behavior for A2A-only
65
+ Task controls requires an actual Task context and is specified by ADR 0015.
66
+
62
67
  | Exclude base tools | Exclude Main Sequence MCP | Model-facing sources |
63
68
  | --- | --- | --- |
64
69
  | `false` | `false` | Coding tools, Main Sequence MCP, project extension tools, A2A Task controls. |