ms-tau-sdk 1.2.12.dev20__tar.gz → 1.2.12.dev21__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 (79) hide show
  1. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/CHANGELOG.md +19 -5
  2. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/PKG-INFO +10 -1
  3. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/README.md +9 -0
  4. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/pyproject.toml +1 -1
  5. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/agent_skills/tau_local_development/SKILL.md +10 -0
  6. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/chat.py +20 -3
  7. ms_tau_sdk-1.2.12.dev21/src/ms_tau_sdk/api/local_chat.py +256 -0
  8. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/app.py +6 -1
  9. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/client.py +26 -0
  10. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/local.py +243 -5
  11. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/models.py +31 -0
  12. ms_tau_sdk-1.2.12.dev21/src/ms_tau_sdk/protocols/chat_history.py +496 -0
  13. ms_tau_sdk-1.2.12.dev21/src/ms_tau_sdk/runtime/live_turns.py +135 -0
  14. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/manager.py +171 -4
  15. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/settings.py +5 -0
  16. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/.gitignore +0 -0
  17. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/__init__.py +0 -0
  18. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/app.py +0 -0
  19. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/cli.py +0 -0
  20. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/config.py +0 -0
  21. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/env_file.py +0 -0
  22. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/logs.py +0 -0
  23. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/proxy.py +0 -0
  24. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/state.py +0 -0
  25. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/BULMA-LICENSE.txt +0 -0
  26. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/app.js +0 -0
  27. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/board.css +0 -0
  28. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/bulma.min.css +0 -0
  29. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/packages/tau-board/src/ms_tau_board/static/index.html +0 -0
  30. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/__init__.py +0 -0
  31. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/agent_skills/tau_a2a_runtime_adapter/SKILL.md +0 -0
  32. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/agent_skills/tau_project_customization/SKILL.md +0 -0
  33. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/agent_skills/tau_repository_integration/SKILL.md +0 -0
  34. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/__init__.py +0 -0
  35. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/a2a.py +0 -0
  36. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/conversations.py +0 -0
  37. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/dependencies.py +0 -0
  38. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/health.py +0 -0
  39. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/inspection.py +0 -0
  40. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/models.py +0 -0
  41. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/api/sessions.py +0 -0
  42. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/application.py +0 -0
  43. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/__init__.py +0 -0
  44. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/auth.py +0 -0
  45. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/mcp.py +0 -0
  46. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/backend/routes.py +0 -0
  47. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/cli.py +0 -0
  48. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/errors.py +0 -0
  49. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/logging.py +0 -0
  50. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/__init__.py +0 -0
  51. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/a2a_failure.py +0 -0
  52. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/a2a_message.py +0 -0
  53. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/a2a_roles.py +0 -0
  54. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/assistant_ui.py +0 -0
  55. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/protocols/strict_json.py +0 -0
  56. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/providers/__init__.py +0 -0
  57. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/providers/definitions.py +0 -0
  58. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/providers/factory.py +0 -0
  59. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/providers/tau_compat.py +0 -0
  60. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/resources/SYSTEM.md +0 -0
  61. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/resources/__init__.py +0 -0
  62. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/resources/loader.py +0 -0
  63. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/resources/prompts/review-code-repository.md +0 -0
  64. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/__init__.py +0 -0
  65. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/deployment_health.py +0 -0
  66. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/events.py +0 -0
  67. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/extensions.py +0 -0
  68. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/failures.py +0 -0
  69. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/observability.py +0 -0
  70. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/provenance.py +0 -0
  71. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/session.py +0 -0
  72. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/snapshots.py +0 -0
  73. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/runtime/task_context.py +0 -0
  74. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/sessions/__init__.py +0 -0
  75. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/sessions/storage.py +0 -0
  76. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/skills.py +0 -0
  77. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/tools/__init__.py +0 -0
  78. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/tools/mainsequence_mcp.py +0 -0
  79. {ms_tau_sdk-1.2.12.dev20 → ms_tau_sdk-1.2.12.dev21}/src/ms_tau_sdk/tools/task_control.py +0 -0
@@ -1,7 +1,21 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
4
-
3
+ ## 1.2.12 — 2026-09-27
4
+
5
+ - Local `/api/chat` sessions now survive a UI reload and a Tau restart. Local mode records each chat
6
+ session and adds `GET /api/local/v1/chat-sessions` to list them and
7
+ `GET /api/local/v1/chat-sessions/{sessionUid}/history` to read one back. The history uses the
8
+ shape of the platform's `GET /api/v1/agent-sessions/{uid}/history/`: user and assistant text,
9
+ reasoning, and tool calls with their arguments, results, and `isError`, projected from the Tau
10
+ transcript by the platform's rules. A turn still running is returned in `inProgressMessage`.
11
+ `GET /api/local/v1/agent` returns the Agent's name and description from the workspace's
12
+ `.agents/agent_card.json`. A local chat turn now runs to its durable end when its client
13
+ disconnects or reloads, and only `POST /api/chat/session/cancel` stops it. A second turn for a
14
+ session that is still running returns 409 `session_busy`. After a Stop, the next turn in a local
15
+ session used to fail with "Runtime cancellation was requested" until the idle runtime was
16
+ evicted; the session's next turn now runs on a fresh runtime. Managed chat behavior is unchanged.
17
+ With `MAINSEQUENCE_TAU_TRUSTED_ORIGINS` set, CORS also exposes `X-Agent-Session-Uid` and
18
+ `x-vercel-ai-ui-message-stream`. See ADR 0018. Fixes #47.
5
19
  - Added `MAINSEQUENCE_TAU_PROVIDER_TIMEOUT_SECONDS`, the HTTP timeout for model-provider calls.
6
20
  It was fixed at 60 seconds for every provider hydrated from the platform, with no setting
7
21
  or environment variable to change it. A self-hosted OpenAI-compatible provider such as an
@@ -9,15 +23,15 @@
9
23
  prompt exhausted all three attempts, and Tau reported only an empty transport failure. The
10
24
  default stays 60 seconds, and an explicit `timeout_seconds` passed to `ProviderFactory.build`
11
25
  still takes precedence.
26
+
27
+ ## 1.2.11 — 2026-09-27
28
+
12
29
  - Added authenticated-process-scoped discovery and bounded hydration for direct local A2A Message
13
30
  conversations. The SDK now persists exact public requester/responder Messages independently from
14
31
  Tau execution entries, returns stable context and Message identities across restart, supports
15
32
  cursor pagination and exact completed-request replay, and exposes
16
33
  `GET /api/local/v1/conversations` plus its bounded Message-history route. Existing internal
17
34
  sessions are not heuristically reconstructed. Fixes #45.
18
-
19
- ## 1.2.11 — 2026-09-27
20
-
21
35
  - Added a version-locked `tau-ai==0.4.2` compatibility patch for OpenAI-compatible transport
22
36
  failures that exhausted retries with an empty terminal message. The SDK now preserves the
23
37
  concrete HTTPX error type, transport phase, retry evidence, total provider duration, one failure
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: ms-tau-sdk
3
- Version: 1.2.12.dev20
3
+ Version: 1.2.12.dev21
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
@@ -172,6 +172,15 @@ events as distinct sections. Correlated provider failures appear first as one hu
172
172
  incident with cause, phase, retries, duration, and bounded traceback frames; raw JSON is an
173
173
  expandable secondary view.
174
174
 
175
+ Local `/api/chat` sessions are reload-safe as well. List them with
176
+ `GET /api/local/v1/chat-sessions` and read one back with
177
+ `GET /api/local/v1/chat-sessions/{sessionUid}/history`, which returns the platform's chat history
178
+ shape: user and assistant text, reasoning, and tool calls with their results, plus a turn still
179
+ running in `inProgressMessage`. Continue a session by sending its `sessionUid` to `POST /api/chat`.
180
+ A local chat turn keeps running when its client disconnects; stop it with
181
+ `POST /api/chat/session/cancel`. `GET /api/local/v1/agent` returns the Agent's name and description
182
+ from the workspace's `.agents/agent_card.json`.
183
+
175
184
  Direct local A2A Message conversations are also reload-safe without becoming Tasks. List them with
176
185
  `GET /api/local/v1/conversations`, then hydrate one with
177
186
  `GET /api/local/v1/conversations/{contextId}/messages`; both reads are bounded and scoped to the
@@ -141,6 +141,15 @@ events as distinct sections. Correlated provider failures appear first as one hu
141
141
  incident with cause, phase, retries, duration, and bounded traceback frames; raw JSON is an
142
142
  expandable secondary view.
143
143
 
144
+ Local `/api/chat` sessions are reload-safe as well. List them with
145
+ `GET /api/local/v1/chat-sessions` and read one back with
146
+ `GET /api/local/v1/chat-sessions/{sessionUid}/history`, which returns the platform's chat history
147
+ shape: user and assistant text, reasoning, and tool calls with their results, plus a turn still
148
+ running in `inProgressMessage`. Continue a session by sending its `sessionUid` to `POST /api/chat`.
149
+ A local chat turn keeps running when its client disconnects; stop it with
150
+ `POST /api/chat/session/cancel`. `GET /api/local/v1/agent` returns the Agent's name and description
151
+ from the workspace's `.agents/agent_card.json`.
152
+
144
153
  Direct local A2A Message conversations are also reload-safe without becoming Tasks. List them with
145
154
  `GET /api/local/v1/conversations`, then hydrate one with
146
155
  `GET /api/local/v1/conversations/{contextId}/messages`; both reads are bounded and scoped to the
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ms-tau-sdk"
7
- version = "1.2.12.dev20"
7
+ version = "1.2.12.dev21"
8
8
  description = "Workspace-bound Tau application primitives for Main Sequence projects"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.13"
@@ -81,6 +81,16 @@ a platform AgentSession. Local mode supports:
81
81
  rescheduling safe unclaimed work and terminalizing stale uncertain work rather than replaying
82
82
  possible side effects.
83
83
 
84
+ For a chat UI over `POST /api/chat`, do not read `runtime.sqlite3` or keep a browser-only
85
+ transcript either. List sessions with `GET /api/local/v1/chat-sessions?limit=<n>&cursor=<opaque>`
86
+ and hydrate one with `GET /api/local/v1/chat-sessions/{sessionUid}/history`. The history has the
87
+ platform's `GET /api/v1/agent-sessions/{uid}/history/` shape, including reasoning, tool calls with
88
+ results, and a running turn in `inProgressMessage`, so one reader serves local and deployed
89
+ Agents. Reuse the listed `sessionUid` in `POST /api/chat`. A local chat turn keeps running when
90
+ the client disconnects or reloads; stop it only with `POST /api/chat/session/cancel`, and queue a
91
+ new message while a session is `working` (`POST /api/chat` returns 409 `session_busy`). Title the
92
+ UI from `GET /api/local/v1/agent`, which reads the workspace's `.agents/agent_card.json`.
93
+
84
94
  For the default direct Message flow, do not read `runtime.sqlite3` or keep a browser-only source of
85
95
  truth. Discover conversations with `GET /api/local/v1/conversations?limit=<n>&cursor=<opaque>` and
86
96
  hydrate the latest bounded Message tail with
@@ -95,11 +95,24 @@ async def chat(
95
95
  message_count=len(body.messages) if body.messages else None,
96
96
  ),
97
97
  )
98
+ # A local turn is recorded as a chat session and outlives this request, so a
99
+ # reload or a closed tab leaves it running; only session/cancel stops it. A
100
+ # managed turn stays bound to the request that streams it.
101
+ local_turn = (
102
+ await manager.start_local_chat_turn(session_uid, prompt, provenance=provenance)
103
+ if manager.settings.local_mode
104
+ else None
105
+ )
106
+ events = (
107
+ local_turn.events()
108
+ if local_turn is not None
109
+ else manager.prompt(session_uid, prompt, provenance=provenance)
110
+ )
98
111
 
99
112
  async def stream() -> AsyncIterator[bytes]:
100
113
  encoder = AssistantUiEncoder()
101
114
  try:
102
- async for event in manager.prompt(session_uid, prompt, provenance=provenance):
115
+ async for event in events:
103
116
  for payload in encoder.encode(event):
104
117
  yield encoder.sse(payload)
105
118
  try:
@@ -107,9 +120,10 @@ async def chat(
107
120
  yield encoder.sse(payload)
108
121
  yield encoder.done()
109
122
  finally:
110
- manager.mark_response_delivered(session_uid)
123
+ if local_turn is None:
124
+ manager.mark_response_delivered(session_uid)
111
125
  except asyncio.CancelledError:
112
- if not encoder.finished:
126
+ if local_turn is None and not encoder.finished:
113
127
  await manager.cancel(session_uid)
114
128
  raise
115
129
  except Exception as error:
@@ -144,6 +158,9 @@ async def chat(
144
158
  }
145
159
  )
146
160
  yield encoder.done()
161
+ finally:
162
+ if local_turn is not None:
163
+ local_turn.detach()
147
164
 
148
165
  return StreamingResponse(
149
166
  stream(),
@@ -0,0 +1,256 @@
1
+ """Reload-safe local `/api/chat` sessions and the local Agent's identity (ADR 0018)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ from dataclasses import dataclass
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Annotated, Any
11
+
12
+ import structlog
13
+ from fastapi import APIRouter, Depends, HTTPException, Query
14
+
15
+ from ms_tau_sdk.backend.client import MainSequenceClient
16
+ from ms_tau_sdk.backend.models import LocalChatSessionSummary
17
+ from ms_tau_sdk.protocols.chat_history import (
18
+ HISTORY_VERSION,
19
+ HistoryMessage,
20
+ current_branch,
21
+ iso_timestamp,
22
+ latest_message_preview,
23
+ project_history,
24
+ )
25
+ from ms_tau_sdk.runtime.manager import SessionRuntimeManager
26
+ from ms_tau_sdk.settings import TauSDKSettings
27
+
28
+ from .dependencies import backend, runtime_manager, settings
29
+
30
+ router = APIRouter(prefix="/api/local/v1")
31
+ BackendDep = Annotated[MainSequenceClient, Depends(backend)]
32
+ RuntimeManagerDep = Annotated[SessionRuntimeManager, Depends(runtime_manager)]
33
+ SettingsDep = Annotated[TauSDKSettings, Depends(settings)]
34
+ logger = structlog.get_logger(__name__)
35
+
36
+ AGENT_CARD_PATH = Path(".agents") / "agent_card.json"
37
+ LOCAL_AGENT_NAME = "Local Main Sequence TAU Agent"
38
+
39
+
40
+ def _require_local(config: TauSDKSettings) -> None:
41
+ if not config.local_mode:
42
+ raise HTTPException(
43
+ status_code=409,
44
+ detail="Local chat sessions require TAU_LOCAL_MODE=true",
45
+ )
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class AgentIdentity:
50
+ name: str | None = None
51
+ description: str | None = None
52
+
53
+
54
+ def _read_agent_identity(workspace: Path) -> AgentIdentity:
55
+ """Read the name and description of the workspace's `.agents/agent_card.json`.
56
+
57
+ The platform takes a deployed Agent's name and description from the same file.
58
+ A missing or unreadable card leaves both unset rather than inventing them.
59
+ """
60
+
61
+ path = workspace / AGENT_CARD_PATH
62
+ try:
63
+ card = json.loads(path.read_text(encoding="utf-8"))
64
+ except FileNotFoundError:
65
+ return AgentIdentity()
66
+ except (OSError, UnicodeDecodeError, json.JSONDecodeError) as error:
67
+ logger.warning(
68
+ "local_agent.card_unreadable",
69
+ message="The workspace Agent Card could not be read",
70
+ path=str(AGENT_CARD_PATH),
71
+ error_type=type(error).__name__,
72
+ )
73
+ return AgentIdentity()
74
+ if not isinstance(card, dict):
75
+ return AgentIdentity()
76
+
77
+ def text(key: str) -> str | None:
78
+ value = card.get(key)
79
+ return value.strip() if isinstance(value, str) and value.strip() else None
80
+
81
+ return AgentIdentity(name=text("name"), description=text("description"))
82
+
83
+
84
+ async def workspace_agent_identity(workspace: Path) -> AgentIdentity:
85
+ return await asyncio.to_thread(_read_agent_identity, workspace)
86
+
87
+
88
+ @dataclass(frozen=True, slots=True)
89
+ class _SessionProjection:
90
+ summary: LocalChatSessionSummary
91
+ messages: list[HistoryMessage]
92
+ in_progress: HistoryMessage | None
93
+ status: str
94
+ error: str | None
95
+ updated_at: datetime
96
+
97
+
98
+ async def _project_session(
99
+ client: MainSequenceClient,
100
+ manager: SessionRuntimeManager,
101
+ config: TauSDKSettings,
102
+ session_uid: str,
103
+ ) -> _SessionProjection:
104
+ """Project a session's durable branch plus the turn this process is running.
105
+
106
+ The running turn is read before the durable entries. If that turn committed in
107
+ between, its durable entries already hold everything the live view had.
108
+ """
109
+
110
+ live_turn = manager.live_turn(session_uid)
111
+ live_entries = live_turn.entries() if live_turn is not None else None
112
+ transcript = await client.get_local_chat_transcript(
113
+ session_uid,
114
+ turn_uid=live_turn.turn_uid if live_turn is not None else None,
115
+ )
116
+ if transcript.turn_committed:
117
+ live_entries = None
118
+ durable = [
119
+ item.entry
120
+ for item in transcript.entries
121
+ if live_entries is None or live_turn is None or item.turn_uid != live_turn.turn_uid
122
+ ]
123
+ branch = current_branch(durable)
124
+ projected = project_history(
125
+ [*branch, *(live_entries or [])],
126
+ target_agent_uid=config.local_agent_uid,
127
+ )
128
+ messages = projected.messages
129
+ in_progress: HistoryMessage | None = None
130
+ if (
131
+ live_entries is not None
132
+ and messages
133
+ and messages[-1]["role"] == "assistant"
134
+ and projected.source_indexes[-1] >= len(branch)
135
+ ):
136
+ # The running turn's newest assistant message, streaming or awaiting a tool.
137
+ in_progress = {**messages.pop(), "completedAt": None}
138
+ running = (
139
+ live_entries is not None
140
+ or transcript.session.working
141
+ or manager.local_session_running(session_uid)
142
+ )
143
+ status = "running" if running else ("error" if projected.last_turn_error else "completed")
144
+ updated_at = transcript.session.updated_at
145
+ if projected.last_timestamp is not None:
146
+ updated_at = max(updated_at, projected.last_timestamp)
147
+ return _SessionProjection(
148
+ summary=transcript.session,
149
+ messages=messages,
150
+ in_progress=in_progress,
151
+ status=status,
152
+ error=projected.last_turn_error if status == "error" else None,
153
+ updated_at=updated_at,
154
+ )
155
+
156
+
157
+ def _session_summary(
158
+ summary: LocalChatSessionSummary,
159
+ *,
160
+ message_count: int,
161
+ latest_message_preview: str | None,
162
+ updated_at: datetime,
163
+ working: bool,
164
+ ) -> dict[str, Any]:
165
+ return {
166
+ "sessionUid": summary.session_uid,
167
+ "title": summary.title,
168
+ "messageCount": message_count,
169
+ "latestMessagePreview": latest_message_preview,
170
+ "createdAt": iso_timestamp(summary.created_at),
171
+ "updatedAt": iso_timestamp(updated_at),
172
+ "working": working,
173
+ }
174
+
175
+
176
+ @router.get("/chat-sessions")
177
+ async def list_chat_sessions(
178
+ client: BackendDep,
179
+ manager: RuntimeManagerDep,
180
+ config: SettingsDep,
181
+ limit: int = Query(default=50, ge=1, le=100),
182
+ cursor: str | None = Query(default=None, min_length=1, max_length=1024),
183
+ ) -> dict[str, Any]:
184
+ _require_local(config)
185
+ try:
186
+ page = await client.list_local_chat_sessions(limit=limit, cursor=cursor)
187
+ except ValueError as error:
188
+ raise HTTPException(status_code=400, detail=str(error)) from error
189
+ sessions: list[dict[str, Any]] = []
190
+ for summary in page.sessions:
191
+ if manager.live_turn(summary.session_uid) is None:
192
+ sessions.append(
193
+ _session_summary(
194
+ summary,
195
+ message_count=summary.message_count,
196
+ latest_message_preview=summary.latest_message_preview,
197
+ updated_at=summary.updated_at,
198
+ working=(summary.working or manager.local_session_running(summary.session_uid)),
199
+ )
200
+ )
201
+ continue
202
+ projection = await _project_session(client, manager, config, summary.session_uid)
203
+ visible = [
204
+ *projection.messages,
205
+ *([projection.in_progress] if projection.in_progress is not None else []),
206
+ ]
207
+ sessions.append(
208
+ _session_summary(
209
+ summary,
210
+ message_count=len(visible),
211
+ latest_message_preview=latest_message_preview(visible),
212
+ updated_at=max(summary.updated_at, projection.updated_at),
213
+ working=projection.status == "running",
214
+ )
215
+ )
216
+ return {"sessions": sessions, "nextCursor": page.next_cursor}
217
+
218
+
219
+ @router.get("/chat-sessions/{session_uid}/history")
220
+ async def chat_session_history(
221
+ session_uid: str,
222
+ client: BackendDep,
223
+ manager: RuntimeManagerDep,
224
+ config: SettingsDep,
225
+ ) -> dict[str, Any]:
226
+ _require_local(config)
227
+ canonical = config.local_session_uid(session_uid)
228
+ projection = await _project_session(client, manager, config, canonical)
229
+ identity = await workspace_agent_identity(config.workspace)
230
+ return {
231
+ "version": HISTORY_VERSION,
232
+ "session": {
233
+ "sessionId": canonical,
234
+ "threadId": canonical,
235
+ "agentName": identity.name or LOCAL_AGENT_NAME,
236
+ "agentUid": config.local_agent_uid,
237
+ "agentSessionUid": canonical,
238
+ "status": projection.status,
239
+ "startedAt": iso_timestamp(projection.summary.created_at),
240
+ "updatedAt": iso_timestamp(projection.updated_at),
241
+ "error": projection.error,
242
+ },
243
+ "messages": projection.messages,
244
+ "inProgressMessage": projection.in_progress,
245
+ }
246
+
247
+
248
+ @router.get("/agent")
249
+ async def local_agent(config: SettingsDep) -> dict[str, str | None]:
250
+ _require_local(config)
251
+ identity = await workspace_agent_identity(config.workspace)
252
+ return {
253
+ "name": identity.name,
254
+ "displayName": identity.name,
255
+ "description": identity.description,
256
+ }
@@ -13,7 +13,7 @@ from fastapi.middleware.cors import CORSMiddleware
13
13
  from fastapi.responses import JSONResponse
14
14
 
15
15
  from ms_tau_sdk import __version__
16
- from ms_tau_sdk.api import a2a, chat, conversations, health, inspection, sessions
16
+ from ms_tau_sdk.api import a2a, chat, conversations, health, inspection, local_chat, sessions
17
17
  from ms_tau_sdk.application import ApplicationServices
18
18
  from ms_tau_sdk.errors import TauSDKError
19
19
  from ms_tau_sdk.logging import RequestContextMiddleware, configure_logging
@@ -24,6 +24,9 @@ from ms_tau_sdk.settings import TauSDKSettings, get_settings
24
24
 
25
25
  logger = structlog.get_logger(__name__)
26
26
  LOCAL_UNSUPPORTED_PATH_PREFIXES = ("/internal/a2a",)
27
+ # Response headers a trusted cross-origin chat client must read: the canonical
28
+ # session of a `POST /api/chat` stream and the stream protocol marker.
29
+ CORS_EXPOSED_HEADERS = ("X-Agent-Session-Uid", "x-vercel-ai-ui-message-stream")
27
30
 
28
31
 
29
32
  def create_app(
@@ -113,6 +116,7 @@ def create_app(
113
116
  allow_credentials=True,
114
117
  allow_methods=["*"],
115
118
  allow_headers=["*"],
119
+ expose_headers=list(CORS_EXPOSED_HEADERS),
116
120
  )
117
121
  app.add_middleware(RequestContextMiddleware)
118
122
 
@@ -152,6 +156,7 @@ def create_app(
152
156
  app.include_router(chat.router)
153
157
  app.include_router(a2a.router)
154
158
  app.include_router(conversations.router)
159
+ app.include_router(local_chat.router)
155
160
  app.include_router(sessions.router)
156
161
  app.include_router(inspection.router)
157
162
  return app
@@ -33,6 +33,8 @@ from .models import (
33
33
  AgentTaskEventPage,
34
34
  AgentTaskExecutionAttempt,
35
35
  AgentTaskSnapshot,
36
+ LocalChatSessionPage,
37
+ LocalChatTranscript,
36
38
  LocalConversationMessagePage,
37
39
  LocalConversationPage,
38
40
  ProviderControl,
@@ -185,6 +187,30 @@ class MainSequenceClient:
185
187
  del context_id, limit, before_sequence
186
188
  raise LocalModeUnsupportedError("Local conversation history requires TAU local mode")
187
189
 
190
+ async def begin_local_chat_turn(self, session_uid: str, prompt: str) -> None:
191
+ """Record that `/api/chat` is starting a turn in a local session."""
192
+
193
+ del session_uid, prompt
194
+ raise LocalModeUnsupportedError("Local chat sessions require TAU local mode")
195
+
196
+ async def list_local_chat_sessions(
197
+ self,
198
+ *,
199
+ limit: int,
200
+ cursor: str | None,
201
+ ) -> LocalChatSessionPage:
202
+ del limit, cursor
203
+ raise LocalModeUnsupportedError("Local chat session discovery requires TAU local mode")
204
+
205
+ async def get_local_chat_transcript(
206
+ self,
207
+ session_uid: str,
208
+ *,
209
+ turn_uid: str | None = None,
210
+ ) -> LocalChatTranscript:
211
+ del session_uid, turn_uid
212
+ raise LocalModeUnsupportedError("Local chat session history requires TAU local mode")
213
+
188
214
  async def _request(
189
215
  self,
190
216
  method: str,