ms-tau-sdk 2.0.20.dev71__tar.gz → 2.0.21.dev72__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.20.dev71 → ms_tau_sdk-2.0.21.dev72}/CHANGELOG.md +26 -0
  2. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/PKG-INFO +1 -1
  3. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/pyproject.toml +1 -1
  4. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/a2a.py +153 -68
  5. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/client.py +42 -12
  6. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/local.py +4 -10
  7. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/protocols/a2a_roles.py +44 -1
  8. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/.gitignore +0 -0
  9. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/README.md +0 -0
  10. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  11. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/app.py +0 -0
  12. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  13. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  14. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  15. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  16. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  17. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/state.py +0 -0
  18. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  19. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
  20. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
  21. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  22. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
  23. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/__init__.py +0 -0
  24. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
  25. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +0 -0
  26. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +0 -0
  27. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
  28. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/agent_skills/tau_security_and_access/SKILL.md +0 -0
  29. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/__init__.py +0 -0
  30. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/chat.py +0 -0
  31. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/conversations.py +0 -0
  32. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/dependencies.py +0 -0
  33. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/health.py +0 -0
  34. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/inspection.py +0 -0
  35. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/local_chat.py +0 -0
  36. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/models.py +0 -0
  37. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/request_identity.py +0 -0
  38. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/api/sessions.py +0 -0
  39. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/app.py +0 -0
  40. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/application.py +0 -0
  41. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/__init__.py +0 -0
  42. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/assertions.py +0 -0
  43. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/auth.py +0 -0
  44. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/mcp.py +0 -0
  45. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/models.py +0 -0
  46. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/routes.py +0 -0
  47. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/backend/team_memberships.py +0 -0
  48. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/cli.py +0 -0
  49. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/errors.py +0 -0
  50. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/logging.py +0 -0
  51. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  52. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
  53. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  54. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  55. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/protocols/chat_history.py +0 -0
  56. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  57. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/providers/__init__.py +0 -0
  58. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/providers/definitions.py +0 -0
  59. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/providers/factory.py +0 -0
  60. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
  61. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  62. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/resources/__init__.py +0 -0
  63. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/resources/loader.py +0 -0
  64. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  65. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  66. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  67. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/events.py +0 -0
  68. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  69. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/failures.py +0 -0
  70. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/live_turns.py +0 -0
  71. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/manager.py +0 -0
  72. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/observability.py +0 -0
  73. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  74. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/requester.py +0 -0
  75. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/session.py +0 -0
  76. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  77. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/runtime/task_context.py +0 -0
  78. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  79. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/sessions/storage.py +0 -0
  80. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/settings.py +0 -0
  81. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/skills.py +0 -0
  82. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/tools/__init__.py +0 -0
  83. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
  84. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/tools/mcp_applications.py +0 -0
  85. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/tools/mcp_connection.py +0 -0
  86. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/tools/skill_read.py +0 -0
  87. {ms_tau_sdk-2.0.20.dev71 → ms_tau_sdk-2.0.21.dev72}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.0.21 — 2026-10-21
4
+
5
+ - **Hosted Tasks can be sent, read and listed with their Messages
6
+ ([#111](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/111), ADR 0002).** On the platform,
7
+ a Task-mode `message:send` and GetTask answered HTTP 500 whenever the answer included the Task's
8
+ Messages. The platform stores a Task Message with the role `user` or `agent` and snake-case
9
+ fields such as `message_id`, and the SDK read it as one of its own binding Messages. The
10
+ platform client now translates every Task Message it reads, in a Task's `latest_message` and
11
+ `history` and in the Message tail, into a binding Message. The Task and context IDs come from
12
+ the Task the Message belongs to, and fields only the platform keeps, such as the request hash and
13
+ the requester, are left out. A2A callers get `ROLE_USER` and `ROLE_AGENT` as before.
14
+ - The platform's wake-up for a submitted hosted Task runs it again; the runtime had refused it
15
+ with HTTP 409 because it could not read the Task's requester Message.
16
+ - On the platform, `historyLength` returns the latest Messages instead of the first ones.
17
+ - `list_task_messages()` takes the Task and a `history_length` instead of a Task UID, `limit`
18
+ and `offset`.
19
+
20
+ - **A Task-mode `message:send` that fails after its Task was accepted names the Task
21
+ ([#111](https://github.com/mainsequence-sdk/ms-tau-sdk/issues/111), ADR 0002).** It answered a
22
+ bare HTTP 500 that looked as if nothing had happened, although the Task existed and could
23
+ already run. It now answers HTTP 500 `INTERNAL` with an `ErrorInfo` whose reason is
24
+ `ACCEPTED_TASK_STATE_UNAVAILABLE` and whose metadata gives the `taskId` and `contextId`, and
25
+ JSON-RPC answers `-32603` with the same `ErrorInfo`. The caller reads the Task with GetTask
26
+ instead of sending the Message again. See the
27
+ [runtime contract](docs/reference/runtime-contract.md#task-terminalization-and-failure).
28
+
3
29
  ## 2.0.20 — 2026-10-19
4
30
 
5
31
  - **ADR 0024 records the requirements for file attachments in chat.** The proposed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 2.0.20.dev71
3
+ Version: 2.0.21.dev72
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "2.0.20.dev71"
7
+ version = "2.0.21.dev72"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -8,7 +8,7 @@ import json
8
8
  import re
9
9
  import time
10
10
  import uuid
11
- from collections.abc import AsyncIterator
11
+ from collections.abc import AsyncIterator, Iterator
12
12
  from dataclasses import dataclass, field
13
13
  from datetime import UTC, datetime
14
14
  from enum import StrEnum
@@ -448,6 +448,83 @@ def _message_limit_json_rpc_error(request_id: object, limit_seconds: float) -> d
448
448
  }
449
449
 
450
450
 
451
+ class _AcceptedTaskUnanswered(Exception):
452
+ """A Task was accepted, but the answer that describes it could not be prepared."""
453
+
454
+ def __init__(self, task: AgentTask) -> None:
455
+ super().__init__(f"Task {task.task_id} was accepted, but its state could not be returned")
456
+ self.task_id = task.task_id
457
+ self.context_id = task.context_id
458
+
459
+
460
+ @contextlib.contextmanager
461
+ def _naming_accepted_task(task: AgentTask) -> Iterator[None]:
462
+ """Name ``task`` when the request fails after the Task was accepted.
463
+
464
+ The Task exists and may already run, so a bare server error would suggest that nothing
465
+ happened and invite the caller to send the Message again.
466
+ """
467
+
468
+ try:
469
+ yield
470
+ except Exception as error:
471
+ logger.error(
472
+ "a2a.task.answer_failed",
473
+ message="A Task was accepted, but the answer that describes it could not be prepared",
474
+ task_uid=task.uid,
475
+ task_id=task.task_id,
476
+ error_type=type(error).__name__,
477
+ )
478
+ raise _AcceptedTaskUnanswered(task) from error
479
+
480
+
481
+ def _accepted_task_text(error: _AcceptedTaskUnanswered) -> str:
482
+ return (
483
+ f"Task {error.task_id} was accepted, but its state could not be returned. "
484
+ "Read it with GetTask instead of sending the Message again."
485
+ )
486
+
487
+
488
+ def _accepted_task_error_info(error: _AcceptedTaskUnanswered) -> dict[str, Any]:
489
+ return {
490
+ "@type": "type.googleapis.com/google.rpc.ErrorInfo",
491
+ "reason": "ACCEPTED_TASK_STATE_UNAVAILABLE",
492
+ "domain": "mainsequence.ai",
493
+ # ErrorInfo metadata values are strings.
494
+ "metadata": {"taskId": error.task_id, "contextId": error.context_id},
495
+ }
496
+
497
+
498
+ def _accepted_task_rest_error(error: _AcceptedTaskUnanswered) -> JSONResponse:
499
+ return JSONResponse(
500
+ status_code=500,
501
+ media_type="application/a2a+json",
502
+ content={
503
+ "error": {
504
+ "code": 500,
505
+ "status": "INTERNAL",
506
+ "message": _accepted_task_text(error),
507
+ "details": [_accepted_task_error_info(error)],
508
+ }
509
+ },
510
+ )
511
+
512
+
513
+ def _accepted_task_json_rpc_error(
514
+ request_id: object,
515
+ error: _AcceptedTaskUnanswered,
516
+ ) -> dict[str, Any]:
517
+ return {
518
+ "jsonrpc": "2.0",
519
+ "id": request_id,
520
+ "error": {
521
+ "code": -32603,
522
+ "message": _accepted_task_text(error),
523
+ "data": [_accepted_task_error_info(error)],
524
+ },
525
+ }
526
+
527
+
451
528
  def _without_push_notification_capability(
452
529
  agent_card: dict[str, Any] | None,
453
530
  ) -> dict[str, Any] | None:
@@ -631,7 +708,7 @@ async def _task_with_history(
631
708
  ) -> AgentTask:
632
709
  if history_length == 0:
633
710
  return task.model_copy(update={"history": None})
634
- history = await client.list_task_messages(task.uid, limit=history_length, offset=0)
711
+ history = await client.list_task_messages(task, history_length=history_length)
635
712
  return task.model_copy(update={"history": history})
636
713
 
637
714
 
@@ -2376,6 +2453,8 @@ async def message_send(
2376
2453
  return await _message_send(body, client, manager, config, request, a2a_extensions)
2377
2454
  except _MessageLimitExceeded as error:
2378
2455
  return _message_limit_rest_error(error.limit_seconds) # type: ignore[return-value]
2456
+ except _AcceptedTaskUnanswered as error:
2457
+ return _accepted_task_rest_error(error) # type: ignore[return-value]
2379
2458
 
2380
2459
 
2381
2460
  async def _message_send(
@@ -2392,7 +2471,8 @@ async def _message_send(
2392
2471
  bounds a Message-mode request as a whole, including output-contract repair turns, and it ends
2393
2472
  the wait of a Task-mode request that does not return immediately. A Message-mode request that
2394
2473
  reaches its limit, or its turn's own limit, raises ``_MessageLimitExceeded``, which each
2395
- transport answers in its own form.
2474
+ transport answers in its own form. A Task-mode request that fails after its Task was accepted
2475
+ raises ``_AcceptedTaskUnanswered``, which each transport answers with the Task named.
2396
2476
  """
2397
2477
  platform_limit = config.a2a_message_limit_seconds
2398
2478
  deadline = (
@@ -2447,48 +2527,14 @@ async def _message_send(
2447
2527
  continued_message,
2448
2528
  **_presented_assertion(caller_assertion),
2449
2529
  )
2450
- if config.local_mode:
2451
- task = await _persist_local_recovery_contract(
2452
- client,
2453
- task,
2454
- output_contract=output_contract,
2455
- provenance=provenance,
2456
- )
2457
- await _schedule_task_accelerator(
2458
- client,
2459
- manager,
2460
- task,
2461
- prompt=prompt,
2462
- output_contract=output_contract,
2463
- max_output_bytes=config.max_turn_output_bytes,
2464
- provenance=provenance,
2465
- config=config,
2466
- caller=_request_caller(caller_assertion),
2467
- )
2468
- if return_immediately:
2469
- task = await _task_with_history(client, task, history_length=history_length)
2470
- return {"task": _task_payload(task, history_length=history_length)}
2471
- task = await _wait_for_task_return_state(
2472
- client,
2473
- task,
2474
- poll_interval_seconds=config.a2a_task_event_poll_interval_seconds,
2475
- wait_timeout_seconds=_task_wait_seconds(config, deadline),
2476
- )
2477
- task = await _task_with_history(client, task, history_length=history_length)
2478
- return {"task": _task_payload(task, history_length=history_length)}
2479
- if response_kind is ResponseKind.TASK:
2480
- caller_assertion = _task_caller_assertion(request, config)
2481
- creation = await _create_backend_task(
2482
- client,
2483
- message=message,
2484
- task_id=_task_id(body),
2485
- local_mode=config.local_mode,
2486
- output_contract=output_contract,
2487
- provenance=provenance,
2488
- caller_assertion=caller_assertion,
2489
- )
2490
- task = creation.task
2491
- if creation.created:
2530
+ with _naming_accepted_task(task):
2531
+ if config.local_mode:
2532
+ task = await _persist_local_recovery_contract(
2533
+ client,
2534
+ task,
2535
+ output_contract=output_contract,
2536
+ provenance=provenance,
2537
+ )
2492
2538
  await _schedule_task_accelerator(
2493
2539
  client,
2494
2540
  manager,
@@ -2511,31 +2557,68 @@ async def _message_send(
2511
2557
  )
2512
2558
  task = await _task_with_history(client, task, history_length=history_length)
2513
2559
  return {"task": _task_payload(task, history_length=history_length)}
2560
+ if response_kind is ResponseKind.TASK:
2561
+ caller_assertion = _task_caller_assertion(request, config)
2562
+ creation = await _create_backend_task(
2563
+ client,
2564
+ message=message,
2565
+ task_id=_task_id(body),
2566
+ local_mode=config.local_mode,
2567
+ output_contract=output_contract,
2568
+ provenance=provenance,
2569
+ caller_assertion=caller_assertion,
2570
+ )
2571
+ task = creation.task
2572
+ if creation.created:
2573
+ with _naming_accepted_task(task):
2574
+ await _schedule_task_accelerator(
2575
+ client,
2576
+ manager,
2577
+ task,
2578
+ prompt=prompt,
2579
+ output_contract=output_contract,
2580
+ max_output_bytes=config.max_turn_output_bytes,
2581
+ provenance=provenance,
2582
+ config=config,
2583
+ caller=_request_caller(caller_assertion),
2584
+ )
2585
+ if return_immediately:
2586
+ task = await _task_with_history(client, task, history_length=history_length)
2587
+ return {"task": _task_payload(task, history_length=history_length)}
2588
+ task = await _wait_for_task_return_state(
2589
+ client,
2590
+ task,
2591
+ poll_interval_seconds=config.a2a_task_event_poll_interval_seconds,
2592
+ wait_timeout_seconds=_task_wait_seconds(config, deadline),
2593
+ )
2594
+ task = await _task_with_history(client, task, history_length=history_length)
2595
+ return {"task": _task_payload(task, history_length=history_length)}
2514
2596
  # An existing Task with this ID is answered only to a caller who may address its session.
2515
2597
  await _require_task_session_access(client, task, checked=requested_context_id)
2516
- if config.local_mode and task.status == "submitted":
2517
- recovered_prompt, recovered_contract, recovered_provenance = (
2518
- _recovery_execution_contract(task, config)
2519
- )
2520
- await _schedule_task_accelerator(
2521
- client,
2522
- manager,
2523
- task,
2524
- prompt=recovered_prompt,
2525
- output_contract=recovered_contract,
2526
- max_output_bytes=config.max_turn_output_bytes,
2527
- provenance=recovered_provenance,
2528
- config=config,
2529
- )
2530
- if not return_immediately and task.status not in TASK_RETURN_STATES:
2531
- task = await _wait_for_task_return_state(
2532
- client,
2533
- task,
2534
- poll_interval_seconds=config.a2a_task_event_poll_interval_seconds,
2535
- wait_timeout_seconds=_task_wait_seconds(config, deadline),
2536
- )
2537
- task = await _task_with_history(client, task, history_length=history_length)
2538
- return {"task": _task_payload(task, history_length=history_length)}
2598
+ with _naming_accepted_task(task):
2599
+ if config.local_mode and task.status == "submitted":
2600
+ recovered_prompt, recovered_contract, recovered_provenance = (
2601
+ _recovery_execution_contract(task, config)
2602
+ )
2603
+ await _schedule_task_accelerator(
2604
+ client,
2605
+ manager,
2606
+ task,
2607
+ prompt=recovered_prompt,
2608
+ output_contract=recovered_contract,
2609
+ max_output_bytes=config.max_turn_output_bytes,
2610
+ provenance=recovered_provenance,
2611
+ config=config,
2612
+ )
2613
+ if not return_immediately and task.status not in TASK_RETURN_STATES:
2614
+ task = await _wait_for_task_return_state(
2615
+ client,
2616
+ task,
2617
+ poll_interval_seconds=config.a2a_task_event_poll_interval_seconds,
2618
+ wait_timeout_seconds=_task_wait_seconds(config, deadline),
2619
+ )
2620
+ task = await _task_with_history(client, task, history_length=history_length)
2621
+ return {"task": _task_payload(task, history_length=history_length)}
2539
2622
  request_message_id = str(message["messageId"])
2540
2623
  context_id = str(message["contextId"])
2541
2624
  if config.local_mode:
@@ -2845,6 +2928,8 @@ async def json_rpc(
2845
2928
  )
2846
2929
  except _MessageLimitExceeded as error:
2847
2930
  return _message_limit_json_rpc_error(request_id, error.limit_seconds)
2931
+ except _AcceptedTaskUnanswered as error:
2932
+ return _accepted_task_json_rpc_error(request_id, error)
2848
2933
  elif method in {"SendStreamingMessage", "message/stream"}:
2849
2934
  _response_kind(
2850
2935
  params,
@@ -23,6 +23,7 @@ from ms_tau_sdk.errors import (
23
23
  LocalModeUnsupportedError,
24
24
  SessionNotFoundError,
25
25
  )
26
+ from ms_tau_sdk.protocols.a2a_roles import platform_message_to_binding
26
27
  from ms_tau_sdk.settings import TauSDKSettings
27
28
 
28
29
  from .assertions import ASSERTION_HEADER
@@ -172,6 +173,30 @@ def _backend_rejection_log_fields(detail: object) -> dict[str, object]:
172
173
  return fields
173
174
 
174
175
 
176
+ def _binding_message(task: AgentTask, record: Mapping[str, Any]) -> dict[str, Any]:
177
+ try:
178
+ return platform_message_to_binding(
179
+ record,
180
+ context_id=task.context_id,
181
+ task_id=task.task_id,
182
+ )
183
+ except ValueError as error:
184
+ raise BackendError(
185
+ f"Backend Task {task.task_id} has an invalid Message: {error}"
186
+ ) from error
187
+
188
+
189
+ def _with_binding_messages(task: AgentTask) -> AgentTask:
190
+ """Return ``task`` with the Messages the platform stores as Main Sequence binding Messages."""
191
+
192
+ update: dict[str, Any] = {}
193
+ if task.latest_message is not None:
194
+ update["latest_message"] = _binding_message(task, task.latest_message)
195
+ if task.history is not None:
196
+ update["history"] = [_binding_message(task, record) for record in task.history]
197
+ return task.model_copy(update=update) if update else task
198
+
199
+
175
200
  class MainSequenceClient:
176
201
  def __init__(
177
202
  self,
@@ -970,7 +995,7 @@ class MainSequenceClient:
970
995
  headers={ASSERTION_HEADER: caller_assertion} if caller_assertion else None,
971
996
  )
972
997
  return AgentTaskCreateResult(
973
- task=AgentTask.model_validate(data),
998
+ task=_with_binding_messages(AgentTask.model_validate(data)),
974
999
  created=status_code == 201,
975
1000
  )
976
1001
 
@@ -979,7 +1004,10 @@ class MainSequenceClient:
979
1004
  path = AGENT_TASKS + (f"?{query}" if query else "")
980
1005
  data = await self._request("GET", path, idempotent=True)
981
1006
  values = data.get("results", []) if isinstance(data, dict) else data
982
- return TypeAdapter(list[AgentTask]).validate_python(values or [])
1007
+ return [
1008
+ _with_binding_messages(task)
1009
+ for task in TypeAdapter(list[AgentTask]).validate_python(values or [])
1010
+ ]
983
1011
 
984
1012
  async def get_task_by_protocol_id(
985
1013
  self,
@@ -1008,25 +1036,26 @@ class MainSequenceClient:
1008
1036
  f"{AGENT_TASKS}{task_uid}/{query}",
1009
1037
  idempotent=True,
1010
1038
  )
1011
- return AgentTask.model_validate(data)
1039
+ return _with_binding_messages(AgentTask.model_validate(data))
1012
1040
 
1013
1041
  async def list_task_messages(
1014
1042
  self,
1015
- task_uid: str,
1043
+ task: AgentTask,
1016
1044
  *,
1017
- limit: int = 100,
1018
- offset: int = 0,
1045
+ history_length: int,
1019
1046
  ) -> list[dict[str, Any]]:
1020
- query = urlencode({"limit": max(0, min(int(limit), 100)), "offset": max(0, int(offset))})
1047
+ """Return the latest ``history_length`` Messages of ``task``, oldest first."""
1048
+
1049
+ query = urlencode({"history_length": max(0, min(int(history_length), 100))})
1021
1050
  data = await self._request(
1022
1051
  "GET",
1023
- f"{agent_task_operation(task_uid, 'messages')}?{query}",
1052
+ f"{agent_task_operation(task.uid, 'messages')}?{query}",
1024
1053
  idempotent=True,
1025
1054
  )
1026
1055
  values = data.get("results", []) if isinstance(data, dict) else data
1027
1056
  if not isinstance(values, list) or any(not isinstance(item, dict) for item in values):
1028
1057
  raise BackendError("Backend Task-message response is invalid")
1029
- return [dict(item) for item in values]
1058
+ return [_binding_message(task, item) for item in values]
1030
1059
 
1031
1060
  async def get_task_snapshot(self, task_uid: str) -> AgentTaskSnapshot:
1032
1061
  data = await self._request(
@@ -1034,7 +1063,8 @@ class MainSequenceClient:
1034
1063
  agent_task_operation(task_uid, "snapshot"),
1035
1064
  idempotent=True,
1036
1065
  )
1037
- return AgentTaskSnapshot.model_validate(data)
1066
+ snapshot = AgentTaskSnapshot.model_validate(data)
1067
+ return snapshot.model_copy(update={"task": _with_binding_messages(snapshot.task)})
1038
1068
 
1039
1069
  async def get_task_snapshot_by_protocol_id(self, task_id: str) -> AgentTaskSnapshot:
1040
1070
  task = await self.get_task_by_protocol_id(task_id)
@@ -1262,7 +1292,7 @@ class MainSequenceClient:
1262
1292
  idempotent=True,
1263
1293
  headers={ASSERTION_HEADER: caller_assertion} if caller_assertion else None,
1264
1294
  )
1265
- return AgentTask.model_validate(data)
1295
+ return _with_binding_messages(AgentTask.model_validate(data))
1266
1296
 
1267
1297
  async def cancel_task(self, task_uid: str) -> AgentTask:
1268
1298
  data = await self._request(
@@ -1271,4 +1301,4 @@ class MainSequenceClient:
1271
1301
  json={},
1272
1302
  idempotent=True,
1273
1303
  )
1274
- return AgentTask.model_validate(data)
1304
+ return _with_binding_messages(AgentTask.model_validate(data))
@@ -2590,20 +2590,14 @@ class LocalDevelopmentBackend(MainSequenceClient):
2590
2590
 
2591
2591
  async def list_task_messages(
2592
2592
  self,
2593
- task_uid: str,
2593
+ task: AgentTask,
2594
2594
  *,
2595
- limit: int = 100,
2596
- offset: int = 0,
2595
+ history_length: int,
2597
2596
  ) -> list[dict[str, Any]]:
2598
2597
  def operation() -> list[dict[str, Any]]:
2599
2598
  with closing(self._connect()) as connection:
2600
- self._task_row(connection, task_uid)
2601
- return self._task_messages(
2602
- connection,
2603
- task_uid,
2604
- limit=limit,
2605
- offset=offset,
2606
- )
2599
+ self._task_row(connection, task.uid)
2600
+ return self._task_messages(connection, task.uid, limit=history_length)
2607
2601
 
2608
2602
  return await self._run(operation)
2609
2603
 
@@ -1,4 +1,4 @@
1
- """Role translation between A2A v1 and the Main Sequence Tau binding."""
1
+ """Role translation between A2A v1, the Main Sequence Tau binding, and platform storage."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -19,11 +19,22 @@ class A2AProtocolRole(StrEnum):
19
19
  AGENT = "ROLE_AGENT"
20
20
 
21
21
 
22
+ class PlatformMessageRole(StrEnum):
23
+ """Direction-neutral roles of a Task Message the platform stores."""
24
+
25
+ USER = "user"
26
+ AGENT = "agent"
27
+
28
+
22
29
  _TO_BINDING = {
23
30
  A2AProtocolRole.USER.value: A2AMessageDirection.REQUESTER.value,
24
31
  A2AProtocolRole.AGENT.value: A2AMessageDirection.RESPONDER.value,
25
32
  }
26
33
  _TO_PROTOCOL = {value: key for key, value in _TO_BINDING.items()}
34
+ _FROM_PLATFORM = {
35
+ PlatformMessageRole.USER.value: A2AMessageDirection.REQUESTER.value,
36
+ PlatformMessageRole.AGENT.value: A2AMessageDirection.RESPONDER.value,
37
+ }
27
38
 
28
39
 
29
40
  def _validate_message_shape(message: Mapping[str, object]) -> None:
@@ -60,6 +71,36 @@ def message_to_protocol(message: Mapping[str, object]) -> dict[str, object]:
60
71
  return {**message, "role": _TO_PROTOCOL[role]}
61
72
 
62
73
 
74
+ def platform_message_to_binding(
75
+ record: Mapping[str, object],
76
+ *,
77
+ context_id: str,
78
+ task_id: str,
79
+ ) -> dict[str, object]:
80
+ """Translate a Task Message the platform stores into a Main Sequence binding Message.
81
+
82
+ The platform stores the role ``user`` or ``agent`` and snake-case fields. The Task that owns
83
+ the Message supplies its context and Task IDs. Fields only the platform keeps, such as the
84
+ request hash and the requester, are left out.
85
+ """
86
+
87
+ role = str(record.get("role") or "")
88
+ if role not in _FROM_PLATFORM:
89
+ raise ValueError("Platform Task Message role must be user or agent")
90
+ message: dict[str, object] = {
91
+ "messageId": record.get("message_id"),
92
+ "contextId": context_id,
93
+ "taskId": task_id,
94
+ "role": _FROM_PLATFORM[role],
95
+ "parts": record.get("parts"),
96
+ "metadata": record.get("metadata", {}),
97
+ "extensions": record.get("extensions", []),
98
+ "referenceTaskIds": record.get("reference_task_ids", []),
99
+ }
100
+ _validate_message_shape(message)
101
+ return message
102
+
103
+
63
104
  def has_a2a_direction(
64
105
  message: Mapping[str, object],
65
106
  direction: A2AMessageDirection,
@@ -72,7 +113,9 @@ def has_a2a_direction(
72
113
  __all__ = [
73
114
  "A2AMessageDirection",
74
115
  "A2AProtocolRole",
116
+ "PlatformMessageRole",
75
117
  "has_a2a_direction",
76
118
  "message_to_binding",
77
119
  "message_to_protocol",
120
+ "platform_message_to_binding",
78
121
  ]